Skip to content
Guides

Subscribe to Webhooks

This guide walks you through standing up a production-quality webhook consumer. By the end you’ll have:

  • An endpoint that verifies signatures with your secret(s).
  • Dedupe via X-Kirim-Event-Id.
  • Fast ack + asynchronous processing.
  • Replay logic for handling the dead-letter queue.
  1. Stand up an HTTPS endpoint.

    Kirim only delivers to https:// URLs (HTTP is rejected at subscription creation with 400 invalid_webhook_url). For local testing, expose your dev server via ngrok / cloudflared / similar.

    The endpoint must:

    • Accept POST application/json.
    • Respond within 10 seconds (per-attempt timeout).
    • Return any 2xx for success. 3xx, 4xx (except 408/429), and 5xx mark the delivery as failed.
  2. Subscribe.

    Terminal window
    curl -X POST https://api-kckit.kirim.chat/v1/webhook_subscriptions \
    -H "Authorization: Bearer $KIRIM_KEY" \
    -H "Content-Type: application/json" \
    -d '{
    "url": "https://your-server.example.com/kirim-webhook",
    "events": [
    "message.received",
    "message.status",
    "conversation.assigned",
    "conversation.closed",
    "contact.created",
    "contact.updated"
    ],
    "description": "Production handler"
    }'

    The response includes initial_secret once — store it in your secrets manager immediately. Kirim cannot show it again.

    {
    "data": {
    "id": "wbs_01HXYZ…",
    "object": "webhook_subscription",
    "url": "https://your-server.example.com/kirim-webhook",
    "status": "active",
    "events": ["message.received", "..."],
    "secrets": [
    {
    "id": "sec_01HXYZ…",
    "created_at": "2026-05-23T10:00:00Z",
    "expires_at": null
    }
    ],
    "initial_secret": "whsec_…",
    "...": "..."
    }
    }
  3. Wire up verification.

    Use the recipe in Verifying Signatures. Critical bits in your handler:

    • Read the raw request body bytes before any JSON parsing.
    • Verify against every active secret (your in-memory list).
    • Use a timing-safe comparison.
    • Reject signatures older than 5 minutes.
  4. Dedupe on X-Kirim-Event-Id.

    At-least-once delivery means the same event may arrive twice (typically when your handler processed the original but the response didn’t reach Kirim before the 10 s timeout).

    const eventId = req.headers['x-kirim-event-id'] as string
    const fresh = await redis.set(
    `webhook:event:${eventId}`,
    '1',
    { EX: 604800, NX: true }, // 7 days covers all retry windows
    )
    if (!fresh) {
    return res.status(200).send('duplicate-ack')
    }
  5. Ack first, process second.

    A handler that synchronously processes events is a recipe for timeouts when your downstream slows down. Persist + ack inside the request, hand off to your own queue:

    app.post('/kirim-webhook', express.raw({ type: 'application/json' }), async (req, res) => {
    if (!verifyKirimSignature(req.body.toString('utf8'), req.headers['x-kirim-signature'] as string, secrets)) {
    return res.status(401).send('invalid signature')
    }
    const eventId = req.headers['x-kirim-event-id'] as string
    const fresh = await claimEvent(eventId)
    if (!fresh) return res.status(200).send('duplicate-ack')
    // Cheap persistent enqueue — bytes only, no parsing yet.
    await db.insert(rawEvents).values({
    eventId,
    eventType: req.headers['x-kirim-event'] as string,
    source: req.headers['x-kirim-source'] as 'meta' | 'kirim',
    payload: req.body, // raw bytes
    receivedAt: new Date(),
    })
    res.status(200).send('ok')
    // Worker picks up from rawEvents and processes asynchronously.
    })
  6. Handle the dead-letter queue.

    Deliveries that fail 8 times land in the DLQ (status: "dead"). Browse via the dashboard’s Developers → Webhook Deliveries page or:

    Terminal window
    curl "https://api-kckit.kirim.chat/v1/webhook_deliveries?subscription_id=wbs_…&status=dead" \
    -H "Authorization: Bearer $KIRIM_KEY"

    Once your endpoint is healthy again, replay either one at a time:

    Terminal window
    curl -X POST https://api-kckit.kirim.chat/v1/webhook_deliveries/wbd_…/replay \
    -H "Authorization: Bearer $KIRIM_KEY"

    Or bulk:

    Terminal window
    curl -X POST https://api-kckit.kirim.chat/v1/webhook_deliveries/bulk_replay \
    -H "Authorization: Bearer $KIRIM_KEY" \
    -H "Content-Type: application/json" \
    -d '{
    "subscription_id": "wbs_…",
    "status": "dead",
    "created_after": "2026-05-20T00:00:00Z"
    }'
  7. Wire secret rotation into your deploys.

    A leaked secret should be rotatable in minutes. Standard flow:

    1. POST /v1/webhook_subscriptions/{id}/secrets — Kirim returns a new plaintext.
    2. Add the new secret to your handler’s secret list (env var, secrets manager) and re-deploy. Now you verify against both.
    3. Wait for one full delivery cycle to confirm signatures from the new secret are accepted.
    4. DELETE /v1/webhook_subscriptions/{id}/secrets/{old_sec_id} — Kirim stops signing with the old one.
    5. Remove the old secret from your handler on the next deploy.

Use the payloads from Payload Examples as test inputs in your handler suite. Sign them with a known test secret and assert your verifier accepts them.