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

# Edit and unsend

> Change the text of an iMessage you sent, take one back, and track edits and unsends through webhooks.

iMessage lets the sender change a message's text for a short time after sending it, or take it back completely. You edit with `PATCH /v1/messages/{message_id}` and unsend with `POST /v1/messages/{message_id}/unsend`, and the change shows on the recipient's device. Both endpoints honor `Idempotency-Key`.

## Know which messages you can change

You can edit or unsend a message when all of these are true:

* Your workspace sent it. Messages your contacts sent can't be changed.
* It went out as iMessage. SMS and RCS messages can't be changed.
* It has been sent. A failed message, or one that is still sending, can't be changed.
* It is text only. Messages with media or contact cards can't be changed.
* It hasn't been unsent already.

Apple's time limits apply:

| Action | Limit |
| - | - |
| Edit | Within 15 minutes of sending, up to 5 edits |
| Unsend | Within 2 minutes of sending |

## Edit a message

Send the new `text`. Only text-only messages can be edited, so if you send `part_index`, it must be `0`. A text that contains a link counts as having an attachment, because iMessage adds a link preview to it, so it can't be edited or unsent yet (`409` [`MESSAGE_NOT_EDITABLE`](/errors/message-not-editable)).

```bash curl theme={null}
curl -X PATCH https://api.blujay.io/v1/messages/msg_abc123 \
  -H "Authorization: Bearer blu_your_api_key" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: edit-msg-abc123-001" \
  -d '{ "text": "See you at 4pm instead." }'
```

Blujay returns `200` once the change is confirmed, with the updated message:

```json Response (200) theme={null}
{
  "status": "success",
  "data": {
    "message_id": "msg_abc123",
    "chat_id": "chat_abc123",
    "contact_id": "contact_abc123",
    "phone_number_id": "phone_abc123",
    "preferred_service": null,
    "service": "iMessage",
    "status": "delivered",
    "parts": [{ "type": "text", "text": "See you at 4pm instead." }],
    "degradations": [],
    "reply_to": null,
    "edited_at": "2026-10-14T17:04:10.000Z",
    "edit_count": 1,
    "unsent_at": null,
    "unsent_scope": null,
    "trace_id": "trc_0b2d4f6a8c1e",
    "created_at": "2026-10-14T17:00:00.000Z",
    "updated_at": "2026-10-14T17:04:10.000Z"
  },
  "trace_id": "trc_9e1a3c5b7d2f",
  "timestamp": "2026-10-14T17:04:11.000Z"
}
```

`parts` always holds the current text. `edited_at` is when the last edit happened and `edit_count` is how many edits the message has had.

Blujay returns `200` once the change is confirmed on your phone number. If it can't confirm the change in time, you get `503` [`CHANGE_UNCONFIRMED`](/errors/change-unconfirmed); retry with the same `Idempotency-Key` to check.

One rule decides what happens when `text` matches the message's current text:

* If the message has been edited before (`edit_count` is 1 or more), Blujay treats the request as a repeat of that edit and returns `200` with the current message. Nothing is sent again.
* If the message has never been edited, Blujay returns `400` [`EDIT_UNCHANGED`](/errors/edit-unchanged).

Sending the original text to a message that was later edited is a normal edit back to that text.

## Unsend a message

Unsend takes no body.

```bash curl theme={null}
curl -X POST https://api.blujay.io/v1/messages/msg_abc123/unsend \
  -H "Authorization: Bearer blu_your_api_key" \
  -H "Idempotency-Key: unsend-msg-abc123-001"
```

The `200` response is the message with `unsent_at` and `unsent_scope` set and its text removed from `parts`. Repeating an unsend returns the current message.

```json Response (200) theme={null}
{
  "status": "success",
  "data": {
    "message_id": "msg_abc123",
    "chat_id": "chat_abc123",
    "contact_id": "contact_abc123",
    "phone_number_id": "phone_abc123",
    "preferred_service": null,
    "service": "iMessage",
    "status": "delivered",
    "parts": [],
    "degradations": [],
    "reply_to": null,
    "edited_at": null,
    "edit_count": 0,
    "unsent_at": "2026-10-14T17:01:20.000Z",
    "unsent_scope": "all",
    "created_at": "2026-10-14T17:00:00.000Z",
    "updated_at": "2026-10-14T17:01:20.000Z"
  },
  "trace_id": "trc_4c6e8a0b2d1f",
  "timestamp": "2026-10-14T17:01:21.000Z"
}
```

Unsent text is never returned by the API, not in `parts`, not in edit history and not in later webhooks. Recipients on older Apple software may still see the message.

## Read edit history

Add `include=edit_history` to `GET /v1/messages/{message_id}`, `GET /v1/messages` or `GET /v1/messages/{message_id}/thread` to get the earlier versions of an edited message, oldest first.

```bash curl theme={null}
curl "https://api.blujay.io/v1/messages/msg_abc123?include=edit_history" \
  -H "Authorization: Bearer blu_your_api_key"
```

```json theme={null}
{
  "message_id": "msg_abc123",
  "parts": [{ "type": "text", "text": "See you at 4pm instead." }],
  "edited_at": "2026-10-14T17:04:10.000Z",
  "edit_count": 1,
  "edit_history": [
    { "text": "See you at 3pm.", "replaced_at": "2026-10-14T17:04:10.000Z" }
  ]
}
```

An unsent message always has an empty `edit_history`, because its earlier versions are unsent text too.

## Handle errors

