Advanced

    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:

    DirectionWhen
    Webhook toolAgent → your APIMid-conversation, to look something up or act
    Post-call webhookurvo → your APIAfter the call, to report what happened

    Adding an endpoint

    1. Open the agent and go to Configuration.
    2. In the Advanced section, find Post-call Webhooks.
    3. Add a webhook with a URL and an optional name to identify it.
    4. Choose which data sections that endpoint should receive.
    5. 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

    EventMeaning
    call.completedThe call finished and its transcript is available. This is the one most integrations care about.
    call.failedThe call could not be placed or connected.
    call.audioThe 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:

    SectionContains
    AgentAgent id and name
    StatusWhether the call completed or failed
    SummaryA generated summary of the call
    DurationCall length in seconds
    TranscriptThe full conversation as speaker/message turns
    PhoneCaller and agent numbers
    Dynamic variablesThe per-call variables used
    AudioA link to the recording
    OutcomeThe 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:

    1. Parse t (a Unix timestamp) and v0 (a hex digest) out of the header.
    2. Build the signed string by joining the timestamp and the raw request body with a dot: {t}.{body}.
    3. Compute HMAC-SHA256 over that string using your endpoint's signing secret.
    4. Compare your hex digest to v0 using a constant-time comparison.
    5. Reject the request if t is 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.

    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

    SymptomLikely causeFix
    Signature never matchesBody was re-serialised after parsingSign the raw body bytes exactly as received.
    Signature matched, now rejectedServer clock drift beyond the 5-minute windowEnable NTP on the receiving host.
    Nothing arrives at allURL unreachable from the internet, or not savedCheck the delivery log, then confirm the endpoint is publicly reachable over HTTPS.
    Duplicate records createdHandler is not idempotentDe-duplicate on Idempotency-Key.
    Outcome field emptyExtraction failed, or no outcome schema is configuredConfigure a schema and handle the empty case.
    Recording link 404s laterThe 7-day expiry passedDownload and store audio on receipt.
    Deliveries time outHandler does slow work before respondingAcknowledge 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.
    Was this page helpful?