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

# Edit a message

> Change the text of a message you sent over iMessage, within 14 minutes 30 seconds of `sent_at` and up to five times, as on an iPhone.

The edit is accepted with `edit_status: "pending"`, and `body` shows the new text at once. When it reaches the recipient, `message.edited` fires with `direction: "outbound"` and `edit_count` goes up by one. If it doesn't, `message.edit_failed` fires and `body` goes back to the text they have.

On iOS 16, iPadOS 16.1, macOS 13 Ventura, visionOS 1 and later the message changes in place and is marked Edited. On earlier versions the recipient gets a follow-up message starting "Edited to" with the new text, and the original stays. In a group, only members using iMessage see the edit.

Sending the text the message already has answers `200` and changes nothing, so a retried request never spends a second edit.

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

### Edit a message

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

**Description:** Change the text of a message you sent over iMessage, the way you would on an iPhone: within 14 minutes 30 seconds of sending, up to five times. 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

| Field | Type | Required | Description |
| - | - | - | - |
| `body` | string, 1–4,000 characters | Yes | The new text. It replaces the whole text of the message. Leading and trailing whitespace is trimmed. |

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. Fields not listed above are rejected with `422 validation_error`.

#### How it behaves

* **It's accepted first, applied second.** The answer is `202` with `edit_status: "pending"`, and `body` already shows the new text. A few seconds later the edit reaches the recipient: [`message.edited`](https://docs.ibubbl.com/webhooks/events#message-edited) fires with `direction: "outbound"`, `edit_status` becomes `edited`, and `edit_count` goes up by one.
* **If it doesn't land, the text comes back.** [`message.edit_failed`](https://docs.ibubbl.com/webhooks/events#message-edit-failed) fires, `edit_status` becomes `failed`, and `body` goes back to the text the recipient has. You can edit again if the window is still open.
* **One edit at a time.** While one is on its way, a different text gets `409 edit_in_flight`; wait for `message.edited` and send the next.
* **It's safe to call twice.** Sending the text the message already has (a retry of an accepted edit included) answers `200` with the message and changes nothing. It doesn't use up one of the five edits.
* **14 minutes 30 seconds, five edits.** Measured from `sent_at`. iMessage allows 15 minutes; iBubbl stops 30 seconds early so an edit never arrives too late to apply. After the fifth edit, `422 edit_limit_reached`.
* **iMessage only.** A message that went out by SMS (`channel: "sms"`, RCS included) can't be edited: `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) and send a new one instead.
* **Text only.** A message's text can be edited, including the caption of a message with files. A message that is only files has nothing to edit (`422 not_editable`).
* **Messages with a link preview.** A message typed in the dashboard with a link goes out as the text and a preview card. Only the text before the link can change: keep the link exactly as sent and at the end, or you get `422 link_changed`. Messages sent through the API never have a preview card, so this doesn't apply to them.
* **Groups work.** In a group, only members using iMessage see the edit.
* **Blocked and opted-out contacts get none.** An edit reaches the recipient as a change, so in a one-to-one conversation a [blocked](https://docs.ibubbl.com/api-reference/contacts/block.md) contact gets `422 contact_blocked` and an [opted-out](https://docs.ibubbl.com/concepts/suppression.md) one `422 contact_opted_out`.

> [!NOTE]
>
> **What the recipient sees depends on their software.** On iOS 16, iPadOS 16.1, macOS 13 Ventura, visionOS 1 and later, the bubble changes in place and is marked **Edited**. On earlier versions they get a follow-up message starting "Edited to" with the new text in quotation marks, and the original stays. See [What the recipient sees](https://docs.ibubbl.com/guides/editing-and-unsending.md#what-the-recipient-sees).

```bash cURL theme={null}
curl -X POST https://api.ibubbl.com/v1/messages/8b3a4e72-1c2d-4e5f-9a0b-2c3d4e5f6a7b/edit \
  -H "Authorization: Bearer $IBUBBL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"body": "Hey Sam — quick check-in about your order #1042."}'
```

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

const res = await fetch(
  `https://api.ibubbl.com/v1/messages/${encodeURIComponent(messageId)}/edit`,
  {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.IBUBBL_API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ body: "Hey Sam — quick check-in about your order #1042." }),
    signal: AbortSignal.timeout(10_000),
  },
);

const data = await res.json();
if (!res.ok) throw new Error(`${res.status} ${data.detail?.error}`);
// 202: the edit is on its way; 200: the message already had this text.
console.log(res.status, data.edit_status, data.body);
```

```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='')}/edit",
    headers={"Authorization": f"Bearer {API_KEY}"},
    json={"body": "Hey Sam — quick check-in about your order #1042."},
    timeout=10,
)
data = res.json()
if not res.ok:
    raise RuntimeError(f"{res.status_code} {data['detail']['error']}")
