Webhooks
ModelsExpand Collapse
MessageReceivedWebhookEvent object { id, created_at, data, type }
data: object { brand_id, chat_id, message, 3 more }
The logical brand this chat is scoped under, the value to answer as. Distinct from the Apple business UUID, which is served beside it as business_id; brand_unknown:<business_id> for an unregistered business.
message: object { reply, text, attachments, 4 more }
reply: object { kind, answers, items, 3 more } The normalized interactive response when the inbound was type:interactive; null otherwise (including a customer answering a DEGRADED prompt in plain text).
The normalized interactive response when the inbound was type:interactive; null otherwise (including a customer answering a DEGRADED prompt in plain text).
Normalized reply kind. One of app_extension, apple_pay, choices, form, invitation_response, list_picker, time_slots. Each kind carries its own shape. The six that answer a part you sent are shaped by that part’s published schema, served at GET /v1/parts/{type}; invitation_response answers an invitation rather than a part, and reports the customer’s verdict in item_id. This set describes replies classified on or after 2026-08-27. app_extension was called imessage_app before that, and a recorded event is never rewritten, so events recorded earlier keep the old spelling wherever you read them back: the event log, a chat’s own events, the transcript, and a redelivered webhook. Accept imessage_app as a historical synonym when you read history.
answers: optional array of object { field_id, item_ids, kind, 4 more } One record per answered page, on form replies; a page whose submission carried no items emits no record. question is the page’s title, the prompt the customer answered; kind says what shape of answer it is (input, single_select, multi_select, picker, date). A page answered with one item reports field_id and value; one answered with several reports item_ids and values, aligned entry for entry. Records carry the wire’s raw fields, while the reply’s own values map (unchanged) applies a display fallback per item: its value, else its identifier, else its title. So rebuild a page’s values entry from a record by that fallback, never by joining the record’s values alone. The one leg a record cannot rebuild is the title: records carry no titles, so when an item brought neither a value nor an identifier, read that page’s answer from values, where the title already landed. Every field is present only when the submission carried its source.
One record per answered page, on form replies; a page whose submission carried no items emits no record. question is the page’s title, the prompt the customer answered; kind says what shape of answer it is (input, single_select, multi_select, picker, date). A page answered with one item reports field_id and value; one answered with several reports item_ids and values, aligned entry for entry. Records carry the wire’s raw fields, while the reply’s own values map (unchanged) applies a display fallback per item: its value, else its identifier, else its title. So rebuild a page’s values entry from a record by that fallback, never by joining the record’s values alone. The one leg a record cannot rebuild is the title: records carry no titles, so when an item brought neither a value nor an identifier, read that page’s answer from values, where the title already landed. Every field is present only when the submission carried its source.
items: optional array of object { id, subtitle, title } The chosen options with their display text, on list_picker replies: one entry per selected item, in the order the reply listed them. Display only: item_ids is unchanged and stays the set to match on. Present only when at least one chosen item carried display text; each entry’s title and subtitle are themselves present only when set.
The chosen options with their display text, on list_picker replies: one entry per selected item, in the order the reply listed them. Display only: item_ids is unchanged and stays the set to match on. Present only when at least one chosen item carried display text; each entry’s title and subtitle are themselves present only when set.
The identifier of the interactive message this reply answers: the same value the sending message’s transcript row reports as its own request_id, so a tap is matched to the exact send that asked rather than inferred from item ids (the same card sent twice shares item ids, while each send delivers under a fresh identifier). Present when the send recorded one.
On form replies only: the customer’s device timezone offset at submission, verbatim from the wire. Minutes, positive west of UTC (the JavaScript getTimezoneOffset convention, so a US Central device reads +300). Present exactly when the device reported one; never fabricated. Other reply kinds never carry it: the similarly named field on a time-slot reply is an echo of the event your own send defined, not customer data.
attachments: optional array of object { decrypted, id, decrypted_size, 6 more } Present when the inbound carried files: one entry per attachment, with the outcome of our decrypt-and-retain pass. Absent when the message carried none, so an ordinary text inbound’s shape is unchanged.
Present when the inbound carried files: one entry per attachment, with the outcome of our decrypt-and-retain pass. Absent when the message carried none, so an ordinary text inbound’s shape is unchanged.
Whether we could decrypt the file. false means no bytes exist to serve and error says what happened.
The retained file’s att_id; GET /v1/attachments/{att}/content serves the bytes and this part sends them onward. Absent when nothing was retained, and retention then says why.
The entry point’s groupID, same delivery and same present-only-when-set rule as intent.
The entry point’s intentID, exactly as Apple delivered it. The button, URL or QR code the customer started from sets this, not the message body. It is present only when the entry point set one, so an ordinary chat’s shape is unchanged. Route your own skills on it — our brand routing matches the same representation.
The full inbound Apple envelope, verbatim, when present. A legacy escape hatch, not a surface to automate on: everything a workflow needs is in the normalized fields (text, reply, attachments, business_id), and this key is ABSENT on ordinary deliveries unless the platform is configured to send it. Two promises hold whatever that configuration says: a delivery carrying reply_unresolved always includes raw (the payload we could not normalize), and the verbatim envelope is always readable from GET /v1/chats/{chat}/events, which is the forensic record.
MessageFailedWebhookEvent object { id, created_at, data, type }
data: object { chat_id, message_id, reason, 4 more }
reason: "permanent" or "undelivered" or "auth_error" or 5 moreplatform_error and invitation_undeliverable occur only for invitation-class sends, so those two are worth resubmitting only there.
platform_error and invitation_undeliverable occur only for invitation-class sends, so those two are worth resubmitting only there.
Same meaning as on message.sent — ties a fan-out member’s failure back to the one request.
ChatOwnerChangedWebhookEvent object { id, created_at, data, type }
data: object { chat_id, owner, seq, 2 more }
The NEW owner — partner, flow, human_pending or human. flow is a platform flow holding the turn (the survey /resolve starts, the question /clarify asks): it ends on the customer’s answer or its own timeout, and ownership then returns through this same event.
ChatAuthenticatedWebhookEvent object { id, created_at, data, type }
data: object { chat_id, seq, sub, 2 more }
The provider’s opaque subject identifier. This is the whole of what we pass through from the claims.
The id of the authenticate message this sign-in completed, equal to the send response’s message_id and the transcript business row’s message_id. A correlation id, not a claim. Absent on a sign-in whose bubble was sent before this shipped.
The requestIdentifier the authenticate message this sign-in completed was delivered under, equal to the transcript business row’s request_id. A correlation id, not a claim. Every authenticate send has minted one since the flow shipped, so it is present on a sign-in whose bubble predates this field.
ChatAuthenticationClosedWebhookEvent object { id, created_at, data, type }
data: object { chat_id, request_id, seq, 2 more }
The requestIdentifier the authenticate message was delivered under, equal to the transcript business row’s request_id and to chat.authenticated’s. A correlation id, not a claim; always present.
status: "cancel" or "failure" or "unknown" or "expired"Why the bubble is dead. cancel: the customer dismissed it. failure: the sign-in did not complete. unknown: the device could not say, or reported a status outside this set. expired: the 30-minute sign-in window, counted from when the message was accepted, closed with no chat.authenticated and no earlier close.
Why the bubble is dead. cancel: the customer dismissed it. failure: the sign-in did not complete. unknown: the device could not say, or reported a status outside this set. expired: the 30-minute sign-in window, counted from when the message was accepted, closed with no chat.authenticated and no earlier close.
ChatTriggerWordWebhookEvent object { id, created_at, data, type }
data: object { action, chat_id, handled_by, 8 more }
handled_by: "partner" or "platform"Who the event asks something of. platform: informational, nothing owed. partner: yours, and the delegation fields say how much. With queued_for_human present, routing was already seized and only the acknowledgement is delegated to you, deadline ack_by. With queued_for_human ABSENT, your brand runs self-managed escalation and the whole escalation is yours: nothing was queued, and the chat moves only on your /handoff or /clarify.
Who the event asks something of. platform: informational, nothing owed. partner: yours, and the delegation fields say how much. With queued_for_human present, routing was already seized and only the acknowledgement is delegated to you, deadline ack_by. With queued_for_human ABSENT, your brand runs self-managed escalation and the whole escalation is yours: nothing was queued, and the chat moves only on your /handoff or /clarify.
The deadline for your acknowledgement, about 10 seconds out. Send anything before it and yours is what the customer reads; past it we send our own. It is not a routing fallback.
The escalation entered the escalation-channel flow (out-of-hours/exempt brand) — no ack is delegated and no ack_by is promised.
InvitationBatchRecipientFailedWebhookEvent object { id, created_at, data, type }
data: object { batch_id, brand_id, idx, 4 more }
InvitationBatchCompletedWebhookEvent object { id, created_at, data, type }
data: object { accepted_count, batch_id, brand_id, 3 more }
ReactionAddedWebhookEvent object { id, created_at, data, type }
data: object { brand_id, chat_id, correlation, 12 more }
correlation: "inferred" or "ambiguous" or "unmatched" or "unavailable"HOW the target was reached, and the same value GET /v1/chats/{chat}/transcript serves for this reaction. inferred means exactly one message in the recent window carried that text; ambiguous means several did and candidate_count says how many; unmatched means none did; unavailable means Apple named no target. There is no exact, because no reaction on this wire carries an id.
HOW the target was reached, and the same value GET /v1/chats/{chat}/transcript serves for this reaction. inferred means exactly one message in the recent window carried that text; ambiguous means several did and candidate_count says how many; unmatched means none did; unavailable means Apple named no target. There is no exact, because no reaction on this wire carries an id.
kind: "like" or "love" or "dislike" or 5 moreWhat the customer did. The same closed set the transcript’s reaction.kind uses, spelled the same way.
What the customer did. The same closed set the transcript’s reaction.kind uses, spelled the same way.
Who owned the turn when this was recorded (partner, flow, human_pending, human). It rides every envelope so an orchestrator branches structurally instead of guessing.
The seq of the customer message that CARRIED the reaction. It is not the message reacted to; that is target_seq, and it is an inference.
The sticker’s image, when kind is sticker and we retained it. Pass it to GET /v1/attachments/{att}/content. Absent when the image was not retained, or when more than one file arrived and the wire does not say which is the sticker. Do not assume a format; read the response’s own content type. A present id is NOT a promise of bytes, exactly as for att_id. Retained attachments are swept after their published TTL and the event is immutable, so an old reaction still names the id it was minted with and the content read answers 404.
How many messages carried that exact text, on ambiguous. No winner is picked for you.
The character the customer picked, present only when kind is emoji. Served verbatim.
Apple named no target at all, which is what a reaction to a non-text message looks like. correlation is then unavailable.
ReactionRemovedWebhookEvent object { id, created_at, data, type }
data: object { brand_id, chat_id, correlation, 11 more }
correlation: "inferred" or "ambiguous" or "unmatched" or "unavailable"HOW the target was reached, and the same value GET /v1/chats/{chat}/transcript serves for this reaction. inferred means exactly one message in the recent window carried that text; ambiguous means several did and candidate_count says how many; unmatched means none did; unavailable means Apple named no target. There is no exact, because no reaction on this wire carries an id.
HOW the target was reached, and the same value GET /v1/chats/{chat}/transcript serves for this reaction. inferred means exactly one message in the recent window carried that text; ambiguous means several did and candidate_count says how many; unmatched means none did; unavailable means Apple named no target. There is no exact, because no reaction on this wire carries an id.
kind: "like" or "love" or "dislike" or 5 moreWhat the customer did. The same closed set the transcript’s reaction.kind uses, spelled the same way.
What the customer did. The same closed set the transcript’s reaction.kind uses, spelled the same way.
Who owned the turn when this was recorded (partner, flow, human_pending, human). It rides every envelope so an orchestrator branches structurally instead of guessing.
The seq of the customer message that CARRIED the reaction. It is not the message reacted to; that is target_seq, and it is an inference.
How many messages carried that exact text, on ambiguous. No winner is picked for you.
The character the customer picked, present only when kind is emoji. Served verbatim.
Apple named no target at all, which is what a reaction to a non-text message looks like. correlation is then unavailable.
ChoiceReceivedWebhookEvent object { id, created_at, data, type }
data: object { brand_id, chat_id, item_id, 5 more }
The same seq as the inbound event that produced it, the join to GET /v1/chats/{chat}/events.
The Apple business UUID the chat arrived on; present when the inbound named one.
PickerReceivedWebhookEvent object { id, created_at, data, type }
data: object { brand_id, chat_id, items, 4 more }
The same seq as the inbound event that produced it, the join to GET /v1/chats/{chat}/events.
TimeslotReceivedWebhookEvent object { id, created_at, data, type }
data: object { brand_id, chat_id, owner, 5 more }
The same seq as the inbound event that produced it, the join to GET /v1/chats/{chat}/events.
The Apple business UUID the chat arrived on; present when the inbound named one.
FormCompletedWebhookEvent object { id, created_at, data, type }
data: object { brand_id, chat_id, owner, 11 more }
The seq of the event that concluded the form, the join to GET /v1/chats/{chat}/events.
transport: "native" or "paged"How the form reached the customer. native is a single-page card submission; paged is the degraded multi-page collection.
How the form reached the customer. native is a single-page card submission; paged is the degraded multi-page collection.
The flat answers, keyed by your original page ids, multi-select values comma-joined. Byte-congruent with the message-level reply’s values and with form.response.
answers: optional array of object { kind, page_id, field_id, 4 more } The typed per-field record; present when captured at ingest. One entry per answered field, preserving page grouping and field kind.
The typed per-field record; present when captured at ingest. One entry per answered field, preserving page grouping and field kind.
The Apple business UUID the chat arrived on; present when the inbound named one (native transport only).
Paged transport; the correlation id tying the record to the page messages that asked.
Every selected item id across the form’s choice pages; present when any page was a choice and the ids were captured.
ApplePayReceivedWebhookEvent object { id, created_at, data, type }
data: object { brand_id, chat_id, owner, 4 more }
The completion message’s state, advisory only. NON-AUTHORITATIVE; reconcile against your own payment record, never this field.
The same seq as the inbound event that produced it, the join to GET /v1/chats/{chat}/events.
AppExtensionReceivedWebhookEvent object { id, created_at, data, type }
data: object { bid, brand_id, chat_id, 8 more }
The same seq as the inbound event that produced it, the join to GET /v1/chats/{chat}/events.
The Apple business UUID the chat arrived on; present when the inbound named one.