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

# Retrieve a message

> Fetch a single message by its ID. Useful for polling the current delivery state when you can't subscribe to webhooks.

Returns `404 not_found` for both missing messages and messages owned by a different customer — existence is not leaked across tenants.

### Retrieve a message

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

**Description:** Fetch one message by its ID to check its delivery state. Use it when you can't receive webhooks. To read many messages, or catch up after missed webhooks, use [List messages](https://docs.ibubbl.com/api-reference/messages/list.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

None. The path parameter `message_id` is the message's ID (a UUID), from the send response or a webhook event.

```bash cURL theme={null}
curl https://api.ibubbl.com/v1/messages/8b3a4e72-1c2d-4e5f-9a0b-2c3d4e5f6a7b \
  -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/${messageId}`, {
  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}`);
console.log(data.status, data.channel);
```

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

import requests

API_KEY = os.environ["IBUBBL_API_KEY"]

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

res = requests.get(
    f"https://api.ibubbl.com/v1/messages/{message_id}",
    headers={"Authorization": f"Bearer {API_KEY}"},
    timeout=10,
)
data = res.json()
if not res.ok:
    raise RuntimeError(f"{res.status_code} {data['detail']['error']}")
print(data["status"], data["channel"])
```

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

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

#### Responses

**200 OK**

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

`direction` is `outbound` or `inbound`: an inbound message (for example the `message_id` of a `message.inbound` event) can be retrieved too, and then `status` is `delivered`, or `read` once it has been read in the dashboard, and `to` is the contact who sent it. `created_at` is when iBubbl recorded the message (UTC, microseconds, never changes). `status` is one of `queued`, `pending`, `sent`, `delivered`, `read`, `failed`, `expired`, `cancelled`. `channel` is how it went out (`imessage` or `sms`), or `null` before it is sent. For an inbound message, it is how the message arrived (`null` for messages received before iBubbl recorded it). `attachments` lists the message's files in order, each as `kind` (`image`, `gif`, `video`, `audio` or `file`), `mime_type` and `size_bytes`, for files you sent and files the contact sent you; it's `[]` when there are none. File contents and URLs aren't returned. An inbound file can show `size_bytes: 0` for a few seconds, until iBubbl has received it.

**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 for a message that belongs to another account.


## OpenAPI

````yaml GET /v1/messages/{message_id}
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}:
    get:
      tags:
        - Messages
      summary: Retrieve a message
      description: >-
        Fetch a single message by its ID. Useful for polling the current
        delivery state when you can't subscribe to webhooks.


        Returns `404 not_found` for both missing messages and messages owned by
        a different customer — existence is not leaked across tenants.
      operationId: v1_get_message_v1_messages__message_id__get
      parameters:
        - name: message_id
          in: path
          required: true
          schema:
            type: string
            title: Message Id
      responses:
        '200':
          description: The message.
          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'
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.
      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.