# 202: the edit is on its way; 200: the message already had this text.
print(res.status_code, data["edit_status"], data["body"])
```

```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": "Hey Sam — quick check-in about your order #1042.",
  "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": "pending",
  "unsend_status": null,
  "unsent_at": null
}
```

```json 422 Unprocessable Entity theme={null}
{
  "detail": {
    "error": "edit_window_expired",
    "message": "This message can no longer be edited. Send a follow-up message instead."
  }
}
```

#### Responses

**202 Accepted**

The edit is on its way. The body is the [message](https://docs.ibubbl.com/api-reference/messages/retrieve.md) with the new text and `edit_status: "pending"`. `edit_count` and `edited_at` change when the edit reaches the recipient.

| Field | Description |
| - | - |
| `body` | The new text, already. It goes back to the previous text if the edit fails. |
| `edit_status` | `pending` now; then `edited` when it reaches the recipient, or `failed` if it doesn't. |
| `edit_count` | Your edits that have reached the recipient, 0–5. |
| `edited_at` | When the latest edit reached the recipient (UTC), or `null`. |

**200 OK**

The message already has this text. The body is the message as it is; nothing was sent and no edit was used.

**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 earlier edit is still being dispatched. Wait a few seconds and try again."
  }
}
```

Nothing changed. The `error` is one of:

* `edit_in_flight`: your previous edit hasn't reached the recipient yet. Wait for `message.edited` (usually a few seconds), then send this one.
* `already_unsent`: the message was unsent, so there's nothing left to edit.

**422 Unprocessable Entity**

```json theme={null}
{
  "detail": {
    "error": "not_sent_yet",
    "message": "This message hasn't been sent yet, so it can't be edited. Cancel it instead."
  }
}
```

Nothing changed. The `error` is one of:

* `validation_error`: no `body`, an empty one, more than 4,000 characters, or an unknown field.
* `invalid_target`: the message is one you received. Only messages you sent can be edited.
* `sms_not_supported`: the message went out by SMS (RCS included), where messages can't be edited.
* `not_sent_yet`: the message is still `pending` or `queued`. [Cancel it](https://docs.ibubbl.com/api-reference/messages/cancel.md) and send a new one.
* `not_editable`: the message has no text (only files), or it failed, was cancelled or expired, or iMessage never confirmed it.
* `edit_window_expired`: more than 14 minutes 30 seconds since `sent_at`. Send a follow-up message instead.
* `edit_limit_reached`: the message has been edited five times.
* `link_changed`: a message sent with a link preview, where the new text moves, changes or removes the link.
* `contact_blocked`: a one-to-one conversation with a contact you [blocked](https://docs.ibubbl.com/api-reference/contacts/block.md).
* `contact_opted_out`: a one-to-one conversation with a contact who opted out.

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

**503 Service Unavailable**

```json theme={null}
{
  "detail": {
    "error": "opt_out_check_unavailable",
    "message": "Couldn't check the contact's opt-out. Try again."
  }
}
```

iBubbl couldn't confirm the contact hasn't opted out, so it changed nothing. Retry after a short backoff.


## OpenAPI

````yaml POST /v1/messages/{message_id}/edit
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}/edit:
    post:
      tags:
        - Messages
      summary: Edit a message
      description: >-
        Change the text of a message you sent over iMessage, within 14 minutes
        30 seconds of `sent_at` and up to five times, as on an iPhone.


        The edit is accepted with `edit_status: "pending"`, and `body` shows the
        new text at once. When it reaches the recipient, `message.edited` fires
        with `direction: "outbound"` and `edit_count` goes up by one. If it
        doesn't, `message.edit_failed` fires and `body` goes back to the text
        they have.


        On iOS 16, iPadOS 16.1, macOS 13 Ventura, visionOS 1 and later the
        message changes in place and is marked Edited. On earlier versions the
        recipient gets a follow-up message starting "Edited to" with the new
        text, and the original stays. In a group, only members using iMessage
        see the edit.


        Sending the text the message already has answers `200` and changes
        nothing, so a retried request never spends a second edit.


        Counts toward the conversation-actions rate limit (300 a minute per
        account).
      operationId: v1_edit_message_v1_messages__message_id__edit_post
      parameters:
        - name: message_id
          in: path
          required: true
          schema:
            type: string
            title: Message Id
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/MessageEdit'
      responses:
        '200':
          description: The message already has this text; nothing changed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Message'
        '202':
          description: Edit 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` (your previous edit hasn't reached the recipient
            yet; retry in a few seconds) or `already_unsent`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'
        '422':
          description: >-
            `validation_error` (bad body), `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_editable` (no
            text to edit, or it failed or never went out),
            `edit_window_expired`, `edit_limit_reached`, `link_changed`,
            `contact_blocked`, `contact_opted_out`.
          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'
        '503':
          description: >-
            `opt_out_check_unavailable`: the contact's opt-out couldn't be
            checked, so nothing was changed. Retry.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'
components:
  schemas:
    MessageEdit:
      properties:
        body:
          type: string
          maxLength: 4000
          minLength: 1
          title: Body
          description: >-
            The new text. It replaces the whole text of the message; leading and
            trailing whitespace is trimmed. For a message that went out with a
            link preview, keep the link exactly as sent and at the end, and
            change only the text before it. Fields not listed here are rejected
            with `422 validation_error`.
      additionalProperties: false
      type: object
      required:
        - body
      title: MessageEdit
      example:
        body: 'Hey Sam — quick check-in about your order #1042.'
    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.