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

# Error Reference

> Stable error codes and what they mean.

Every iBubbl API error response uses the same envelope shape:

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

The `error` field is a **stable machine-readable code** — branch your integration's logic on this, not on the HTTP status or `message`. The `message` field is human-readable and may change without notice.

## Request IDs

Every response from `api.ibubbl.com`, successful or not, carries an `X-Request-Id` header. It names that one request in iBubbl's logs. When something goes wrong, log it next to the error and quote it to [support@ibubbl.com](mailto:support@ibubbl.com): it finds your request faster than a timestamp and a request body can.

```bash cURL theme={null}
# -D - prints the response headers, including X-Request-Id.
curl -sS -D - 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) {
  const requestId = res.headers.get("x-request-id");
  throw new Error(`${res.status} ${data.detail?.error} (request ${requestId})`);
}
```

```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:
    request_id = res.headers.get("X-Request-Id")
    raise RuntimeError(f"{res.status_code} {data['detail']['error']} (request {request_id})")
```

**Send your own ID to trace a request end to end.** If your request carries an `X-Request-Id` header, iBubbl uses it as the request's ID and returns it unchanged, so the same ID appears in your logs and ours. It must be 1–128 characters of letters, digits, `.`, `_`, `:` and `-`. Anything else is ignored, and iBubbl generates an ID instead. Reusing one ID for several requests is allowed, but a unique ID per request is easier to find.

## Error catalog

**unauthorized** `401`

Missing, malformed, or revoked API key. Verify the `Authorization: Bearer ib_pk_<hex>` header and that the key is active in your dashboard.

**forbidden** `403`

Your API key is valid, but your iBubbl account is suspended or cancelled (`message` is `Account is suspended` or `Account is cancelled`). Every API-key endpoint returns this. A resource that belongs to another account is never a 403: it returns `not_found`.

**not\_found** `404`

