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

# Threaded replies

> Reply to a specific iMessage in a thread, read reply details back, and page through a whole thread.

A threaded reply answers one specific message, and the recipient sees it in that message's iMessage thread. You send one by adding `reply_to` to a follow-up message. Threaded replies are iMessage only: Blujay never sends a reply without its thread, so a reply that can't go out as iMessage is refused or fails instead of being sent as a plain message.

## Send a reply

Add `reply_to` to `POST /v1/chats/{chat_id}/messages`. Set `message_id` to the message you're answering.

```bash curl theme={null}
curl https://api.blujay.io/v1/chats/chat_abc123/messages \
  -H "Authorization: Bearer blu_your_api_key" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: reply-msg_def456-1" \
  -d '{
    "message": {
      "parts": [{ "type": "text", "text": "Yes, that time works." }],
      "reply_to": { "message_id": "msg_def456", "part_index": 0 }
    }
  }'
```

```json Response (202) theme={null}
{
  "status": "success",
  "data": {
    "chat_id": "chat_abc123",
    "message": {
      "id": "msg_ghi789",
      "direction": "outbound",
      "delivery_status": "queued",
      "preferred_service": null,
      "service": null,
      "from": "+15555550100",
      "to": ["+15555550123"],
      "created_at": "2026-10-14T17:05:00.000Z",
      "parts": [{ "type": "text", "text": "Yes, that time works." }],
      "reply_to": {
        "message_id": "msg_def456",
        "part_index": 0,
        "thread_root_message_id": "msg_def456"
      }
    }
  },
  "trace_id": "trc_3c5e7a9b1d2f",
  "timestamp": "2026-10-14T17:05:00.150Z"
}
```

`part_index` is optional and defaults to `0`. It names which piece of the target message your reply hangs under, using Apple's numbering for that message. Blujay doesn't check it against the target's `parts`; it passes the number to iMessage as you sent it, and it reports Apple's number back on replies it receives (the same numbering as `part_index` in the `reaction.added` webhook). For a text-only message, `0` is the only part. For a message with media and a caption, Apple's numbering may not match the order of `parts` in Blujay, so if a reply lands under the wrong piece, use the other index. Blujay doesn't publish Apple's numbering rule.

`reply_to` works only on follow-ups. `POST /v1/chats` rejects it with `400` [`VALIDATION_ERROR`](/errors/validation-error), because a chat's first message can't be a reply.

## Choose a valid target

The target must be a sent iMessage in the same chat, on the same Blujay phone number. Blujay checks the target before it accepts the message, so nothing is sent when one of these errors comes back:

| Error | Status | Cause |
| - | - | - |
| [`REPLY_TARGET_NOT_FOUND`](/errors/reply-target-not-found) | 404 | The message doesn't exist, or it belongs to another workspace or phone number. Blujay looks the target up only among the chat's own phone number's messages, so a message on another number looks the same as a missing one. Check `phone_number_id` on the target. |
| [`REPLY_TARGET_NOT_IN_CHAT`](/errors/reply-target-not-in-chat) | 400 | The message is in a different chat. |
| [`REPLY_TARGET_NOT_REPLYABLE`](/errors/reply-target-not-replyable) | 400 | The message is a reaction, was unsent, or was sent or received as SMS or RCS. |
| [`REPLY_TARGET_NOT_SENT`](/errors/reply-target-not-sent) | 409 | The message hasn't been sent yet. Retry once it has. |

## Know when a reply can go out

