Webhooks
Receive a server-to-server notification with the full call result when an in-app agent call ends.
Overview
When an in-app agent call finishes, RevRag can send a call.ended webhook — a POST request to an HTTPS endpoint on your server — containing the complete result of that call: status, duration, summary, transcript, recording URL, and your app_user_id.
The payload below shows exactly what your server receives when an in-app call ends.
Webhooks are server-to-server. They are delivered to your backend, not to the SDK running in your app. The in-app SDK's own agent lifecycle events (e.g. agent_start / agent_end) are a separate, in-process mechanism — they are not the same as this webhook.
Enabling webhooks
Webhooks are opt-in and configured per agent. They are not enabled by default.
To turn them on for your in-app agent, contact contact@revrag.ai with:
- Webhook URL — the HTTPS endpoint that will receive the
POST(HTTP is not supported). - Signing secret — a shared secret used to verify each request (see Security & headers).
Security & headers
In-app webhooks use the same signing scheme as every RevRag webhook — HMAC-SHA256 computed over "{timestamp}.{raw_body}" with your shared secret. Each request carries:
| Header | Purpose |
|---|---|
X-Webhook-Event | Event type (call.ended) |
X-Webhook-Timestamp | Unix timestamp (seconds) the webhook was sent |
X-Webhook-Signature | t=<timestamp>,v1=<hmac_sha256_hex> |
X-Webhook-ID | Unique id for deduplication |
Always verify the signature before trusting a payload. See Webhook Security for the full verification steps and ready-to-use Python / Node.js examples.
Payload
The webhook body is the same JSON shape as the Get Call Status response. For an in-app call it carries your app_user_id as a top-level correlation field.
{
"call_status": "ENDED",
"call_id": "123e4567-e89b-12d3-a456-426614174000",
"agent_id": "33333333-3333-3333-3333-333333333333",
"summary": "User asked about their loan eligibility and was guided to the application.",
"start_time": "2026-05-27T03:05:00+00:00",
"end_time": "2026-05-27T03:06:12+00:00",
"duration": 72,
"disconnection_reason": "user_hangup",
"recording_url": "https://.../recording.mp3",
"transcription": {
"messages": [
{ "role": "assistant", "content": "Hi! How can I help you today?" },
{ "role": "user", "content": "What documents do I need for a loan?" }
]
},
"custom_variables": [
{ "key": "interested_in_loan", "type": "boolean", "value": true }
],
"variables_fields": { "plan": "gold" },
"app_user_id": "user_12345"
}Payload parameters
| Field | Type | Description |
|---|---|---|
call_status | string | Final call status — ENDED once the call has finished |
call_id | string (uuid) | The call this notification is for |
agent_id | string (uuid) | The in-app agent that handled the call |
summary | string | LLM-generated call summary (when a transcript exists) |
start_time / end_time | string | null | ISO-8601 timestamps |
duration | number | Call duration in seconds |
disconnection_reason | string | null | Normalized hangup reason |
recording_url | string | null | Presigned recording URL (for transcribed calls) |
transcription | object | null | Turn-by-turn messages (role, content) |
custom_variables | array | Post-call variables extracted by the agent (key, type, value) |
variables_fields | object | null | Variables associated with the call |
app_user_id | string | null | Your user id, echoed back top-level — see Correlation IDs |
Retries & failure recovery
If your endpoint is temporarily unreachable, RevRag retries automatically with exponential backoff. If every attempt fails, the delivery is retained as a dead-letter so you can recover it later via the polling API below.
Retry policy
Each webhook goes through up to 3 attempts in total:
| Attempt | Timing |
|---|---|
| 1 | Fires immediately after the call ends |
| 2 | Retries 5 minutes after attempt 1 fails |
| 3 | Retries 15 minutes after attempt 2 fails |
| — | If attempt 3 fails, the delivery is moved to dead-letter |
Any non-2xx response — including network errors, timeouts, and 4xx — is treated as a failure and triggers a retry.
Every retry carries the same X-Webhook-ID as the original attempt. Dedupe on this header so a delivered-but-ack-lost webhook doesn't get processed twice.
Discovering failed deliveries
Poll this endpoint at a regular cadence (for example, every 5 minutes) to discover any calls whose webhook could not be delivered. Only dead-letter deliveries are returned — successful and in-progress deliveries are not included.
GET /webhook-failures
Header Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
X-API-Key | string | Yes | API Key for authentication |
Query Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
since | string (ISO 8601) | Yes | Start of the window (exclusive). On subsequent polls, pass your previous request's until value here. |
until | string (ISO 8601) | No | End of the window (inclusive). Defaults to the current time. |
limit | integer | No | Rows per page. Default 100, maximum 500. |
cursor | string | No | Opaque pagination cursor. Pass unchanged from the previous response's next_cursor to fetch the next page. |
Response
{
"items": [
{
"call_id": "123e4567-e89b-12d3-a456-426614174000",
"failed_at": "2026-05-27T03:20:00.000Z",
"last_status_code": 500,
"attempts": 3
}
],
"next_cursor": null
}| Field | Type | Description |
|---|---|---|
items[].call_id | string (uuid) | Identifier of the call whose webhook failed. Use this to fetch the full payload (see below). |
items[].failed_at | string (ISO 8601) | When the delivery entered the failed state, after all retries were exhausted. |
items[].last_status_code | integer | null | HTTP status returned by your endpoint on the final attempt. null if every attempt failed at the network layer (timeout, connection refused, DNS failure). |
items[].attempts | integer | Total delivery attempts made before giving up. |
next_cursor | string | null | Pass this value as cursor on the next request to fetch more results within the same window. null when the window is fully drained. |
Example
curl -X GET "https://api.revrag.ai/webhook-failures?since=2026-05-27T00:00:00Z&limit=100" \
-H "X-API-Key: YOUR_API_KEY"Fetching the missed payload
For each call_id returned above, fetch the full call payload — the same JSON shape as the webhook body — from the Get Call Status endpoint:
curl -X GET "https://api.revrag.ai/v1/campaigns/trigger/status/{call_id}" \
-H "X-API-Key: YOUR_API_KEY"Recommended polling loop
- Persist the timestamp of your last successful poll (
last_poll_at). - Every few minutes, call
GET /webhook-failures?since=<last_poll_at>&until=<now>. - For each
call_idin the response, callGET /v1/campaigns/trigger/status/{call_id}and process the payload the same way your webhook handler would. - If
next_cursoris present, paginate through by re-calling with&cursor=<next_cursor>untilnext_cursorisnull. - Set
last_poll_at = <now>for the next poll.
This guarantees no call is lost even if your endpoint is briefly unreachable.
Correlation IDs
To tie a webhook back to the right user in your own system, use app_user_id together with call_id.
The app_user_id you pass when requesting an in-app call token is echoed back top-level in every webhook, so you can attribute the call to the exact user who initiated it.
In-app events vs webhooks
Don't confuse these two mechanisms:
| In-app SDK events | Webhooks (this page) | |
|---|---|---|
| Where | In your app process (browser / mobile) | Server-to-server, to your backend |
| When | Live, during the call (agent_start, agent_end, …) | After the call ends (call.ended) |
| Use for | Driving UI, real-time state | Persisting the call result, transcript, summary, analytics |
For the in-process events, see your platform's integration guide.