Core concepts

Errors

Every error comes back in the same shape so you can handle them uniformly.

Error response shape

{
"error": {
"code": "VALIDATION_ERROR",
"message": "The 'to' field must be a valid E.164 phone number.",
"details": {
"field": "to",
"issue": "Invalid format"
}
}
}
  • error.code — stable string identifier. Use this in your error-handling logic.
  • error.message — human-readable explanation. Show this to engineers, never to end-users directly.
  • error.details — optional structured payload with field-level info.

Status code reference

StatusCodeWhen you see it
400VALIDATION_ERRORThe request body or query string is invalid. Look at error.details for which field is wrong.
401INVALID_API_KEYAPI key is missing, revoked, or malformed.
403FORBIDDENAuthenticated, but your account isn't allowed (suspended, sandbox, etc).
404NOT_FOUNDThe resource (message, line, customer) does not exist.
409CONFLICTState conflict — e.g. recipient already opted out, message already unsent.
422DELIVERY_ERRORApple/iMessage rejected the send. Recipient may not be iMessage-capable, line frozen, warmup cap hit.
429RATE_LIMITEDToo many requests. Back off and retry after the Retry-After header.
500INTERNAL_ERRORSomething went wrong on our end. Safe to retry with exponential backoff.
502UPSTREAM_ERRORA downstream service (Apple, Telnyx) returned an error. Usually transient.
503SERVICE_UNAVAILABLEWe're temporarily overloaded or maintenance. Retry after a delay.

When to retry

  • 4xx (except 429): Don't retry. The request will fail again the same way.
  • 429: Retry after the Retry-After header. Use exponential backoff if you hit it repeatedly.
  • 5xx: Retry with exponential backoff (e.g. 1s, 2s, 4s, 8s). Cap at 5 retries.
  • Network errors: Use an idempotency key so retries don't double-send.

The official Node SDK handles this for you

blueapi retries 5xx and 429 by default with exponential backoff. Disable with { retries: 0 } on the client constructor if you'd rather handle it yourself.

Common scenarios

Recipient not iMessage-capable

You'll get a 422 DELIVERY_ERROR with details:

{
"error": {
"code": "DELIVERY_ERROR",
"message": "Recipient is not iMessage-capable.",
"details": { "reason": "not_imessage_capable", "to": "+15551234567" }
}
}

Either flip allowSmsFallback: true on the send (if you have SMS fallback enabled), or check upfront with POST /v1/lookup.

Recipient opted out

{
"error": {
"code": "CONFLICT",
"message": "Recipient has opted out of messages from this customer.",
"details": { "reason": "opted_out", "to": "+15551234567" }
}
}

Respect opt-outs. To opt them back in (only if they request it), use DELETE /v1/opt-outs/:phone.

Per-contact unanswered cutoff

{
"error": {
"code": "DELIVERY_ERROR",
"message": "Recipient +15551234567 has 6 consecutive unanswered messages (cutoff 6); paused until they reply.",
"details": { "reason": "unanswered_cutoff", "consecutiveUnanswered": 6 }
}
}

Protective ceiling that prevents Apple spam reports. The counter resets on any inbound reply. Tunable via MAX_UNANSWERED_PER_CONTACT env var on self-hosted; on cloud, contact support.