Messages
Every call needs Authorization: Bearer <key>. 8 calls:
- Check whether a number is on iMessage:
POST /v1/lookup - List messages:
GET /v1/messages - Send a message:
POST /v1/messages - Check a message before sending it:
POST /v1/messages/check - Get a message and its status:
GET /v1/messages/:id - Get a message’s delivery timeline:
GET /v1/messages/:id/events - Sandbox state:
GET /v1/sandbox - Simulate an inbound reply (sandbox only):
POST /v1/sandbox/inbound
Check whether a number is on iMessage
POST /v1/lookup
Cached for 30 days per workspace. Pass refresh: true to check again. Test keys and test numbers are answered by the simulator.
Headers
| Name | Type | Required | Description |
|---|---|---|---|
x-workspace-id | string | No | Workspace for session (JWT) callers. API keys carry their own workspace. |
idempotency-key | string | No | Makes the write safe to retry for 24 hours. 1 to 255 printable characters. Keys starting with outreach:, workflow: or agent: are reserved for messages bubbl3 sends itself and are refused with 400. Up to 255 characters. |
Request body
| Field | Type | Required | Description |
|---|---|---|---|
phone | string | Yes | Phone number in E.164 format. |
refresh | boolean | No | Ignore the 30-day cache. Default false. |
Response 200
| Field | Type | Description |
|---|---|---|
cached | boolean | |
checked_at | string (date-time) | ISO 8601 timestamp. |
imessage | boolean | |
phone | string | Phone number in E.164 format. |
Errors: 400, 401, 403, 404, 409, 413, 415, 429, 500. See errors for the shape and codes.
List messages
GET /v1/messages
Newest first. Agents see messages in conversations they can see; service identities see messages on their lines and messages they sent.
Query parameters
| Name | Type | Required | Description |
|---|---|---|---|
limit | integer | No | From 1 to 100. Default 50. |
cursor | string | No | Opaque cursor from next_cursor. Up to 512 characters. |
status | enum | No | One of: queued, scheduled, sending, sent, delivered, read, failed, received. |
direction | enum | No | One of: outbound, inbound. |
channel | enum | No | One of: imessage, sms. |
contact_id | string | No | Contact id (ctc_...). |
conversation_id | string | No | Conversation id (cnv_...). |
line_id | string | No | Line id (ln_...). |
external_id | string | No | Up to 255 characters. |
created_after | string (date-time) | No | ISO 8601 timestamp. |
created_before | string (date-time) | No | ISO 8601 timestamp. |
Headers
| Name | Type | Required | Description |
|---|---|---|---|
x-workspace-id | string | No | Workspace for session (JWT) callers. API keys carry their own workspace. |
Response 200
| Field | Type | Description |
|---|---|---|
data | object[] | |
data[].body | string | |
data[].channel | enum | One of: imessage, sms. |
data[].contact_id | string | Contact id (ctc_...). |
data[].conversation_id | string | Conversation id (cnv_...). |
data[].created_at | string (date-time) | ISO 8601 timestamp. |
data[].delivered_at | string (date-time) or null | ISO 8601 timestamp. |
data[].direction | enum | One of: outbound, inbound. |
data[].error_code | string or null | |
data[].error_details | object or null | |
data[].external_id | string or null | |
data[].from | string or null | Phone number in E.164 format. |
data[].id | string | Message id (msg_...). |
data[].is_test | boolean | |
data[].line_id | string or null | Line id (ln_...). |
data[].media | object[] | |
data[].media[].filename | string or null | |
data[].media[].mime | string or null | |
data[].media[].size | integer or null | From 0 to 9007199254740991. |
data[].media[].url | string | |
data[].metadata | object | |
data[].read_at | string (date-time) or null | ISO 8601 timestamp. |
data[].scheduled_for | string (date-time) or null | ISO 8601 timestamp. |
data[].sent_at | string (date-time) or null | ISO 8601 timestamp. |
data[].sent_by_staff | boolean | |
data[].source | enum or null | One of: api, dashboard, workflow, agent, outreach, mcp. |
data[].status | enum | One of: queued, scheduled, sending, sent, delivered, read, failed, received. |
data[].to | string or null | Phone number in E.164 format. |
data[].updated_at | string (date-time) | ISO 8601 timestamp. |
next_cursor | string or null | Pass as cursor to get the next page; null at the end. |
Errors: 400, 401, 403, 404, 409, 413, 415, 429, 500. See errors for the shape and codes.
Send a message
POST /v1/messages
Queues the message and returns it. The worker routes, governs, paces and sends it; follow progress with GET /v1/messages/:id or the message.status webhook.
Headers
| Name | Type | Required | Description |
|---|---|---|---|
x-workspace-id | string | No | Workspace for session (JWT) callers. API keys carry their own workspace. |
idempotency-key | string | No | Makes the write safe to retry for 24 hours. 1 to 255 printable characters. Keys starting with outreach:, workflow: or agent: are reserved for messages bubbl3 sends itself and are refused with 400. Up to 255 characters. |
Request body
| Field | Type | Required | Description |
|---|---|---|---|
body | string | No | Up to 2000 characters. |
contact_id | string | No | Contact id (ctc_...). |
external_id | string | No | Up to 255 characters. |
fallback | boolean | No | Set false to forbid SMS fallback; otherwise the pool's fallback_sms_mode decides. |
line_id | string | No | Line id (ln_...). |
media | object[] | No | Up to 10 items. |
media[].filename | string | No | Up to 255 characters. |
media[].mime | string | No | Up to 100 characters. |
media[].url | string (uri) | Yes | Publicly fetchable URL of the file. Up to 2048 characters. |
metadata | object | No | Up to 50 key/value pairs. |
pool_id | string | No | Pool id (pool_...). |
send_at | string (date-time) | No | Schedule for later, at most one year ahead; the governor may still defer it. |
to | string | No | Recipient phone number. Provide to or contact_id. |
Response 201
| Field | Type | Description |
|---|---|---|
body | string | |
channel | enum | One of: imessage, sms. |
contact_id | string | Contact id (ctc_...). |
conversation_id | string | Conversation id (cnv_...). |
created_at | string (date-time) | ISO 8601 timestamp. |
delivered_at | string (date-time) or null | ISO 8601 timestamp. |
direction | enum | One of: outbound, inbound. |
error_code | string or null | |
error_details | object or null | |
external_id | string or null | |
from | string or null | Phone number in E.164 format. |
id | string | Message id (msg_...). |
is_test | boolean | |
line_id | string or null | Line id (ln_...). |
media | object[] | |
media[].filename | string or null | |
media[].mime | string or null | |
media[].size | integer or null | From 0 to 9007199254740991. |
media[].url | string | |
metadata | object | |
read_at | string (date-time) or null | ISO 8601 timestamp. |
scheduled_for | string (date-time) or null | ISO 8601 timestamp. |
sent_at | string (date-time) or null | ISO 8601 timestamp. |
sent_by_staff | boolean | |
source | enum or null | One of: api, dashboard, workflow, agent, outreach, mcp. |
status | enum | One of: queued, scheduled, sending, sent, delivered, read, failed, received. |
to | string or null | Phone number in E.164 format. |
updated_at | string (date-time) | ISO 8601 timestamp. |
Errors: 400, 401, 403, 404, 409, 413, 415, 429, 500. See errors for the shape and codes.
Check a message before sending it
POST /v1/messages/check
Takes the same body as POST /v1/messages and returns what the governor would decide: allow, defer (with until and the reason) or reject (with the code and the reason), and how many new contacts the line can still text today and this hour. Nothing is sent, queued, counted or stored, and no option skips a rule. An Idempotency-Key is ignored, so the same key still works for the send. With send_at, the text is checked as of that time (quiet hours and content); the line’s caps are predicted only for a send now. A contact bubbl3 knows isn’t on iMessage is checked under the SMS rules (consent, content and quiet hours, and whether the pool can fall back to SMS). Anyone else is checked as on iMessage, and if they turn out not to be, the SMS rules decide at send time: a text that would be deferred or refused by the line’s caps or limits here may then go out by SMS straight away. Rate-limited like other reads.
Headers
| Name | Type | Required | Description |
|---|---|---|---|
x-workspace-id | string | No | Workspace for session (JWT) callers. API keys carry their own workspace. |
Request body
| Field | Type | Required | Description |
|---|---|---|---|
body | string | No | Up to 2000 characters. |
contact_id | string | No | Contact id (ctc_...). |
external_id | string | No | Up to 255 characters. |
fallback | boolean | No | Set false to forbid SMS fallback; otherwise the pool's fallback_sms_mode decides. |
line_id | string | No | Line id (ln_...). |
media | object[] | No | Up to 10 items. |
media[].filename | string | No | Up to 255 characters. |
media[].mime | string | No | Up to 100 characters. |
media[].url | string (uri) | Yes | Publicly fetchable URL of the file. Up to 2048 characters. |
metadata | object | No | Up to 50 key/value pairs. |
pool_id | string | No | Pool id (pool_...). |
send_at | string (date-time) | No | Schedule for later, at most one year ahead; the governor may still defer it. |
to | string | No | Recipient phone number. Provide to or contact_id. |
Response 200
| Field | Type | Description |
|---|---|---|
line | object or null | The line that would send it, and how many new contacts it can still text. |
line.id | string | Line id (ln_...). |
line.new_contacts_left_this_hour | integer | From 0 to 9007199254740991. |
line.new_contacts_left_today | integer | From 0 to 9007199254740991. |
recipients | object[] | |
recipients[].code | string or null | For reject, the error code the message would fail with; for defer, the reason. |
recipients[].contact_id | string or null | The existing contact, if there is one. |
recipients[].decision | enum | What a send now would get: allow (queued to send), defer (queued until until) or reject. One of: allow, defer, reject. |
recipients[].line_id | string or null | The line that would send it. |
recipients[].reason | string or null | The decision in plain words, for people. |
recipients[].to | string or null | The recipient’s number, when the request named one. |
recipients[].until | string (date-time) or null | For defer, when the message would go out. |
Errors: 400, 401, 403, 404, 409, 413, 415, 429, 500. See errors for the shape and codes.
Get a message and its status
GET /v1/messages/:id
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Message id (msg_...). |
Headers
| Name | Type | Required | Description |
|---|---|---|---|
x-workspace-id | string | No | Workspace for session (JWT) callers. API keys carry their own workspace. |
Response 200
| Field | Type | Description |
|---|---|---|
body | string | |
channel | enum | One of: imessage, sms. |
contact_id | string | Contact id (ctc_...). |
conversation_id | string | Conversation id (cnv_...). |
created_at | string (date-time) | ISO 8601 timestamp. |
delivered_at | string (date-time) or null | ISO 8601 timestamp. |
direction | enum | One of: outbound, inbound. |
error_code | string or null | |
error_details | object or null | |
external_id | string or null | |
from | string or null | Phone number in E.164 format. |
id | string | Message id (msg_...). |
is_test | boolean | |
line_id | string or null | Line id (ln_...). |
media | object[] | |
media[].filename | string or null | |
media[].mime | string or null | |
media[].size | integer or null | From 0 to 9007199254740991. |
media[].url | string | |
metadata | object | |
read_at | string (date-time) or null | ISO 8601 timestamp. |
scheduled_for | string (date-time) or null | ISO 8601 timestamp. |
sent_at | string (date-time) or null | ISO 8601 timestamp. |
sent_by_staff | boolean | |
source | enum or null | One of: api, dashboard, workflow, agent, outreach, mcp. |
status | enum | One of: queued, scheduled, sending, sent, delivered, read, failed, received. |
to | string or null | Phone number in E.164 format. |
updated_at | string (date-time) | ISO 8601 timestamp. |
Errors: 400, 401, 403, 404, 409, 413, 415, 429, 500. See errors for the shape and codes.
Get a message’s delivery timeline
GET /v1/messages/:id/events
Oldest first: accepted, routed, deferred, sent, delivered, read and so on.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Message id (msg_...). |
Headers
| Name | Type | Required | Description |
|---|---|---|---|
x-workspace-id | string | No | Workspace for session (JWT) callers. API keys carry their own workspace. |
Response 200
| Field | Type | Description |
|---|---|---|
data | object[] | |
data[].detail | object | Event-specific detail, e.g. the deferral reason. On receipt_missing, unverified: true means the device could not be asked, so the receipt is not known to be missing and does not count against the line. |
data[].id | string (uuid) | |
data[].occurred_at | string (date-time) | ISO 8601 timestamp. |
data[].type | enum | One of: accepted, routed, capability_checked, deferred, rejected, sending, sent, delivered, read, failed, retried, fallback_sms, received, receipt_missing, send_unconfirmed. |
Errors: 400, 401, 403, 404, 409, 413, 415, 429, 500. See errors for the shape and codes.
Sandbox state
GET /v1/sandbox
The organisation's sandbox setting and the workspace's sandbox lines. A sandbox line is created by the first sandboxed message, so the list starts empty.
Headers
| Name | Type | Required | Description |
|---|---|---|---|
x-workspace-id | string | No | Workspace for session (JWT) callers. API keys carry their own workspace. |
Response 200
| Field | Type | Description |
|---|---|---|
enabled | boolean | The organisation's sandbox setting. |
lines | object[] | The workspace’s sandbox lines. Empty until the first sandboxed message. |
lines[].contact_card | object | |
lines[].contact_card.image_url | string | Up to 2048 characters. |
lines[].contact_card.name | string | Up to 100 characters. |
lines[].created_at | string (date-time) | ISO 8601 timestamp. |
lines[].daily_new_cap | integer | From 0 to 9007199254740991. |
lines[].display_name | string or null | |
lines[].health_score | integer | From 0 to 100. |
lines[].id | string | Line id (ln_...). |
lines[].is_sandbox | boolean | |
lines[].phone | string | Phone number in E.164 format. |
lines[].pool_id | string or null | Pool id (pool_...). |
lines[].state | enum | One of: provisioning, ready, warming, active, throttled, paused, quarantined, retired. |
lines[].state_changed_at | string (date-time) or null | ISO 8601 timestamp. |
lines[].state_reason | string or null | |
lines[].usage_today | object | |
lines[].usage_today.day | string (date) | Usage day in America/New_York, rolling over at 3 AM. |
lines[].usage_today.new_contacts | integer | From 0 to 9007199254740991. |
lines[].usage_today.new_contacts_cap | integer | From 0 to 9007199254740991. |
lines[].usage_today.sends | integer | From 0 to 9007199254740991. |
lines[].usage_today.sends_cap | integer | From 0 to 9007199254740991. |
lines[].warmup_day | integer | From 0 to 9007199254740991. |
Errors: 400, 401, 403, 404, 409, 413, 415, 429, 500. See errors for the shape and codes.
Simulate an inbound reply (sandbox only)
POST /v1/sandbox/inbound
Records a text from a test number (+1 NPA 555 01xx) to a sandbox line, as if the contact had replied: the contact is matched or created, the conversation opens and message.received fires. STOP and START keywords apply. Without line_id the workspace’s sandbox line is used (created on first use).
Headers
| Name | Type | Required | Description |
|---|---|---|---|
x-workspace-id | string | No | Workspace for session (JWT) callers. API keys carry their own workspace. |
idempotency-key | string | No | Makes the write safe to retry for 24 hours. 1 to 255 printable characters. Keys starting with outreach:, workflow: or agent: are reserved for messages bubbl3 sends itself and are refused with 400. Up to 255 characters. |
Request body
| Field | Type | Required | Description |
|---|---|---|---|
body | string | Yes | Up to 2000 characters. |
from | string | Yes | Sender. Sandbox only accepts test numbers. |
line_id | string | No | A sandbox line in the workspace. Defaults to the workspace’s sandbox line. |
media | object[] | No | Up to 10 items. |
media[].filename | string | No | Up to 255 characters. |
media[].mime | string | No | Up to 100 characters. |
media[].url | string (uri) | Yes | Publicly fetchable URL of the file. Up to 2048 characters. |
Response 201
| Field | Type | Description |
|---|---|---|
body | string | |
channel | enum | One of: imessage, sms. |
contact_id | string | Contact id (ctc_...). |
conversation_id | string | Conversation id (cnv_...). |
created_at | string (date-time) | ISO 8601 timestamp. |
delivered_at | string (date-time) or null | ISO 8601 timestamp. |
direction | enum | One of: outbound, inbound. |
error_code | string or null | |
error_details | object or null | |
external_id | string or null | |
from | string or null | Phone number in E.164 format. |
id | string | Message id (msg_...). |
is_test | boolean | |
line_id | string or null | Line id (ln_...). |
media | object[] | |
media[].filename | string or null | |
media[].mime | string or null | |
media[].size | integer or null | From 0 to 9007199254740991. |
media[].url | string | |
metadata | object | |
read_at | string (date-time) or null | ISO 8601 timestamp. |
scheduled_for | string (date-time) or null | ISO 8601 timestamp. |
sent_at | string (date-time) or null | ISO 8601 timestamp. |
sent_by_staff | boolean | |
source | enum or null | One of: api, dashboard, workflow, agent, outreach, mcp. |
status | enum | One of: queued, scheduled, sending, sent, delivered, read, failed, received. |
to | string or null | Phone number in E.164 format. |
updated_at | string (date-time) | ISO 8601 timestamp. |
Errors: 400, 401, 403, 404, 409, 413, 415, 429, 500. See errors for the shape and codes.