Error codes
Catalog of error.code values that appear on the BlueSender →
CRM status-update channel (PUT /conversations/messages/{messageId}/status),
plus the synchronous HTTP error labels and the OAuth-callback codes
for completeness.
The codes split into three sources, which is worth knowing when debugging:
- BlueSender-emitted on the status-update channel. Only
CLIENT_NOT_CONNECTED(no Mac bound) andINTERNAL_ERROR(backend failure) are produced by BlueSender itself. - Pass-through from the Mac. Most of the codes below
(
IMESSAGE_*,UI_AUTOMATION_DISABLED,AX_DENIED,RATE_LIMITED,TIMEOUT, the reaction codes, and the sticker codes) are emitted by the Mac client and forwarded verbatim by BlueSender on the same channel. - Synchronous HTTP responses. Returned on the request the CRM made; not on the status-update channel. The HTTP-response error labels and the OAuth-callback codes below fall here.
The list is non-exhaustive — codes are free-form strings and new codes can be added without a wire-version bump. CRMs should tolerate unknown codes (see conventions).
HTTP-response error labels (synchronous, on POST /crm/webhook)
Route-handled 4xx responses carry both a human-readable error
label (stable for backwards compatibility) and a machine-readable
code (SCREAMING_SNAKE_CASE, stable). Two paths short-circuit
before the route handler and therefore don't emit code:
unparseable JSON (rejected by Express's body parser; response body
is the parser's default — typically an HTML error page rather than
JSON) and 401 signature-verification failures. Both rows are
flagged with — in the Code column below. Branch on code
when behaviour needs to differ between sibling coded 4xx cases,
fall back to HTTP status code for the uncoded cases; the error
label is the primary human signal. Secondary fields (required,
received, limit) provide detail.
:::tip CRM UIs typically don't render HTTP response bodies
Most CRM webhook integrations only surface the HTTP status code in
the conversation UI, not the JSON body. To make the specific failure
visible in the message thread, BlueSender also fires an async
message_status: failed ack (keyed by code) whenever the request
carries a resolvable messageId — so validation errors and runtime
errors now reach the CRM thread through the same channel. Build
CRM-side error handling against the async ack, not the HTTP body.
:::
| Label | code | HTTP | Meaning |
|---|---|---|---|
Invalid content type | INVALID_CONTENT_TYPE | 400 | The request Content-Type is not application/json. |
Invalid request body | INVALID_REQUEST_BODY | 400 | The Content-Type header passed the route's substring check for application/json but body-parser did not parse the body (e.g. application/json-patch+json, x-application/json, text/application/json), so req.body is absent. null and scalar JSON bodies (string, number, boolean) do not reach this branch — body-parser's default strict: true rejects them upstream with the malformed-JSON 400 in the row below. JSON arrays do reach the route and fall through to MISSING_REQUIRED_FIELDS rather than this row, because arrays are typeof 'object'. |
| (malformed JSON) | — | 400 | A request whose body is not parseable as JSON is rejected by Express's body parser before the route runs. The response body in that case is body-parser's default — typically an HTML error page rather than the JSON shapes listed here. Send valid JSON. |
Missing required fields | MISSING_REQUIRED_FIELDS | 400 | One or more of locationId, contactId, phone, and message-or-attachments are missing. The body includes required and received arrays. |
Message body exceeds maximum length of 20000 characters | MESSAGE_BODY_TOO_LONG | 400 | The message body length exceeds the configured cap (MESSAGE_BODY_MAX_LENGTH_CHARS, default 20,000). The body includes received (actual length) and limit. |
Invalid phone number format | RECIPIENT_PHONE_INVALID | 400 | The supplied phone failed format validation. |
Unsupported message type: … | UNSUPPORTED_MESSAGE_TYPE | 400 | type was present on the request body but was not "SMS". BlueSender only routes through iMessage / SMS; Email (and any other value) is rejected. Omit type to use the default behavior. |
| (Signature error) | — | 401 | Signature verification failed (when enabled). |
Location not connected | LOCATION_NOT_CONNECTED | 404 | No Mac client is bound to the target locationId. The async message_status: failed ack for this case carries error.code = "CLIENT_NOT_CONNECTED" — the historical wire-contract code that Mac and CRM consumers already branch on. The HTTP-body code and the async error.code deliberately differ here; treat them as two contract surfaces. |
TOO_MANY_REQUESTS | TOO_MANY_REQUESTS | 429 | Per-locationId HTTP webhook rate limit exceeded — or per-source-IP when locationId is missing/malformed. See Rate limiting. |
The label and code sets are not closed — new entries may be added
in future backend revisions. CRMs should branch on HTTP status code
first, and match the code field (or error label, for backwards
compatibility) only when behaviour needs to differ between sibling
4xx cases.
Validation errors on the async message_status channel
When POST /crm/webhook rejects a request that carried a resolvable
messageId, BlueSender also dispatches PUT /conversations/messages/{messageId}/status
with status: "failed" as a best-effort outbound call (fire-and-forget;
it may complete before, alongside, or after the synchronous HTTP
response — no ordering guarantee), so the specific failure is visible
in the CRM message thread. CRM UIs that render the message thread are
event-driven on this PUT and don't correlate it with the webhook
response, so the lack of ordering is not user-visible — but consumers
should not poll message status immediately after a 4xx and assume
the failed status is already set. The async ack fires for these
route-handled rows:
MISSING_REQUIRED_FIELDS(400)MESSAGE_BODY_TOO_LONG(400)RECIPIENT_PHONE_INVALID(400)- the 404 — with one wire-contract exception: the async ack carries
error.code = "CLIENT_NOT_CONNECTED"instead ofLOCATION_NOT_CONNECTED(historical wire-contract code, preserved). Treat the HTTP-bodycodeand the asyncerror.codeas two contract surfaces for this row.
For the remaining 4xx codes, no async ack fires — only the HTTP
response carries the code:
INVALID_CONTENT_TYPE(400) andINVALID_REQUEST_BODY(400) reject the request precisely because the body was not parsed into a JSON object (either theContent-Typewas wrong, or it passed the route's substring check but body-parser declined to parse it), so there is nomessageIdto ack against by construction.- the 401 signature-verification path and the 429 rate-limit path are short-circuited by middleware before the route handler runs. The throttling middleware in particular exists to shed load, so amplifying it with an outbound CRM PUT per rejected request would defeat its purpose.
The async ack is best-effort: it also skips silently when messageId
is missing or unrecoverable on an otherwise-parseable body, when
OAuth is not configured on the backend, or when no OAuth token is
stored for the target location. Failures of the async ack do not
affect the HTTP response.
Routing (status-update channel)
| Code | Meaning | Retry |
|---|---|---|
CLIENT_NOT_CONNECTED | No Mac is currently bound to the target locationId. The backend does not buffer the request — see No backend queue. | After Mac reconnects. |
IMESSAGE_GROUP_INVALID_RECIPIENTS | The Mac could not resolve the group recipient list to a valid iMessage thread. Common cause: a participant is not iMessage-capable on the sending Mac (mixed iMessage / SMS group). | No — out-of-band fix. |
IMESSAGE_ERROR | Generic iMessage send failure surfaced by the Mac's message-send pipeline. The accompanying error.message carries the underlying detail. | Sometimes — depends on the underlying failure. |
INVALID_FRAME | An internal validation in the BlueSender pipeline rejected the request before the Mac could act on it. | No — fix the request. |
Mac-side dispatch (status-update channel)
These codes appear when the Mac fails while handling outbound work that requires advanced features (attachments, animations, reactions, standalone stickers).
| Code | Meaning | Retry |
|---|---|---|
UI_AUTOMATION_DISABLED | The relevant feature is disabled at the Mac (operator config or user preference). | No — out-of-band fix. |
AX_DENIED | The Mac is missing a required macOS permission. | No — user action required. |
RATE_LIMITED | The Mac's outbound queue is full (cap: 500 pending jobs). The frame was rejected without enqueueing. See Rate limiting. | Yes, after backoff. |
INTERNAL_ERROR | An unexpected error occurred on the Mac while handling the request. | Yes — typically transient. |
TIMEOUT | A dispatch step exceeded its internal timeout. | Yes. |
Reactions
Outbound reactions ship inside the existing /crm/webhook request via
a reaction block in the metadata footer
— see Reactions: Outbound CRM → Mac.
The codes below appear on the status-update channel.
| Code | Meaning | Retry |
|---|---|---|
TARGET_NOT_FOUND | The target message is not visible / resolvable on the Mac. | Sometimes — see Reactions. |
INVALID_KIND_FIELD_COMBO | The required sibling field for the requested reaction.kind is missing or empty (e.g., kind=classic without reaction.type, or kind=emoji without reaction.emoji). | No — fix the request. |
PICKER_FAILED | The reaction picker failed to open or respond. Distinct from EMOJI_NOT_IN_RECENTS. | Yes, briefly. |
EMOJI_NOT_IN_RECENTS | The requested emoji couldn't be landed via either the inline Tapback Details palette (which on macOS 26 exposes only the user's recent picks) or the full system emoji picker fallback. Most emojis succeed via the system picker fallback on macOS 26+; this code surfaces edge cases where neither path lands the reaction — including the post-dispatch case where the gesture appeared to complete but the reaction wasn't observed in chat.db within ~2s (picker mis-fire). The name is legacy from before the system-picker fallback existed; treat the code itself as "couldn't deliver — fall back." | No — fall back. |
STICKER_NOT_FEASIBLE | Outbound sticker reaction couldn't be applied. Returned when the build-time kill-switch is set or when the post-gesture chat-state probe didn't observe a new sticker reaction within the budget. (URL-based payloads are fully supported — fetch failures ack STICKER_DOWNLOAD_FAILED and non-image content acks STICKER_ENCODE_FAILED, neither of which are this code.) | Yes, briefly — same shape as other dispatch failures; or fall back to a classic Tapback. |
NOT_IMPLEMENTED | Reserved for unknown / future kind values. | No. |
Stickers
Outbound standalone-sticker sends ship inside the existing
/crm/webhook request via a sticker block in the
metadata footer — see
Stickers: Outbound CRM → Mac.
The codes below appear on the status-update channel.
| Code | Meaning | Retry |
|---|---|---|
STICKER_DOWNLOAD_FAILED | Failed to fetch sticker.url or decode supplied bytes. | Yes — typical transient network error. |
STICKER_ENCODE_FAILED | Failed to re-encode supplied bytes as HEIC. | No — fix the source. |
COMPOSE_FAILED | The Mac could not compose the sticker send. Includes the macOS 14 case (sticker sends require macOS 15+). | Possibly. |
OAuth callback (/oauth/callback/crm)
These codes appear on the synchronous JSON body of the CRM marketplace OAuth callback. Visible only during the install / re-auth handshake, not on the regular message-routing path.
| Code | HTTP | Meaning |
|---|---|---|
OAUTH_DISABLED | 503 | OAuth is disabled on this deployment (CRM_OAUTH_ENABLED is not true). The deployment is using PIT tokens instead. |
OAUTH_NOT_CONFIGURED | 500 | OAuth is enabled but one or more required settings are missing on the backend: CRM_OAUTH_CLIENT_ID, CRM_OAUTH_CLIENT_SECRET, CRM_OAUTH_TOKEN_URL, CRM_OAUTH_ENCRYPTION_KEY, a Postgres connection string at CRM_OAUTH_DATABASE_URL (or the generic DATABASE_URL fallback), or a numeric CRM_OAUTH_REFRESH_THRESHOLD_SEC. Operator action required. |
INVALID_REQUEST | 400 | The callback was hit without an authorization code (neither query string nor request body carried it). |
OAUTH_EXCHANGE_FAILED | 400 / 500 | The token exchange with the CRM failed. 400 if the CRM rejected the code, 500 otherwise. |
Tolerance for unknown codes
CRMs should treat any code not in this list as transient + non- retryable — surface a generic "operation failed" indicator without aggressive retry. The contract may add new codes over time; reasonable defaults beat hardcoding the catalog.