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

# Receive messages

> How replies, photos, group messages, reactions and edits reach you, and what to do with each.

Everything a contact sends to one of your phone numbers reaches you two ways: as a webhook event the moment it arrives, and on the message and chat endpoints whenever you read them. This page is the inbound half of the API in one place. Payload fields are listed in full on [Events](/webhooks/events.md).

## Set up

1. Create a [webhook subscription](/webhooks/overview.md) with `message.received`. Add `reaction.added`, `reaction.removed`, `message.edited`, `message.unsent` and `chat.typing_indicator.started` if you want those too.
2. [Verify the signature](/webhooks/verify-signatures.md) on every delivery, store the event by `event_id`, and answer `2xx` within 10 seconds.
3. Do your work from your own queue. If you reply from inside the webhook handler, use `ack-${event_id}` as the `Idempotency-Key` so a redelivered event never sends a second reply. See [Examples](/examples.md).

Polling works too: `GET /v1/messages?status=received&updated_after=…` returns inbound messages across every chat, and `GET /v1/chats/{chat_id}/messages` returns one chat. An inbound message has `status: "received"`. Both lists are ordered by `updated_at`, most recently updated first, so a read receipt or an edit moves an older message up; that is what makes `updated_after` a reliable sync cursor. To show a conversation in order, sort the page by `created_at`.

## A text reply

A direct reply arrives as `message.received` with `contact_id` set, the text in `text` and in `parts`, and the `chat_id` to reply into:

```json theme={null}
{
  "message_id": "msg_def456",
  "chat_id": "chat_abc123",
  "phone_number_id": "phone_abc123",
  "contact_id": "contact_abc123",
  "text": "Sounds good, see you then!",
  "status": "received",
  "direction": "inbound",
  "service": "imessage",
  "parts": [{ "type": "text", "text": "Sounds good, see you then!" }],
  "reply_to": null
}
```

Reply with `POST /v1/chats/{chat_id}/messages`. A reply from the contact lifts the [pre-reply rules](/messaging/sending-rules.md) for that number, so your next message can carry links and files.

If the person isn't a contact yet, Blujay creates one when their first message arrives, so `contact_id` is always present on a direct message.

## Photos, videos and files

Media arrives as `media` parts. The file may still be processing when the event is sent, so a part can carry only `type` and `attachment_id` at first. The event is a snapshot and never changes: read `GET /v1/messages/{message_id}` or `GET /v1/attachments/{attachment_id}` again until `filename`, `content_type`, `size_bytes` and `download_url` are there. Until then the attachment endpoints return `409` [`ATTACHMENT_NOT_READY`](/errors/attachment-not-ready). There is no "attachment ready" event, so poll with a short backoff.

```json theme={null}
{
  "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"
    }
  ]
}
```

`download_url` is a path on `https://api.blujay.io`. Call it with your API key. Never send the key to a URL from any other host. See [Media](/messaging/media.md).

Voice memos arrive as audio media, and a contact card someone shares arrives as a `text/vcard` media part.

## Messages in a group

A message in a group chat has a `sender` object instead of `contact_id`:

```json theme={null}
{ "sender": { "type": "phone", "address": "+15555550124", "contact_id": "contact_def456" } }
```

`sender.contact_id` is present only when the sender is a contact in your workspace. `type` is `phone` or `email`. A reply from anyone in the group counts as a reply for the group's [sending rules](/messaging/sending-rules.md).

To follow who is in the group, subscribe to `participant.added`, `participant.removed` and `chat.group_name_updated`, and read `GET /v1/chats/{chat_id}/participants` for the current state. See [Group chats](/messaging/group-chats.md).

## Reactions

A tapback is not a message. It arrives only as `reaction.added` or `reaction.removed` with the `message_id` it applies to, the reaction type (`love`, `like`, `dislike`, `laugh`, `emphasize`, `question`) and `part_index`. It never arrives as `message.received`, and `GET /v1/messages` doesn't list it. Reactions can arrive about a minute after the person tapped.

A reaction from the contact counts as a response for the [6-text rule](/messaging/sending-rules.md#texts-in-a-row-without-a-response).

## Replies to a specific message

When a contact replies to one of your messages in a thread, `reply_to` on the inbound message names the message and part they replied to. Read the whole thread with `GET /v1/messages/{message_id}/thread`. See [Threaded replies](/messaging/replies.md).

## Edits and unsends

When a contact edits or unsends an iMessage, you get `message.edited` with the new text or `message.unsent` with no text. Only messages up to a week old are tracked. When a message is unsent, delete its text on your side too: earlier events you stored still carry it. See [Edit and unsend](/messaging/edit-and-unsend.md).

## Typing

`chat.typing_indicator.started` and `stopped` tell you a contact is typing in an iMessage chat, with an `expires_at` to clear the indicator by. See [Typing and read receipts](/messaging/typing-and-read-receipts.md).

## What you don't get

* **Content the API has no part type for.** A message Blujay stores with no text and no file, which is how iMessage app content such as polls, Apple Pay and app cards usually arrives, has an empty `parts` list and no `text`. Stickers sent as images, voice memos and shared contact cards arrive as media.
* **Effects and text styles on inbound messages** aren't reported.
* **Delivery and read receipts for your messages** are their own events (`message.delivered`, `message.read`), not inbound messages.
* **Calls** aren't part of the messaging API.

## Common mistakes

> [!WARNING]
>
> * Don't overwrite newer state with an older event. Events can arrive out of order and more than once; dedupe by `event_id` and compare timestamps.
> * Don't reply to every message forever. The reply bot in [Examples](/examples.md) acknowledges once per chat, then stays quiet.
> * Don't treat a reaction as text. It has no `text` and no `parts`.
> * Don't follow a full URL from a payload with your API key. Build the download URL from `download_url` on `https://api.blujay.io`.

## Next steps

> **[Events](/webhooks/events.md)**
>
> Every event type with its full payload.

> **[Examples](/examples.md)**
>
> A complete reply bot you can run.

> **[Send messages](/messaging/send-messages.md)**
>
> Reply into the chat.

> **[Sending rules](/messaging/sending-rules.md)**
>
> What a reply unlocks.


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