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

# Send a message

> Send a message to one contact (`contact_id`, or `to` — an E.164 phone number; the contact is created on first use), into an existing group (`chat_guid`), or to a new group (`participant_phones`). Exactly one of the four is required.

The response returns immediately with `status: "pending"` or `"queued"`. Delivery progress arrives as [webhook events](/webhooks/events) (`message.sent`, `message.delivered`, `message.read`, `message.failed`), or poll `GET /v1/messages/{id}`.

**Idempotency is required.** Provide a unique `idempotency_key` per logical send (UUIDv4 recommended); a retry with the same key and payload replays the original response, while a reused key with a different payload returns `409 idempotency_conflict`. See the [Idempotency](/idempotency) guide.

A send to an opted-out contact returns `422 contact_opted_out`; see [Suppression](/concepts/suppression).

To send files, list up to 10 `https` URLs in `attachments` (1:1 only); `body`, if set, is their caption. iBubbl downloads them before answering, which can take up to about 30 seconds, so allow 60 seconds before your client times out. See [Attachments](/guides/attachments).

On a `5xx` (including a `502`/`503` from the network edge during a deploy), retry with the same `idempotency_key`.

### Send a message

**Method:** `POST` | **Endpoint:** `/v1/messages`

**Description:** Send a message to one contact (by `contact_id` or phone number), into an existing iMessage group (`chat_guid`), or to a new group (`participant_phones`). One-to-one messages can carry files by URL; see [Send attachments](https://docs.ibubbl.com/guides/attachments.md). The call returns at once; delivery progress arrives as [webhook events](https://docs.ibubbl.com/webhooks/events.md), or poll [Retrieve a message](https://docs.ibubbl.com/api-reference/messages/retrieve.md).

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

#### Request Body

| Field | Type | Required | Description |
| - | - | - | - |
| `contact_id` | string (UUID) | Exactly one of these four | Send one-to-one to an existing contact. A deleted contact's ID returns `404 not_found`. |
| `to` | string (E.164, e.g. `+14155551234`) | Exactly one of these four | Send one-to-one by phone number. If no contact has this number, one is created. |
| `chat_guid` | string | Exactly one of these four | Send into an existing group. Read it from the response to a send into the group, or from [Retrieve a message](https://docs.ibubbl.com/api-reference/messages/retrieve.md) for any message in it. It changes when a participant is added; an old value returns `404 group_not_found`. |
| `participant_phones` | array of strings (E.164) | Exactly one of these four | Create a new group and send the first message. At least 2 numbers; your iBubbl line is added automatically, so don't include it. |
| `group_name` | string, at most 80 characters | No | Display name for the new group. Only allowed with `participant_phones`. |
| `body` | string, at least 1 character | Unless `attachments` is set | The message text. Plain text; emoji and Unicode are supported. For a line break, include a newline (`\n` in the JSON string); use `\n`, not `\r\n`. A retry with the same `idempotency_key` must send the identical body, line breaks included, or it returns `409 idempotency_conflict`. With `attachments` it's their caption. The first message to a contact must have one (`422 first_message_needs_text`). |
| `attachments` | array of 1–10 strings (`https` URLs) | No | Files to send: photos, GIFs, videos, PDFs or audio. iBubbl downloads each URL when the request arrives. One-to-one only (`to` or `contact_id`), and not with `effect` or `reply_to_message_id`. Types, size limits and the combinations allowed are in [Send attachments](https://docs.ibubbl.com/guides/attachments.md#supported-files-and-limits). Allow up to 60 seconds for the response. |
| `reply_to_message_id` | string (UUID) | No | Send as an inline reply that quotes an earlier message: a message `id`, or the `message_id` of a [`message.inbound`](https://docs.ibubbl.com/webhooks/events) event. It must be in the conversation you're sending to (the same contact, or the same group); otherwise `422 reply_target_not_found`. iMessage only. Not allowed with `participant_phones` or `attachments`. See [Reply and send with effects](https://docs.ibubbl.com/guides/send-messages.md#reply-to-a-message-and-send-with-effects). |
| `effect` | string | No | An iMessage effect: `slam`, `loud`, `gentle`, `invisible`, `echo`, `spotlight`, `balloons`, `confetti`, `love`, `lasers`, `fireworks`, `celebration` or `shooting_star`. iMessage only. Not allowed with `participant_phones` or `attachments`. |
| `idempotency_key` | string, 1–128 characters | Yes | Your unique key for this send; a UUIDv4 is recommended. The same key with the same payload replays the original message; the same key with a different payload returns `409`. A key whose message ended `failed` or `expired` is released, and a retry sends a new message. See [Idempotency](https://docs.ibubbl.com/idempotency.md). |

Fields not listed above are ignored without an error: the request still succeeds, and they are not stored.

`reply_to_message_id` and `effect` only show on iMessage. If the message goes out as SMS (RCS included), it is sent as plain text without the quote or the effect, and the API doesn't refuse it. `channel` on [Retrieve a message](https://docs.ibubbl.com/api-reference/messages/retrieve.md) tells you which way it went. Neither can be combined with `attachments`: that request is refused with `422 validation_error`. Both fields count toward the [idempotency](https://docs.ibubbl.com/idempotency.md) payload when set.

`attachments` count toward the idempotency payload too, in order. A replay never downloads them again. A request whose files can't be downloaded or don't meet the limits is refused with a `422` and creates no message; see [Send attachments](https://docs.ibubbl.com/guides/attachments.md#errors).

```bash cURL theme={null}
# One key per message: generate it once, and reuse it if you retry.
IDEMPOTENCY_KEY=$(uuidgen)

curl -X POST https://api.ibubbl.com/v1/messages \
  -H "Authorization: Bearer $IBUBBL_API_KEY" \
  -H "Content-Type: application/json" \
  -d "{
    \"to\": \"+14155551234\",
    \"body\": \"Hey Sam — quick check-in about your order.\",
    \"reply_to_message_id\": \"5f6a7b8c-9d0e-4f1a-8b2c-3d4e5f6a7b8c\",
    \"effect\": \"confetti\",
    \"idempotency_key\": \"$IDEMPOTENCY_KEY\"
  }"
```

```typescript TypeScript theme={null}
import { randomUUID } from "node:crypto";

// One key per message: generate it once, and reuse it if you retry.
const idempotencyKey = randomUUID();

const res = await fetch("https://api.ibubbl.com/v1/messages", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.IBUBBL_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    to: "+14155551234",
    body: "Hey Sam — quick check-in about your order.",
    // Optional: quote an earlier message, and add an iMessage effect.
    reply_to_message_id: "5f6a7b8c-9d0e-4f1a-8b2c-3d4e5f6a7b8c",
    effect: "confetti",
    idempotency_key: idempotencyKey,
  }),
  signal: AbortSignal.timeout(10_000),
});

const data = await res.json();
if (!res.ok) throw new Error(`${res.status} ${data.detail?.error}`);
console.log(data.id, data.status);
```

```python Python theme={null}
import os
import uuid

import requests

API_KEY = os.environ["IBUBBL_API_KEY"]

# One key per message: generate it once, and reuse it if you retry.
idempotency_key = str(uuid.uuid4())

res = requests.post(
    "https://api.ibubbl.com/v1/messages",
    headers={"Authorization": f"Bearer {API_KEY}"},
    json={
        "to": "+14155551234",
        "body": "Hey Sam — quick check-in about your order.",
        # Optional: quote an earlier message, and add an iMessage effect.
        "reply_to_message_id": "5f6a7b8c-9d0e-4f1a-8b2c-3d4e5f6a7b8c",
        "effect": "confetti",
        "idempotency_key": idempotency_key,
    },
    timeout=10,
)
data = res.json()
if not res.ok:
    raise RuntimeError(f"{res.status_code} {data['detail']['error']}")
print(data["id"], data["status"])
```

```json 202 Accepted theme={null}
{
  "id": "8b3a4e72-1c2d-4e5f-9a0b-2c3d4e5f6a7b",
  "direction": "outbound",
  "created_at": "2026-11-05T15:04:10.112730Z",
  "status": "pending",
  "to": "+14155551234",
  "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",
  "chat_guid": null,
  "channel": null,
  "sent_at": "2026-11-05T15:04:10.112Z",
  "held_until": null,
  "held_reason": null,
  "effect": "confetti",
  "reply_to_message_id": "5f6a7b8c-9d0e-4f1a-8b2c-3d4e5f6a7b8c",
  "attachments": []
}
```

```json 422 Unprocessable Entity theme={null}
{
  "detail": {
    "error": "validation_error",
    "message": "Exactly one of contact_id, to, chat_guid, or participant_phones is required"
  }
}
```

#### Responses

**202 Accepted**

```json theme={null}
{
  "id": "8b3a4e72-1c2d-4e5f-9a0b-2c3d4e5f6a7b",
  "direction": "outbound",
  "created_at": "2026-11-05T15:04:10.112730Z",
  "status": "pending",
  "to": "+14155551234",
  "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",
  "chat_guid": null,
  "channel": null,
  "sent_at": "2026-11-05T15:04:10.112Z",
  "held_until": null,
  "held_reason": null,
  "effect": null,
  "reply_to_message_id": null,
  "attachments": []
}
```

`status` is `pending` or `queued`. A message held by a capacity limit or an offline line is still accepted with `queued` and retried automatically. A message waiting for the contact's [working hours](https://docs.ibubbl.com/concepts/working-hours.md) or the line's hourly limit for new contacts is `pending`, with `held_until` and `held_reason` set. For a group send, `to` is `null` and `conversation_kind` is `"group"`; a new group's `chat_guid` is `null` until the group is created a few seconds later. `effect` and `reply_to_message_id` echo what you sent, or `null`. `attachments` lists the files the message carries, in your order, as `kind` (`image`, `gif`, `video`, `audio` or `file`), `mime_type` and `size_bytes`; it's `[]` for a text message. `direction` is always `outbound` here, and `created_at` is when iBubbl recorded the message; it never changes, and it's what [List messages](https://docs.ibubbl.com/api-reference/messages/list.md) sorts and filters by.

**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": "Contact not found"
  }
}
```

Also returned for a `contact_id` that is malformed, belongs to another account, or was [deleted](https://docs.ibubbl.com/api-reference/contacts/delete.md); send with `to` to reach the number again (that creates a new contact). Other `error` value: `group_not_found` (no group with that `chat_guid` on your account).

**409 Conflict**

```json theme={null}
{
  "detail": {
    "error": "idempotency_conflict",
    "message": "idempotency_key reused with a different request payload"
  }
}
```

**422 Unprocessable Entity**

```json theme={null}
{
  "detail": {
    "error": "validation_error",
    "message": "Exactly one of contact_id, to, chat_guid, or participant_phones is required"
  }
}
```

Other `error` values: `contact_opted_out`, `contact_blocked`, `no_eligible_line`, `unresponsive_contact`, `cold_cap_exhausted`, `daily_cap_exhausted`, `followup_cap_exhausted`, `group_outbound_blocked_suppression` (with a `blocked_phones` list), `insufficient_participants`, `reply_target_not_found`, `reply_target_unsent`, `reply_target_partly_unsent`, and for attachments `first_message_needs_text`, `attachment_fetch_failed`, `attachment_too_large`, `unsupported_attachment` and `attachment_combination_not_supported`. See [Errors](https://docs.ibubbl.com/errors.md).

**429 Too Many Requests**

```json theme={null}
{
  "detail": {
    "error": "rate_limited",
    "message": "Too many attachment sends in progress; wait 5s and retry with the same idempotency_key."
  }
}
```

Sends with `attachments` only, and rarely: iBubbl is already downloading as many attachment sends as it can. The `Retry-After` header says how many seconds to wait (5); then send the same request with the same `idempotency_key`. Text-only sends never get a `429`.

**500 Internal Server Error**

```json theme={null}
{
  "detail": {
    "error": "no_owner_rep",
    "message": "Customer is missing an Owner rep"
  }
}
```

Other `error` value: `internal_error`. Retry with the same `idempotency_key`.


## OpenAPI

````yaml POST /v1/messages
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:
    post:
      tags:
        - Messages
      summary: Send a message
      description: >-
        Send a message to one contact (`contact_id`, or `to` — an E.164 phone
        number; the contact is created on first use), into an existing group
        (`chat_guid`), or to a new group (`participant_phones`). Exactly one of
        the four is required.


        The response returns immediately with `status: "pending"` or `"queued"`.
        Delivery progress arrives as [webhook events](/webhooks/events)
        (`message.sent`, `message.delivered`, `message.read`, `message.failed`),
        or poll `GET /v1/messages/{id}`.


        **Idempotency is required.** Provide a unique `idempotency_key` per
        logical send (UUIDv4 recommended); a retry with the same key and payload
        replays the original response, while a reused key with a different
        payload returns `409 idempotency_conflict`. See the
        [Idempotency](/idempotency) guide.


        A send to an opted-out contact returns `422 contact_opted_out`; see
        [Suppression](/concepts/suppression).


        To send files, list up to 10 `https` URLs in `attachments` (1:1 only);
        `body`, if set, is their caption. iBubbl downloads them before
        answering, which can take up to about 30 seconds, so allow 60 seconds
        before your client times out. See [Attachments](/guides/attachments).


        On a `5xx` (including a `502`/`503` from the network edge during a
        deploy), retry with the same `idempotency_key`.
      operationId: v1_send_message_v1_messages_post
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/MessageCreate'
      responses:
        '202':
          description: Message accepted for delivery.
          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: >-
            `not_found` (no contact with that `contact_id`) or `group_not_found`
            (no group with that `chat_guid`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'
        '409':
          description: Idempotency key reused with a different payload.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'
        '422':
          description: >-
            Request rejected; see `detail.error`. `validation_error` (bad
            request shape), `contact_opted_out`, `contact_blocked`,
            `no_eligible_line`, `unresponsive_contact`, `cold_cap_exhausted`,
            `followup_cap_exhausted`, `daily_cap_exhausted`,
            `group_outbound_blocked_suppression`, `insufficient_participants`,
            `reply_target_not_found`, `reply_target_unsent`,
            `reply_target_partly_unsent`, `first_message_needs_text`,
            `attachment_fetch_failed`, `attachment_too_large`,
            `unsupported_attachment`, or `attachment_combination_not_supported`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'
        '429':
          description: >-
            `rate_limited`: too many sends with attachments are being downloaded
            right now. Wait `Retry-After` seconds and retry with the same
            `idempotency_key`.
          headers:
            Retry-After:
              description: Seconds to wait before retrying.
              schema:
                type: integer
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'
        '500':
          description: >-
            `no_owner_rep` (your account has no active owner rep to send as) or
            `internal_error`. Retry with the same `idempotency_key`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'
components:
  schemas:
    MessageCreate:
      properties:
        contact_id:
          anyOf:
            - type: string
            - type: 'null'
          title: Contact Id
          description: >-
            1:1 send by existing contact ID (a UUID). Mutually exclusive with
            `to`, `chat_guid`, and `participant_phones`.
          examples:
            - c8a1f4e2-3b9d-4f7c-a8e5-1d2c3b4a5e6f
        to:
          anyOf:
            - type: string
              pattern: ^\+[1-9]\d{7,14}$
            - type: 'null'
          title: To
          description: >-
            1:1 send by E.164 phone number (e.g. `+14155551234`). If the number
            doesn't match an existing contact, one is created automatically.
            Mutually exclusive with `contact_id`, `chat_guid`, and
            `participant_phones`.
          examples:
            - '+14155551234'
        chat_guid:
          anyOf:
            - type: string
            - type: 'null'
          title: Chat Guid
          description: >-
            Send into an existing iMessage group identified by its opaque
            `chat_guid` (e.g. `any;+;<hex>`). Webhook events don't carry it:
            read it from the response to a send into the group, or from `GET
            /v1/messages/{id}` for any message in the group (an inbound
            `message_id` works too). The value changes when a participant is
            added to the group; an old value then returns `404 group_not_found`.
            Mutually exclusive with `contact_id`, `to`, and
            `participant_phones`.
          examples:
            - any;+;067ace6caf6c43aaa2f7d643975e15ea
        participant_phones:
          anyOf:
            - items:
                type: string
              type: array
            - type: 'null'
          title: Participant Phones
          description: >-
            Create a brand-new iMessage group and send the first message in one
            call. List of 2 or more E.164 phone numbers (your iBubbl line is
            added automatically; do not include it). Mutually exclusive with
            `contact_id`, `to`, and `chat_guid`. Optionally pair with
            `group_name` to set the group's display name.
          examples:
            - - '+14155551234'
              - '+14155557654'
        group_name:
          anyOf:
            - type: string
              maxLength: 80
            - type: 'null'
          title: Group Name
          description: >-
            Display name for a new iMessage group. Only valid alongside
            `participant_phones`. Optional — iMessage will fall back to the
            participants' names if omitted.
          examples:
            - Team Alpha
        body:
          anyOf:
            - type: string
              minLength: 1
            - type: 'null'
          title: Body
          description: >-
            The message text. Plain text only; emoji + Unicode are supported.
            For a line break, include a newline (`\n` in the JSON string); use
            `\n`, not `\r\n`. A retry with the same `idempotency_key` must send
            the identical body, line breaks included, or it returns `409
            idempotency_conflict`. Required unless `attachments` is set; with
            attachments it is sent as their caption, and it is required for the
            first message to a contact (`422 first_message_needs_text`).
          examples:
            - Hey Sam — quick check-in about your order.
        attachments:
          anyOf:
            - items:
                type: string
                maxLength: 2083
                minLength: 1
                format: uri
              type: array
              maxItems: 10
              minItems: 1
            - type: 'null'
          title: Attachments
          description: >-
            Up to 10 files to send, each an `https` URL iBubbl downloads when
            the request arrives. Images (JPEG, PNG, WebP, HEIC, HEIF) and GIFs
            up to 25 MiB each, MP4/MOV video up to 50 MiB, PDF up to 10 MB,
            audio (MP3, M4A, AAC) up to 25 MiB on its own; 86 MB in all. With
            other files, audio must be one MP3 of at most 2 MB. The type comes
            from the response's `Content-Type` (or the URL's extension when that
            is `application/octet-stream`). A URL that can't be downloaded, or a
            file that breaks these rules, rejects the whole request (`422
            attachment_fetch_failed`, `attachment_too_large`,
            `unsupported_attachment`, `attachment_combination_not_supported`)
            and nothing is sent. Only with `to` or `contact_id`, and not with
            `effect` or `reply_to_message_id`. The URLs, in order, are part of
            the payload compared for idempotency; a replay never downloads them
            again.
          examples:
            - - https://example.com/photos/order-1042.jpg
        reply_to_message_id:
          anyOf:
            - type: string
            - type: 'null'
          title: Reply To Message Id
          description: >-
            Send as an inline reply that quotes an earlier message (a UUID: the
            `id` of a message, or the `message_id` of a `message.inbound`
            event). The message must be in the conversation you are sending to:
            the same contact for a 1:1 send, the same group for a `chat_guid`
            send; otherwise `422 reply_target_not_found`. A message the contact
            unsent returns `422 reply_target_unsent`, and one they unsent part
            of `422 reply_target_partly_unsent`. iMessage only: when the message
            goes out as SMS (see `channel`) it is sent as plain text, without
            the quote. Not allowed with `participant_phones` or `attachments`.
          examples:
            - 8b3a4e72-1c2d-4e5f-9a0b-2c3d4e5f6a7b
        effect:
          anyOf:
            - type: string
              enum:
                - slam
                - loud
                - gentle
                - invisible
                - echo
                - spotlight
                - balloons
                - confetti
                - love
                - lasers
                - fireworks
                - celebration
                - shooting_star
            - type: 'null'
          title: Effect
          description: >-
            An iMessage send effect. Bubble effects: `slam`, `loud`, `gentle`,
            `invisible` (Invisible Ink). Screen effects: `echo`, `spotlight`,
            `balloons`, `confetti`, `love`, `lasers`, `fireworks`,
            `celebration`, `shooting_star`. iMessage only: when the message goes
            out as SMS (see `channel`) it is sent without the effect. Not
            allowed with `participant_phones` or `attachments`.
          examples:
            - confetti
        idempotency_key:
          type: string
          maxLength: 128
          minLength: 1
          title: Idempotency Key
          description: >-
            Client-supplied unique key per logical send. Keys never expire:
            reusing a key with the same payload replays the original message,
            however long ago it was sent, and the same key with a different
            payload returns `409 idempotency_conflict`. The one exception is a
            message that ended `failed` or `expired`: its key is released and
            the retry is sent as a new message. The payload compared is the
            routing field exactly as sent (`to`, `contact_id`, `chat_guid`, or
            the sorted `participant_phones` plus `group_name`), `body`, and
            `effect`, `reply_to_message_id` and `attachments` when set, so a
            retry that switches from `to` to the same contact's `contact_id` is
            a 409. Generate a new UUIDv4 for every distinct message.
          examples:
            - a3f2c8d9-e0f1-4b4d-9c5a-7b8e2d1f0c4a
      type: object
      required:
        - idempotency_key
      title: MessageCreate
      description: >-
        Fields not listed here are ignored without an error: the request still
        succeeds and they are not stored.
      example:
        body: Hey Sam — quick check-in about your order.
        idempotency_key: a3f2c8d9-e0f1-4b4d-9c5a-7b8e2d1f0c4a
        to: '+14155551234'
    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.
      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.