Activity
A chat is a conversation between one customer and your brand. It starts when a customer messages you, or when they accept an invitation you sent. No endpoint creates one directly, and you can’t message someone who has never contacted you. Invitations are the one way to reach a customer first, and they send an Apple-templated card rather than a message you write.
A chat is identified by its chat_id, like
chat_2d4f8a91-6c03-47be-b5d2-9e81c3f60a27. Treat it as an opaque string,
don’t parse it, and don’t depend on its prefix or length.
A chat_id belonging to another brand returns 404, the same response you get
for one that doesn’t exist. You can’t tell the two apart, which is deliberate.
GET /v1/chats/{chat} returns the full object. The fields that govern whether
you can send:
state; open or closed. A customer deletes a conversation on their
device, and the chat closes. Sends to a closed chat return 409 chat_closed. A
new inbound message reopens it.
owner, who currently holds the conversation: partner, flow,
human_pending, or human. Only one of them holds it at a time, and only the
holder can send. Sending while owner is anything but partner returns
409 chat_owned, which is what stops your automation from talking over a human
agent mid-sentence.
POST /v1/chats/{chat}/handoff passes the conversation to someone else.
?force=true is on the SEND, not the handoff: it sends into a chat you do not
own and records that you overrode the gate (chat.send_forced). It does not
override consent: a customer who opted out still returns
403 consent_required.
capabilities; what the customer’s device told us it can display, as of
their most recent message. An empty array means we don’t know yet, not that the
device supports nothing.
send_blocked; whether a send would be refused right now, and with which
error:
Response
{"send_blocked": {"blocked": true,
"code": "chat_owned",
"message": "a human agent holds this chat"}}
We run the same checks, in the same order, that a real send runs. So code and
message are exactly what you would get back if you tried to send.
"blocked": false means nothing about the chat itself would refuse you. It is
not a promise that your message will be accepted. Anything that depends on the
message: its size, or whether the customer’s device can render the parts you
composed; is checked when you send it.
Listing
GET /v1/chats returns your chats newest-first, a page at a time. Pass the
previous response’s next_cursor back verbatim; the list ends when it comes
back as an empty string. Filters are optional: state, owner,
assigned_operator.
Reading a chat
GET /v1/chats/{chat}/transcript returns what the customer saw, in order.
Response
{"data": [{"seq": 13,
"kind": "customer_message",
"event_type": "message_received",
"actor": "customer",
"created_at": "2026-08-06T14:01:58Z",
"parts": [{"type": "text", "body": "where is my driver?"}]}]}
Decide what to render from kind. It has three values and will never have
more: customer_message, business_message, and system. When we add a new
event type in a later release it arrives as system, so a switch you write
today keeps working.
event_type says what the event actually was (message_received,
owner_changed, consent_send_refused). Use it to fill in the detail once
you’ve decided how to render the row, but don’t build your control flow on
it, because new values appear here over time.
seq numbers the events in a chat and never skips. Page with
?after_seq=<the last seq you saw>&limit=…. GET /v1/chats/{chat}/events
returns the same events without the transcript’s formatting.