Skip to main content

Webhook Integration

Webhooks push real-time events from Cronozen to your endpoint — decision sealed, payment completed, attendance verified, certificate issued. This guide covers setup, security verification, and retry handling.

When to Use Webhooks vs Polling

Use webhooks

Real-time events (decisions, payments, attendance), low-latency UX requirements, or events that don’t have predictable timing.

Use polling

Batch reconciliation, simple dashboards, or environments where receiving inbound requests is impractical.

Setup (3 Steps)

1. Register your endpoint

curl -X POST https://cronozen.com/api/v1/webhooks \
  -H "Authorization: Bearer $CRONOZEN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://your-app.example.com/webhooks/cronozen",
    "events": ["dpu.sealed", "payment.completed", "attendance.verified"],
    "active": true
  }'
Response includes a signingSecret — store this securely. It is shown only once.

2. Verify the signature on every request

Every webhook payload includes an X-Cronozen-Signature header. Verify it with HMAC-SHA256:
import crypto from 'crypto'

function verifyWebhook(payload: string, signature: string, secret: string): boolean {
  const expected = crypto
    .createHmac('sha256', secret)
    .update(payload)
    .digest('hex')
  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expected),
  )
}
Reject any request that fails verification. Without this check, anyone can forge events to your endpoint.

3. Respond fast

Return 2xx within 5 seconds. If your handler does heavy work, queue it and return immediately:
app.post('/webhooks/cronozen', async (req, res) => {
  if (!verifyWebhook(req.rawBody, req.headers['x-cronozen-signature'], SECRET)) {
    return res.status(401).end()
  }

  await queue.publish('webhook.cronozen', req.body)
  res.status(200).end()
})
Slow responses cause retries (see below), creating duplicate processing.

Retry Behavior

If your endpoint returns non-2xx or times out, Cronozen retries with exponential backoff:
AttemptDelay
1Immediate
230s
32 min
410 min
51 hour
66 hours
724 hours
After 7 failed attempts the event is marked failed. You can manually retry from the dashboard or via the API:
curl -X POST https://cronozen.com/api/v1/webhooks/events/{eventId}/retry \
  -H "Authorization: Bearer $CRONOZEN_API_KEY"

Idempotency — Critical

Webhook retries mean the same event can arrive multiple times. Always treat handlers as idempotent. Every event carries a unique eventId. Store it on first receipt and reject duplicates:
async function handleEvent(event) {
  const seen = await db.webhookEvents.findUnique({
    where: { eventId: event.id }
  })
  if (seen) return // already processed

  await db.$transaction([
    db.webhookEvents.create({ data: { eventId: event.id, type: event.type } }),
    // ...your actual processing
  ])
}

Event Ordering

Cronozen does not guarantee delivery order. If event A is created before event B, your endpoint may receive B before A. Design handlers to be order-independent, or use the event’s createdAt and your local state to reconcile. For workflows that strictly depend on order (e.g., “approve then disburse”), use the Workflows API instead — it enforces sequence within a single workflow instance.

Local Development

For local testing, use a tunnel like ngrok or cloudflared:
ngrok http 3000
# → https://abc123.ngrok.io
Register the ngrok URL as your webhook endpoint. Events arrive in your local environment with the same headers and signature as production.

Security Checklist

  • Verify signature on every request
  • Use crypto.timingSafeEqual (not string comparison) to prevent timing attacks
  • Return 2xx within 5 seconds
  • Store event IDs for idempotency
  • Rotate signing secret periodically (POST /webhooks/{id}/rotate-secret)
  • Restrict endpoint to inbound traffic only (no auth-required pages)
  • Log signature failures — repeated failures may indicate replay attacks

See Also