The requested resource doesn't exist, OR it exists but belongs to a different customer (existence is hidden across tenants). Verify the resource ID is correct and was created on the same account whose key you're using. A [deleted contact](https://docs.ibubbl.com/api-reference/contacts/delete.md) answers `404` too, including as the `contact_id` of a send.

**idempotency\_conflict** `409`

You reused an `idempotency_key` with a request payload that doesn't match the one originally cached. Either generate a new key for this distinct send, or fix your retry logic to reuse the original payload byte-for-byte. See [Idempotency](https://docs.ibubbl.com/idempotency.md).

**validation\_error** `422`

Generic request-shape failure. The `message` will identify the field. Common causes: missing required body field, malformed E.164 phone, body length zero, `to` and `contact_id` both omitted, neither `body` nor `attachments`, an `attachments` entry that isn't an `https` URL (or more than 10 of them), `attachments` with `chat_guid` or `participant_phones`, or `attachments` with `effect` or `reply_to_message_id`. On a list endpoint: an unknown or repeated query parameter, `limit` outside 1–100, or a time without a time zone.

**invalid\_cursor** `422`

List endpoints only: `starting_after` is a UUID but not the `id` of one of your items (another account's ID, and a contact deleted since, are answered the same way). Pass the `id` of the last item from the previous page, with the same endpoint. See [Pagination](https://docs.ibubbl.com/concepts/pagination.md).

**contact\_opted\_out** `422`

The destination contact has opted out and cannot receive new sends. iBubbl enforces this server-side; there is no programmatic way to override. [Add a tapback](https://docs.ibubbl.com/api-reference/messages/react.md) and [Start typing](https://docs.ibubbl.com/api-reference/conversations/start-typing.md) return it too, in a one-to-one conversation. See [Suppression & Opt-Outs](https://docs.ibubbl.com/concepts/suppression.md).

**contact\_blocked** `422`

A one-to-one send to a contact your account has blocked, in the dashboard or with [Block a contact](https://docs.ibubbl.com/api-reference/contacts/block.md). Nothing was sent. The send works again once the contact is [unblocked](https://docs.ibubbl.com/api-reference/contacts/unblock.md). If a contact is both opted out and blocked, you get `contact_opted_out`. [Add a tapback](https://docs.ibubbl.com/api-reference/messages/react.md) and [Start typing](https://docs.ibubbl.com/api-reference/conversations/start-typing.md) return it too, in a one-to-one conversation.

**unresponsive\_contact** `422`

Refused on the 7th consecutive message to a contact who has never replied: iBubbl allows 6 unanswered messages in a row, then stops. Messages still waiting to go out (`pending` or `queued`) count toward the 6. Nothing was sent. The limit lifts as soon as the contact replies; waiting does not reset it.

**invalid\_timezone** `422`

The `timezone` field on a contact upsert is not in the supported IANA allowlist. The supported set covers US/Canada timezones plus common international zones; if you need a zone that isn't supported, email [support@ibubbl.com](mailto:support@ibubbl.com).

**invalid\_email** `422`

The `email` on a contact create or update doesn't look like an email address (`name@domain.tld`, no spaces, at most 254 characters). Nothing was saved.

**invalid\_tag** `422`

A tag name is empty, longer than 64 characters, or holds an invisible character. On a contact create, nothing was saved.

**duplicate\_tag** `422`

A tag name collides with an existing tag in a way iBubbl can't resolve. Rare: choose a different name.

**line\_offline** `queued`

Your line's Mac is offline. This is not returned as an error: a one-to-one send to a contact your line has messaged before is accepted (`202`, `status: "queued"`) and goes out automatically when the Mac reconnects. The reason appears only as `failed_reason` on the [`message.queued`](https://docs.ibubbl.com/webhooks/events) webhook, not in the API response. If it is still queued 60–65 minutes after it was accepted it expires (`status: "expired"`), and [`message.failed`](https://docs.ibubbl.com/webhooks/events#message-failed) fires with `failed_reason: "queue_expired"`. A contact with no line yet gets `no_eligible_line` instead. See [Limits and queues](https://docs.ibubbl.com/concepts/limits.md#offline-lines).

**daily\_cap\_exhausted** `422`

The line has sent 1,000 messages today, its daily ceiling. Every outbound message counts: new contacts, replies, follow-ups, groups, messages still waiting and failed ones. The send fails and is not queued; the count resets at midnight ET (America/New\_York), so send again after that. See [Limits and queues](https://docs.ibubbl.com/concepts/limits.md).

**cold\_cap\_exhausted** `422`

The line has messaged 50 new contacts today (contacts your account has no history with). The send fails and is not queued. Messages to contacts already counted today, and to anyone you've messaged before, still go out. The count resets at midnight ET (America/New\_York). See [Limits and queues](https://docs.ibubbl.com/concepts/limits.md).

**hourly\_cap\_hit** `queued`

Legacy: not used on any current line. The hourly limit on new contacts is a hold instead: the message stays `pending` with `held_reason: "hourly_limit"` and goes out by itself. See [Limits and queues](https://docs.ibubbl.com/concepts/limits.md).

**followup\_cap\_exhausted** `422`

Legacy: not used on any current line. It was a daily cap on follow-ups under the older per-line caps.

**no\_eligible\_line** `422`

No line on your account can take this send right now: for example, a contact you haven't messaged before while your line is offline, or a group whose line is no longer active. Nothing was sent. Send again once your line is back online.

**group\_not\_found** `404`

The `chat_guid` doesn't match a group conversation on your account. A group's `chat_guid` can change when someone is added to it, so look up the current one. Retrying the same request won't help.

**group\_outbound\_blocked\_suppression** `422`

One or more participants in the group have opted out, so iBubbl won't send to the group. When the send would create a new group, `detail` also carries a `blocked_phones` list naming them. Opt-outs are permanent, so retrying won't help; send to a group without them.

**insufficient\_participants** `422`

The `participant_phones` resolve to fewer than 2 different people: for example, the same number written two ways. A group needs at least 2 different participants; for one person, send one-to-one.

**reply\_target\_not\_found** `422`

`POST /v1/messages` only: `reply_to_message_id` doesn't name a message in the conversation you're sending to. For a `to` or `contact_id` send it must be a message with that contact, one-to-one; for a `chat_guid` send, a message in that group. A message from another account is answered the same way. Check the ID and the addressing field. See [Reply and send with effects](https://docs.ibubbl.com/guides/send-messages.md#reply-to-a-message-and-send-with-effects).

**reply\_target\_unsent** `422`

`POST /v1/messages` only: the contact unsent the message `reply_to_message_id` points to, so it can't be quoted. Send without `reply_to_message_id`.

**reply\_target\_partly\_unsent** `422`

`POST /v1/messages` only: the contact unsent part of the message `reply_to_message_id` points to, and your line can't quote what's left. Send without `reply_to_message_id`.

**invalid\_target** `422`

[Add a tapback](https://docs.ibubbl.com/api-reference/messages/react.md) only: the message is one you sent. Tapbacks go only on messages you received.

**sms\_not\_supported** `422`

[Add a tapback](https://docs.ibubbl.com/api-reference/messages/react.md) only: the message came in by SMS, or the conversation's latest messages are going out by SMS, and tapbacks can't be sent over SMS. They work over iMessage and RCS. Nothing was sent. See [Tapbacks, read receipts and typing](https://docs.ibubbl.com/guides/reactions-read-receipts-typing.md#tapbacks).

**target\_unsent** `422`

[Add a tapback](https://docs.ibubbl.com/api-reference/messages/react.md) and [Remove a tapback](https://docs.ibubbl.com/api-reference/messages/remove-reaction.md): the contact unsent the message, so it can't get (or lose) a tapback. Also a `failed_reason` on [`message.reaction.failed`](https://docs.ibubbl.com/webhooks/events) when they unsent it before the tapback went out.

**target\_partly\_unsent** `422`

[Add a tapback](https://docs.ibubbl.com/api-reference/messages/react.md) and [Remove a tapback](https://docs.ibubbl.com/api-reference/messages/remove-reaction.md): the contact unsent part of the message, and your line can't place a tapback on what's left.

**opt\_out\_check\_unavailable** `503`

[Add a tapback](https://docs.ibubbl.com/api-reference/messages/react.md) and [Start typing](https://docs.ibubbl.com/api-reference/conversations/start-typing.md): iBubbl couldn't confirm that the contact hasn't opted out, so it sent nothing. Retry after a short backoff. (Mark read doesn't fail this way: it answers `200` with `read_receipt_skipped_reason: "opt_out_check_unavailable"` and sends no receipt.)

**first\_message\_needs\_text** `422`

`POST /v1/messages` only: you sent `attachments` without a `body` to a contact you've never exchanged a message with. Messages can't open a new conversation with files alone. Add a `body`. See [Captions and the first message](https://docs.ibubbl.com/guides/attachments.md#captions-and-the-first-message).

**attachment\_fetch\_failed** `422`

`POST /v1/messages` only: iBubbl couldn't download one of the `attachments` URLs. The `message` names it by position, for example `attachments[1]: attachment fetch answered 404`. Causes: the host doesn't resolve or is a private or internal address, the server answered something other than `200`, more than 3 redirects (or one to a non-`https` URL), an empty file, or the download took too long (about 30 seconds for all the files, or 10 seconds without data). Check the URL works from the public internet, then send again. Nothing was created. See [Send attachments](https://docs.ibubbl.com/guides/attachments.md#how-the-urls-are-read).

**attachment\_too\_large** `422`

`POST /v1/messages` only: a file is over its kind's limit (images and GIFs 25 MiB, video 50 MiB, PDF 10 MB, audio 25 MiB), or the files are over 86 MB in all. The `message` names the file. Send a smaller file, or split the files across messages. See [Supported files and limits](https://docs.ibubbl.com/guides/attachments.md#supported-files-and-limits).

**unsupported\_attachment** `422`

`POST /v1/messages` only: a file's type can't be sent through the API, for example a calendar invite (`.ics`), WebM or Ogg audio, SVG or a ZIP file. The type comes from the response's `Content-Type` (or the URL's extension when that is `application/octet-stream`). Convert the file to a supported type.

**attachment\_combination\_not\_supported** `422`

`POST /v1/messages` only: the files can't go out together with their caption. With other files, audio must be a single MP3 of 2 MB or less; any other audio, and a second audio file, needs a message of its own. Send it separately.

**imessage\_undeliverable** `message.failed`

Not an API error: it appears only as `failed_reason` on a failed message and its [`message.failed`](https://docs.ibubbl.com/webhooks/events) webhook. iMessage couldn't deliver the message to this number because the number isn't reachable on iMessage. Resending fails the same way, so reach the contact another way. Other iMessage failures appear as `imessage_error:<code>`.

**internal\_error** `500`

An unexpected error on iBubbl's side. These are paired with our error-tracking system; we get paged when they happen. If you see a sustained pattern, email [support@ibubbl.com](mailto:support@ibubbl.com) with the response's [`X-Request-Id`](#request-ids) header.

One known cause: two `POST /v1/messages` requests with the same `idempotency_key` arriving at the same moment. Only one message is created, and the other request gets this `500`. Retrying it with the same key returns the `202` replay. See [Duplicates and retries](https://docs.ibubbl.com/concepts/message-lifecycle.md#duplicates-and-retries).

**no\_owner\_rep** `500`

Your account has no active owner rep to send as, so `POST /v1/messages` can't send. Retrying won't help until the account has one; email [support@ibubbl.com](mailto:support@ibubbl.com).

**invalid\_url** `422`

A [`POST /v1/webhooks`](https://docs.ibubbl.com/api-reference/webhooks/create) `url` that doesn't start with `https://`. Webhook endpoints must use HTTPS.

**invalid\_signature** `403`

An [inbound event](https://docs.ibubbl.com/inbound-events/overview.md) arrived with a missing or incorrect signature. For Recharge, the usual cause is that the source was created with your API token pasted where the **API Client Secret** belongs — they're different values.

**payload\_too\_large** `413`

An [inbound event](https://docs.ibubbl.com/inbound-events/overview.md) body exceeded 256 KB.

**rate\_limited** `429`

Returned in four places, and nowhere else:

* **Conversation actions** ([Add](https://docs.ibubbl.com/api-reference/messages/react.md) and [remove](https://docs.ibubbl.com/api-reference/messages/remove-reaction.md) a tapback, [Mark read](https://docs.ibubbl.com/api-reference/conversations/mark-read.md), [Start](https://docs.ibubbl.com/api-reference/conversations/start-typing.md) and [stop](https://docs.ibubbl.com/api-reference/conversations/stop-typing.md) typing): your account made more than 300 of these requests in 60 seconds (all API keys and all five endpoints together; a budget separate from lists). The response has a `Retry-After` header; wait that long and call again. See [Rate limits](https://docs.ibubbl.com/concepts/limits.md#rate-limits).
* **List endpoints** ([List messages](https://docs.ibubbl.com/api-reference/messages/list.md), [List contacts](https://docs.ibubbl.com/api-reference/contacts/list.md)): your account made more than 120 list requests in 60 seconds (all API keys and all lists together). The response has a `Retry-After` header with the seconds to wait; then send the same request again. See [Rate limits](https://docs.ibubbl.com/concepts/limits.md#rate-limits).
* **Sends with `attachments`**, rarely: iBubbl was already downloading as many attachment sends as it can, and no slot freed within 10 seconds. `Retry-After` is `5`; then send the same request with the same `idempotency_key`. Nothing was created.
* **`/v1/events`**: a secret-URL [inbound event](https://docs.ibubbl.com/inbound-events/overview.md) source received more than 60 events in 60 seconds. There is no `Retry-After` header here; send again after about a minute.

Text-only sends (`POST /v1/messages` without `attachments`) never return it.

## When to retry

| Code | HTTP | Retryable? |
| - | - | - |
| `unauthorized` | 401 | ❌ Fix the key. |
| `forbidden` | 403 | ❌ Account suspended or cancelled; email [support@ibubbl.com](mailto:support@ibubbl.com). |
| `not_found` | 404 | ❌ Verify the ID. |
| `group_not_found` | 404 | ❌ Use the group's current `chat_guid`. |
| `idempotency_conflict` | 409 | ❌ Fix the retry logic. |
| `validation_error` | 422 | ❌ Fix the payload or query parameters. |
| `invalid_cursor` | 422 | ❌ Use the last `id` from the previous page. |
| `contact_opted_out` | 422 | ❌ Cannot bypass. |
| `contact_blocked` | 422 | ❌ Not until the contact is [unblocked](https://docs.ibubbl.com/api-reference/contacts/unblock.md). |
| `unresponsive_contact` | 422 | ❌ Not until the contact replies. |
| `group_outbound_blocked_suppression` | 422 | ❌ Cannot bypass; leave out the opted-out participants. |
| `insufficient_participants` | 422 | ❌ Fix the participant list, or send one-to-one. |
| `reply_target_not_found` | 422 | ❌ Fix `reply_to_message_id`, or the addressing field. |
| `reply_target_unsent` / `reply_target_partly_unsent` | 422 | ❌ Send without `reply_to_message_id`. |
| `invalid_target` | 422 | ❌ Tapbacks go only on messages you received. |
| `sms_not_supported` | 422 | ❌ Not over SMS. |
| `target_unsent` / `target_partly_unsent` | 422 | ❌ The contact unsent the message. |
| `first_message_needs_text` | 422 | ❌ Add a `body`. |
| `attachment_fetch_failed` | 422 | ⚠️ Fix or check the URL, then send again; the key is still unused. |
| `attachment_too_large` / `unsupported_attachment` / `attachment_combination_not_supported` | 422 | ❌ Change the files. |
| `invalid_url` | 422 | ❌ Use an `https://` URL. |
| `invalid_timezone` | 422 | ❌ Fix the timezone. |
| `invalid_email` / `invalid_tag` / `duplicate_tag` | 422 | ❌ Fix the value. |
| `line_offline` | — (`202`, `queued`) | ⚠️ Not an error: iBubbl queues and retries for up to an hour. Don't resend. |
| `daily_cap_exhausted` / `cold_cap_exhausted` | 422 | ⚠️ Not queued. Send again after the limit resets (midnight ET). |
| `hourly_cap_hit` / `followup_cap_exhausted` | — | Legacy: not used on any current line. |
| `no_eligible_line` | 422 | ⚠️ Send again once your line is back online. |
| `rate_limited` | 429 | ✅ Lists, conversation actions and attachment sends: send the same request again (same `idempotency_key`) after `Retry-After` seconds. `/v1/events`: after about a minute. |
| `invalid_signature` | 403 | ❌ Fix the signing secret. |
| `payload_too_large` | 413 | ❌ Fix the payload size. |
| `no_owner_rep` | 500 | ❌ Not until your account has an active owner rep; email [support@ibubbl.com](mailto:support@ibubbl.com). |
| `internal_error` | 500 | ✅ Retry with the same idempotency key after a short backoff. |
| `opt_out_check_unavailable` | 503 | ✅ Retry after a short backoff. |

The general rule: **don't retry 4xx**. Most 4xxes are payload-level problems that retrying won't fix. The exception is `429 rate_limited`: wait, then send the same request again (on a list, a conversation action or an attachment send, `Retry-After` says how long). The limit 422s (`daily_cap_exhausted`, `cold_cap_exhausted`) are retryable only after the limit resets at midnight ET; iBubbl does not queue them for you. The hourly new-contact limit and an offline line are different: those sends are accepted and wait (`pending` with `held_reason: "hourly_limit"`, or `queued`), so resending just creates a duplicate. The numbers are on [Limits and queues](https://docs.ibubbl.com/concepts/limits.md). A send refused with a `404` or `422` creates no message, so its `idempotency_key` stays unused and you can reuse it when you send again.

## HTTP status mapping summary

| Status | When |
| - | - |
| `200` / `201` / `202` / `204` | Success. `202` for accepted-but-queued sends; `201` for created resources; `204` (no body) for [Delete a contact](https://docs.ibubbl.com/api-reference/contacts/delete.md); `200` everywhere else. |
| `401` | Auth failure. |
| `403` | Account suspended or cancelled (`forbidden`), or an inbound event's signature failed (`invalid_signature`). |
| `404` | Resource not found (or hidden across tenants), or `group_not_found`. |
| `409` | Idempotency conflict. |
| `413` | Inbound event body over 256 KB. |
| `422` | Validation, routing or send-limit failure. |
| `429` | `rate_limited`: too many list requests or conversation actions from your account, or attachment downloads busy (all with `Retry-After`), or too many inbound events for one source on `/v1/events`. |
| `500` | No owner rep (`no_owner_rep`) or an unexpected iBubbl error (`internal_error`). |
| `503` | `opt_out_check_unavailable` on a tapback or typing call. Retry after a short backoff. |
| `502` / `503` without a `detail.error` | The network edge during a deployment blip, not iBubbl itself. Retry with the same idempotency key after a short backoff. |


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