Skip to content
Sending & receiving

Receive messages

Kirimdev never polls for new messages — it pushes them to your webhook URL the moment Meta delivers them. Subscribe once; every inbound message and status update fans out as a signed POST.

Create a webhook subscription that listens for the inbound event families you care about:

Terminal window
curl -X POST https://api.kirimdev.com/v1/webhook_subscriptions \
-H "Authorization: Bearer $KIRIM_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-app.example.com/webhooks/kirim",
"events": ["message.received", "message.status"]
}'

The response carries initial_secret once — store it server-side so you can verify HMAC signatures on every incoming delivery. See Verifying signatures for the verification recipe.

message.received and message.status are Meta passthrough events — Kirimdev forwards the exact JSON Meta sent. The top-level body is always Meta’s envelope:

{
"object": "whatsapp_business_account",
"entry": [
{
"id": "<WABA_ID>",
"changes": [
{ "field": "messages", "value": { /* Meta value object */ } }
]
}
]
}

Branch on the X-Kirim-Event request header to decide what’s inside entry[0].changes[0].value:

  • X-Kirim-Event: message.receivedvalue.messages[] carries the inbound message. value.metadata.phone_number_id is your connected number; messages[0].from is the customer’s WhatsApp id.
  • X-Kirim-Event: message.statusvalue.statuses[] carries an outbound delivery update. statuses[0].id is the wamid of the message you sent.

messages[0].type selects the type-specific subobject. The shapes below are Meta’s, not Kirimdev’s — see Meta’s WhatsApp Cloud API webhook reference:

Inbound messages[0].typeSubobject
texttext.body
image / video / audio / document / sticker{type}.{id, mime_type, sha256, caption?, filename?} — fetch bytes via GET /v1/{phone_number_id}/messages/{wamid}/media
locationlocation.{latitude, longitude, name?, address?}
contactscontacts[] (Meta-shaped)
interactiveinteractive.{type, button_reply?, list_reply?}
reactionreaction.{message_id, emoji} (empty emoji = unreact)
buttonbutton.{payload, text} — quick-reply tap on a template

Kirimdev-native events (conversation.assigned, conversation.closed, contact.created, contact.updated) use a different envelope — { id, type, created_at, data }. See Event catalogue for those.

See Webhook payloads for full copy-paste fixtures per event type.

Once you’ve persisted the inbound message, mark it as read so the sender’s double-tick turns blue:

import { Kirim } from '@kirimdev/sdk'
const kirim = new Kirim({ apiKey: process.env.KIRIM_KEY! })
const phone = kirim.phoneNumbers(process.env.PHONE_ID!)
// `wamid` is the Meta-native id from
// body.entry[0].changes[0].value.messages[0].id
await phone.messages.markAsRead(wamid)

See Mark messages as read for the full Meta-style body shape and optional typing indicator.

  • Retries: Failed deliveries (non-2xx response, timeout, or network error) retry with exponential backoff up to ~24 hours. See Retries & auto-disable.
  • Replays: Inspect the dead-letter queue or re-deliver any failed event via the Webhooks overview — every subscription exposes a /deliveries collection plus POST /replay and POST /bulk_replay actions.
  • Ordering: Events for a single conversation arrive in causal order, but Kirimdev does not guarantee global ordering across conversations.