Skip to main content
POST

Edit a message

Method: POST | Endpoint: /v1/messages/{message_id}/edit Description: Change the text of a message you sent over iMessage, the way you would on an iPhone: within 14 minutes 30 seconds of sending, up to five times. 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

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. Fields not listed above are rejected with 422 validation_error.

How it behaves

  • It’s accepted first, applied second. The answer is 202 with edit_status: "pending", and body already shows the new text. A few seconds later the edit reaches the recipient: message.edited fires with direction: "outbound", edit_status becomes edited, and edit_count goes up by one.
  • If it doesn’t land, the text comes back. message.edit_failed fires, edit_status becomes failed, and body goes back to the text the recipient has. You can edit again if the window is still open.
  • One edit at a time. While one is on its way, a different text gets 409 edit_in_flight; wait for message.edited and send the next.
  • It’s safe to call twice. Sending the text the message already has (a retry of an accepted edit included) answers 200 with the message and changes nothing. It doesn’t use up one of the five edits.
  • 14 minutes 30 seconds, five edits. Measured from sent_at. iMessage allows 15 minutes; iBubbl stops 30 seconds early so an edit never arrives too late to apply. After the fifth edit, 422 edit_limit_reached.
  • iMessage only. A message that went out by SMS (channel: "sms", RCS included) can’t be edited: 422 sms_not_supported. One that hasn’t gone out yet gets 422 not_sent_yet: cancel it and send a new one instead.
  • Text only. A message’s text can be edited, including the caption of a message with files. A message that is only files has nothing to edit (422 not_editable).
  • Messages with a link preview. A message typed in the dashboard with a link goes out as the text and a preview card. Only the text before the link can change: keep the link exactly as sent and at the end, or you get 422 link_changed. Messages sent through the API never have a preview card, so this doesn’t apply to them.
  • Groups work. In a group, only members using iMessage see the edit.
  • Blocked and opted-out contacts get none. An edit reaches the recipient as a change, so in a one-to-one conversation a blocked contact gets 422 contact_blocked and an opted-out one 422 contact_opted_out.
What the recipient sees depends on their software. On iOS 16, iPadOS 16.1, macOS 13 Ventura, visionOS 1 and later, the bubble changes in place and is marked Edited. On earlier versions they get a follow-up message starting “Edited to” with the new text in quotation marks, and the original stays. See What the recipient sees.

Responses

202 Accepted The edit is on its way. The body is the message with the new text and edit_status: "pending". edit_count and edited_at change when the edit reaches the recipient. 200 OK The message already has this text. The body is the message as it is; nothing was sent and no edit was used. 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
Nothing changed. The error is one of:
  • edit_in_flight: your previous edit hasn’t reached the recipient yet. Wait for message.edited (usually a few seconds), then send this one.
  • already_unsent: the message was unsent, so there’s nothing left to edit.
422 Unprocessable Entity
Nothing changed. The error is one of:
  • validation_error: no body, an empty one, more than 4,000 characters, or an unknown field.
  • invalid_target: the message is one you received. Only messages you sent can be edited.
  • sms_not_supported: the message went out by SMS (RCS included), where messages can’t be edited.
  • not_sent_yet: the message is still pending or queued. Cancel it and send a new one.
  • not_editable: the message has no text (only files), or it failed, was cancelled or expired, or iMessage never confirmed it.
  • edit_window_expired: more than 14 minutes 30 seconds since sent_at. Send a follow-up message instead.
  • edit_limit_reached: the message has been edited five times.
  • link_changed: a message sent with a link preview, where the new text moves, changes or removes the link.
  • contact_blocked: a one-to-one conversation with a contact you blocked.
  • contact_opted_out: a one-to-one conversation with a contact who opted out.
429 Too Many Requests
Wait the Retry-After header’s seconds, then send the same request again. 503 Service Unavailable
iBubbl couldn’t confirm the contact hasn’t opted out, so it changed nothing. Retry after a short backoff.

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

Body

application/json
body
string
required

The new text. It replaces the whole text of the message; leading and trailing whitespace is trimmed. For a message that went out with a link preview, keep the link exactly as sent and at the end, and change only the text before it. Fields not listed here are rejected with 422 validation_error.

Required string length: 1 - 4000

Response

The message already has this text; 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"