Handle Failed Sends
A POST /v1/messages returns 202 immediately with status: "pending",
but the actual send happens asynchronously. The send can still fail
after the API call returns — most commonly because Meta rejects the
recipient, the template, or the conversation window.
When that happens, GET /v1/messages/{id} returns status: "failed"
plus a structured error block:
{ "data": { "id": "msg_…", "object": "message", "status": "failed", "error": { "code": "outside_24h_window", "message": "Message failed to send because more than 24 hours have passed since the recipient last messaged you.", "provider_code": 131047 }, "...": "..." }}You’ll also receive a message.status webhook event with the same
information — react there instead of polling.
Stable error codes for failed sends
Section titled “Stable error codes for failed sends”error.code | Meta provider_code | Meaning | Recommended action |
|---|---|---|---|
outside_24h_window | 131047 | Conversation window closed; free-form text rejected. | Re-engage via an approved template (type: "template"). |
template_paused | 132015 | Meta paused this template due to quality rating. | Pick a different template; investigate quality complaints in Meta Business Manager. |
template_disabled | 132016 | Meta permanently disabled this template. | Replace with a new template; re-submit for approval. |
template_param_count_mismatch | 132000 | Wrong number of parameters for the template’s body / header / button. | Fix the components array to match the template’s schema. |
template_param_format_mismatch | 132001 | A parameter value violates the template’s example format (e.g. wrong currency code, malformed date). | Validate parameter shapes against GET /v1/templates/{name} before sending. |
recipient_not_on_whatsapp | 1006 | The recipient phone has no WhatsApp account. | Don’t retry; remove from your send list. |
recipient_blocked | 1013 | The recipient blocked your business number. | Don’t retry; remove from your send list. |
media_download_failed | 131053 | Meta could not fetch the media URL you supplied. | Verify the URL is HTTPS, publicly reachable, returns the right Content-Type, and is under the size limit. |
phone_number_not_registered | 131045 | Your sending number is not registered to send. | Re-verify the number in Meta Business Manager. |
account_suspended | 132035 | Your WhatsApp Business Account is suspended. | Contact Meta support; nothing Kirim can do. |
rate_limited_by_meta | 130472 | Meta throttled your send rate. | Slow down; respect your Meta-side messaging tier. Kirim’s per-org limits sit BELOW Meta’s, so this is rare. |
whatsapp_upstream_error | (any 5xx) | Meta returned a 5xx unexpectedly. | Transient; Kirim retries 8 times before marking dead. If status: "failed" persists, alert. |
Branch recipe
Section titled “Branch recipe”type FailedMessage = { status: 'failed' error: { code: string; message: string; provider_code: number | null }}
async function handleFailedStatus(msg: FailedMessage, contact: Contact) { switch (msg.error.code) { case 'outside_24h_window': // Re-engage via template. return sendTemplate(contact.phone, 'reengagement', 'id_ID')
case 'recipient_not_on_whatsapp': case 'recipient_blocked': // Stop bothering this contact. return markContactUnreachable(contact.id, msg.error.code)
case 'template_paused': case 'template_disabled': // Pick a fallback template, alert the team. await notifyOpsTeam(`Template down: ${msg.error.code}`) return sendTemplate(contact.phone, 'generic_fallback', 'id_ID')
case 'template_param_count_mismatch': case 'template_param_format_mismatch': // Caller bug — never retry. Log full context. logger.error({ msg, contact }, 'Template parameter mismatch') return
case 'rate_limited_by_meta': // Slow down across the board. await pauseSendingFor(60_000) return retryWithBackoff(msg, contact)
case 'whatsapp_upstream_error': // Kirim already retried 8 times. This is permanent. return markMessageDead(msg, contact)
default: // New code Kirim ships. Don't crash — log + surface. logger.warn({ code: msg.error.code }, 'Unrecognised Kirim error code') }}Where the codes come from
Section titled “Where the codes come from”Kirim’s error.code is a stable identifier we maintain, mapped
from Meta’s raw provider_code numbers. Meta occasionally renumbers
their codes — when that happens we update the mapping table without
breaking your code, because your code branches on the Kirim code.
The provider_code is exposed for debugging only. Do not branch on
it.
Subscribe to message.status instead of polling
Section titled “Subscribe to message.status instead of polling”If you’re polling GET /v1/messages/{id} to learn about status
changes, you’re wasting your read budget and adding latency. Subscribe
to the message.status webhook event:
curl -X PATCH https://api-kckit.kirim.chat/v1/webhook_subscriptions/wbs_… \ -H "Authorization: Bearer $KIRIM_KEY" \ -H "Content-Type: application/json" \ -d '{ "events": ["message.received", "message.status"] }'Each message.status delivery is the raw Meta callback (passthrough
format). Parse entry[0].changes[0].value.statuses[0] for the
status, recipient_id, and errors[] if failed.
The mapped Kirim error.code is not in the Meta passthrough
payload — fetch it from GET /v1/messages/{id} if you need the
stable code (or read the raw Meta errors[0].code from the webhook
and look it up in the table above).
What’s next
Section titled “What’s next”- Errors — the full error envelope contract, including non-send errors.
- Verifying Signatures — secure your
message.statuswebhook.