> ## Documentation Index
> Fetch the complete documentation index at: https://docs.blujay.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Errors

> Every error has a stable code, a retry signal and a page that explains how to fix it.

Every error response uses the same envelope. Branch on `error.code`, which never changes for a given condition. `error.message` is written for people and may change.

```json theme={null}
{
  "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 for `Retry-After` when present, otherwise back off exponentially, and retry with the **same** `Idempotency-Key`. Blujay returns the original result instead of sending twice. Check the flag, not the status: most `409` codes are not retryable, and `429` and `503` usually are.
* **Everything else** (`retryable: false`): retrying the same request returns the same error. Fix the request first. The [sending rules](/messaging/sending-rules.md) errors are all in this group.
* **Support**: when you contact [support@blujay.io](mailto:support@blujay.io), include the `trace_id` and your `X-Request-ID`. Never send your API key.

A failed **message** is different from a failed **request**. A request can succeed (`202 Accepted`) and the message can fail later. Failed messages report `failure_code` on the message and in `message.failed` webhooks. See [Message lifecycle](/messaging/lifecycle.md).

## All error codes

## Request errors

| Code | HTTP | Retryable | What to do |
| - | - | - | - |
| [`VALIDATION_ERROR`](/errors/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`](/errors/invalid-public-id) | 400 | No | Use the exact ID returned by the API for that resource type. |
| [`INVALID_CURSOR`](/errors/invalid-cursor) | 400 | No | Pass `next_cursor` from the previous page unchanged, with the same filters. |
| [`NOT_FOUND`](/errors/not-found) | 404 | No | Check the ID and the path. |
| [`FEATURE_NOT_ENABLED`](/errors/feature-not-enabled) | 400 | No | Read `error.message` for the specific field. |

## Authentication

| Code | HTTP | Retryable | What to do |
| - | - | - | - |
| [`UNAUTHORIZED`](/errors/unauthorized) | 401 | No | Send `Authorization: Bearer <key>` with your workspace API key (`blu_...`). |
| [`API_KEY_REVOKED`](/errors/api-key-revoked) | 401 | No | Use a current key from the **API** tab in Blujay, or contact [support@blujay.io](mailto:support@blujay.io) if you need a new one. |
| [`API_KEY_EXPIRED`](/errors/api-key-expired) | 401 | No | Use a current key from the **API** tab in Blujay, or contact [support@blujay.io](mailto:support@blujay.io) if you need a new one. |
| [`OAUTH_TOKEN_INVALID`](/errors/oauth-token-invalid) | 401 | No | If you're calling the API yourself, use your workspace API key as the bearer token. |
| [`OAUTH_SCOPE_REQUIRED`](/errors/oauth-scope-required) | 403 | No | If you're calling the API yourself, use your workspace API key. |
| [`OAUTH_ROUTE_NOT_ENABLED`](/errors/oauth-route-not-enabled) | 403 | No | Call it with your workspace API key. |

## Idempotency

| Code | HTTP | Retryable | What to do |
| - | - | - | - |
| [`IDEMPOTENCY_REQUEST_MISMATCH`](/errors/idempotency-request-mismatch) | 409 | No | Use a new key for a new request. |
| [`IDEMPOTENCY_KEY_MISMATCH`](/errors/idempotency-key-mismatch) | 409 | No | Send the key in one place, or make both values identical. |
| [`IDEMPOTENCY_IN_PROGRESS`](/errors/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`](/errors/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`](/errors/queue-full) | 429 | Yes | Wait for `Retry-After` seconds, then retry with the same idempotency key. |
| [`ATTACHMENT_CAPACITY_EXCEEDED`](/errors/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`](/errors/first-message-link-not-allowed) | 400 | No | Send the first message without a link. |
| [`PRE_REPLY_CONTENT_BLOCKED`](/errors/pre-reply-content-blocked) | 400 | No | Send a short text without the link or attachment. |
| [`PRE_REPLY_LENGTH_EXCEEDED`](/errors/pre-reply-length-exceeded) | 400 | No | Shorten the message to 300 characters or less. |
| [`PRE_REPLY_LIMIT_REACHED`](/errors/pre-reply-limit-reached) | 400 | No | Wait for the contact to respond, or reach them another way. |
| [`RECIPIENT_SUPPRESSED`](/errors/recipient-suppressed) | 403 | No | Do not retry. |
| [`CONTACT_NOT_VERIFIED`](/errors/contact-not-verified) | 403 | No | Don't retry until the contact is verified. |
| [`CONTACT_MUST_TEXT_FIRST`](/errors/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`](/errors/sender-suspended) | 403 | No | Do not retry. |
| [`SENDER_RETIRED`](/errors/sender-retired) | 409 | No | Call `GET /v1/phone-numbers` and send from an active number. |
| [`FROM_NUMBER_REQUIRED`](/errors/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`](/errors/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`](/errors/retry-not-allowed) | 409 | No | Don't automatically send a replacement. |

## Chats and groups

