> ## Documentation Index
> Fetch the complete documentation index at: https://docs.ibubbl.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Unsend a message

> Take back a message you sent over iMessage, within 110 seconds of `sent_at` (iMessage allows two minutes).

The Undo Send is accepted with `unsend_status: "pending"`, and `body` is empty at once. When it reaches the recipient, `message.unsent` fires with `direction: "outbound"`. If it doesn't, `message.unsend_failed` fires and `body` comes back.

On iOS 16, iPadOS 16.1, macOS 13 Ventura, visionOS 1 and later the message disappears and both sides see a note that it was unsent. On earlier versions the original stays in the conversation: never rely on Undo Send to retract sensitive information.

A message that went out with a link preview is two bubbles; both are unsent. A message with files (text and files, or several files) can't be unsent through the API (`422 multi_part`).

Repeating the request while the Undo Send is pending or done answers `200` and changes nothing; after `message.unsend_failed` it tries again, if it's still within the window.

Counts toward the conversation-actions rate limit (300 a minute per account).

### Unsend a message

**Method:** `POST` | **Endpoint:** `/v1/messages/{message_id}/unsend`

**Description:** Take back a message you sent over iMessage, the way Undo Send works on an iPhone: within 110 seconds of sending. See [Edit, Undo Send and Cancel](https://docs.ibubbl.com/guides/editing-and-unsending.md).

**Authentication:** API key in the `Authorization` header: `Authorization: Bearer ib_pk_<64 hex>`. Create keys in the dashboard under **Settings → API access**.

**Rate limit:** 300 conversation actions per 60 seconds per account (edits, unsends, cancels, tapbacks, mark read and typing together, across all your API keys). Over that, you get `429 rate_limited` with a `Retry-After` header; see [Rate limits](https://docs.ibubbl.com/concepts/limits.md#rate-limits).

#### Request Body

None. The path parameter `message_id` is the `id` of a message you **sent**: from [Send a message](https://docs.ibubbl.com/api-reference/messages/send.md), or from [List messages](https://docs.ibubbl.com/api-reference/messages/list.md), where messages sent from the dashboard appear too.

> [!WARNING]
>
> **Never rely on Undo Send to retract sensitive information.** A recipient on an older iPhone, iPad or Mac keeps the original message, and anyone may have read it, copied it or seen it in a notification in the seconds before. See [What the recipient sees](https://docs.ibubbl.com/guides/editing-and-unsending.md#what-the-recipient-sees).

#### How it behaves

* **It's accepted first, applied second.** The answer is `202` with `unsend_status: "pending"`, and `body` is already empty. A few seconds later the message is gone from the recipient's conversation: [`message.unsent`](https://docs.ibubbl.com/webhooks/events#message-unsent) fires with `direction: "outbound"` and `scope: "message"`, and `unsend_status` becomes `unsent`.
* **If it doesn't land, the text comes back.** [`message.unsend_failed`](https://docs.ibubbl.com/webhooks/events#message-unsend-failed) fires, `unsend_status` becomes `failed`, and `body` returns: the recipient still has the message. Calling again tries once more, if the 110 seconds aren't up.
* **It's safe to call twice.** While the Undo Send is pending, or once it's done, calling again answers `200` with the message and changes nothing.
* **110 seconds.** Measured from `sent_at`. iMessage allows 2 minutes; iBubbl stops 10 seconds early so the Undo Send never arrives too late. After that, `422 unsend_window_expired`.
* **Not while an edit is on its way.** `409 edit_in_flight`: wait for `message.edited` (a few seconds), then unsend.
* **iMessage only.** A message that went out by SMS (`channel: "sms"`, RCS included) can't be unsent: `422 sms_not_supported`. One that hasn't gone out yet gets `422 not_sent_yet`: [cancel it](https://docs.ibubbl.com/api-reference/messages/cancel.md) instead.
* **One bubble at a time.** A text message, or a message that is one file and nothing else, is unsent whole. A message with files and text, or several files, shows as several bubbles and gets `422 multi_part`: unsending part of a message is available in the dashboard only, for now.
* **Messages with a link preview.** A message typed in the dashboard with a link goes out as two bubbles, the text and a preview card. Unsending it takes back both. Messages sent through the API never have a preview card, so they are one bubble.
* **Opted-out contacts don't block it.** An Undo Send only removes content, so it works whatever the contact's opt-out or block status. Groups work too.

```bash cURL theme={null}
curl -X POST https://api.ibubbl.com/v1/messages/8b3a4e72-1c2d-4e5f-9a0b-2c3d4e5f6a7b/unsend \
  -H "Authorization: Bearer $IBUBBL_API_KEY"
```

```typescript TypeScript theme={null}
const messageId = "8b3a4e72-1c2d-4e5f-9a0b-2c3d4e5f6a7b";

const res = await fetch(
  `https://api.ibubbl.com/v1/messages/${encodeURIComponent(messageId)}/unsend`,
  {
    method: "POST",
    headers: { Authorization: `Bearer ${process.env.IBUBBL_API_KEY}` },
    signal: AbortSignal.timeout(10_000),
  },
);

const data = await res.json();
if (!res.ok) throw new Error(`${res.status} ${data.detail?.error}`);
// 202: the Undo Send is on its way; 200: already unsent or being unsent.
console.log(res.status, data.unsend_status);
```

```python Python theme={null}
import os
import urllib.parse

import requests

API_KEY = os.environ["IBUBBL_API_KEY"]

message_id = "8b3a4e72-1c2d-4e5f-9a0b-2c3d4e5f6a7b"

res = requests.post(
    f"https://api.ibubbl.com/v1/messages/{urllib.parse.quote(message_id, safe='')}/unsend",
    headers={"Authorization": f"Bearer {API_KEY}"},
    timeout=10,
)
data = res.json()
if not res.ok:
    raise RuntimeError(f"{res.status_code} {data['detail']['error']}")
# 202: the Undo Send is on its way; 200: already unsent or being unsent.
print(res.status_code, data["unsend_status"])
```

```json 202 Accepted theme={null}
{
  "id": "8b3a4e72-1c2d-4e5f-9a0b-2c3d4e5f6a7b",
  "direction": "outbound",
  "created_at": "2026-11-05T15:04:10.112730Z",
  "status": "read",
  "to": "+14155551234",
  "body": "",
  "contact_id": "c8a1f4e2-3b9d-4f7c-a8e5-1d2c3b4a5e6f",
  "conversation_id": "3a4b5c6d-7e8f-9a0b-1c2d-3e4f5a6b7c8d",
  "conversation_kind": "one_to_one",
  "chat_guid": null,
  "channel": "imessage",
  "sent_at": "2026-11-05T15:04:12.381Z",
  "held_until": null,
  "held_reason": null,
  "effect": null,
  "reply_to_message_id": null,
  "attachments": [],
  "edited_at": null,
  "edit_count": 0,
  "edit_status": null,
  "unsend_status": "pending",
  "unsent_at": null
}
```

```json 422 Unprocessable Entity theme={null}
{
  "detail": {
    "error": "unsend_window_expired",
    "message": "This message can no longer be unsent."
  }
}
```

#### Responses

**202 Accepted**

The Undo Send is on its way. The body is the [message](https://docs.ibubbl.com/api-reference/messages/retrieve.md) with `unsend_status: "pending"`.

| Field | Description |
| - | - |
| `body` | `""` at once (for a dashboard message with a link preview, the link alone until the card is gone too). The text comes back if the Undo Send fails. |
| `unsend_status` | `pending` now; then `unsent` when it reaches the recipient, or `failed` if it doesn't. |
| `unsent_at` | When it was unsent (UTC); `null` until then. |

**200 OK**

The message is already unsent, or being unsent. The body is the message as it is; nothing new was sent.

**401 Unauthorized**

```json theme={null}
{
  "detail": {
    "error": "unauthorized",
    "message": "Invalid or revoked API key"
  }
}
```

Other `message` values: `Authorization Bearer token required`, `API key required (use Authorization: Bearer ib_pk_…)`, `Invalid API key format`, `Customer account not found`.

**403 Forbidden**

```json theme={null}
{
  "detail": {
    "error": "forbidden",
    "message": "Account is suspended"
  }
}
```

Your iBubbl account is suspended or cancelled (`message` is then `Account is cancelled`). Every API-key endpoint returns this.

**404 Not Found**

```json theme={null}
{
  "detail": {
    "error": "not_found",
    "message": "Message not found"
  }
}
```

Also returned for a malformed ID and a message that belongs to another account.

**409 Conflict**

```json theme={null}
{
  "detail": {
    "error": "edit_in_flight",
    "message": "An edit is being dispatched; wait for it to complete before unsending."
  }
}
```

An edit of this message hasn't reached the recipient yet. Wait for `message.edited` (usually a few seconds), then unsend. Nothing changed.

**422 Unprocessable Entity**

```json theme={null}
{
  "detail": {
    "error": "multi_part",
    "message": "This message has more than one part (text and files, or several files), so it can't be unsent through the API."
  }
}
```

Nothing changed. The `error` is one of:

* `invalid_target`: the message is one you received. Only messages you sent can be unsent.
* `sms_not_supported`: the message went out by SMS (RCS included), where messages can't be unsent.
* `not_sent_yet`: the message is still `pending` or `queued`. [Cancel it](https://docs.ibubbl.com/api-reference/messages/cancel.md) instead.
* `not_unsendable`: the message failed, was cancelled or expired, or iMessage never confirmed it.
* `unsend_window_expired`: more than 110 seconds since `sent_at`.
* `multi_part`: the message has files and text, or several files. Unsending part of a message is available in the dashboard only.

**429 Too Many Requests**

```json theme={null}
{
  "detail": {
    "error": "rate_limited",
    "message": "Too many conversation actions; wait 12s (limit 300 a minute per account)."
  }
}
```

Wait the `Retry-After` header's seconds, then send the same request again.


## OpenAPI

````yaml POST /v1/messages/{message_id}/unsend
openapi: 3.1.0
info:
  title: iBubbl API
  version: v1
  description: >-
    The iBubbl REST API. Send iMessages, manage contacts, and subscribe to
    webhook events from your backend.


    Authenticate every request with a Bearer API key (`ib_pk_<hex>`) — generate
    one from your dashboard's API Access settings. See the
    [Authentication](/authentication) guide.
servers:
  - url: https://api.ibubbl.com
security:
  - BearerAuth: []
tags:
  - name: Messages
  - name: Contacts
  - name: Conversations
  - name: Webhooks
  - name: Inbound Events
paths:
  /v1/messages/{message_id}/unsend:
    post:
      tags:
        - Messages
      summary: Unsend a message
      description: >-
        Take back a message you sent over iMessage, within 110 seconds of
        `sent_at` (iMessage allows two minutes).


        The Undo Send is accepted with `unsend_status: "pending"`, and `body` is
        empty at once. When it reaches the recipient, `message.unsent` fires
        with `direction: "outbound"`. If it doesn't, `message.unsend_failed`
        fires and `body` comes back.


        On iOS 16, iPadOS 16.1, macOS 13 Ventura, visionOS 1 and later the
        message disappears and both sides see a note that it was unsent. On
        earlier versions the original stays in the conversation: never rely on
        Undo Send to retract sensitive information.


        A message that went out with a link preview is two bubbles; both are
        unsent. A message with files (text and files, or several files) can't be
        unsent through the API (`422 multi_part`).


        Repeating the request while the Undo Send is pending or done answers
        `200` and changes nothing; after `message.unsend_failed` it tries again,
        if it's still within the window.


        Counts toward the conversation-actions rate limit (300 a minute per
        account).
      operationId: v1_unsend_message_v1_messages__message_id__unsend_post
      parameters:
        - name: message_id
          in: path
          required: true
          schema:
            type: string
            title: Message Id
      responses:
        '200':
          description: Already unsent or being unsent; nothing changed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Message'
        '202':
          description: Undo Send accepted.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Message'
        '401':
          description: Missing or invalid API key.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'
        '403':
          description: '`forbidden`: your iBubbl account is suspended or cancelled.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'
        '404':
          description: Message not found.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'
        '409':
          description: >-
            `edit_in_flight`: an edit of this message hasn't reached the
            recipient yet. Retry in a few seconds.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'
        '422':
          description: >-
            `invalid_target` (a message you received), `sms_not_supported` (it
            went out by SMS), `not_sent_yet` (still waiting to go out: cancel it
            instead), `not_unsendable` (it failed or never went out),
            `unsend_window_expired`, `multi_part`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'
        '429':
          description: >-
            `rate_limited`: more than 300 conversation actions in 60 seconds
            from your account (all keys, and tapbacks, read, typing, edit,
            unsend and cancel requests, count together; list requests have their
            own budget). Wait `Retry-After` seconds, then send the same request
            again.
          headers:
            Retry-After:
              description: Seconds to wait before retrying.
              schema:
                type: integer
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'
components:
  schemas:
    Message:
      properties:
        id:
          type: string
          title: Id
          description: Unique message identifier (UUID).
        direction:
          type: string
          enum:
            - inbound
            - outbound
          title: Direction
          description: >-
            `outbound` = sent from your line (through the API, the dashboard or
            an automation); `inbound` = received from a contact. An inbound
            message has `status` `delivered`, or `read` once it has been read in
            the dashboard.
        created_at:
          type: string
          title: Created At
          description: >-
            ISO 8601 UTC timestamp, with microseconds and a `Z`, of when iBubbl
            recorded the message. It never changes, so it is what `GET
            /v1/messages` sorts and filters by; pass it back as `created_after`
            / `created_before` unchanged.
          examples:
            - '2026-05-17T14:23:45.123456Z'
        status:
          type: string
          enum:
            - queued
            - pending
            - sent
            - delivered
            - read
            - failed
            - expired
            - cancelled
          title: Status
          description: >-
            Lifecycle state. `pending` = accepted and on its way out; it may be
            waiting for the contact's working hours or the line's hourly limit
            for new contacts (see `held_until`). `queued` = parked by a capacity
            hold (`hourly_cap_hit` / `line_offline`), retried automatically.
            `sent` / `delivered` / `read` = delivery progression. `failed` = the
            send failed — see webhook `message.failed` for the reason; it can
            follow `sent` (or a `read` with no confirmed delivery) when iMessage
            reports the send failed after accepting it (`failed_reason`
            `imessage_undeliverable` / `imessage_error:<code>`), and such a
            message can still move to `delivered` / `read` if Apple later
            confirms it. `expired` = still queued after an hour, so dropped; it
            will not send. `cancelled` = stopped in the dashboard before it was
            sent; it will not send (webhook `message.cancelled`).
        to:
          anyOf:
            - type: string
            - type: 'null'
          title: To
          description: >-
            The contact's phone number (E.164) for 1:1 messages: the destination
            of an outbound message, and the sender of an inbound one. Null for
            group messages — the group is identified by `chat_guid` instead.
          examples:
            - '+14155551234'
        body:
          type: string
          title: Body
          description: The message text as sent.
        contact_id:
          anyOf:
            - type: string
            - type: 'null'
          title: Contact Id
          description: >-
            ID of the contact this message was sent to. For groups, this is the
            'primary' participant the message was addressed to (the first
            participant in `participant_phones` on a new group create); the
            conversation's full participant list is available via the
            conversation.
        conversation_id:
          anyOf:
            - type: string
            - type: 'null'
          title: Conversation Id
          description: >-
            ID of the conversation this message belongs to. Stable for the
            lifetime of the conversation; use it to fetch the thread or
            correlate webhook events.
        conversation_kind:
          type: string
          enum:
            - one_to_one
            - group
          title: Conversation Kind
          description: >-
            `one_to_one` for sends to a single contact, `group` for iMessage
            groups (2+ participants in addition to your iBubbl line).
          default: one_to_one
        chat_guid:
          anyOf:
            - type: string
            - type: 'null'
          title: Chat Guid
          description: >-
            Opaque identifier for the group chat (e.g. `any;+;<hex>`). Returned
            for `group` messages — use it to address later messages to the same
            group via the `chat_guid` request shape. Null for `one_to_one`
            messages, and for a brand-new group until the group has been created
            in iMessage (that happens when the first message goes out, which can
            wait for sending hours); poll `GET /v1/messages/{id}` to see it fill
            in. It changes when a participant is added to the group.
          examples:
            - any;+;067ace6caf6c43aaa2f7d643975e15ea
        channel:
          anyOf:
            - type: string
              enum:
                - imessage
                - sms
            - type: 'null'
          title: Channel
          description: >-
            How the message travelled: `imessage`, or `sms` (a text, including
            RCS). For an outbound message, how it actually went out (`sms` when
            the recipient couldn't be reached on iMessage); null until it has
            been sent. For an inbound message, how it arrived; null when that
            wasn't recorded.
        sent_at:
          anyOf:
            - type: string
            - type: 'null'
          title: Sent At
          description: >-
            ISO 8601 UTC timestamp. Set when the message is accepted, then
            replaced by the actual send time once it goes out. Use `status`
            (`sent` / `delivered` / `read`), not `sent_at`, to tell whether it
            has actually gone out.
          examples:
            - '2026-05-17T14:23:45.123Z'
        held_until:
          anyOf:
            - type: string
            - type: 'null'
          title: Held Until
          description: >-
            While a `pending` message is being held, the expected time (ISO 8601
            UTC) it will go out: the next opening of the contact's working
            hours; for `hourly_limit`, an estimate of when a slot frees.
            Recomputed within about 10 seconds. Null when the message is not
            held, when `held_reason` is `working_hours` but no sending day is
            enabled, and when it is `hourly_limit` with no estimate.
          examples:
            - '2026-10-08T16:00:00+00:00'
        held_reason:
          anyOf:
            - type: string
              enum:
                - working_hours
                - hourly_limit
            - type: 'null'
          title: Held Reason
          description: >-
            Why a `pending` message is being held right now: `working_hours`
            (outside the contact's working hours) or `hourly_limit` (the line
            has started its limit of new contacts for this hour; the message
            goes out by itself when a slot frees, at about `held_until`). Null
            when it is not held.
        effect:
          anyOf:
            - type: string
              enum:
                - slam
                - loud
                - gentle
                - invisible
                - echo
                - spotlight
                - balloons
                - confetti
                - love
                - lasers
                - fireworks
                - celebration
                - shooting_star
            - type: 'null'
          title: Effect
          description: >-
            The iMessage send effect requested for this message, or null. It
            shows on the recipient's device only when `channel` is `imessage`.
        reply_to_message_id:
          anyOf:
            - type: string
            - type: 'null'
          title: Reply To Message Id
          description: >-
            The ID of the message this one quotes as an inline reply, or null.
            The quote shows on the recipient's device only when `channel` is
            `imessage`.
        attachments:
          items:
            $ref: '#/components/schemas/MessageAttachment'
          type: array
          title: Attachments
          description: >-
            The files on this message, in the order they were sent: sent with
            `attachments`, from the dashboard or an automation, or received from
            the contact. Empty when there are none. File contents and URLs are
            not returned.
        edited_at:
          anyOf:
            - type: string
            - type: 'null'
          title: Edited At
          description: >-
            ISO 8601 UTC timestamp of the latest edit that reached the
            recipient: one of yours (`POST /v1/messages/{id}/edit`, or the
            dashboard), or, on an inbound message, the contact's own edit. Null
            when it was never edited.
          examples:
            - '2026-05-17T14:25:00.000000Z'
        edit_count:
          type: integer
          maximum: 5
          minimum: 0
          title: Edit Count
          description: >-
            How many of your edits reached the recipient. iMessage allows five
            per message. Always `0` on an inbound message.
          default: 0
        edit_status:
          anyOf:
            - type: string
              enum:
                - pending
                - edited
                - failed
            - type: 'null'
          title: Edit Status
          description: >-
            `pending` = your latest edit is on its way (`body` already shows the
            new text); `failed` = it didn't reach the recipient, and `body` is
            back to the text they have (webhook `message.edit_failed`); `edited`
            = the message was edited. Null when it never was.
        unsend_status:
          anyOf:
            - type: string
              enum:
                - pending
                - unsent
                - partly_unsent
                - failed
            - type: 'null'
          title: Unsend Status
          description: >-
            `pending` = your Undo Send is on its way; `unsent` = the whole
            message was unsent (by you, or by the contact on an inbound message)
            and `body` is empty; `partly_unsent` = part of it was (its text, a
            link preview or a file); `failed` = your Undo Send didn't happen and
            the recipient still has the message (webhook
            `message.unsend_failed`). Null when nothing was unsent.
        unsent_at:
          anyOf:
            - type: string
            - type: 'null'
          title: Unsent At
          description: >-
            ISO 8601 UTC timestamp of when the message (or the latest part of
            it) was unsent. Null unless `unsend_status` is `unsent` or
            `partly_unsent`.
          examples:
            - '2026-05-17T14:24:10.000000Z'
      type: object
      required:
        - id
        - direction
        - created_at
        - status
        - body
        - attachments
      title: Message
      example:
        attachments: []
        body: Hey Sam — quick check-in about your order.
        contact_id: c8a1f4e2-3b9d-4f7c-a8e5-1d2c3b4a5e6f
        conversation_id: 3a4b5c6d-7e8f-9a0b-1c2d-3e4f5a6b7c8d
        conversation_kind: one_to_one
        created_at: '2026-10-08T11:40:44.770512Z'
        direction: outbound
        held_reason: working_hours
        held_until: '2026-10-08T16:00:00+00:00'
        id: 8b3a4e72-1c2d-4e5f-9a0b-2c3d4e5f6a7b
        sent_at: '2026-10-08T11:40:44.770Z'
        status: pending
        to: '+14155551234'
    ApiError:
      properties:
        detail:
          allOf:
            - $ref: '#/components/schemas/ApiErrorDetail'
          description: >-
            Error envelope. Every `/v1` error response wraps a stable `error`
            code and a human-readable `message` in `detail`.
      type: object
      required:
        - detail
      title: ApiError
      example:
        detail:
          error: validation_error
          message: >-
            Exactly one of contact_id, to, chat_guid, or participant_phones is
            required
    MessageAttachment:
      properties:
        kind:
          type: string
          enum:
            - image
            - gif
            - video
            - audio
            - file
          title: Kind
          description: '`file` is a PDF (or another document).'
        mime_type:
          type: string
          title: Mime Type
          examples:
            - image/jpeg
        size_bytes:
          type: integer
          title: Size Bytes
          description: >-
            Size in bytes. An inbound file can show `0` for a few seconds, until
            iBubbl has received it.
      type: object
      required:
        - kind
        - mime_type
        - size_bytes
      title: MessageAttachment
    ApiErrorDetail:
      properties:
        error:
          type: string
          title: Error
          description: >-
            Stable machine-readable error code. Use this for branching logic in
            your integration; the human-readable `message` is subject to change.
            See the Error Reference for the catalog.
          examples:
            - validation_error
        message:
          type: string
          title: Message
          description: Human-readable description of what went wrong.
          examples:
            - >-
              Exactly one of contact_id, to, chat_guid, or participant_phones is
              required
      type: object
      required:
        - error
        - message
      title: ApiErrorDetail
  securitySchemes:
    BearerAuth:
      type: http
      scheme: bearer
      bearerFormat: ib_pk_<64 hex>
      description: >-
        Bearer API key. Format: `Authorization: Bearer ib_pk_<64 hex>` (70
        characters). Generate keys from Dashboard → Settings → API Access.

````

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