error.code, which never changes for a given condition. error.message is written for people and may change.
{
"status": "error",
"error": {
"code": "RATE_LIMIT_EXCEEDED",
"message": "Rate limit exceeded",
"type": "rate_limit",
"http_status": 429,
"retryable": true,
"doc_url": "https://docs.blujay.io/errors/rate-limit-exceeded",
"details": [{ "limit_type": "public_api_write", "limit": 120, "remaining": 0, "reset_at": "2026-10-14T17:31:00.000Z" }],
"retry_after": 12,
"reset_at": "2026-10-14T17:31:00.000Z"
},
"trace_id": "trc_8f2a1c0e4b7d",
"timestamp": "2026-10-14T17:30:48.000Z"
}
| Field | Meaning |
|---|---|
error.code | Stable, machine-readable code. Each code has its own page, linked from doc_url. |
error.type | Broad category: validation, auth, authorization, not_found, idempotency, rate_limit, queue, feature_gate, phone_number or server. |
error.http_status | The HTTP status, repeated in the body. |
error.retryable | true when the same request can succeed later. Retry it with the same Idempotency-Key. |
error.details | Extra context, such as the field that failed validation or the limit that was hit. |
error.retry_after | Seconds to wait before retrying. Also sent as the Retry-After header. |
trace_id | Identifies this request. Also sent as the X-Trace-ID header on every response. |
client_request_id | Your own X-Request-ID header, echoed back when you send one. |
Handling errors
- Retryable errors (
retryable: true): wait forRetry-Afterwhen present, otherwise back off exponentially, and retry with the sameIdempotency-Key. Blujay returns the original result instead of sending twice. Check the flag, not the status: most409codes are not retryable, and429and503usually are. - Everything else (
retryable: false): retrying the same request returns the same error. Fix the request first. The sending rules errors are all in this group. - Support: when you contact support@blujay.io, include the
trace_idand yourX-Request-ID. Never send your API key.
202 Accepted) and the message can fail later. Failed messages report failure_code on the message and in message.failed webhooks. See Message lifecycle.
All error codes
Request errors
| Code | HTTP | Retryable | What to do |
|---|---|---|---|
VALIDATION_ERROR | 400 | No | Read error.message and error.details for the field that failed, correct the request, and send it again. |
INVALID_PUBLIC_ID | 400 | No | Use the exact ID returned by the API for that resource type. |
INVALID_CURSOR | 400 | No | Pass next_cursor from the previous page unchanged, with the same filters. |
NOT_FOUND | 404 | No | Check the ID and the path. |
FEATURE_NOT_ENABLED | 400 | No | Read error.message for the specific field. |
Authentication
| Code | HTTP | Retryable | What to do |
|---|---|---|---|
UNAUTHORIZED | 401 | No | Send Authorization: Bearer <key> with your workspace API key (blu_...). |
API_KEY_REVOKED | 401 | No | Use a current key from the API tab in Blujay, or contact support@blujay.io if you need a new one. |
API_KEY_EXPIRED | 401 | No | Use a current key from the API tab in Blujay, or contact support@blujay.io if you need a new one. |
OAUTH_TOKEN_INVALID | 401 | No | If you’re calling the API yourself, use your workspace API key as the bearer token. |
OAUTH_SCOPE_REQUIRED | 403 | No | If you’re calling the API yourself, use your workspace API key. |
OAUTH_ROUTE_NOT_ENABLED | 403 | No | Call it with your workspace API key. |
Idempotency
| Code | HTTP | Retryable | What to do |
|---|---|---|---|
IDEMPOTENCY_REQUEST_MISMATCH | 409 | No | Use a new key for a new request. |
IDEMPOTENCY_KEY_MISMATCH | 409 | No | Send the key in one place, or make both values identical. |
IDEMPOTENCY_IN_PROGRESS | 409 | Yes | Wait a moment and retry with the same key and the same request. |
Rate limits and capacity
| Code | HTTP | Retryable | What to do |
|---|---|---|---|
RATE_LIMIT_EXCEEDED | 429 | Yes | Wait for the number of seconds in the Retry-After header (also error.retry_after; RateLimit-Reset is the same number of seconds), then retry with the same idempotency key. |
QUEUE_FULL | 429 | Yes | Wait for Retry-After seconds, then retry with the same idempotency key. |
ATTACHMENT_CAPACITY_EXCEEDED | 429 | Yes | Finish or abandon pending uploads, wait for Retry-After seconds, and try again. |
Sending messages
| Code | HTTP | Retryable | What to do |
|---|---|---|---|
FIRST_MESSAGE_LINK_NOT_ALLOWED | 400 | No | Send the first message without a link. |
PRE_REPLY_CONTENT_BLOCKED | 400 | No | Send a short text without the link or attachment. |
PRE_REPLY_LENGTH_EXCEEDED | 400 | No | Shorten the message to 300 characters or less. |
PRE_REPLY_LIMIT_REACHED | 400 | No | Wait for the contact to respond, or reach them another way. |
RECIPIENT_SUPPRESSED | 403 | No | Do not retry. |
CONTACT_NOT_VERIFIED | 403 | No | Don’t retry until the contact is verified. |
CONTACT_MUST_TEXT_FIRST | 403 | No | For a direct message, don’t retry: wait for the person to text your number, then reply. |
SENDER_SUSPENDED | 403 | No | Do not retry. |
SENDER_RETIRED | 409 | No | Call GET /v1/phone-numbers and send from an active number. |
FROM_NUMBER_REQUIRED | 422 | No | Send from, or assign the contact to a teammate and set that teammate’s default line in Team Members. |
INVALID_MESSAGE | 400 | No | Retry only messages you sent: the IDs that came back from your own send responses, or messages whose status is failed rather than received. |
RETRY_NOT_ALLOWED | 409 | No | Don’t automatically send a replacement. |
Chats and groups
| Code | HTTP | Retryable | What to do |
|---|---|---|---|
UNSUPPORTED_CHAT_OPERATION | 400 | No | Check the chat’s capabilities with GET /v1/chats/{chat_id} and the feature guide for where the operation is supported. |
REPLY_TARGET_NOT_FOUND | 404 | No | Use a message ID from this chat, for example from GET /v1/chats/{chat_id}/messages. |
REPLY_TARGET_NOT_IN_CHAT | 400 | No | Send the reply to the chat that contains the target message. |
REPLY_TARGET_NOT_REPLYABLE | 400 | No | Reply to a sent iMessage, or send a normal message without reply_to. |
REPLY_TARGET_NOT_SENT | 409 | Yes | Wait until the target message is sent, then retry with the same idempotency key. |
PHONE_NUMBER_REQUIRED | 400 | No | Include from_phone_number_id. |
LAST_PHONE_NUMBER_IN_GROUP | 409 | No | No action is possible through the API. |
GROUP_MINIMUM_PARTICIPANTS | 409 | No | Add a participant first, or leave the group as it is. |
PHONE_NUMBER_HAS_SCHEDULED_MESSAGES | 409 | No | Cancel or wait for the scheduled messages, then try again. |
PHONE_NUMBER_HAS_MESSAGES_SENDING | 409 | Yes | Wait until they finish, then retry. |
Edit and unsend
| Code | HTTP | Retryable | What to do |
|---|---|---|---|
MESSAGE_NOT_SENT_YET | 409 | Yes | Retry shortly with the same idempotency key. |
MESSAGE_NOT_EDITABLE | 409 | No | Do not retry. |
EDIT_WINDOW_EXPIRED | 409 | No | Send a follow-up message instead. |
EDIT_LIMIT_REACHED | 409 | No | Send a follow-up message instead. |
UNSEND_WINDOW_EXPIRED | 409 | No | Send a follow-up message if you need to correct something. |
EDIT_UNCHANGED | 400 | No | Send different text, or skip the edit. |
SENDER_UNAVAILABLE | 409 | No | The message cannot be changed. |
CHANGE_REJECTED | 502 | No | Do not retry the same change. |
CHANGE_UNCONFIRMED | 503 | Yes | Retry with the same idempotency key. |
Media
| Code | HTTP | Retryable | What to do |
|---|---|---|---|
ATTACHMENT_NOT_READY | 409 | Yes | PUT the file to the upload_url from POST /v1/attachments, then retry the read or send. |
ATTACHMENT_UPLOAD_INVALID | 422 | No | Create a new attachment with the correct size_bytes and content_type, and upload the file again. |
ATTACHMENT_TOO_LARGE | 413 | No | Send a smaller file, or upload it with POST /v1/attachments if you used a URL. |
UNSUPPORTED_ATTACHMENT_TYPE | 415 | No | Send a supported type. |
ATTACHMENT_FETCH_FAILED | 422 | No | Use a public HTTPS URL that returns the file directly, or upload the file with POST /v1/attachments. |
ATTACHMENT_STORAGE_UNAVAILABLE | 503 | Yes | Wait for Retry-After seconds and retry. |
Contacts
| Code | HTTP | Retryable | What to do |
|---|---|---|---|
CONTACT_ALREADY_EXISTS | 409 | No | Find it with GET /v1/contacts/search and update that contact, or use POST /v1/contacts/import to create or update by phone number. |
CONTACT_MATCH_CONFLICT | 409 | No | Leave out the email, or send the phone number of the contact that already has this email. |
Workflows
| Code | HTTP | Retryable | What to do |
|---|---|---|---|
WORKFLOW_LIFECYCLE_ERROR | 409 | No | Activate the workflow in Blujay, or enroll the contact in a manual workflow from GET /v1/workflows. |
WORKFLOW_VALIDATION_ERROR | 422 | No | Read error.message. |
Webhooks
| Code | HTTP | Retryable | What to do |
|---|---|---|---|
UNSUPPORTED_WEBHOOK_EVENT | 400 | No | Use event names from the webhook events list. |
WEBHOOK_URL_INVALID | 400 | No | Use a plain https:// URL on a public host. |
WEBHOOK_URL_UNSAFE | 400 | No | Use a publicly reachable HTTPS endpoint. |
WEBHOOK_URL_UNRESOLVABLE | 400 | No | Check the host name and its DNS records, then try again. |
WEBHOOK_VERSION_UNSUPPORTED | 409 | No | Update the subscription to a supported version with PATCH /v1/webhook-subscriptions/{subscription_id}. |
WEBHOOK_SUBSCRIPTION_INACTIVE | 409 | No | Re-enable the subscription or add the event type back, then try again. |
WEBHOOK_PHONE_NUMBER_EXCLUDED | 409 | No | Add the phone number to the subscription’s phone_number_filter, or set the filter to all numbers. |
Server and availability
| Code | HTTP | Retryable | What to do |
|---|---|---|---|
CAPABILITY_CHECK_UNAVAILABLE | 503 | Yes | Wait for Retry-After seconds and retry. |
QUEUE_UNAVAILABLE | 503 | Yes | Wait for Retry-After seconds and retry with the same idempotency key. |
PHONE_NUMBER_UNAVAILABLE | 503 | Yes | Wait for Retry-After seconds and retry. |
INTERNAL_ERROR | 500 | Yes | Retry with the same idempotency key after a short wait. |