| Code | HTTP | Retryable | What to do |
| - | - | - | - |
| [`UNSUPPORTED_CHAT_OPERATION`](/errors/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`](/errors/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`](/errors/reply-target-not-in-chat) | 400 | No | Send the reply to the chat that contains the target message. |
| [`REPLY_TARGET_NOT_REPLYABLE`](/errors/reply-target-not-replyable) | 400 | No | Reply to a sent iMessage, or send a normal message without `reply_to`. |
| [`REPLY_TARGET_NOT_SENT`](/errors/reply-target-not-sent) | 409 | Yes | Wait until the target message is sent, then retry with the same idempotency key. |
| [`PHONE_NUMBER_REQUIRED`](/errors/phone-number-required) | 400 | No | Include `from_phone_number_id`. |
| [`LAST_PHONE_NUMBER_IN_GROUP`](/errors/last-phone-number-in-group) | 409 | No | No action is possible through the API. |
| [`GROUP_MINIMUM_PARTICIPANTS`](/errors/group-minimum-participants) | 409 | No | Add a participant first, or leave the group as it is. |
| [`PHONE_NUMBER_HAS_SCHEDULED_MESSAGES`](/errors/phone-number-has-scheduled-messages) | 409 | No | Cancel or wait for the scheduled messages, then try again. |
| [`PHONE_NUMBER_HAS_MESSAGES_SENDING`](/errors/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`](/errors/message-not-sent-yet) | 409 | Yes | Retry shortly with the same idempotency key. |
| [`MESSAGE_NOT_EDITABLE`](/errors/message-not-editable) | 409 | No | Do not retry. |
| [`EDIT_WINDOW_EXPIRED`](/errors/edit-window-expired) | 409 | No | Send a follow-up message instead. |
| [`EDIT_LIMIT_REACHED`](/errors/edit-limit-reached) | 409 | No | Send a follow-up message instead. |
| [`UNSEND_WINDOW_EXPIRED`](/errors/unsend-window-expired) | 409 | No | Send a follow-up message if you need to correct something. |
| [`EDIT_UNCHANGED`](/errors/edit-unchanged) | 400 | No | Send different text, or skip the edit. |
| [`SENDER_UNAVAILABLE`](/errors/sender-unavailable) | 409 | No | The message cannot be changed. |
| [`CHANGE_REJECTED`](/errors/change-rejected) | 502 | No | Do not retry the same change. |
| [`CHANGE_UNCONFIRMED`](/errors/change-unconfirmed) | 503 | Yes | Retry with the same idempotency key. |

## Media

| Code | HTTP | Retryable | What to do |
| - | - | - | - |
| [`ATTACHMENT_NOT_READY`](/errors/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`](/errors/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`](/errors/attachment-too-large) | 413 | No | Send a smaller file, or upload it with `POST /v1/attachments` if you used a URL. |
| [`UNSUPPORTED_ATTACHMENT_TYPE`](/errors/unsupported-attachment-type) | 415 | No | Send a supported type. |
| [`ATTACHMENT_FETCH_FAILED`](/errors/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`](/errors/attachment-storage-unavailable) | 503 | Yes | Wait for `Retry-After` seconds and retry. |

## Contacts

| Code | HTTP | Retryable | What to do |
| - | - | - | - |
| [`CONTACT_ALREADY_EXISTS`](/errors/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`](/errors/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`](/errors/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`](/errors/workflow-validation-error) | 422 | No | Read `error.message`. |

## Webhooks

| Code | HTTP | Retryable | What to do |
| - | - | - | - |
| [`UNSUPPORTED_WEBHOOK_EVENT`](/errors/unsupported-webhook-event) | 400 | No | Use event names from the webhook events list. |
| [`WEBHOOK_URL_INVALID`](/errors/webhook-url-invalid) | 400 | No | Use a plain `https://` URL on a public host. |
| [`WEBHOOK_URL_UNSAFE`](/errors/webhook-url-unsafe) | 400 | No | Use a publicly reachable HTTPS endpoint. |
| [`WEBHOOK_URL_UNRESOLVABLE`](/errors/webhook-url-unresolvable) | 400 | No | Check the host name and its DNS records, then try again. |
| [`WEBHOOK_VERSION_UNSUPPORTED`](/errors/webhook-version-unsupported) | 409 | No | Update the subscription to a supported version with `PATCH /v1/webhook-subscriptions/{subscription_id}`. |
| [`WEBHOOK_SUBSCRIPTION_INACTIVE`](/errors/webhook-subscription-inactive) | 409 | No | Re-enable the subscription or add the event type back, then try again. |
| [`WEBHOOK_PHONE_NUMBER_EXCLUDED`](/errors/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`](/errors/capability-check-unavailable) | 503 | Yes | Wait for `Retry-After` seconds and retry. |
| [`QUEUE_UNAVAILABLE`](/errors/queue-unavailable) | 503 | Yes | Wait for `Retry-After` seconds and retry with the same idempotency key. |
| [`PHONE_NUMBER_UNAVAILABLE`](/errors/phone-number-unavailable) | 503 | Yes | Wait for `Retry-After` seconds and retry. |
| [`INTERNAL_ERROR`](/errors/internal-error) | 500 | Yes | Retry with the same idempotency key after a short wait. |


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.