Every status an outbound message can have, which ones are final, which webhook fires for each, and how duplicates and retries behave.
A message you send with POST /v1/messages moves through a small set of statuses. The 202 Accepted response only means iBubbl has the message. It goes out later, from the Mac that runs your line, and its status keeps changing after that.Read the current status at any time with GET /v1/messages/{id}, or follow it with webhooks.
Accepted, but parked by a capacity hold. Today that means your line’s Mac is offline (line_offline). One-to-one messages only. iBubbl retries every 30 seconds.
No
message.queued with status: "queued", at acceptance
pending
Accepted and waiting for your line’s Mac to send it. This includes a message being held for the contact’s sending hours or the hourly new-contact limit.
No
message.queued with status: "pending", at acceptance (one-to-one only)
sent
Your line sent it.
No
message.sent
delivered
The recipient’s device confirmed delivery.
No
message.delivered
read
The recipient read it. A read is reported only when the recipient has read receipts on, and a plain SMS never reports one.
No. A late iMessage error can still turn it into failed, if no delivery receipt was recorded.
message.read
failed
It didn’t go out, or iMessage reported it undeliverable. failed_reason says why. That includes a queued message the queue’s retry fails.
Usually. A failure with an imessage_* reason moves to delivered or read if Apple later confirms the message. A few connection failures can move to sent if the Mac confirms the send late (see below).
message.failed, with failed_reason
expired
It was still queued about an hour after it was accepted, so iBubbl dropped it (failed_reason: "queue_expired").
Yes
message.failed, with status: "expired" and failed_reason: "queue_expired"
cancelled
Stopped before it went out. That happens when someone on your account cancels it in the dashboard, or when an after-hours auto-reply misses its window. There is no API endpoint to cancel a message.
Almost. It moves to sent only if your line had already sent it when the cancel arrived.
message.cancelled (it carries no reason)
message.queued fires once, when a one-to-one message is accepted, whether it starts as queued or pending. It does not fire again when a queued message moves to pending. Group sends never fire it.
held_until is when it is expected to go out. It is null when there is no estimate: for example, when no sending day is enabled.
Both are null when the message isn’t held. iBubbl marks holds within about 10 seconds of accepting a message, so the 202 response and an immediate GET usually show null. A held message has no separate status or webhook: you get message.sent when it goes out.
These changes happen without a webhook. If you need to know about them, poll GET /v1/messages/{id}, or list recent messages with GET /v1/messages, which returns each one’s current status. To catch up on webhooks you missed, see Recover from downtime.
queued → pending. The capacity hold cleared and the message is on its way. The next event is message.sent.
A group send is accepted. Groups never fire message.queued. Their first event is message.sent.
A read receipt marks earlier messages read. When the recipient reads a message, iBubbl also marks your earlier sent and delivered messages in that conversation read, but message.read fires only for the message the receipt was about.
A failed message heals straight to delivered or read. You get message.delivered or message.read, with no message.sent before it.
A queued message that ends without going out always fires message.failed. One still queued 60–65 minutes after it was accepted expires: the event carries status: "expired" and failed_reason: "queue_expired", and its idempotency_key is released, so send it again if it still matters. One the queue’s retry fails carries status: "failed" and the reason.A pending message never becomes expired. If its line’s Mac has been offline for more than an hour, it moves to failed with failed_reason: "agent_offline", and message.failed fires.
sent, delivered or read → failed. Messages.app can report an iMessage error after the message left your line: for example, the number isn’t on iMessage. The message becomes failed with failed_reasonimessage_undeliverable or imessage_error:<code>. This happens only when no delivery receipt was recorded.
failed → delivered or read. If a message that failed with an imessage_* reason is later confirmed by Apple, it moves to delivered or read.
failed → sent. A message failed as line_inactive or agent_offline, or as mac_send_failed / bluebubbles_unreachable within 30 minutes, moves to sent if the Mac reports that it did send it after all. message.sent fires.
cancelled → sent. The Mac had already sent it when the cancel arrived. message.sent fires.
Order events by the envelope’s created_at, and when in doubt treat GET /v1/messages/{id} as the truth. See ordering and duplicates.
Every send carries a required idempotency_key. Keys are scoped to your iBubbl account, shared by all of its API keys, and never expire. Full rules: Idempotency.
Earlier message with this key
Same key, same request
Same key, different request
None
Sent as a new message
—
failed or expired
The key is released: sent as a new message, with a new id
409 idempotency_conflict
Any other status, including cancelled
Nothing is sent. 202 with the original message, showing its current status
iBubbl never compares message bodies. Nothing in the API, the queue or the Mac drops a message because an identical one was sent a moment ago. A new idempotency_key with an identical body sends a second, real message, even milliseconds after the first. The idempotency key is the only thing that prevents duplicates.
Generate the key once per logical message, before the first attempt, and store it with the message in your system. Never generate it inside the retry loop.
Retry only network errors, timeouts and 5xx responses, with the same key and the identical request body.
Cap the attempts (3 to 5 is plenty) and back off between them: 1 s, 2 s, 4 s.
Don’t retry 4xx responses. They create no message, and repeating them won’t change the answer. See Errors.
If several workers can send the same message, make sure they share the stored key, rather than each generating its own.
If your request timed out, you don’t know whether iBubbl accepted it. Send it again with the same key and the same body. If the first request was accepted, you get 202 with that message and nothing is sent twice. If it wasn’t, this request sends it once. Either way the contact gets one message.
Two simultaneous requests with the same key. If two requests with one key arrive at the same moment (for example, a retry fired while the first request is still running), one of them can return 500 internal_error. Only one message is created. Retry the failed one with the same key: it returns the 202 replay.
New groups (participant_phones). For a send that creates a new group, the key is attached just after the message is created. If such a request times out, a retry can create a second group and a second message. Don’t retry a timed-out participant_phones send automatically. First wait for its message.sent webhook (its to_number is the group’s first participant), or check the conversation in the dashboard. Sends into an existing group (chat_guid) and one-to-one sends are not affected.
{"docsConfig":{"theme":"mint","$schema":"https://mintlify.com/docs.json","name":"iBubbl","colors":{"primary":"#3B82F6","light":"#60A5FA","dark":"#2563EB"},"logo":{"light":"https://mintcdn.com/ibubbl/vKOMJPyDWV9K1NYc/logo/light.png?fit=max\u0026auto=format\u0026n=vKOMJPyDWV9K1NYc\u0026q=85\u0026s=b8846160ee4db226f79f0e4f7f331870","dark":"https://mintcdn.com/ibubbl/vKOMJPyDWV9K1NYc/logo/dark.png?fit=max\u0026auto=format\u0026n=vKOMJPyDWV9K1NYc\u0026q=85\u0026s=0067c7441443a7b4d08cfd7357f9c219"},"favicon":"https://mintcdn.com/ibubbl/vKOMJPyDWV9K1NYc/favicon.ico?fit=max\u0026auto=format\u0026n=vKOMJPyDWV9K1NYc\u0026q=85\u0026s=e6bd8c62275573958611eefad9499589","api":{"openapi":"openapi/public.json","playground":{"display":"simple"},"examples":{"languages":["curl","typescript","python"]}},"navbar":{"links":[{"href":"https://ibubbl-dashboard.vercel.app","label":"Dashboard"}],"primary":{"type":"button","label":"Sign up","href":"https://ibubbl-dashboard.vercel.app/signup"}},"contextual":{"options":["copy","view","chatgpt","claude","mcp","cursor","vscode"],"display":"header"},"styling":{"latex":false},"navigation":{"pages":[]}},"deployment":{"deploymentMetadataValue":{"subdomain":"ibubbl","hasAudioNarration":false,"actualSubdomain":"ibubbl","customDomains":["docs.ibubbl.com"],"gitSource":{"type":"github","owner":"piter-chern","repo":"ibubbl-docs","deployBranch":"main","contentDirectory":"","isPrivate":false,"mountPath":"","label":""},"entitlements":{"AI_CHAT":{"status":"DISABLED"}},"buildId":"6af49652c5d18096e68004bc","clientVersion":"0.0.4102","searchFilterCounts":[{"language":"default","tag":"API reference","count":18},{"language":"default","tag":"Contacts","count":10},{"language":"default","tag":"Concepts","count":9},{"language":"default","tag":"Guides","count":7},{"language":"default","tag":"Webhooks","count":6},{"language":"default","tag":"Resources","count":5},{"language":"default","tag":"Get started","count":4},{"language":"default","tag":"Messages","count":3},{"language":"default","tag":"Inbound events","count":1}]},"assistantConfig":{"enableAskAiButton":false,"enableStarterQuestions":false,"enableContextualStarterQuestions":false,"enableAskAiSearchBar":true},"basePath":"","hasChatPermissions":false,"hasWebMcpPermissions":false,"toggles":[{"name":"kill-isr","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"custom-rbac-roles","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"agent-mintlify-slack","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"agent-integrations-settings","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"rbac-expansion","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"automations-pr-grouping","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"custom-prefix","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"analytics-v7","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"user-flows-analytics","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"no-publish-flow","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"workos-onboarding","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"llms-txt-v2","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"snapshot-serving-live","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"stripTemperatureMiddleware","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"support-platform-pylon-zendesk","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"snapshot-serving","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"instant-rollback","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"granular-notifications-server","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"granular-notifications","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"automations-fable","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"fixed-ai-action-pricing","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"automation-resolve-merge-conflict","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"scoped-publishing","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"mintql","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"page-level-analytics","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"editor-version-history-v2","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"billing-page-v2","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"full-page-search","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"workspace-projects","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"assistant-v5","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"fork-pr-previews","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"revalidation-jwt","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"workos-sso-scim","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"hobby-abuse-scan","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"dashboard-inbox","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"workos-signup","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"turbopuffer-dual-write","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"workos-enabled","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"approve-review-flow","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"editor-metadata","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"sso-setup-deadline-banner","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"turbopuffer-serve","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"live-agents","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false}],"googleFontsInlined":false},"customCss":[],"page":{"pagePath":"concepts/message-lifecycle","slug":"/concepts/message-lifecycle","pageMetadata":{"title":"Message lifecycle","description":"Every status an outbound message can have, which ones are final, which webhook fires for each, and how duplicates and retries behave.","href":"/concepts/message-lifecycle"},"description":{"compiledSource":"\"use strict\";\nconst {jsx: _jsx} = arguments[0];\nconst {useMDXComponents: _provideComponents} = arguments[0];\nfunction _createMdxContent(props) {\n const _components = {\n p: \"p\",\n ..._provideComponents(),\n ...props.components\n };\n return _jsx(_components.p, {\n children: \"Every status an outbound message can have, which ones are final, which webhook fires for each, and how duplicates and retries behave.\"\n });\n}\nfunction MDXContent(props = {}) {\n const {wrapper: MDXLayout} = {\n ..._provideComponents(),\n ...props.components\n };\n return MDXLayout ? _jsx(MDXLayout, {\n ...props,\n children: _jsx(_createMdxContent, {\n ...props\n })\n }) : _createMdxContent(props);\n}\nreturn {\n default: MDXContent\n};\n","frontmatter":{},"scope":{}},"pageType":"default","mdx":{"mdxSource":{"compiledSource":"\"use strict\";const{Fragment:_Fragment,jsx:_jsx,jsxs:_jsxs}=arguments[0];const{useMDXComponents:_provideComponents}=arguments[0];function _createMdxContent(props){const _components={a:`a`,code:`code`,li:`li`,p:`p`,strong:`strong`,tbody:`tbody`,td:`td`,th:`th`,thead:`thead`,tr:`tr`,ul:`ul`,..._provideComponents(),...props.components},{Heading,Mermaid,Note,Table,Warning}=_components;if(!Heading)_missingMdxReference(`Heading`,true);if(!Mermaid)_missingMdxReference(`Mermaid`,true);if(!Note)_missingMdxReference(`Note`,true);if(!Table)_missingMdxReference(`Table`,true);if(!Warning)_missingMdxReference(`Warning`,true);return _jsxs(_Fragment,{children:[_jsxs(_components.p,{children:[`A message you send with `,_jsx(_components.a,{href:`/api-reference/messages/send`,children:_jsx(_components.code,{children:`POST /v1/messages`})}),` moves through a small set of statuses. The `,_jsx(_components.code,{children:`202 Accepted`}),` response only means iBubbl has the message. It goes out later, from the Mac that runs your line, and its status keeps changing after that.`]}),`\n`,_jsxs(_components.p,{children:[`Read the current status at any time with `,_jsx(_components.a,{href:`/api-reference/messages/retrieve`,children:_jsx(_components.code,{children:`GET /v1/messages/{id}`})}),`, or follow it with `,_jsx(_components.a,{href:`/webhooks/events`,children:`webhooks`}),`.`]}),`\n`,_jsx(Heading,{level:`2`,id:`the-diagram`,children:`The diagram`}),`\n`,_jsx(Mermaid,{chart:`flowchart TD\n accepted((\"POST\")) --> queued\n accepted --> pending\n queued --> pending\n pending --> sent\n sent --> delivered\n delivered --> read\n queued --> expired\n queued --> failed\n pending --> failed\n queued --> cancelled\n pending --> cancelled\n sent -.-> failed\n delivered -.-> failed\n read -.-> failed\n failed -.-> sent\n failed -.-> delivered\n failed -.-> read\n cancelled -.-> sent`}),`\n`,_jsxs(_components.p,{children:[`Solid arrows are the normal path. Dashed arrows are the `,_jsx(_components.a,{href:`#statuses-that-move-backwards`,children:`moves that go backwards`}),`, which are rare.`]}),`\n`,_jsx(Heading,{level:`2`,id:`statuses`,children:`Statuses`}),`\n`,_jsxs(Table,{children:[_jsx(_components.thead,{children:_jsxs(_components.tr,{children:[_jsx(_components.th,{children:`Status`}),_jsx(_components.th,{children:`Description`}),_jsx(_components.th,{children:`Is final?`}),_jsx(_components.th,{children:`Webhook emitted`})]})}),_jsxs(_components.tbody,{children:[_jsxs(_components.tr,{children:[_jsx(_components.td,{children:_jsx(_components.code,{children:`queued`})}),_jsxs(_components.td,{children:[`Accepted, but parked by a capacity hold. Today that means your line’s Mac is offline (`,_jsx(_components.code,{children:`line_offline`}),`). One-to-one messages only. iBubbl retries every 30 seconds.`]}),_jsx(_components.td,{children:`No`}),_jsxs(_components.td,{children:[_jsx(_components.code,{children:`message.queued`}),` with `,_jsx(_components.code,{children:`status: \"queued\"`}),`, at acceptance`]})]}),_jsxs(_components.tr,{children:[_jsx(_components.td,{children:_jsx(_components.code,{children:`pending`})}),_jsxs(_components.td,{children:[`Accepted and waiting for your line’s Mac to send it. This includes a message being `,_jsx(_components.a,{href:`#held-messages`,children:`held`}),` for the contact’s sending hours or the hourly new-contact limit.`]}),_jsx(_components.td,{children:`No`}),_jsxs(_components.td,{children:[_jsx(_components.code,{children:`message.queued`}),` with `,_jsx(_components.code,{children:`status: \"pending\"`}),`, at acceptance (one-to-one only)`]})]}),_jsxs(_components.tr,{children:[_jsx(_components.td,{children:_jsx(_components.code,{children:`sent`})}),_jsx(_components.td,{children:`Your line sent it.`}),_jsx(_components.td,{children:`No`}),_jsx(_components.td,{children:_jsx(_components.code,{children:`message.sent`})})]}),_jsxs(_components.tr,{children:[_jsx(_components.td,{children:_jsx(_components.code,{children:`delivered`})}),_jsx(_components.td,{children:`The recipient’s device confirmed delivery.`}),_jsx(_components.td,{children:`No`}),_jsx(_components.td,{children:_jsx(_components.code,{children:`message.delivered`})})]}),_jsxs(_components.tr,{children:[_jsx(_components.td,{children:_jsx(_components.code,{children:`read`})}),_jsx(_components.td,{children:`The recipient read it. A read is reported only when the recipient has read receipts on, and a plain SMS never reports one.`}),_jsxs(_components.td,{children:[_jsx(_components.strong,{children:`No.`}),` A late iMessage error can still turn it into `,_jsx(_components.code,{children:`failed`}),`, if no delivery receipt was recorded.`]}),_jsx(_components.td,{children:_jsx(_components.code,{children:`message.read`})})]}),_jsxs(_components.tr,{children:[_jsx(_components.td,{children:_jsx(_components.code,{children:`failed`})}),_jsxs(_components.td,{children:[`It didn’t go out, or iMessage reported it undeliverable. `,_jsx(_components.code,{children:`failed_reason`}),` says why. That includes a `,_jsx(_components.code,{children:`queued`}),` message the queue’s retry fails.`]}),_jsxs(_components.td,{children:[_jsx(_components.strong,{children:`Usually.`}),` A failure with an `,_jsx(_components.code,{children:`imessage_*`}),` reason moves to `,_jsx(_components.code,{children:`delivered`}),` or `,_jsx(_components.code,{children:`read`}),` if Apple later confirms the message. A few connection failures can move to `,_jsx(_components.code,{children:`sent`}),` if the Mac confirms the send late (see below).`]}),_jsxs(_components.td,{children:[_jsx(_components.code,{children:`message.failed`}),`, with `,_jsx(_components.code,{children:`failed_reason`})]})]}),_jsxs(_components.tr,{children:[_jsx(_components.td,{children:_jsx(_components.code,{children:`expired`})}),_jsxs(_components.td,{children:[`It was still `,_jsx(_components.code,{children:`queued`}),` about an hour after it was accepted, so iBubbl dropped it (`,_jsx(_components.code,{children:`failed_reason: \"queue_expired\"`}),`).`]}),_jsx(_components.td,{children:_jsx(_components.strong,{children:`Yes`})}),_jsxs(_components.td,{children:[_jsx(_components.code,{children:`message.failed`}),`, with `,_jsx(_components.code,{children:`status: \"expired\"`}),` and `,_jsx(_components.code,{children:`failed_reason: \"queue_expired\"`})]})]}),_jsxs(_components.tr,{children:[_jsx(_components.td,{children:_jsx(_components.code,{children:`cancelled`})}),_jsxs(_components.td,{children:[`Stopped before it went out. That happens when someone on your account cancels it in the dashboard, or when an `,_jsx(_components.a,{href:`/concepts/after-hours-reply`,children:`after-hours auto-reply`}),` misses its window. There is no API endpoint to cancel a message.`]}),_jsxs(_components.td,{children:[_jsx(_components.strong,{children:`Almost.`}),` It moves to `,_jsx(_components.code,{children:`sent`}),` only if your line had already sent it when the cancel arrived.`]}),_jsxs(_components.td,{children:[_jsx(_components.code,{children:`message.cancelled`}),` (it carries no reason)`]})]})]})]}),`\n`,_jsx(Note,{children:_jsxs(_components.p,{children:[_jsx(_components.code,{children:`message.queued`}),` fires once, when a one-to-one message is accepted, whether it starts as `,_jsx(_components.code,{children:`queued`}),` or `,_jsx(_components.code,{children:`pending`}),`. It does not fire again when a `,_jsx(_components.code,{children:`queued`}),` message moves to `,_jsx(_components.code,{children:`pending`}),`. Group sends never fire it.`]})}),`\n`,_jsx(Heading,{level:`2`,id:`held-messages`,children:`Held messages`}),`\n`,_jsxs(_components.p,{children:[`A `,_jsx(_components.code,{children:`pending`}),` message can wait before it goes out. `,_jsx(_components.code,{children:`GET /v1/messages/{id}`}),` shows why with two fields:`]}),`\n`,_jsxs(_components.ul,{children:[`\n`,_jsxs(_components.li,{children:[_jsx(_components.code,{children:`held_reason`}),` is `,_jsx(_components.code,{children:`working_hours`}),` (outside the contact’s `,_jsx(_components.a,{href:`/concepts/working-hours`,children:`sending hours`}),`) or `,_jsx(_components.code,{children:`hourly_limit`}),` (the line has used its `,_jsx(_components.a,{href:`/concepts/limits`,children:`hourly new-contact limit`}),`).`]}),`\n`,_jsxs(_components.li,{children:[_jsx(_components.code,{children:`held_until`}),` is when it is expected to go out. It is `,_jsx(_components.code,{children:`null`}),` when there is no estimate: for example, when no sending day is enabled.`]}),`\n`]}),`\n`,_jsxs(_components.p,{children:[`Both are `,_jsx(_components.code,{children:`null`}),` when the message isn’t held. iBubbl marks holds within about 10 seconds of accepting a message, so the `,_jsx(_components.code,{children:`202`}),` response and an immediate `,_jsx(_components.code,{children:`GET`}),` usually show `,_jsx(_components.code,{children:`null`}),`. A held message has no separate status or webhook: you get `,_jsx(_components.code,{children:`message.sent`}),` when it goes out.`]}),`\n`,_jsx(Heading,{level:`2`,id:`events-that-don’t-fire`,children:`Events that don’t fire`}),`\n`,_jsxs(_components.p,{children:[`These changes happen without a webhook. If you need to know about them, poll `,_jsx(_components.code,{children:`GET /v1/messages/{id}`}),`, or list recent messages with `,_jsx(_components.a,{href:`/api-reference/messages/list`,children:_jsx(_components.code,{children:`GET /v1/messages`})}),`, which returns each one’s current status. To catch up on webhooks you missed, see `,_jsx(_components.a,{href:`/guides/webhook-retries#recover-from-downtime`,children:`Recover from downtime`}),`.`]}),`\n`,_jsxs(_components.ul,{children:[`\n`,_jsxs(_components.li,{children:[_jsxs(_components.strong,{children:[_jsx(_components.code,{children:`queued`}),` → `,_jsx(_components.code,{children:`pending`}),`.`]}),` The capacity hold cleared and the message is on its way. The next event is `,_jsx(_components.code,{children:`message.sent`}),`.`]}),`\n`,_jsxs(_components.li,{children:[_jsx(_components.strong,{children:`A group send is accepted.`}),` Groups never fire `,_jsx(_components.code,{children:`message.queued`}),`. Their first event is `,_jsx(_components.code,{children:`message.sent`}),`.`]}),`\n`,_jsxs(_components.li,{children:[_jsx(_components.strong,{children:`A read receipt marks earlier messages read.`}),` When the recipient reads a message, iBubbl also marks your earlier `,_jsx(_components.code,{children:`sent`}),` and `,_jsx(_components.code,{children:`delivered`}),` messages in that conversation `,_jsx(_components.code,{children:`read`}),`, but `,_jsx(_components.code,{children:`message.read`}),` fires only for the message the receipt was about.`]}),`\n`,_jsxs(_components.li,{children:[_jsxs(_components.strong,{children:[`A failed message heals straight to `,_jsx(_components.code,{children:`delivered`}),` or `,_jsx(_components.code,{children:`read`}),`.`]}),` You get `,_jsx(_components.code,{children:`message.delivered`}),` or `,_jsx(_components.code,{children:`message.read`}),`, with no `,_jsx(_components.code,{children:`message.sent`}),` before it.`]}),`\n`]}),`\n`,_jsxs(_components.p,{children:[`A `,_jsx(_components.code,{children:`queued`}),` message that ends without going out always fires `,_jsx(_components.code,{children:`message.failed`}),`. One still `,_jsx(_components.code,{children:`queued`}),` 60–65 minutes after it was accepted expires: the event carries `,_jsx(_components.code,{children:`status: \"expired\"`}),` and `,_jsx(_components.code,{children:`failed_reason: \"queue_expired\"`}),`, and its `,_jsx(_components.code,{children:`idempotency_key`}),` is released, so send it again if it still matters. One the queue’s retry fails carries `,_jsx(_components.code,{children:`status: \"failed\"`}),` and the reason.`]}),`\n`,_jsxs(_components.p,{children:[`A `,_jsx(_components.code,{children:`pending`}),` message never becomes `,_jsx(_components.code,{children:`expired`}),`. If its line’s Mac has been offline for more than an hour, it moves to `,_jsx(_components.code,{children:`failed`}),` with `,_jsx(_components.code,{children:`failed_reason: \"agent_offline\"`}),`, and `,_jsx(_components.code,{children:`message.failed`}),` fires.`]}),`\n`,_jsx(Heading,{level:`2`,id:`statuses-that-move-backwards`,children:`Statuses that move backwards`}),`\n`,_jsx(_components.p,{children:`Most messages only move forward. The exceptions:`}),`\n`,_jsxs(_components.ul,{children:[`\n`,_jsxs(_components.li,{children:[_jsxs(_components.strong,{children:[_jsx(_components.code,{children:`sent`}),`, `,_jsx(_components.code,{children:`delivered`}),` or `,_jsx(_components.code,{children:`read`}),` → `,_jsx(_components.code,{children:`failed`}),`.`]}),` Messages.app can report an iMessage error after the message left your line: for example, the number isn’t on iMessage. The message becomes `,_jsx(_components.code,{children:`failed`}),` with `,_jsx(_components.code,{children:`failed_reason`}),` `,_jsx(_components.code,{children:`imessage_undeliverable`}),` or `,_jsx(_components.code,{children:`imessage_error:\u003ccode>`}),`. This happens only when no delivery receipt was recorded.`]}),`\n`,_jsxs(_components.li,{children:[_jsxs(_components.strong,{children:[_jsx(_components.code,{children:`failed`}),` → `,_jsx(_components.code,{children:`delivered`}),` or `,_jsx(_components.code,{children:`read`}),`.`]}),` If a message that failed with an `,_jsx(_components.code,{children:`imessage_*`}),` reason is later confirmed by Apple, it moves to `,_jsx(_components.code,{children:`delivered`}),` or `,_jsx(_components.code,{children:`read`}),`.`]}),`\n`,_jsxs(_components.li,{children:[_jsxs(_components.strong,{children:[_jsx(_components.code,{children:`failed`}),` → `,_jsx(_components.code,{children:`sent`}),`.`]}),` A message failed as `,_jsx(_components.code,{children:`line_inactive`}),` or `,_jsx(_components.code,{children:`agent_offline`}),`, or as `,_jsx(_components.code,{children:`mac_send_failed`}),` / `,_jsx(_components.code,{children:`bluebubbles_unreachable`}),` within 30 minutes, moves to `,_jsx(_components.code,{children:`sent`}),` if the Mac reports that it did send it after all. `,_jsx(_components.code,{children:`message.sent`}),` fires.`]}),`\n`,_jsxs(_components.li,{children:[_jsxs(_components.strong,{children:[_jsx(_components.code,{children:`cancelled`}),` → `,_jsx(_components.code,{children:`sent`}),`.`]}),` The Mac had already sent it when the cancel arrived. `,_jsx(_components.code,{children:`message.sent`}),` fires.`]}),`\n`]}),`\n`,_jsxs(_components.p,{children:[`Order events by the envelope’s `,_jsx(_components.code,{children:`created_at`}),`, and when in doubt treat `,_jsx(_components.code,{children:`GET /v1/messages/{id}`}),` as the truth. See `,_jsx(_components.a,{href:`/webhooks/events#ordering-and-duplicates`,children:`ordering and duplicates`}),`.`]}),`\n`,_jsx(Heading,{level:`2`,id:`duplicates-and-retries`,children:`Duplicates and retries`}),`\n`,_jsx(Heading,{level:`3`,id:`what-the-idempotency-key-does`,children:`What the idempotency key does`}),`\n`,_jsxs(_components.p,{children:[`Every send carries a required `,_jsx(_components.code,{children:`idempotency_key`}),`. Keys are scoped to your iBubbl account, shared by all of its API keys, and never expire. Full rules: `,_jsx(_components.a,{href:`/idempotency`,children:`Idempotency`}),`.`]}),`\n`,_jsxs(Table,{children:[_jsx(_components.thead,{children:_jsxs(_components.tr,{children:[_jsx(_components.th,{children:`Earlier message with this key`}),_jsx(_components.th,{children:`Same key, same request`}),_jsx(_components.th,{children:`Same key, different request`})]})}),_jsxs(_components.tbody,{children:[_jsxs(_components.tr,{children:[_jsx(_components.td,{children:`None`}),_jsx(_components.td,{children:`Sent as a new message`}),_jsx(_components.td,{children:`—`})]}),_jsxs(_components.tr,{children:[_jsxs(_components.td,{children:[_jsx(_components.code,{children:`failed`}),` or `,_jsx(_components.code,{children:`expired`})]}),_jsxs(_components.td,{children:[`The key is released: sent as a `,_jsx(_components.strong,{children:`new`}),` message, with a new `,_jsx(_components.code,{children:`id`})]}),_jsx(_components.td,{children:_jsx(_components.code,{children:`409 idempotency_conflict`})})]}),_jsxs(_components.tr,{children:[_jsxs(_components.td,{children:[`Any other status, `,_jsxs(_components.strong,{children:[`including `,_jsx(_components.code,{children:`cancelled`})]})]}),_jsxs(_components.td,{children:[`Nothing is sent. `,_jsx(_components.code,{children:`202`}),` with the original message, showing its `,_jsx(_components.strong,{children:`current`}),` status`]}),_jsx(_components.td,{children:_jsx(_components.code,{children:`409 idempotency_conflict`})})]})]})]}),`\n`,_jsx(Heading,{level:`3`,id:`there-is-no-content-deduplication`,children:`There is no content deduplication`}),`\n`,_jsxs(_components.p,{children:[`iBubbl never compares message bodies. Nothing in the API, the queue or the Mac drops a message because an identical one was sent a moment ago. `,_jsxs(_components.strong,{children:[`A new `,_jsx(_components.code,{children:`idempotency_key`}),` with an identical body sends a second, real message, even milliseconds after the first.`]}),` The idempotency key is the only thing that prevents duplicates.`]}),`\n`,_jsx(Heading,{level:`3`,id:`avoid-retry-storms`,children:`Avoid retry storms`}),`\n`,_jsxs(_components.ul,{children:[`\n`,_jsxs(_components.li,{children:[`Generate the key `,_jsx(_components.strong,{children:`once per logical message, before the first attempt`}),`, and store it with the message in your system. Never generate it inside the retry loop.`]}),`\n`,_jsxs(_components.li,{children:[`Retry only network errors, timeouts and `,_jsx(_components.code,{children:`5xx`}),` responses, with the same key and the identical request body.`]}),`\n`,_jsx(_components.li,{children:`Cap the attempts (3 to 5 is plenty) and back off between them: 1 s, 2 s, 4 s.`}),`\n`,_jsxs(_components.li,{children:[`Don’t retry `,_jsx(_components.code,{children:`4xx`}),` responses. They create no message, and repeating them won’t change the answer. See `,_jsx(_components.a,{href:`/errors`,children:`Errors`}),`.`]}),`\n`,_jsx(_components.li,{children:`If several workers can send the same message, make sure they share the stored key, rather than each generating its own.`}),`\n`]}),`\n`,_jsx(Heading,{level:`3`,id:`after-a-timed-out-request`,children:`After a timed-out request`}),`\n`,_jsxs(_components.p,{children:[`If your request timed out, you don’t know whether iBubbl accepted it. `,_jsx(_components.strong,{children:`Send it again with the same key and the same body.`}),` If the first request was accepted, you get `,_jsx(_components.code,{children:`202`}),` with that message and nothing is sent twice. If it wasn’t, this request sends it once. Either way the contact gets one message.`]}),`\n`,_jsx(Heading,{level:`3`,id:`two-known-edge-cases`,children:`Two known edge cases`}),`\n`,_jsx(Warning,{children:_jsxs(_components.p,{children:[_jsx(_components.strong,{children:`Two simultaneous requests with the same key.`}),` If two requests with one key arrive at the same moment (for example, a retry fired while the first request is still running), one of them can return `,_jsx(_components.code,{children:`500 internal_error`}),`. Only one message is created. Retry the failed one with the same key: it returns the `,_jsx(_components.code,{children:`202`}),` replay.`]})}),`\n`,_jsx(Warning,{children:_jsxs(_components.p,{children:[_jsxs(_components.strong,{children:[`New groups (`,_jsx(_components.code,{children:`participant_phones`}),`).`]}),` For a send that creates a new group, the key is attached just after the message is created. If such a request times out, a retry can create a `,_jsx(_components.strong,{children:`second group and a second message`}),`. Don’t retry a timed-out `,_jsx(_components.code,{children:`participant_phones`}),` send automatically. First wait for its `,_jsx(_components.code,{children:`message.sent`}),` webhook (its `,_jsx(_components.code,{children:`to_number`}),` is the group’s first participant), or check the conversation in the dashboard. Sends into an existing group (`,_jsx(_components.code,{children:`chat_guid`}),`) and one-to-one sends are not affected.`]})}),`\n`,_jsx(Heading,{level:`2`,id:`next`,children:`Next`}),`\n`,_jsxs(_components.ul,{children:[`\n`,_jsxs(_components.li,{children:[_jsx(_components.a,{href:`/concepts/limits`,children:`Limits and queues`}),`: what holds or refuses a send.`]}),`\n`,_jsxs(_components.li,{children:[_jsx(_components.a,{href:`/concepts/lines-and-routing`,children:`Lines and routing`}),`: which line sends, and whether it goes as iMessage or SMS.`]}),`\n`,_jsxs(_components.li,{children:[_jsx(_components.a,{href:`/webhooks/events`,children:`Webhook events`}),`: the payload of every event above.`]}),`\n`]})]})}function MDXContent(props={}){const{wrapper:MDXLayout}={..._provideComponents(),...props.components};return MDXLayout?_jsx(MDXLayout,{...props,children:_jsx(_createMdxContent,{...props})}):_createMdxContent(props)}return{default:MDXContent};function _missingMdxReference(id,component){throw new Error(`Expected `+(component?`component`:`object`)+\" `\"+id+\"` to be defined: you likely forgot to import, pass, or provide it.\")}","frontmatter":{},"scope":{"config":{},"pageMetadata":{"title":"Message lifecycle","description":"Every status an outbound message can have, which ones are final, which webhook fires for each, and how duplicates and retries behave.","href":"/concepts/message-lifecycle"}}},"mdxSourceWithNoJs":null},"mdxExtracts":{"tableOfContents":[{"title":"The diagram","slug":"the-diagram","depth":2,"children":[]},{"title":"Statuses","slug":"statuses","depth":2,"children":[]},{"title":"Held messages","slug":"held-messages","depth":2,"children":[]},{"title":"Events that don’t fire","slug":"events-that-don’t-fire","depth":2,"children":[]},{"title":"Statuses that move backwards","slug":"statuses-that-move-backwards","depth":2,"children":[]},{"title":"Duplicates and retries","slug":"duplicates-and-retries","depth":2,"children":[{"title":"What the idempotency key does","slug":"what-the-idempotency-key-does","depth":3,"children":[]},{"title":"There is no content deduplication","slug":"there-is-no-content-deduplication","depth":3,"children":[]},{"title":"Avoid retry storms","slug":"avoid-retry-storms","depth":3,"children":[]},{"title":"After a timed-out request","slug":"after-a-timed-out-request","depth":3,"children":[]},{"title":"Two known edge cases","slug":"two-known-edge-cases","depth":3,"children":[]}]},{"title":"Next","slug":"next","depth":2,"children":[]}],"codeExamples":{}},"jsLinks":[],"subdomain":"ibubbl","siteName":"iBubbl","title":"Message lifecycle - iBubbl","navMaps":{"firstHrefInVersion":{},"firstHrefInLanguage":{}},"lastModified":"2026-11-11T01:11:05.078Z","originalFileLocation":"concepts/message-lifecycle.mdx"},"headHtml":"\u003cscript data-mint-head>(function(){var d=document.documentElement,p=null;if(!p){try{p=localStorage.getItem(\"isDarkMode\")}catch(e){}if(p==='true')p='dark';else if(p==='false')p='light';if(p!=='light'\u0026\u0026p!=='dark'\u0026\u0026p!=='system')p=\"system\"}var r=p==='system'?(matchMedia(\"(prefers-color-scheme: dark)\").matches?'dark':'light'):p;d.classList.remove(r==='dark'?'light':'dark');d.classList.add(r);d.setAttribute(\"data-theme-preference\",p)})();\u003c/script>\u003cmeta data-mint-head name=\"description\" content=\"Every status an outbound message can have, which ones are final, which webhook fires for each, and how duplicates and retries behave.\">\u003cmeta data-mint-head name=\"application-name\" content=\"iBubbl\">\u003cmeta data-mint-head name=\"generator\" content=\"Mintlify\">\u003clink data-mint-head rel=\"icon\" type=\"image/png\" sizes=\"192x192\" href=\"/mintlify-assets/_mintlify/favicons/ibubbl/ohHXW5JhUEQpLDyU/_generated/favicon/android-chrome-192x192.png\" media=\"(prefers-color-scheme: light)\">\u003clink data-mint-head rel=\"apple-touch-icon\" type=\"image/png\" sizes=\"180x180\" href=\"/mintlify-assets/_mintlify/favicons/ibubbl/ohHXW5JhUEQpLDyU/_generated/favicon/apple-touch-icon.png\">\u003clink data-mint-head rel=\"icon\" type=\"image/png\" sizes=\"16x16\" href=\"/mintlify-assets/_mintlify/favicons/ibubbl/ohHXW5JhUEQpLDyU/_generated/favicon/favicon-16x16.png\" media=\"(prefers-color-scheme: light)\">\u003clink data-mint-head rel=\"icon\" type=\"image/png\" sizes=\"32x32\" href=\"/mintlify-assets/_mintlify/favicons/ibubbl/ohHXW5JhUEQpLDyU/_generated/favicon/favicon-32x32.png\" media=\"(prefers-color-scheme: light)\">\u003clink data-mint-head rel=\"shortcut icon\" type=\"image/x-icon\" href=\"/mintlify-assets/_mintlify/favicons/ibubbl/ohHXW5JhUEQpLDyU/_generated/favicon/favicon.ico\" media=\"(prefers-color-scheme: light)\">\u003clink data-mint-head rel=\"icon\" type=\"image/png\" sizes=\"192x192\" href=\"/mintlify-assets/_mintlify/favicons/ibubbl/ohHXW5JhUEQpLDyU/_generated/favicon-dark/android-chrome-192x192.png\" media=\"(prefers-color-scheme: dark)\">\u003clink data-mint-head rel=\"icon\" type=\"image/png\" sizes=\"16x16\" href=\"/mintlify-assets/_mintlify/favicons/ibubbl/ohHXW5JhUEQpLDyU/_generated/favicon-dark/favicon-16x16.png\" media=\"(prefers-color-scheme: dark)\">\u003clink data-mint-head rel=\"icon\" type=\"image/png\" sizes=\"32x32\" href=\"/mintlify-assets/_mintlify/favicons/ibubbl/ohHXW5JhUEQpLDyU/_generated/favicon-dark/favicon-32x32.png\" media=\"(prefers-color-scheme: dark)\">\u003clink data-mint-head rel=\"shortcut icon\" type=\"image/x-icon\" href=\"/mintlify-assets/_mintlify/favicons/ibubbl/ohHXW5JhUEQpLDyU/_generated/favicon-dark/favicon.ico\" media=\"(prefers-color-scheme: dark)\">\u003cmeta data-mint-head property=\"og:title\" content=\"Message lifecycle - iBubbl\">\u003cmeta data-mint-head property=\"og:description\" content=\"Every status an outbound message can have, which ones are final, which webhook fires for each, and how duplicates and retries behave.\">\u003cmeta data-mint-head property=\"og:site_name\" content=\"iBubbl\">\u003cmeta data-mint-head property=\"og:url\" content=\"https://docs.ibubbl.com/concepts/message-lifecycle\">\u003cmeta data-mint-head property=\"og:type\" content=\"website\">\u003cmeta data-mint-head property=\"og:image\" content=\"https://ibubbl.mintlify.site/_mintlify/api/og/v2?division=Concepts\u0026amp;title=Message+lifecycle\u0026amp;description=Every+status+an+outbound+message+can+have%2C+which+ones+are+final%2C+which+webhook+fires+for+each%2C+and+how+duplicates+and+retries+behave.\u0026amp;theme=f46070295e80383f74aa319a\u0026amp;signature=42b3da12a49ec5163473f4e97b8b13ce907089c2146e0e978ac477d725e285b8\">\u003cmeta data-mint-head property=\"og:image:width\" content=\"1200\">\u003cmeta data-mint-head property=\"og:image:height\" content=\"630\">\u003cmeta data-mint-head name=\"twitter:card\" content=\"summary_large_image\">\u003cmeta data-mint-head name=\"twitter:title\" content=\"Message lifecycle - iBubbl\">\u003cmeta data-mint-head name=\"twitter:description\" content=\"Every status an outbound message can have, which ones are final, which webhook fires for each, and how duplicates and retries behave.\">\u003cmeta data-mint-head name=\"twitter:image\" content=\"https://ibubbl.mintlify.site/_mintlify/api/og/v2?division=Concepts\u0026amp;title=Message+lifecycle\u0026amp;description=Every+status+an+outbound+message+can+have%2C+which+ones+are+final%2C+which+webhook+fires+for+each%2C+and+how+duplicates+and+retries+behave.\u0026amp;theme=f46070295e80383f74aa319a\u0026amp;signature=42b3da12a49ec5163473f4e97b8b13ce907089c2146e0e978ac477d725e285b8\">\u003cmeta data-mint-head name=\"twitter:image:width\" content=\"1200\">\u003cmeta data-mint-head name=\"twitter:image:height\" content=\"630\">\u003cmeta data-mint-head name=\"msapplication-config\" content=\"/mintlify-assets/_mintlify/favicons/ibubbl/ohHXW5JhUEQpLDyU/_generated/favicon/browserconfig.xml\">\u003cmeta data-mint-head name=\"apple-mobile-web-app-title\" content=\"iBubbl\">\u003cmeta data-mint-head name=\"msapplication-TileColor\" content=\"#3B82F6\">\u003clink data-mint-head rel=\"canonical\" href=\"https://docs.ibubbl.com/concepts/message-lifecycle\">\u003clink data-mint-head rel=\"alternate\" type=\"application/xml\" href=\"/sitemap.xml\">\u003clink data-mint-head rel=\"alternate\" type=\"text/markdown\" href=\"/concepts/message-lifecycle.md\">\u003cscript data-mint-head type=\"application/ld+json\">{\"@context\":\"https://schema.org\",\"@graph\":[{\"@type\":\"Organization\",\"@id\":\"https://docs.ibubbl.com/#organization\",\"name\":\"iBubbl\",\"url\":\"https://docs.ibubbl.com\",\"logo\":{\"@type\":\"ImageObject\",\"url\":\"https://mintcdn.com/ibubbl/vKOMJPyDWV9K1NYc/logo/light.png?fit=max\u0026auto=format\u0026n=vKOMJPyDWV9K1NYc\u0026q=85\u0026s=b8846160ee4db226f79f0e4f7f331870\"}},{\"@type\":\"WebSite\",\"@id\":\"https://docs.ibubbl.com#website\",\"name\":\"iBubbl\",\"url\":\"https://docs.ibubbl.com\",\"publisher\":{\"@id\":\"https://docs.ibubbl.com/#organization\"}},{\"@type\":\"WebPage\",\"@id\":\"https://docs.ibubbl.com/concepts/message-lifecycle#webpage\",\"url\":\"https://docs.ibubbl.com/concepts/message-lifecycle\",\"name\":\"Message lifecycle\",\"description\":\"Every status an outbound message can have, which ones are final, which webhook fires for each, and how duplicates and retries behave.\",\"dateModified\":\"2026-11-11T01:11:05.078Z\",\"isPartOf\":{\"@id\":\"https://docs.ibubbl.com#website\"},\"breadcrumb\":{\"@id\":\"https://docs.ibubbl.com/concepts/message-lifecycle#breadcrumb\"}},{\"@type\":\"BreadcrumbList\",\"@id\":\"https://docs.ibubbl.com/concepts/message-lifecycle#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"iBubbl\",\"item\":\"https://docs.ibubbl.com\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Message lifecycle\",\"item\":\"https://docs.ibubbl.com/concepts/message-lifecycle\"}]},{\"@type\":[\"Article\",\"TechArticle\"],\"@id\":\"https://docs.ibubbl.com/concepts/message-lifecycle#article\",\"headline\":\"Message lifecycle\",\"name\":\"Message lifecycle\",\"description\":\"Every status an outbound message can have, which ones are final, which webhook fires for each, and how duplicates and retries behave.\",\"url\":\"https://docs.ibubbl.com/concepts/message-lifecycle\",\"mainEntityOfPage\":{\"@id\":\"https://docs.ibubbl.com/concepts/message-lifecycle#webpage\"},\"dateModified\":\"2026-11-11T01:11:05.078Z\",\"publisher\":{\"@id\":\"https://docs.ibubbl.com/#organization\"},\"isPartOf\":{\"@id\":\"https://docs.ibubbl.com#website\"}}]}\u003c/script>\u003cscript data-mint-head type=\"application/ld+json\">{\"@context\":\"https://schema.org\",\"@type\":\"WebSite\",\"name\":\"iBubbl\",\"publisher\":{\"@type\":\"Organization\",\"name\":\"iBubbl\"},\"provider\":{\"@type\":\"Organization\",\"name\":\"Mintlify\",\"url\":\"https://mintlify.com\"}}\u003c/script>","agentPointerHref":"/concepts/message-lifecycle.md","siteName":"iBubbl","nav":{"global":null,"groups":[{"group":"Get started","pages":[{"title":"Overview","href":"/introduction"},{"title":"Quickstart","href":"/quickstart"},{"title":"Authentication","href":"/authentication"},{"title":"Build with AI","description":"Give your coding agent the iBubbl docs, the OpenAPI spec and a system prompt with the rules that matter.","href":"/build-with-ai"}]},{"group":"Concepts","pages":[{"title":"Message lifecycle","href":"/concepts/message-lifecycle"},{"title":"Limits and queues","description":"The exact send limits on every iBubbl line, what happens when you reach them, and how fast a line can send.","href":"/concepts/limits"},{"title":"Lines and routing","href":"/concepts/lines-and-routing"},{"title":"Idempotency","href":"/idempotency"},{"title":"Pagination","href":"/concepts/pagination"},{"title":"Working Hours \u0026 Scheduling","href":"/concepts/working-hours"},{"title":"After-Hours Auto-Reply","href":"/concepts/after-hours-reply"},{"title":"Suppression \u0026 Opt-Outs","href":"/concepts/suppression"},{"title":"Group Conversations","href":"/concepts/group-conversations"}]},{"group":"Guides","pages":[{"title":"Send messages","href":"/guides/send-messages"},{"title":"Send attachments","href":"/guides/attachments"},{"title":"Tapbacks, Read Receipts \u0026 Typing Indicators","href":"/guides/reactions-read-receipts-typing"},{"title":"Receive messages","href":"/guides/receive-messages"},{"title":"Signature verification","href":"/guides/webhook-signatures"},{"title":"Test webhooks locally","href":"/guides/test-webhooks-locally"},{"title":"Retries and deduplication","href":"/guides/webhook-retries"},{"title":"Inbound events","href":"/inbound-events/overview"}]},{"group":"Webhooks","pages":[{"title":"Webhooks Overview","href":"/webhooks/overview"},{"title":"Webhook Events","href":"/webhooks/events"}]},{"group":"API reference","pages":[{"group":"Messages","pages":[{"title":"Send a message","openapi":"POST /v1/messages","href":"/api-reference/messages/send"},{"title":"List messages","openapi":"GET /v1/messages","href":"/api-reference/messages/list"},{"title":"Retrieve a message","openapi":"GET /v1/messages/{message_id}","href":"/api-reference/messages/retrieve"},{"title":"Add a tapback","openapi":"POST /v1/messages/{message_id}/reactions","href":"/api-reference/messages/react"},{"title":"Remove a tapback","openapi":"DELETE /v1/messages/{message_id}/reactions/{reaction_id}","href":"/api-reference/messages/remove-reaction"}]},{"group":"Contacts","pages":[{"title":"Create or update a contact","openapi":"POST /v1/contacts","href":"/api-reference/contacts/upsert"},{"title":"List contacts","openapi":"GET /v1/contacts","href":"/api-reference/contacts/list"},{"title":"Retrieve a contact","openapi":"GET /v1/contacts/{contact_id}","href":"/api-reference/contacts/retrieve"},{"title":"Update a contact","openapi":"PATCH /v1/contacts/{contact_id}","href":"/api-reference/contacts/update"},{"title":"Delete a contact","openapi":"DELETE /v1/contacts/{contact_id}","href":"/api-reference/contacts/delete"},{"title":"Add a tag to a contact","openapi":"POST /v1/contacts/{contact_id}/tags","href":"/api-reference/contacts/add-tag"},{"title":"Remove a tag from a contact","openapi":"DELETE /v1/contacts/{contact_id}/tags/{name}","href":"/api-reference/contacts/remove-tag"},{"title":"Opt a contact out","openapi":"POST /v1/contacts/{contact_id}/opt-out","href":"/api-reference/contacts/opt-out"},{"title":"Block a contact","openapi":"POST /v1/contacts/{contact_id}/block","href":"/api-reference/contacts/block"},{"title":"Unblock a contact","openapi":"DELETE /v1/contacts/{contact_id}/block","href":"/api-reference/contacts/unblock"}]},{"group":"Conversations","pages":[{"title":"Mark a conversation read","openapi":"POST /v1/conversations/{conversation_id}/read","href":"/api-reference/conversations/mark-read"},{"title":"Start typing","openapi":"POST /v1/conversations/{conversation_id}/typing","href":"/api-reference/conversations/start-typing"},{"title":"Stop typing","openapi":"DELETE /v1/conversations/{conversation_id}/typing","href":"/api-reference/conversations/stop-typing"}]},{"group":"Webhooks","pages":[{"title":"Create a webhook subscription","openapi":"POST /v1/webhooks","href":"/api-reference/webhooks/create"},{"title":"List webhook subscriptions","openapi":"GET /v1/webhooks","href":"/api-reference/webhooks/list"},{"title":"Retrieve a webhook subscription","openapi":"GET /v1/webhooks/{subscription_id}","href":"/api-reference/webhooks/retrieve"},{"title":"Revoke a webhook subscription","openapi":"DELETE /v1/webhooks/{subscription_id}","href":"/api-reference/webhooks/revoke"}]},{"group":"Inbound events","pages":[{"title":"Receive an event","openapi":"POST /v1/events/{token}","href":"/api-reference/inbound-events/receive"}]}]},{"group":"Resources","pages":[{"title":"Error Reference","href":"/errors"},{"title":"OpenAPI, Postman and Insomnia","href":"/openapi"},{"title":"Versioning","href":"/versioning"},{"title":"Changelog","href":"/changelog"},{"title":"Support","href":"/support"}]}]},"basePath":"","title":"Message lifecycle - iBubbl","buildId":"dpl_596ePT575z4qnAFksq1biRW97aLY","deploymentId":"dpl_596ePT575z4qnAFksq1biRW97aLY","dynamics":[],"chunks":[]}