Skip to main content
POST

Cancel a message

Method: POST | Endpoint: /v1/messages/{message_id}/cancel Description: Stop a message you sent before it goes out: one waiting for the contact’s sending hours or the line’s hourly limit for new contacts (status: "pending" with a held_reason), or one queued by a capacity hold (status: "queued"). It will never be sent. See Edit, Undo Send and Cancel. Authentication: API key in the Authorization header: Authorization: Bearer ib_pk_<64 hex>. Create keys in the dashboard under Settings → API access. Rate limit: 300 conversation actions per 60 seconds per account (edits, unsends, cancels, tapbacks, mark read and typing together, across all your API keys). Over that, you get 429 rate_limited with a Retry-After header; see Rate limits.

Request Body

None. The path parameter message_id is the id from Send a message, or from List messages, where messages sent from the dashboard and by automations appear too.

How it behaves

  • It’s immediate. The answer is 200 with status: "cancelled", and message.cancelled fires. The message stays in the conversation history as cancelled; the contact never sees it.
  • It’s safe to call twice. Cancelling a cancelled message answers 200 with it, and no second message.cancelled fires.
  • Only while it’s waiting. A message already sent, delivered, read, failed or expired, or one you received, gets 422 not_cancellable. To take back a message that already went out, unsend it within 110 seconds.
  • Not once your line has picked it up. In the second or so before a waiting message goes out, your line takes it to send; from then on, cancelling gets 409 message_dispatching, and the message is sent (message.sent fires).
  • Retrying the send doesn’t bring it back. POST /v1/messages again with the same idempotency_key returns this cancelled message and sends nothing. To send it after all, use a new key.
  • Any channel. It works whether the message would have gone out by iMessage or SMS, one-to-one or to a group.

Responses

200 OK The message, now cancelled (or already cancelled). held_until and held_reason are null, and channel stays null: it never went out. 401 Unauthorized
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
Your iBubbl account is suspended or cancelled (message is then Account is cancelled). Every API-key endpoint returns this. 404 Not Found
Also returned for a malformed ID and a message that belongs to another account. 409 Conflict message_dispatching: your line already has the message and will send it. Nothing changed; message.sent follows. 422 Unprocessable Entity
not_cancellable: the message isn’t one of yours that is still waiting to go out. It was sent, failed or expired, it’s a message you received, or it’s a meeting invite. Nothing changed. 429 Too Many Requests
Wait the Retry-After header’s seconds, then send the same request again.

Authorizations

Authorization
string
header
required

Bearer API key. Format: Authorization: Bearer ib_pk_<64 hex> (70 characters). Generate keys from Dashboard → Settings → API Access.

Path Parameters

message_id
string
required

Response

The message, now cancelled.

id
string
required

Unique message identifier (UUID).

direction
enum<string>
required

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.

Available options:
inbound,
outbound
created_at
string
required

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.

Example:

"2026-05-17T14:23:45.123456Z"

status
enum<string>
required

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

Available options:
queued,
pending,
sent,
delivered,
read,
failed,
expired,
cancelled
body
string
required

The message text as sent.

attachments
MessageAttachment · object[]
required

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.

to
string | null

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.

Example:

"+14155551234"

contact_id
string | null

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
string | null

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
enum<string>
default:one_to_one

one_to_one for sends to a single contact, group for iMessage groups (2+ participants in addition to your iBubbl line).

Available options:
one_to_one,
group
chat_guid
string | null

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.

Example:

"any;+;067ace6caf6c43aaa2f7d643975e15ea"

channel
enum<string> | null

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.

Available options:
imessage,
sms
sent_at
string | null

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.

Example:

"2026-05-17T14:23:45.123Z"

held_until
string | null

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.

Example:

"2026-10-08T16:00:00+00:00"

held_reason
enum<string> | null

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.

Available options:
working_hours,
hourly_limit
effect
enum<string> | null

The iMessage send effect requested for this message, or null. It shows on the recipient's device only when channel is imessage.

Available options:
slam,
loud,
gentle,
invisible,
echo,
spotlight,
balloons,
confetti,
love,
lasers,
fireworks,
celebration,
shooting_star
reply_to_message_id
string | null

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.

edited_at
string | null

ISO 8601 UTC timestamp of the latest edit that reached the recipient: one of yours (POST /v1/messages/{id}/edit, or the dashboard), or, on an inbound message, the contact's own edit. Null when it was never edited.

Example:

"2026-05-17T14:25:00.000000Z"

edit_count
integer
default:0

How many of your edits reached the recipient. iMessage allows five per message. Always 0 on an inbound message.

Required range: 0 <= x <= 5
edit_status
enum<string> | null

pending = your latest edit is on its way (body already shows the new text); failed = it didn't reach the recipient, and body is back to the text they have (webhook message.edit_failed); edited = the message was edited. Null when it never was.

Available options:
pending,
edited,
failed
unsend_status
enum<string> | null

pending = your Undo Send is on its way; unsent = the whole message was unsent (by you, or by the contact on an inbound message) and body is empty; partly_unsent = part of it was (its text, a link preview or a file); failed = your Undo Send didn't happen and the recipient still has the message (webhook message.unsend_failed). Null when nothing was unsent.

Available options:
pending,
unsent,
partly_unsent,
failed
unsent_at
string | null

ISO 8601 UTC timestamp of when the message (or the latest part of it) was unsent. Null unless unsend_status is unsent or partly_unsent.

Example:

"2026-05-17T14:24:10.000000Z"