Webhooks
Webhooks deliver real-time notifications to your server when events occur in your AgentPhone project. Each project has a single master webhook endpoint that receives all events.
You can also configure per-agent webhooks that override the project default for a specific agent. See Per-agent webhooks below.
Events
All inbound messages (SMS, MMS, iMessage, RCS, WhatsApp, and voice) are delivered as a unified agent.message event. The channel field tells you the source. When a call ends, agent.call_ended delivers the full transcript and call analysis. Reactions trigger agent.reaction.
Webhook payload
Each webhook delivery includes the following structure. SMS, iMessage, and voice share the same top-level format:
agent.message fields by channel
agent.message keeps one envelope and changes only the data shape by channel:
Inbound WhatsApp messages arrive as the same agent.message event with channel: "whatsapp". Nothing extra to configure once a WhatsApp Business Account is connected.
Button and list taps arrive as ordinary inbound messages. data.message is the label the customer tapped, so code that already handles text handles taps for free:
The replyTo block is what makes taps genuinely useful. It points at the message that offered the choice, so when you have sent five product cards each with a Buy button, the label alone cannot tell you which product they meant but replyTo.messageId can. It is present on list taps and on customer quote replies too, and omitted entirely on an ordinary message. mediaUrls carries the quoted message’s attachments when it had any (a second attachment would be ?index=1), and is an empty array otherwise.
Reactions on WhatsApp arrive as agent.reaction, the same event iMessage tapbacks use, with the emoji in reactionType.
Group chats
Group chats are an iMessage feature. When your number is part of a group thread, inbound group messages arrive as the same agent.message event (channel: "imessage"), with two additions to data:
data.group— the group roster and metadatadata.senderIdentifier— which member sent this specific message
For one-to-one conversations these two fields are omitted entirely (not null), so the presence of data.group is a reliable “is this a group?” check. SMS numbers cannot participate in group threads.
The webhook is self-contained: you do not need to fetch the conversation before routing or replying. To reply to the group, send to the groupId (not data.from, which would start a one-to-one with that member): call POST /v1/messages with to_number set to the grp_... value.
For voice events, the data field contains the transcript instead:
When a call ends, agent.call_ended delivers the full transcript (not limited by contextLimit), call duration, and optional analysis:
agent.call_ended is fire-and-forget — it does not expect a response body. Return 200 OK to acknowledge receipt.
When someone reacts to an iMessage your agent sent, agent.reaction is delivered:
The reactionType is one of: love, like, dislike, laugh, emphasize, question.
Voice webhook responses
For voice events, your webhook must return a JSON object ({...}) that tells the agent what to say. Non-object responses (numbers, strings, arrays) are ignored and the caller hears silence.
Streaming response (recommended): Return Content-Type: application/x-ndjson with newline-delimited JSON chunks. TTS starts speaking on the first chunk while your server continues processing.
Mark interim chunks with "interim": true — the final chunk (without interim) closes the turn. Voice webhooks have a 30-second default timeout (configurable from 5–120 seconds per webhook) — always stream an interim chunk before doing slow work like LLM tool calls.
Simple response: Return a single JSON object for instant replies.
Example: confirming an appointment by text while the agent stays on the call.
number.released
Sent when a number is released by its expiresAt. Signed and logged like every other event, but it is attempted once with no automatic retries. See the event-specific retry policy below. If your endpoint was down at that moment, the delivery still appears in your webhook deliveries with its status.
Security
Each webhook delivery includes these headers:
The signature is computed over the timestamp + body: the signed string is {timestamp}.{raw_body}, hashed with HMAC-SHA256 using your webhook secret. Always verify the timestamp is within 5 minutes to prevent replay attacks.
Verification example (Node.js)
Save as verify-webhook.mjs. Pass the original request bytes, before JSON parsing. Parsing and then re-serializing JSON changes the signed bytes.
Verification example (Python)
Save as verify_webhook.py:
Both examples reject missing or malformed headers, stale/future timestamps outside a five-minute window, and invalid signatures. Framework handlers are in Code examples.
Retry behavior
Delivery depends on the event:
For non-voice agent.message, the delay after the previous failed attempt is:
Delays have ±10% jitter and are picked up by a periodic worker, so they are not exact delivery times. Timeouts, network failures, and non-2xx HTTP responses can be retried. A permanently blocked destination URL is not retried. After the last failed attempt, the delivery is marked failed; inspect delivery history.
Handling duplicate deliveries
The same message delivery keeps its X-Webhook-ID across retries. Verify the signature first, then atomically record the ID and enqueue work in durable storage before acknowledging. If the ID is already recorded, acknowledge without repeating the side effect. An in-memory set is insufficient across restarts or multiple instances.
For text events, use POST /v1/messages to reply. Keep the inbound data.numberId so the response uses the same line, and use data.group.groupId for a group reply. For voice, return speech in the current HTTP response; acknowledging with an empty body produces no speech.
Conversation state
Store custom metadata on conversations to persist context across messages. This state is included in every webhook payload as conversationState.
This metadata appears in subsequent webhook payloads as conversationState, enabling your AI backend to maintain context across messages without managing state yourself.
Create or update webhook
Configure the webhook endpoint for your project. Each project can have one active master webhook. If a webhook already exists, it will be updated.
Request body
Save the secret returned every time you create or update a webhook. It can change; endpoints sharing the same URL within an account can reuse a secret. Do not assume the previous value remains valid.
Example
Get webhook
Get the current webhook configuration for your project. Returns null if no webhook is configured.
Delete webhook
Remove the master webhook configuration. Events will no longer be delivered.
Example
Webhook deliveries
View the delivery history for your master webhook to monitor delivery status and debug issues.
Query parameters
Example
Test webhook
Send a test webhook to verify your endpoint is working correctly. This sends a synthetic agent.message event through the real delivery path (same signature, headers and payload shape as production) to your configured URL. Test events carry a top-level "test": true so your handler can tell them apart from live traffic.
Request body (optional)
By default the event uses sample values (from +15551234567, to +15559876543, a fixed message). Override any of them so a handler that routes on the numbers can be exercised end to end, for example before a business has bought a number or from an automated test suite.
The event is only ever delivered to the calling account’s own webhook. The numbers are reported in the payload as given; they do not need to belong to the account.
Example
Your endpoint receives the usual agent.message payload with data.from, data.to and data.message set to the values above, plus "test": true at the top level. The per-agent variant, POST /v1/agents/{agentId}/webhook/test, accepts the same body.
Per-agent webhooks
In addition to the project-level default webhook, you can configure an individual webhook endpoint for each agent. When a per-agent webhook is set, it overrides the project default for that agent’s events — the event is delivered to exactly one endpoint, never both. This is useful when you run multiple agents and want to route their events to different backend services or pipelines.
How routing works
When an event occurs (e.g., an inbound message to an agent’s number):
- If the agent has its own webhook configured, the event is delivered only to the agent’s webhook.
- If the agent does not have its own webhook, the event is delivered to the project default webhook.
Events are never duplicated across both endpoints. Per-agent webhooks use the same payload format, signature verification, and retry behavior described above.
Create or update an agent webhook
Register or update a webhook URL for a specific agent.
Save the secret returned every time you create or update a webhook. It can change; endpoints sharing the same URL within an account can reuse a secret. Do not assume the previous value remains valid.
Get an agent webhook
Get the webhook configuration for a specific agent. Returns null if no webhook is configured.
Delete an agent webhook
Remove the webhook for a specific agent. Events revert to the project default webhook after deletion.
Agent webhook deliveries
View recent delivery attempts for a specific agent’s webhook.
Test an agent webhook
Send a test webhook to verify the agent’s endpoint is working correctly.

