Post-call Webhooks
Receive a signed event at your own endpoint every time a call finishes
Overview
A post-call webhook is urvo calling you. When a call finishes, urvo POSTs a JSON event to a URL you own, so your systems learn about the call without polling.
This is the opposite direction to a webhook tool. The naming trips people up, so it is worth being explicit:
| Direction | When | |
|---|---|---|
| Webhook tool | Agent → your API | Mid-conversation, to look something up or act |
| Post-call webhook | urvo → your API | After the call, to report what happened |
Adding an endpoint
- Open the agent and go to Configuration.
- In the Advanced section, find Post-call Webhooks.
- Add a webhook with a URL and an optional name to identify it.
- Choose which data sections that endpoint should receive.
- Save.
You can add several endpoints. Each one is configured independently, so a CRM integration can receive the transcript while an internal dashboard receives only durations and outcomes. Every matching endpoint receives its own copy of the event.
Events
| Event | Meaning |
|---|---|
call.completed | The call finished and its transcript is available. This is the one most integrations care about. |
call.failed | The call could not be placed or connected. |
call.audio | The recording for a call has become available. |
Treat the event name as the thing you branch on, and ignore events you do not recognise rather than erroring. New event types can be added over time.
Payload
Every payload carries an event name and a timestamp. Everything else is a section you opt into per endpoint:
| Section | Contains |
|---|---|
| Agent | Agent id and name |
| Status | Whether the call completed or failed |
| Summary | A generated summary of the call |
| Duration | Call length in seconds |
| Transcript | The full conversation as speaker/message turns |
| Phone | Caller and agent numbers |
| Dynamic variables | The per-call variables used |
| Audio | A link to the recording |
| Outcome | The disposition and captured fields from your outcome schema |
Send only what you need. Transcripts and summaries are the most sensitive parts of the payload, and an endpoint that does not need them should not be receiving them.
Outcome may be missing
Outcome extraction can fail. When it does, the outcome comes through empty with a failure status attached, and the event is still delivered. Your handler must cope with an absent outcome rather than assuming one is always present.
Contract stability
The payload is versioned and changes are additive: new fields may appear, existing ones are not removed or repurposed. Parse defensively — ignore unknown fields instead of rejecting the request — and new fields will never break your handler.
Verifying the signature
Your endpoint is a public URL, so anyone could POST to it. Every delivery carries an HMAC signature so you can prove it came from urvo. Verify it before trusting the body.
The header looks like this:
X-Urvo-Signature: t=1735689600,v0=3ba7f0...To verify:
- Parse
t(a Unix timestamp) andv0(a hex digest) out of the header. - Build the signed string by joining the timestamp and the raw request body with a dot:
{t}.{body}. - Compute HMAC-SHA256 over that string using your endpoint's signing secret.
- Compare your hex digest to
v0using a constant-time comparison. - Reject the request if
tis more than 5 minutes from your clock.
Two details that cause almost every failed integration:
- Use the raw body bytes. If your framework parses JSON and you re-serialise it, key order and whitespace change and the digest will never match. Capture the body before parsing.
- Use a constant-time compare (
hmac.compare_digest,crypto.timingSafeEqual) rather than==.
Python example:
import hashlib, hmac, time
def verify(raw_body: bytes, header: str, secret: str) -> bool:
parts = dict(p.split("=", 1) for p in header.split(","))
timestamp, received = parts["t"], parts["v0"]
# Reject anything older than 5 minutes.
if abs(time.time() - int(timestamp)) > 300:
return False
signed = f"{timestamp}.".encode() + raw_body
expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, received)The timestamp check is what stops an attacker replaying a delivery they captured earlier. Skipping it leaves a valid signature valid forever.
Idempotency and retries
Each delivery carries an Idempotency-Key header. urvo also de-duplicates internally per conversation, event type and event time, so the same call event is not fanned out repeatedly.
Even so, build your handler to tolerate receiving the same event twice. Store the idempotency key and skip work you have already done. Networks retry, and a handler that creates a CRM record unconditionally will eventually create two.
Return a 2xx quickly. Do the slow work in a background job rather than while urvo waits on the response.
Recording links
When the audio section is enabled, the payload includes a signed URL to the recording. These links expire after 7 days.
If you need long-term access to audio, download the file when you receive the event and store it yourself. Saving the URL into your CRM and expecting it to work next month will not do what you want.
Checking deliveries
Every attempt is logged with its response status and any error, so you can tell a webhook that was never sent from one that was sent and rejected. Use it as the first stop when data is not arriving.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Signature never matches | Body was re-serialised after parsing | Sign the raw body bytes exactly as received. |
| Signature matched, now rejected | Server clock drift beyond the 5-minute window | Enable NTP on the receiving host. |
| Nothing arrives at all | URL unreachable from the internet, or not saved | Check the delivery log, then confirm the endpoint is publicly reachable over HTTPS. |
| Duplicate records created | Handler is not idempotent | De-duplicate on Idempotency-Key. |
| Outcome field empty | Extraction failed, or no outcome schema is configured | Configure a schema and handle the empty case. |
| Recording link 404s later | The 7-day expiry passed | Download and store audio on receipt. |
| Deliveries time out | Handler does slow work before responding | Acknowledge with 2xx immediately, process asynchronously. |
Next steps
- Call outcomes — define the dispositions and fields that appear in the payload.
- urvo pieces — react to calls inside urvo instead of hosting an endpoint.
- Webhook tools — calls in the other direction, during the conversation.