Skip to content
Guides

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.

error.codeMeta provider_codeMeaningRecommended action
outside_24h_window131047Conversation window closed; free-form text rejected.Re-engage via an approved template (type: "template").
template_paused132015Meta paused this template due to quality rating.Pick a different template; investigate quality complaints in Meta Business Manager.
template_disabled132016Meta permanently disabled this template.Replace with a new template; re-submit for approval.
template_param_count_mismatch132000Wrong number of parameters for the template’s body / header / button.Fix the components array to match the template’s schema.
template_param_format_mismatch132001A 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_whatsapp1006The recipient phone has no WhatsApp account.Don’t retry; remove from your send list.
recipient_blocked1013The recipient blocked your business number.Don’t retry; remove from your send list.
media_download_failed131053Meta 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_registered131045Your sending number is not registered to send.Re-verify the number in Meta Business Manager.
account_suspended132035Your WhatsApp Business Account is suspended.Contact Meta support; nothing Kirim can do.
rate_limited_by_meta130472Meta 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.
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')
}
}

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:

Terminal window
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).

  • Errors — the full error envelope contract, including non-send errors.
  • Verifying Signatures — secure your message.status webhook.