Error codes

Standard HTTP status codes with a consistent error envelope.
View as Markdown

Successful responses are wrapped in a data object. Error responses are not wrapped.

message is always present. errors is optional, and its entry shape depends on which check failed:

messageExtra fieldserrors[] entries
Any business-rule failure-(absent)
Validation failed-field, code, message
Recipient validation failedsummaryrow, line, field, value, code, message
Template-variable mismatch-row, message or field, message

All four can return 400. Parse defensively: read message first, treat errors as optional, and don’t assume every entry carries field or code. See Validation error shapes.

HTTP status reference

StatusWhenExample message
400Body validation failedValidation failed (see errors[] for fields)
400One or more recipients rejectedRecipient validation failed (see errors[] for rows)
400SMS keyword missingSMS keyword is required
400Email sent as a scheduled broadcastScheduled sending is not available for email. Send it as a quick broadcast instead.
400File upload missing or wrong MIMENo file uploaded · Only JPEG, PNG, WebP, MP4, and PDF files are supported
401No / malformed Authorization headerMissing or invalid Authorization header
401Bearer token doesn’t match any active keyInvalid API key
401Key was revoked from the dashboardAPI key has been revoked
401Key was deactivatedAPI key is inactive
401Key has expiredAPI key has expired
402Prepaid balance below required costInsufficient credit. Required: RMx.xx, available: RMy.yy
402Postpaid spending cap exceededSpending cap exceeded. Accrued: RM…, this broadcast: RM…, cap: RM…
402Account suspended for non-paymentAccount suspended for non-payment. N overdue invoice(s) totalling RM…
403Caller IP not in API key whitelistRequest IP is not in the API key whitelist
403Key lacks the endpoint’s scopeAPI key does not have send_sms permission
403Channel subscription inactiveClient does not have an active SMS channel subscription
403Same for WhatsApp / EmailClient does not have an active WhatsApp channel subscription
404SMS keyword not foundKeyword not found
404Template not found by nameTemplate not found
404WhatsApp template not foundWhatsApp template not found
404Client has no WABA + platform-default disabledNo active WhatsApp configuration found for this client
409Ad-hoc SMS template name already takenAn SMS template named "…" already exists. Use templateUid to reuse it or choose a different name.
409WhatsApp sender not provisionedWhatsApp sender is not provisioned
413Request body over 10 MBRequest payload too large. Reduce the recipient list or split into smaller batches.
422WhatsApp template not yet approvedTemplate is not approved (current status: pending)
422WhatsApp template never accepted upstreamTemplate has no provider template code — it was never accepted by the provider. Re-create the template before broadcasting.
429Rate limit hitToo many requests, please try again later (see Rate-limit responses)
500Unexpected server errorInternal server error (no further detail leaked)
500Enqueue failed after the broadcast was recordedFailed to enqueue broadcast — messages marked failed (credit released automatically)

Validation error shapes

Several different 400 bodies exist. Branch on message before parsing errors[].

Schema validation

The request body didn’t match the endpoint’s schema.

1{
2 "message": "Validation failed",
3 "errors": [
4 { "field": "campaignName", "code": "SCHEMA", "message": "Campaign name is required" },
5 { "field": "scheduledAt", "code": "SCHEMA", "message": "scheduledAt is required for schedule broadcast" }
6 ]
7}

field is a dotted JSON path; array indices appear as numeric segments (recipients.0).

Recipient validation

The body was well-formed, but one or more recipients were rejected. Note the top-level summary and the six-field error entries - nothing is sent when this fires.

1{
2 "message": "Recipient validation failed",
3 "summary": { "total": 3, "valid": 1, "invalid": 2 },
4 "errors": [
5 { "row": 2, "line": 2, "field": "msisdn", "value": "0123456789",
6 "code": "DUPLICATE", "message": "Duplicate recipient (also on row 1)" },
7 { "row": 3, "line": 3, "field": "msisdn", "value": "not-a-number",
8 "code": "INVALID_MSISDN", "message": "Must be a valid phone number (E.164 or Malaysia 01X format)" }
9 ]
10}
codeMeaning
INVALID_MSISDNNot a valid phone number after normalisation
INVALID_EMAILNot a valid email address
MISSING_VALUEThe recipient field was empty
DUPLICATEThe same recipient appears on an earlier row - duplicates are rejected, not de-duplicated
MISSING_VARIABLEThe template declares a variable this row doesn’t supply
UNKNOWN_VARIABLEThe row supplies a variable the template doesn’t declare

Use POST /broadcasts/validate to check a list against these rules before committing to a broadcast.

Template-variable mismatch

The recipients were fine, but the values supplied don’t satisfy the variables the template declares. An empty string or whitespace counts as missing. Nothing is sent and no credit is reserved.

In dynamic mode the offending rows are listed - capped at the first 10, even when more fail:

1{
2 "message": "Template \"welcome_promo_v1\" requires: name, discount",
3 "errors": [
4 { "row": 2, "message": "Missing template variables: discount" },
5 { "row": 7, "message": "Missing template variables: name, discount" }
6 ]
7}

In standard mode there is no array - the message names what’s missing:

1{ "message": "Template \"welcome_promo_v1\" requires values for: discount. Declared variables: name, discount." }

The SMS equivalent reports field instead of row, with message = Dynamic SMS variables do not match template parameters.

Rate-limit responses

Every 429 carries a Retry-After header (seconds until the limiter resets). Use it to back off rather than retrying immediately.

The message identifies which limiter fired:

Limitermessage
Global, 200 / 15 min / source IPToo many requests, please try again later
POST /smsToo many SMS requests, please try again in 15 minutes
POST /whatsapp, POST /emailToo many broadcast requests, please try again in 15 minutes
POST /broadcasts/validateToo many validation requests, please try again in 15 minutes

Credit safety on enqueue failure

If the API has already debited credit and then the enqueue fails, Teekrr automatically releases the reserved credit and marks the messages as failed. You’ll see 500 - Failed to enqueue broadcast — messages marked failed in this case. No manual ledger correction is needed on your side.