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
| Status | Code | When you see it |
|---|---|---|
| 400 | VALIDATION_ERROR | The request body or query string is invalid. Look at error.details for which field is wrong. |
| 401 | INVALID_API_KEY | API key is missing, revoked, or malformed. |
| 403 | FORBIDDEN | Authenticated, but your account isn't allowed (suspended, sandbox, etc). |
| 404 | NOT_FOUND | The resource (message, line, customer) does not exist. |
| 409 | CONFLICT | State conflict — e.g. recipient already opted out, message already unsent. |
| 422 | DELIVERY_ERROR | Apple/iMessage rejected the send. Recipient may not be iMessage-capable, line frozen, warmup cap hit. |
| 429 | RATE_LIMITED | Too many requests. Back off and retry after the Retry-After header. |
| 500 | INTERNAL_ERROR | Something went wrong on our end. Safe to retry with exponential backoff. |
| 502 | UPSTREAM_ERROR | A downstream service (Apple, Telnyx) returned an error. Usually transient. |
| 503 | SERVICE_UNAVAILABLE | We'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-Afterheader. 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.