| Error | Status | What to do |
| - | - | - |
| [`MESSAGE_NOT_EDITABLE`](/errors/message-not-editable) | 409 | The message isn't one you can change. See [the rules above](#know-which-messages-you-can-change). |
| [`EDIT_WINDOW_EXPIRED`](/errors/edit-window-expired) | 409 | More than 15 minutes have passed. Send a new message instead. |
| [`FEATURE_NOT_ENABLED`](/errors/feature-not-enabled) | 400 | Editing is switched off for your workspace. Contact support. |
| [`EDIT_LIMIT_REACHED`](/errors/edit-limit-reached) | 409 | The message has been edited 5 times. Send a new message instead. |
| [`UNSEND_WINDOW_EXPIRED`](/errors/unsend-window-expired) | 409 | More than 2 minutes have passed. |
| [`EDIT_UNCHANGED`](/errors/edit-unchanged) | 400 | The new text matches the current text of a message that has never been edited. |
| [`MESSAGE_NOT_SENT_YET`](/errors/message-not-sent-yet) | 409 | The message hasn't finished sending. Retry shortly. |
| [`SENDER_UNAVAILABLE`](/errors/sender-unavailable) | 409 | The Blujay phone number that sent the message is no longer available. |
| [`CHANGE_REJECTED`](/errors/change-rejected) | 502 | The change was refused. It didn't take effect. |
| [`CHANGE_UNCONFIRMED`](/errors/change-unconfirmed) | 503 | Blujay couldn't confirm the change. Retry with the same `Idempotency-Key` to check. |
| [`IDEMPOTENCY_REQUEST_MISMATCH`](/errors/idempotency-request-mismatch) | 409 | You reused an `Idempotency-Key` with different text. |

A request Blujay rejected with a `4xx` isn't remembered under its `Idempotency-Key`, so you can fix the request and send it again with the same key.

## Track edits and unsends with webhooks

Subscribe to `message.edited` and `message.unsent` to hear about changes to messages your workspace sent, whether they were made through the API or elsewhere in Blujay, and about changes your contacts make to their own iMessages. Only messages up to a week old are tracked for contacts' edits and unsends. A contact's message carries `contact_id` in a direct chat or `sender` in a group, as on `message.received`.

```json message.edited theme={null}
{
  "api_version": "v1",
  "webhook_version": "2026-10-03",
  "event_type": "message.edited",
  "event_id": "evt_abc123",
  "created_at": "2026-10-14T17:04:10.200Z",
  "trace_id": "trc_8d0f2b4a6c1e",
  "workspace_id": "workspace_abc123",
  "subscription_id": "whsub_abc123",
  "data": {
    "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:00:01.000Z",
    "delivered_at": "2026-10-14T17:00:02.000Z",
    "read_at": null,
    "failed_at": null,
    "failure": null,
    "degradations": [],
    "parts": [{ "type": "text", "text": "See you at 4pm instead." }],
    "reply_to": null,
    "text": "See you at 4pm instead.",
    "edited_at": "2026-10-14T17:04:10.000Z",
    "edit_count": 1
  }
}
```

* `message.edited` carries the current text, `edited_at` and `edit_count`, never earlier versions. Read those with `include=edit_history`.
* `message.unsent` carries `unsent_at` and `unsent_scope`, never text. With `unsent_scope: "all"` the whole message was taken back. `"text"` only ever applies to a contact's message: they removed the caption and the attachments remain. Messages you send through the API are text only, so your own unsends are always `"all"`. A later full unsend sends a second event with `"all"`.
* Events recorded before an unsend keep their original bodies. If you stored the text from an earlier `message.received` or `message.sent`, delete it when `message.unsent` arrives.
* Deliveries can be repeated or arrive out of order. Deduplicate by `event_id`, keep the newest `edited_at` for each `message_id`, and treat an unsend as final.

> [!WARNING]
>
> **Common mistakes**
>
> * Edit and unsend are synchronous. A `200` means the change is confirmed; a `503` [`CHANGE_UNCONFIRMED`](/errors/change-unconfirmed) means retry with the same key, not that it failed.
> * Editing a message that hasn't been sent yet returns `409` [`MESSAGE_NOT_SENT_YET`](/errors/message-not-sent-yet). Wait for `sent`.
> * Resending the current text of a never-edited message returns `400` [`EDIT_UNCHANGED`](/errors/edit-unchanged); on an edited message it returns `200` with no change.
> * `message.edited` never carries earlier versions. Read them with `include=edit_history`.

## Limitations

* iMessage only. SMS and RCS messages return `409` [`MESSAGE_NOT_EDITABLE`](/errors/message-not-editable).
* Your workspace's messages only; a contact's message can't be edited or unsent (`409` [`MESSAGE_NOT_EDITABLE`](/errors/message-not-editable)).
* Text-only messages. A message with media or a contact card returns `409` [`MESSAGE_NOT_EDITABLE`](/errors/message-not-editable).
* Edit within 15 minutes, up to 5 times (`409` [`EDIT_WINDOW_EXPIRED`](/errors/edit-window-expired), `409` [`EDIT_LIMIT_REACHED`](/errors/edit-limit-reached)). Unsend within 2 minutes (`409` [`UNSEND_WINDOW_EXPIRED`](/errors/unsend-window-expired)).
* Only `part_index` `0` can be edited.
* Contacts' edits and unsends are tracked for messages up to a week old.
* Unsent text is never returned, and recipients on older Apple software may still see it.

## Next steps

> **[Webhook events](/webhooks/events.md)**
>
> See the full list of message events.

> **[Idempotency](/idempotency.md)**
>
> Retry edits and unsends safely after a timeout.

> **[Threaded replies](/messaging/replies.md)**
>
> Reply to a specific message in its iMessage thread.

> **[Track a message](/messaging/lifecycle.md)**
>
> Understand message statuses before you edit or unsend.


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