Skip to content
Webhooks

Webhooks Overview

Kirim pushes events to your server via signed HTTP POSTs. Subscribe once, manage subscriptions via the API or the dashboard, and let Kirim handle retries, dead-letter queueing, and signing-secret rotation.

Webhook deliveries fall into two shapes, distinguished by the X-Kirim-Source header:

Events that originate inside Meta (a customer sends a WhatsApp message, a delivery status callback fires) are forwarded verbatim. The HTTP body is exactly the JSON Meta sent to Kirim, after Meta-signature stripping and Kirim-signature attachment.

Why passthrough? Your existing WhatsApp Cloud API parser keeps working unchanged. Kirim just adds reliable delivery (retries, dead-letter, signature) on top of Meta’s raw stream.

Events that originate inside Kirim (a conversation gets assigned, a contact is created via the dashboard) use a normalised envelope:

{
"id": "evt_01HXYZABCDEFGHJKMNPQRSTVWX",
"type": "conversation.assigned",
"created_at": "2026-05-23T10:00:00Z",
"data": {
"conversation": { "id": "cnv_…", "...": "..." },
"assignee": { "user_id": "usr_…", "team_id": "" }
}
}

Why an envelope? These events have no Meta-side equivalent — there’s no canonical wire format to mirror, so Kirim defines one.

Content-Type: application/json
User-Agent: Kirim-Webhook/1.0
X-Kirim-Source: meta # "meta" or "kirim"
X-Kirim-Event: message.received
X-Kirim-Event-Id: wamid.HBgN... # Meta wamid when source=meta, evt_<id> when source=kirim
X-Kirim-Delivery-Id: wbd_… # Unique per delivery attempt
X-Kirim-Attempt: 1 # 1-based attempt counter (1-8)
X-Kirim-Signature: t=1716480000,v1=<hex>,v1=<hex>
  • At-least-once. Kirim guarantees every event is delivered to a healthy subscription at least once. Dedupe on your side using X-Kirim-Event-Id (see Dedupe below).
  • Order is not guaranteed. Meta’s source events arrive in roughly causal order, but parallel delivery means you may see message.status: delivered before message.status: sent. Treat status as a state machine, not an event sequence.
  • 8 retries with exponential backoff (10 s → 24 h) before dead-letter. See Retries & Auto-Disable.
  • 2xx response = success. Any 2xx (200, 201, 204, …) marks the delivery succeeded. 3xx is treated as failure (we don’t follow redirects).
  • 10-second per-attempt timeout. Your endpoint must respond in under 10 seconds or Kirim treats it as a timeout failure.

Kirim guarantees at-least-once delivery. Your endpoint will occasionally receive the same event twice — typically when a retry fires after your server processed the original but the response didn’t reach Kirim before the 10 s timeout.

Dedupe on X-Kirim-Event-Id. Same event id always represents the same logical event, regardless of attempt number or whether the delivery is a manual replay.

import { redis } from './lib/redis.js'
app.post('/kirim-webhook', async (req, res) => {
const eventId = req.headers['x-kirim-event-id'] as string
// Atomic claim — SET NX with a 7-day TTL covers all retry windows.
const fresh = await redis.set(`webhook:${eventId}`, '1', { EX: 604800, NX: true })
if (!fresh) {
return res.status(200).send('duplicate-ack')
}
// First time we've seen this event — process it.
await handleEvent(req.body)
res.status(200).send('ok')
})

To avoid fanning out the same Meta retry to your subscription multiple times, Kirim itself dedupes meta-sourced events at the publisher layer. The first time Meta’s wamid hits Kirim, a 24-hour Redis claim is set. Subsequent deliveries of the same wamid from Meta are dropped before ever reaching your subscription.

This means the only dedupe scenario you need to worry about is the retry-vs-original race, not Meta’s own at-least-once retry pattern.