Skip to main content
POST

Unsend a message

Method: POST | Endpoint: /v1/messages/{message_id}/unsend Description: Take back a message you sent over iMessage, the way Undo Send works on an iPhone: within 110 seconds of sending. 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 of a message you sent: from Send a message, or from List messages, where messages sent from the dashboard appear too.
Never rely on Undo Send to retract sensitive information. A recipient on an older iPhone, iPad or Mac keeps the original message, and anyone may have read it, copied it or seen it in a notification in the seconds before. See What the recipient sees.

How it behaves

  • It’s accepted first, applied second. The answer is 202 with unsend_status: "pending", and body is already empty. A few seconds later the message is gone from the recipient’s conversation: message.unsent fires with direction: "outbound" and scope: "message", and unsend_status becomes unsent.
  • If it doesn’t land, the text comes back. message.unsend_failed fires, unsend_status becomes failed, and body returns: the recipient still has the message. Calling again tries once more, if the 110 seconds aren’t up.
  • It’s safe to call twice. While the Undo Send is pending, or once it’s done, calling again answers 200 with the message and changes nothing.
  • 110 seconds. Measured from sent_at. iMessage allows 2 minutes; iBubbl stops 10 seconds early so the Undo Send never arrives too late. After that, 422 unsend_window_expired.
  • Not while an edit is on its way. 409 edit_in_flight: wait for message.edited (a few seconds), then unsend.
  • iMessage only. A message that went out by SMS (channel: "sms", RCS included) can’t be unsent: 422 sms_not_supported. One that hasn’t gone out yet gets 422 not_sent_yet: cancel it instead.
  • One bubble at a time. A text message, or a message that is one file and nothing else, is unsent whole. A message with files and text, or several files, shows as several bubbles and gets 422 multi_part: unsending part of a message is available in the dashboard only, for now.
  • Messages with a link preview. A message typed in the dashboard with a link goes out as two bubbles, the text and a preview card. Unsending it takes back both. Messages sent through the API never have a preview card, so they are one bubble.
  • Opted-out contacts don’t block it. An Undo Send only removes content, so it works whatever the contact’s opt-out or block status. Groups work too.

Responses

202 Accepted The Undo Send is on its way. The body is the message with unsend_status: "pending". 200 OK The message is already unsent, or being unsent. The body is the message as it is; nothing new was sent. 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
An edit of this message hasn’t reached the recipient yet. Wait for message.edited (usually a few seconds), then unsend. Nothing changed. 422 Unprocessable Entity
Nothing changed. The error is one of:
  • invalid_target: the message is one you received. Only messages you sent can be unsent.
  • sms_not_supported: the message went out by SMS (RCS included), where messages can’t be unsent.
  • not_sent_yet: the message is still pending or queued. Cancel it instead.
  • not_unsendable: the message failed, was cancelled or expired, or iMessage never confirmed it.
  • unsend_window_expired: more than 110 seconds since sent_at.
  • multi_part: the message has files and text, or several files. Unsending part of a message is available in the dashboard only.
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

Already unsent or being unsent; nothing changed.

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"