Kirimdev pushes events to your server via signed HTTP POSTs. You
subscribe a URL once at the organisation level, pick which events you
care about, and Kirimdev takes care of retries, dead-letter queueing,
and signing-secret rotation.
Polling GET /v1/{phone_number_id}/messages on a timer is the wrong
answer for almost every integration:
Latency. Polling at 1 Hz adds an average 500 ms before you
notice an inbound message — long enough for your bot to feel sluggish.
Webhooks land in under 200 ms from the customer hitting send.
Cost. A 1 Hz poll burns 86,400 requests per day per number
against your rate limit, almost all of them empty.
Race conditions. Status transitions (sent → delivered →
read) happen faster than you can poll; you’ll miss intermediate
states.
Webhooks invert the relationship: Kirimdev tells you when
something happened, you stay idle until it does.
Every webhook POST carries an X-Kirim-Source header. Its value tells
you whether the body is Meta passthrough or a Kirimdev-native
envelope:
X-Kirim-Source: meta
Events that originated inside Meta — a customer messaged you
(message.received), or Meta confirmed a delivery status
(message.status). The body is the exact JSON Meta sent, so
any existing WhatsApp Cloud API parser keeps working unchanged.
X-Kirim-Source: kirim
Events that originated inside Kirimdev — a contact was created or
updated, a conversation got assigned or closed. These use the
Kirimdev envelope: { id, type, created_at, data }.
The two shapes coexist on the same subscription. Your handler
branches on X-Kirim-Source (or just on type after parsing) and
routes accordingly.
A webhook subscription is the binding between a URL and a list of
event types you care about. Subscriptions are organisation-scoped
— by default, one subscription receives events from every WhatsApp
account in your org, so you don’t need one subscription per phone
number.
If you run multiple WhatsApp numbers on the same organisation (e.g. a
“Sales” number and a “Support” number) and want to route their events
to different endpoints, add a phone_number_ids whitelist:
Discover the ids via GET /v1/accounts — phone_number_id on each
row is the Meta business_phone_number_id you pass here.
Up to 50 ids per subscription; each must belong to your organisation.
On PATCH, send phone_number_ids: null to clear the filter and go
back to “all accounts”, or send an array to replace the whitelist.
Only events tied to a phone number are filtered: message.*,
conversation.*, and contact.*. Organisation-level events
(customer.*) always fire regardless of the filter.
Response objects always carry phone_number_ids — null means the
subscription is unrestricted, an array is the active whitelist.
At-least-once. Every event reaches a healthy subscription at
least once. Dedupe on your side using X-Kirim-Event-Id.
2xx response = success. Any 2xx (200, 201, 204, …) marks the
delivery succeeded. 3xx is treated as failure — Kirimdev does
not follow redirects.
10-second per-attempt timeout. Your endpoint must respond in
under 10 seconds or the attempt is recorded as a timeout failure.
8 retries with exponential backoff before dead-letter. Total
window is roughly 24 hours. See
Retries & Auto-Disable for the full schedule.
Order is not guaranteed. Status callbacks may arrive out of
order (you can see delivered before sent). Treat status as a
state machine, not a sequence.
Because delivery is at-least-once, your endpoint will occasionally
receive the same event twice — typically when a retry fires after
your server processed the original but its 200 didn’t reach Kirimdev
before the 10 s timeout.
The dedupe key is X-Kirim-Event-Id. Same id = same logical event,
regardless of attempt number or whether the delivery is a manual
replay.
Meta itself retries inbound webhooks. To avoid fanning the same Meta
retry out to your subscription several times, Kirimdev dedupes
source=meta events at the publisher layer using a 24-hour claim on
the wamid. By the time an event reaches your endpoint, Meta-driven
duplicates are already filtered out.
The only dedupe scenario you need to worry about is the
retry-vs-original race described above, not Meta’s own
at-least-once retry pattern.