Skip to content
Core Concepts

Errors

Every error from /v1/* ships in the same envelope. One branch in your client handles them all — switch on error.code, log request_id, and you’re done.

{
"error": {
"type": "invalid_request_error",
"code": "invalid_phone_number",
"message": "Phone number must be in E.164 format (e.g. +628111222333).",
"param": "to",
"request_id": "req_01HXYZABCDEFGHJKMNPQRSTVWX"
}
}
FieldStabilityMeaning
typeStableOne of seven broad categories: invalid_request_error, authentication_error, permission_error, not_found, conflict, rate_limit_error, api_error.
codePermanentProgrammatic identifier. Branch on this.
messageMay evolveHuman-readable. Show to operators; never branch on text.
paramWhen relevantJSON-pointer-style hint to the offending field.
request_idAlways presentEchoes the X-Request-Id response header. Quote in support tickets.
import {
Kirim,
KirimError,
RateLimitError,
ValidationError,
AuthenticationError,
NotFoundError,
ConflictError,
ApiError,
} from '@kirimdev/sdk'
const kirim = new Kirim({ apiKey: process.env.KIRIM_KEY! })
const phone = kirim.phoneNumbers('106540352242922')
try {
await phone.messages.send({
messaging_product: 'whatsapp',
to: '+628111222333',
type: 'text',
text: { body: 'Halo!' },
})
} catch (err) {
if (err instanceof RateLimitError) {
// SDK already retried; budget exhausted.
return scheduleLater(err.retryAfterMs)
}
if (err instanceof ValidationError) {
// err.code === 'invalid_phone_number' | 'missing_required_field' | ...
return showFieldError(err.param, err.message)
}
if (err instanceof AuthenticationError) {
return rotateKey()
}
if (err instanceof NotFoundError) {
return notify('Resource gone or never existed')
}
if (err instanceof ConflictError) {
return retryWithBackoff()
}
if (err instanceof ApiError) {
// 5xx upstream — exponential backoff, alert if persistent.
return scheduleRetry(err.requestId)
}
if (err instanceof KirimError) {
logger.error({ code: err.code, requestId: err.requestId }, 'Kirim error')
}
throw err
}

This catalogue covers errors returned by the HTTP layer — any non-2xx response from /v1/*.

HTTPtypecodeWhen
400invalid_request_errorinvalid_phone_numberto is not E.164 with leading +.
400invalid_request_errorinvalid_phone_number_idPath {phone_number_id} is malformed or unknown to your org.
400invalid_request_errorinvalid_template_nameTemplate name does not exist on the calling phone number.
400invalid_request_errormissing_required_fieldRequired field absent. param set.
400invalid_request_errorinvalid_field_valueField present but constraint violated. param set.
400invalid_request_errormissing_messaging_productBody missing "messaging_product": "whatsapp".
400invalid_request_errorinvalid_limitlimit query param outside 1–100.
400invalid_request_errorinvalid_cursorCursor undecodable or tampered.
400invalid_request_errorinvalid_webhook_urlURL is not https or fails reachability sanity check.
400invalid_request_errorinvalid_event_typeWebhook event name not recognised.
401authentication_errormissing_api_keyNo Authorization header.
401authentication_errorinvalid_api_keyMalformed, unknown, or hash mismatch.
401authentication_errorrevoked_api_keyKey has been revoked.
401authentication_errorexpired_api_keyexpires_at passed.
403permission_errorfeature_not_availableYour plan does not include the Public API.
403permission_errororganization_suspendedOrg-level block. Contact support.
403permission_errorphone_number_access_denied{phone_number_id} belongs to a different org.
404not_foundresource_not_foundMissing OR belongs to a different organisation. Returned as 404 (not 403) to avoid leaking existence.
409conflictidempotency_in_progressAn identical request with the same Idempotency-Key is still processing.
409conflictwebhook_subscription_disabledTrying to operate on an auto-disabled subscription. Re-enable first.
422invalid_request_erroridempotency_key_reuseSame Idempotency-Key was used with a different request body.
422invalid_request_errorwhatsapp_number_not_verifiedPhone number is not yet approved by Meta.
422invalid_request_errorcannot_revoke_last_secretTried to revoke the only signing secret on a subscription. Add a new secret first.
429rate_limit_errorrate_limit_exceededPer-org token bucket empty. Honour Retry-After. See Rate Limits.
500api_errorinternal_errorUnexpected exception. Logged server-side with request_id. Open a ticket if persistent.
502api_errorwhatsapp_upstream_errorMeta returned 5xx. Retry with backoff.
503api_errorservice_unavailableMaintenance or temporary overload. Honour Retry-After if present.
  • code strings never change. New codes are added only for new failure modes; existing codes keep their meaning forever.
  • message text may evolve — wording polish, i18n, hints. Don’t substring-match.
  • HTTP status, type, and code form the v1 contract. Anything else (param, request_id, response headers) is auxiliary.

Rate limits

Headers, tier table, and 429 handling recipes. Read →

Idempotency

Safely retry POSTs without duplicating side effects. Read →

Handle failed sends

Async delivery failures and the per-error retry playbook. Read →

API reference

Endpoint-by-endpoint error tables. Browse →