Messages
Request
POST /v1/chats/{chat}/messages
Authorization: Bearer sk_live_...
Idempotency-Key: 3f2504e0-4f89-11d3-9a0c-0305e82c3301
{"parts": [{"type": "text", "body": "Your order ships tomorrow."}]}
Response
{"id": "msg_4a1c...",
"chat_id": "chat_2d4f8a91...",
"delivery": "queued",
"messages": [{"id": "msg_4a1c...", "type": "text"}]}
Idempotency-Key is required. A replay of the same key within 24 hours returns
the original response with Idempotent-Replayed: true. A refused send records no
key, so the same key can be reused once the cause is fixed.
Delivery
queued means accepted, not delivered. The outcome arrives as message.sent or
message.failed, or from GET /v1/messages/{msg} if you missed the webhook.
message.failed carries a reason; consent_revoked, capability_rejected,
and so on.
One request, several messages
An https:// URL in a text part becomes a rich link, and the text around it
becomes a message of its own. So one request can produce several messages.
They all appear in messages, they are all covered by the one
Idempotency-Key, and each one gets its own delivery webhook, tied together by
group_id. Count what’s in messages; not the requests you sent.
Parts
A message is an array of parts. Ten types are available:
text · choices · list_picker · time_slots · form · rich_link ·
attachment · apple_pay · authenticate · app_extension
Each type has a JSON Schema at GET /v1/parts/{type}. We serve it unmodified,
so you can hand it straight to a structured-output model as a tool schema.
Some rules can’t be expressed in JSON Schema; length caps, how many choices a
picker may carry, whether an ISO code is upper or lower case. Those ride along
as metadata on each schema, and we enforce them when you send. Breaking one
returns 422 with a pointer to the exact field at fault.
Text plus attachments is the only multi-part combination Apple accepts. The 
character (U+FFFC) positions each attachment within the text.
Sending a file
Send the file and the message in one request: make the POST
multipart/form-data, put the message JSON in a message field, and give
each attachment part the name of the file part carrying its bytes.
POST /v1/chats/{chat}/messages
Idempotency-Key: 7c9e6679-7425-40de-944b-e07fc1f90ae7
Content-Type: multipart/form-data
message = {"parts": [
{"type": "text", "body": "Here's the menu you asked for"},
{"type": "attachment", "file": "photo"}
], "category": "transactional"}
photo = <the JPEG bytes>
The response is the same as any send. A blind retry with the same
Idempotency-Key and the same file replays rather than re-sending. Max
size is 100 MB; per file, and for a message’s files together.
Artwork you reference by id
Some parts name a stored file instead of carrying one, and they take an
attachment id rather than inline base64: a rich link’s image_att_id, the
images inside a list picker, a time-slot picker or a form, and your brand’s
default rich-link image.
Upload the asset once, then reference that id as often as you need it.
POST /v1/attachments
Content-Type: image/png
x-filename: menu-thumb.png
<the PNG bytes>
{"id": "att_1a3f77", "mime": "image/png", "size": 20481}
Uploads are idempotent over your brand and the bytes, so a retry returns the
same id and stores nothing twice. The stored mime and name stay the first
upload’s, so uploading the same bytes under a corrected content-type hands
you the original type back; to change a stored type, upload altered bytes,
which are a different attachment.
This route is for assets you reference. To send a file to a customer, put it in the send, as above.
Capabilities
Each inbound message announces what the customer’s device can render, most recent wins. Sending a part the current device cannot render returns:
Response
{"error": {"type": "capability", "code": "capability_unsupported",
"message": "this device did not announce FORM"},
"request_id": "req_01HQ7X..."}
Set "degrade": "auto" and we rewrite the part into something the device can
display, then return a fallbacks array describing what we swapped. If
fallbacks is present, the customer saw something other than what you
composed; check it before assuming your message landed as written.
Limits
| Attachment | 100 MB; exactly 100,000,000 bytes |
| Attachments per message | 100,000,000 bytes across all of them, together |
Serialized interactiveData | 10 KiB |
| Rich link image | 200 kB decoded, PNG only |
| Invitation batch | 10,000 recipients |
The interactiveData limit is measured over your bytes. Content we add at delivery, such as an Apple Pay
session or an app_extension icon, is not charged against it.
The two attachment rows are the same number for a reason: one 100 MB file is
fine, and so are ten 10 MB files, but a message carrying more than
100,000,000 bytes of files in total is more than we will carry to Apple in
one send. Files sent in the request are refused up front with 413 too_large naming the ceiling.