message.replied webhook. The Conversations endpoints let you answer it with your own content, from the same number, inside the same thread, on the same project. The API never starts a conversation; a project send does that.
The round trip
1
Receive the inbound
Your webhook endpoint gets a
message.replied event. Its payload carries conversation_id, message_id (the inbound message), from (the contact), to (your number), text, is_opt_out, and is_opt_in. Acknowledge with a 2xx quickly and hand the work off.2
Reply inside the conversation
202 and queued at high priority. The response carries the new message_id, the sending number (from), and the text as it will go out.3
Watch for the outcome
message.sent, message.delivered, or message.failed fires for the returned message_id, each carrying the same conversation_id. There is no separate “reply” event.What a reply can and cannot do
- SMS only, up to 1,600 characters. Media is not accepted; the body is
{ "text": "..." }and unknown properties are rejected with400. Text is normalized for GSM encoding before sending, so a curly quote pasted from a document does not push the message into UCS-2. - Same number, always. The sending number is a property of the conversation, not of the request. If that number was released the reply is refused with
409 PHONE_NUMBER_UNAVAILABLE. - Opt-outs are enforced before anything is charged. A contact who replied STOP gets
409 CONTACT_OPTED_OUT. A thread with no sending number (409 CONVERSATION_NOT_SENDABLE) or a deleted project (409 PROJECT_DELETED) is refused the same way.402 INSUFFICIENT_BALANCEfollows the same rule: nothing was charged. - Not subject to quiet hours. A reply is a follow-up to a contact who just texted in, and goes out immediately.
- Billed per segment at the number’s outbound rate, on the organization that owns the project.
Retrying safely
Send anIdempotency-Key (a UUID) on every reply you might retry. Within 24 hours a retry with the same key returns the stored response with X-Idempotent-Replayed: true instead of sending a second text.
Recovering missed inbounds
If your webhook endpoint was down past our four delivery attempts, list the threads that have new inbound messages:GET /conversations returns only conversations with at least one inbound message, newest inbound first, across every organization the key can access. Filter on last_inbound_at with updated_since (default: the last 7 days; more than 90 days back is a 400), narrow with project_id, and page until next_cursor is null. Then read each thread with GET /conversations/{conversation_id}/messages (newest first, both directions, never marks the thread read in the dashboard).
Treat this as recovery, not as a substitute for the webhook: poll at most once a minute, and advance updated_since to the newest last_inbound_at you have processed.
Tenant scope
A conversation that does not exist and one that belongs to an organization outside your key’s scope both answer404 CONVERSATION_NOT_FOUND. The API never confirms the existence of another tenant’s thread.
