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 uses Meta’s WhatsApp webhook envelope,
so existing WhatsApp Cloud API parsers can handle it. Kirimdev preserves
the documented status identifiers, errors, pricing, and conversation
metadata when Meta supplies them.
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. Events may arrive in a different order
than the messages were created — this applies across events of the
same type (e.g. two message.sent in one conversation) as well as
between types (you can see a status before its message.sent). Do
not rely on arrival order or on the event id. To reconstruct
message order, sort by the message timestamp in the payload. Treat
message.status as a state machine, not a sequence — correlate
each status to its message by provider_id (wamid).
Webhook arrival order does not reflect the order messages were
created. Sort events by the message timestamp field in the payload
to reconstruct the sequence — never by arrival time or by the event
id.
Two limitations to design around:
Timestamps are second-precision. Message timestamps follow
WhatsApp, which reports time to the second. Two messages in the
same second cannot be distinguished by timestamp — there is no
finer-grained ordering marker available. Display same-second
messages as a group, or accept an approximate order within that
second.
The event id is not time-ordered. It is a random identifier;
never sort by it.
If you buffer and reorder on your side, sorting by timestamp is
enough for arrival-order scrambling — you only need extra tolerance
for messages that share the same second.
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.