Skip to main content
After Blujay accepts a message, it moves through a series of statuses until it’s read, stops at the last status the service reports, or fails. This page explains each status, what each service can report, how to read failures, and how to follow a message with webhooks or polling.

Statuses

A message doesn’t always pass through every status. It can go straight from accepted to delivered, and a message can stay at sent for good. Always act on the latest status rather than waiting for a particular sequence. failed is normally final. Rarely, Blujay confirms afterwards that a failed message did go out, and it moves on to sent, delivered or read. Treat the newest status as the truth. delivered and read are reported for direct chats. A group chat may not report them: a group message can stay at sent even when everyone has it. Don’t wait for delivered on a group message.

What each service reports

How far a message gets depends on the service that carried it.
Never wait for an SMS message to reach delivered or read. For SMS, sent is usually the last status you get, and it’s a success.

Which service carried the message

Every message has two service fields: With Auto, both start as null. That’s expected, not an error. service fills in as the message is sent. Webhook payloads carry the same information in lowercase (imessage, rcs, sms). In webhooks, the field for what you asked for is called requested_service. From webhook version 2026-08-20 on, it holds the same value as preferred_service. See Webhook versions. To choose a service yourself, see Send messages and iMessage, RCS or SMS.

Failures

A failed message carries failed_at, a failure object with a lowercase code and a readable reason, and retryable, on the message itself (GET /v1/messages/{message_id} and the list endpoints) and in the message.failed webhook alike. retryable: true means POST /v1/messages/{message_id}/retry will be accepted; false means it returns 409 RETRY_NOT_ALLOWED. For five reasons the message also carries a failure_code that names the exact problem: Every other failure has no failure_code; failure.code still says what happened. To find failed messages, call GET /v1/messages?status=failed. In the message.failed webhook, versions 2026-08-01 and later carry the failure object, and 2026-09-02 and later also carry failure_code when there is one. To send a failed message again, see Retry a failed message.

Degradations

Some features only exist on iMessage. When a direct message asks for one of them and goes out as SMS or RCS, Blujay still sends the text and lists what it dropped in degradations:
degradations is an empty array when nothing was dropped. Message reads and message webhooks carry the same entries.

Follow a message

You can follow a message with webhooks, by polling, or both. Subscribe to the message events you need, and Blujay posts each status change to your server as it happens.
message.delivered
  • Webhooks are delivered at least once and can arrive out of order. Deduplicate on event_id, and never replace a newer status with an older one. Use the sent_at, delivered_at and read_at timestamps to order them.
  • message.accepted is sent only for messages sent through the API. message.sent, message.delivered, message.read and message.failed also cover messages your team sends from Blujay.
  • There’s no event for sending.
  • The message object and the webhook payload carry the same tracking fields: direction, sent_at, delivered_at, read_at, failed_at, failure, and sender on inbound group messages. Webhooks tell you when they change; reads give you the current values.
Set up an endpoint in Webhooks, and see every payload in Webhook events.

By polling

GET /v1/messages/{message_id} returns the current status:
Poll with a growing delay (for example 2, 4, 8 seconds, up to 30 seconds between polls) and stop at a status that is final for your use: Webhooks are better than polling for anything after that, including the rare failed that later becomes sent. To catch up on many messages at once, call GET /v1/messages?updated_after=2026-10-14T17:00:00.000Z with the time of your last sync. Reads count toward your read limit; see Rate limits. Polling is fine for testing and for reconciling. In production, use webhooks and fall back to GET /v1/messages/{message_id} when an event seems missing.
Common mistakes
  • The 202 send response uses id and delivery_status: queued. Reads use message_id and status: accepted. They describe the same message.
  • updated_at moves whenever anything changes, including an edit or a read receipt. Order a conversation by created_at.
  • A status: failed with no failure_code is normal. The reason is in the message.failed webhook.

Limitations

  • failure_code covers five reasons you can act on. Every failed message also carries a failure object with a lowercase code and a readable reason, the same one its message.failed webhook carries, and retryable, which says whether POST /v1/messages/{message_id}/retry would be accepted.
  • SMS never reports delivered or read. Group chats may not report them either.
  • sending has no webhook event.

Next steps

Webhooks overview

Get status changes pushed to your server.

Webhook events

Every message event and its payload.

Receive messages

Handle inbound texts, media and group replies.

Send messages

Start chats, send follow-ups and retry failures.