* **Direct chats.** Leave `preferred_service` out or set it to `iMessage`. Setting it to `SMS` or `RCS` returns [`UNSUPPORTED_CHAT_OPERATION`](/errors/unsupported-chat-operation).
* **Group chats.** The chat must be an established iMessage group. Other groups return [`UNSUPPORTED_CHAT_OPERATION`](/errors/unsupported-chat-operation). See [Group chats](/messaging/group-chats.md#know-which-features-need-an-established-imessage-group).

When Blujay chooses the service (Auto) and a direct reply can only go out as SMS or RCS, the message isn't sent. It moves to `failed` with `failure_code: "REPLY_REQUIRES_IMESSAGE"`. On webhook version `2026-09-02` and later, the `message.failed` event carries the same `failure_code`. To reach the person anyway, send the text again without `reply_to`.

If you retry a reply with the same `Idempotency-Key`, keep the body identical. Reusing the key with a different `reply_to` returns `409` [`IDEMPOTENCY_REQUEST_MISMATCH`](/errors/idempotency-request-mismatch). A reply Blujay rejected with a `4xx` isn't remembered under its key, so you can fix it and reuse the key. See [Idempotency](/idempotency.md).

## Read reply details

Every message you read, from `GET /v1/messages/{message_id}`, `GET /v1/messages` or `GET /v1/chats/{chat_id}/messages`, has a `reply_to` field. It is `null` for a message that isn't a reply. For a reply, it holds three public IDs:

| Field | Meaning |
| - | - |
| `message_id` | The message being replied to. |
| `part_index` | Which part of that message the reply targets (0-based). |
| `thread_root_message_id` | The message that started the thread. Use it to group a thread without an extra call. |

For a reply you sent through Blujay, `message_id` is the message you named. For a reply that arrived over iMessage, Apple reports only the start of the thread, so `message_id` equals `thread_root_message_id`. Either ID is `null` when Blujay doesn't have that message, for example when it predates your history in Blujay.

## Read a whole thread

`GET /v1/messages/{message_id}/thread` returns the message that started a thread and every reply in it, including replies that are still sending. Pass any message in the thread.

```bash curl theme={null}
curl "https://api.blujay.io/v1/messages/msg_ghi789/thread?order=asc&limit=25" \
  -H "Authorization: Bearer blu_your_api_key"
```

```json Response (200) theme={null}
{
  "status": "success",
  "data": {
    "data": [
      {
        "message_id": "msg_def456",
        "chat_id": "chat_abc123",
        "contact_id": "contact_abc123",
        "phone_number_id": "phone_abc123",
        "preferred_service": null,
        "service": "iMessage",
        "status": "received",
        "parts": [{ "type": "text", "text": "Does 3pm on Friday work?" }],
        "degradations": [],
        "reply_to": null,
        "edited_at": null,
        "edit_count": 0,
        "unsent_at": null,
        "unsent_scope": null,
        "created_at": "2026-10-14T17:04:12.000Z",
        "updated_at": "2026-10-14T17:04:12.000Z"
      },
      {
        "message_id": "msg_ghi789",
        "chat_id": "chat_abc123",
        "contact_id": "contact_abc123",
        "phone_number_id": "phone_abc123",
        "preferred_service": null,
        "service": "iMessage",
        "status": "delivered",
        "parts": [{ "type": "text", "text": "Yes, that time works." }],
        "degradations": [],
        "reply_to": {
          "message_id": "msg_def456",
          "part_index": 0,
          "thread_root_message_id": "msg_def456"
        },
        "edited_at": null,
        "edit_count": 0,
        "unsent_at": null,
        "unsent_scope": null,
        "trace_id": "trc_3c5e7a9b1d2f",
        "created_at": "2026-10-14T17:05:00.000Z",
        "updated_at": "2026-10-14T17:05:02.000Z"
      }
    ],
    "has_more": false,
    "limit": 25
  },
  "trace_id": "trc_6e8a0c2d4f1b",
  "timestamp": "2026-10-14T17:06:00.000Z"
}
```

* `order` is `asc` (oldest first, the default) or `desc`.
* `limit` defaults to 25 and goes up to 100. When `has_more` is `true`, pass `next_cursor` as `cursor` with the same `order`.
* One thread read covers at most 1,000 messages of the thread, counted from its start with `order=asc` or from its end with `order=desc`. Real iMessage threads are far smaller, so you won't normally hit this.
* Reactions are never part of a thread. A message that isn't in a thread, or a reaction, returns only itself.
* Add `include=edit_history` to get earlier versions of edited messages. See [Edit and unsend](/messaging/edit-and-unsend.md).

## Get reply details in webhooks

Subscriptions on webhook version `2026-10-03`, the default for new subscriptions, include `reply_to` on every message event, in the same shape as message reads. Earlier versions never include it. See [Webhook versions](/webhooks/versions.md).

A `message.received` body is a snapshot taken when the message arrived. If iMessage reports the thread details a moment later, the stored event doesn't change, so read the message with `GET /v1/messages/{message_id}` when you need the latest `reply_to`.

> [!WARNING]
>
> **Common mistakes**
>
> * A reply is a follow-up. `reply_to` on `POST /v1/chats` returns `400` [`VALIDATION_ERROR`](/errors/validation-error).
> * `404` [`REPLY_TARGET_NOT_FOUND`](/errors/reply-target-not-found) for an ID you know exists usually means the target was sent or received on a different phone number than the chat.
> * The pre-reply rules still apply: a reply with a link or media before the contact has replied returns `400` [`PRE_REPLY_CONTENT_BLOCKED`](/errors/pre-reply-content-blocked). See [Sending rules](/messaging/sending-rules.md).
> * A reply that can't go out over iMessage fails. It's never sent as a plain SMS.

## Limitations

* iMessage only. `preferred_service: "SMS"` or `"RCS"` returns `400` [`UNSUPPORTED_CHAT_OPERATION`](/errors/unsupported-chat-operation); an Auto reply that can only go SMS or RCS fails with `REPLY_REQUIRES_IMESSAGE`.
* The target must be an iMessage in the same chat on the same phone number, sent, not a reaction and not unsent (`404` [`REPLY_TARGET_NOT_FOUND`](/errors/reply-target-not-found), `400` [`REPLY_TARGET_NOT_IN_CHAT`](/errors/reply-target-not-in-chat), `400` [`REPLY_TARGET_NOT_REPLYABLE`](/errors/reply-target-not-replyable), `409` [`REPLY_TARGET_NOT_SENT`](/errors/reply-target-not-sent)).
* Groups need an established iMessage group (`400` [`UNSUPPORTED_CHAT_OPERATION`](/errors/unsupported-chat-operation)). See [Group chats](/messaging/group-chats.md).
* Not on a first message (`400` [`VALIDATION_ERROR`](/errors/validation-error)), and subject to the pre-reply rules.
* `part_index` isn't validated. A wrong index threads under the wrong piece of the message.
* For a reply a contact sends, `message_id` is the thread root, not the exact message they replied to.
* One thread read returns at most 1,000 messages.

## Next steps

> **[Send messages](/messaging/send-messages.md)**
>
> Send follow-ups and track their progress.

> **[Track a message](/messaging/lifecycle.md)**
>
> Understand `failed` and `failure_code`, including `REPLY_REQUIRES_IMESSAGE`.

> **[Group chats](/messaging/group-chats.md)**
>
> See which group features need an established iMessage group.

> **[Webhook versions](/webhooks/versions.md)**
>
> Move to `2026-10-03` to get `reply_to` in message events.


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