> ## 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.

# Events

> Every webhook event type, when it is sent and what its payload contains.

Choose events with `event_types` when you create or update a subscription. Payloads below are for the default version, `2026-10-03`. Every event uses the [common envelope](/webhooks/overview.md#the-event-payload); only `data` is shown.

| Event | Sent when |
| - | - |
| [`message.accepted`](#message-lifecycle) | A message you sent through the API is accepted for sending. |
| [`message.sent`](#message-lifecycle) | An outbound message has been sent. |
| [`message.delivered`](#message-lifecycle) | An outbound message reached the recipient's device. |
| [`message.read`](#message-lifecycle) | The recipient read an outbound message. |
| [`message.failed`](#message-lifecycle) | An outbound message failed. |
| [`message.received`](#message-received) | Someone sent a message to one of your numbers. |
| [`message.edited`](#message-edited-and-message-unsent) | An iMessage was edited, by you or by the contact. |
| [`message.unsent`](#message-edited-and-message-unsent) | An iMessage was unsent, by you or by the contact. |
| [`reaction.added`](#reactions) | Someone added a reaction to a message. |
| [`reaction.removed`](#reactions) | Someone removed a reaction. |
| [`chat.typing_indicator.started`](#typing) | A contact started typing in an iMessage chat. |
| [`chat.typing_indicator.stopped`](#typing) | A contact stopped typing. |
| [`participant.added`](#group-changes) | Someone was added to a group chat. |
| [`participant.removed`](#group-changes) | Someone left or was removed from a group chat. |
| [`chat.group_name_updated`](#group-changes) | A group chat was renamed. |
| [`contact.tag_added`](#contacts) | A tag was added to a contact. |
| [`contact.assigned`](#contacts) | A contact's assigned team member changed. |
| [`phone_number.status_updated`](#phone-numbers) | One of your numbers was replaced by a new number. |

### Reserved event types

> [!NOTE]
>
> Nine more event types appear in the API schema but can't be subscribed to today: `chat.created`, `chat.group_icon_updated`, `chat.group_name_update_failed`, `chat.group_icon_update_failed`, `contact.created`, `contact.updated`, `contact.deleted`, `contact.suppressed` and `contact.unsuppressed`. Putting one in `event_types` returns `400` [`FEATURE_NOT_ENABLED`](/errors/feature-not-enabled). They may never be sent. In particular, there is no event for an opt-out and no event when a group rename fails: poll `opted_out` on the contact ([Opt-outs](/opt-outs.md)) and read the group after a rename ([Group chats](/messaging/group-chats.md)). An event type that isn't in the schema at all returns `400` [`UNSUPPORTED_WEBHOOK_EVENT`](/errors/unsupported-webhook-event).

## Message lifecycle

`message.accepted`, `message.sent`, `message.delivered`, `message.read` and `message.failed` follow an outbound message. They cover messages sent from the Blujay apps, workflows, scheduled texts, integrations and the API. `message.accepted` is sent only for messages sent through the API.

```json theme={null}
{
  "message_id": "msg_abc123",
  "chat_id": "chat_abc123",
  "phone_number_id": "phone_abc123",
  "status": "delivered",
  "direction": "outbound",
  "requested_service": null,
  "service": "imessage",
  "sent_at": "2026-10-14T17:01:02.000Z",
  "delivered_at": "2026-10-14T17:01:03.000Z",
  "read_at": null,
  "failed_at": null,
  "failure": null,
  "degradations": [],
  "parts": [{ "type": "text", "text": "Hi Ava, your order 1042 has shipped." }],
  "reply_to": null
}
```

* `requested_service` is the service you asked for (`null` for Auto). `service` is the service that actually carried the message, once confirmed. Webhooks spell services in lowercase (`imessage`, `rcs`, `sms`); message reads use `iMessage`, `RCS` and `SMS`.
* Not every message gets every event. SMS usually stops at `sent`, because carriers don't report delivery or read status reliably. iMessage reports `delivered`, and `read` when the recipient has read receipts on, and it often skips `sent` altogether when delivery is confirmed within the same moment. Treat each event as the message's newest state rather than expecting the full sequence.
* `degradations` lists anything that was not delivered because the message went out as SMS or RCS, for example `{"code": "effect_not_delivered", "reason": "…"}`.
* **Lifecycle events carry no `contact_id`.** They identify the chat. To know the person, keep the `chat_id` from your `POST /v1/chats` response next to your own record, or read `GET /v1/chats/{chat_id}`: a direct chat's `participants` entry carries the `contact_id`. Only `message.received` names the sender directly.

### Failures

A `message.failed` event has `failed_at` and a `failure` object:

```json theme={null}
{
  "status": "failed",
  "failed_at": "2026-10-14T17:01:05.000Z",
  "failure": { "code": "recipient_unavailable", "reason": "The recipient was unavailable on the requested messaging service." }
}
```

| `failure.code` | Meaning |
| - | - |
| `recipient_unavailable` | The recipient can't be reached on the service you asked for with `preferred_service`. |
| `unsupported_content` | The message has content the chosen service can't send, such as a file type SMS can't carry (for example a PDF), media over the SMS/MMS size limit, or an effect or text style. `failure_code` says which, when it is a media problem. |
| `reply_requires_imessage` | A threaded reply could only have gone out as SMS or RCS, so it wasn't sent. |
| `service_mismatch` | The message couldn't be sent with the service you required. |
| `sender_unavailable` | Your phone number couldn't send at the time. |
| `sender_retired` | Your phone number was replaced before the message went out. |
| `network_rejected` | The messaging network rejected the message. |
| `dispatch_unavailable` | Blujay couldn't start sending the message. |
| `delivery_failed` | The message couldn't be delivered, for any other reason. |

Media problems also set `failure_code` to [`ATTACHMENT_FETCH_FAILED`](/errors/attachment-fetch-failed), [`ATTACHMENT_TOO_LARGE`](/errors/attachment-too-large) or [`UNSUPPORTED_ATTACHMENT_TYPE`](/errors/unsupported-attachment-type). The same `failure` object, `failed_at` and `failure_code` are on the message itself (`GET /v1/messages/{message_id}` and the lists), together with `retryable`. See [Track a message](/messaging/lifecycle.md).

## message.received

Sent when someone messages one of your numbers. A direct message has `contact_id`:

```json theme={null}
{
  "message_id": "msg_def456",
  "chat_id": "chat_abc123",
  "phone_number_id": "phone_abc123",
  "contact_id": "contact_abc123",
  "text": "Here is the photo.",
  "status": "received",
  "direction": "inbound",
  "requested_service": null,
  "service": "imessage",
  "sent_at": null,
  "delivered_at": null,
  "read_at": null,
  "failed_at": null,
  "failure": null,
  "degradations": [],
  "parts": [
    { "type": "text", "text": "Here is the photo." },
    {
      "type": "media",
      "attachment_id": "att_abc123",
      "filename": "photo.jpg",
      "content_type": "image/jpeg",
      "size_bytes": 428731,
      "download_url": "/v1/attachments/att_abc123/content"
    }
  ],
  "reply_to": null
}
```

`text` is the first text part, so you can read a plain reply without walking `parts`. It is absent when the message has no text.

A message in a group chat has a `sender` instead of `contact_id`. `sender.contact_id` is present only when the sender is a contact in your workspace; a participant you never saved has none:

```json theme={null}
{ "chat_id": "chat_grp789", "sender": { "type": "phone", "address": "+15555550125" } }
```

A media part can arrive with only `type` and `attachment_id` while the file is still being processed. The event is a snapshot and never changes, so read the message or the attachment again later for its details. See [Media](/messaging/media.md#media-in-received-messages).

### What you don't get inbound

`message.received` is built from the text and files Blujay stored for the message. Anything else is either reported another way or not at all:

* **Tapbacks are reactions, not messages.** A reaction arrives only as [`reaction.added` or `reaction.removed`](#reactions), with the message it applies to. It never arrives as `message.received`, and it isn't listed by `GET /v1/messages`.
* **Content with no part type.** A message Blujay stores with no text and no file, such as an iMessage app payload (Apple Pay, a poll, a shared location), arrives as `message.received` with `"parts": []` and no `text`. You can't tell from the event what kind of content it was.
* **Stickers, voice memos and contact cards** arrive as `media` parts: a sticker sent as an image is image media, a voice memo is audio media, and a shared contact card is a `text/vcard` media part.
* **Effects and text styles** on inbound messages aren't reported.
* **The service** can be `null` on an inbound message when Blujay hasn't confirmed it.

For the full inbound walkthrough, see [Receive messages](/messaging/receive-messages.md).

## message.edited and message.unsent

Sent when an iMessage changes after it was sent, whether you changed it through Blujay or the contact changed theirs. A contact's edits and unsends are tracked for messages up to a week old. Both events carry the same message fields as a lifecycle event, plus the edit or unsend details.

* `message.edited` carries the new `text` and `parts`, plus `edited_at` and `edit_count`. Read earlier versions with `GET /v1/messages/{message_id}?include=edit_history`.
* `message.unsent` carries `unsent_at` and `unsent_scope` but never the text. With `unsent_scope: "all"` the whole message was removed. With `"text"` only its caption was removed and its attachments remain.

```json message.edited theme={null}
{
  "message_id": "msg_abc123",
  "chat_id": "chat_abc123",
  "phone_number_id": "phone_abc123",
  "status": "delivered",
  "direction": "outbound",
  "requested_service": null,
  "service": "imessage",
  "sent_at": "2026-10-14T17:01:02.000Z",
  "delivered_at": "2026-10-14T17:01:03.000Z",
  "read_at": null,
  "failed_at": null,
  "failure": null,
  "degradations": [],
  "parts": [{ "type": "text", "text": "Hi Ava, your order 1042 ships tomorrow." }],
  "reply_to": null,
  "text": "Hi Ava, your order 1042 ships tomorrow.",
  "edited_at": "2026-10-14T17:04:59.000Z",
  "edit_count": 1
}
```

A contact's edited or unsent message carries `contact_id` in a direct chat or `sender` in a group, as on `message.received`. See [Edit and unsend](/messaging/edit-and-unsend.md).

> [!NOTE]
>
> When a message is unsent, delete its text on your side too. Earlier events, such as `message.received`, keep the text they carried, including if they are delivered again.

## Reactions

`reaction.added` and `reaction.removed` report the standard reactions: `love`, `like`, `dislike`, `laugh`, `emphasize` and `question`. They can arrive about a minute after the reaction.

```json theme={null}
{
  "message_id": "msg_abc123",
  "chat_id": "chat_abc123",
  "phone_number_id": "phone_abc123",
  "reaction": { "type": "like", "part_index": 0 },
  "reactor": { "type": "phone", "address": "+15555550123" },
  "occurred_at": "2026-10-14T17:06:00.000Z",
  "observed_at": "2026-10-14T17:06:00.100Z"
}
```

`message_id` is the message that was reacted to. `occurred_at` is when the person reacted; `observed_at` is when Blujay saw it. When the reaction comes from one of your own numbers, `reactor` is `{"type": "phone_number", "phone_number_id": "phone_abc123"}`. Each event is one add or one removal; it doesn't describe the full set of reactions on the message. A reaction from the contact counts as a response under the [sending rules](/messaging/sending-rules.md#what-counts-as-a-response).

## Typing

`chat.typing_indicator.started` and `chat.typing_indicator.stopped` report a contact typing in an iMessage chat:

```json theme={null}
{
  "chat_id": "chat_abc123",
  "phone_number_id": "phone_abc123",
  "occurred_at": "2026-10-14T17:07:00.000Z",
  "expires_at": "2026-10-14T17:08:00.000Z"
}
```

In a group chat, the event adds `participants`: everyone typing in the group at that moment. `started` lists at least one person; `stopped` has an empty list. Replace your whole list of typers with each newer event. Group typing is sent only on webhook version `2026-10-03`, and only for your Dedicated numbers (`access_type: "dedicated"`); earlier versions and other numbers never receive it. See [Typing and read receipts](/messaging/typing-and-read-receipts.md).

To show typing correctly:

* ignore events after their `expires_at`, and clear the indicator at `expires_at` even if no `stopped` arrives;
* per chat, ignore an event older than the last one you applied, and let `stopped` win when timestamps are equal.

## Group changes

`participant.added`, `participant.removed` and `chat.group_name_updated` report changes to iMessage group chats your numbers are in.

```json theme={null}
{
  "chat_id": "chat_grp789",
  "phone_number_id": "phone_abc123",
  "occurred_at": "2026-10-14T17:08:00.000Z",
  "participant": {
    "type": "phone",
    "handle": "+15555550125",
    "address": "+15555550125",
    "status": "active",
    "joined_at": "2026-10-14T17:08:00.050Z",
    "left_at": null
  }
}
```

`chat.group_name_updated` carries `display_name` instead of `participant`. For the current state of a group, read `GET /v1/chats/{chat_id}` and `GET /v1/chats/{chat_id}/participants`. There is no event when an add or rename fails; read the group to confirm.

If two of your numbers are in the same group, each number reports the change: you get one event per number, each with its own `phone_number_id` and `chat_id`. To receive one, limit the subscription to one number with `phone_number_ids`.

## Contacts

`contact.tag_added` carries the full contact and the `tag`. `contact.assigned` carries the contact, its new `assigned_team_member` (`null` when unassigned) and `assignment_changed: true`. Contact events always cover the whole workspace.

```json theme={null}
{
  "contact": {
    "contact_id": "contact_abc123",
    "phone_number": "+15555550123",
    "email": "ava@example.com",
    "name": "Ava Chen",
    "tags": ["VIP"],
    "stage": "Active",
    "assigned_team_member": null,
    "opted_out": false,
    "messaging_allowed": true,
    "created_at": "2026-10-14T17:00:00.000Z",
    "updated_at": "2026-10-14T17:09:00.000Z"
  },
  "tag": "VIP"
}
```

## Phone numbers

`phone_number.status_updated` is sent when one of your numbers is replaced by a new one. You get one event for the old number and one for the new number:

```json theme={null}
{ "phone_number_id": "phone_abc123", "status": "unavailable", "send_status": { "allowed": false, "reason_code": "sender_retired" } }
```

```json theme={null}
{ "phone_number_id": "phone_ghi789", "status": "active", "send_status": { "allowed": true } }
```

Switch your sends to the new number's `phone_number_id`. No event is sent when a number is suspended or its health changes; read `GET /v1/phone-numbers` for that.

## Timestamps

| Field | On | Meaning |
| - | - | - |
| `created_at` (envelope) | Every event | When Blujay recorded the event. Use it with the fields below to order events. |
| `sent_at`, `delivered_at`, `read_at`, `failed_at` | Message events | When the message reached each state. `null` until it does. |
| `occurred_at` | Reactions, typing, group changes | When it happened on the device, as reported to Blujay. |
| `observed_at` | Reactions | When Blujay saw the reaction. Can be about a minute after `occurred_at`. |
| `expires_at` | Typing | When to clear the indicator if no `stopped` arrives. 60 seconds after `occurred_at`. |
| `edited_at`, `unsent_at` | Edits and unsends | When the change happened. Keep the newest per `message_id`. |

## Limitations

* No event for opt-outs, contact creation, updates or deletion, chat creation, or a failed group rename or icon change. Those types return `400` [`FEATURE_NOT_ENABLED`](/errors/feature-not-enabled) on subscribe.
* Delivery events don't name the contact. Keep the `chat_id` to `contact_id` mapping yourself or read the chat.
* A `message.received` snapshot never updates. Media details that weren't ready are only available by reading the attachment again.
* Inbound effects, text styles, and the kind of an iMessage app payload aren't reported.
* Group typing needs version `2026-10-03` and a Dedicated number.

## Next steps

> **[Versions](/webhooks/versions.md)**
>
> What each payload version adds.

> **[Track a message](/messaging/lifecycle.md)**
>
> Statuses and how to track a message.

> **[Receive messages](/messaging/receive-messages.md)**
>
> Handle replies, media, groups and reactions.

> **[Delivery and retries](/webhooks/delivery.md)**
>
> What happens when your endpoint is down.


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