Skip to main content
Docs menu: Webhooks

Guides

Webhooks

Have Telvana send each call's result to your server when the call ends, and check that every request really came from Telvana.

The post-conversation webhook is a POST to your server after each call an agent handles, inbound or outbound, including outbound calls that never connected. It carries the transcript, summary, sentiment, tags, disposition, the variables you sent with the call and a recording link.

Turn it on

  1. In the portal, open Settings, then Webhooks. Enter the HTTPS address that should receive results and choose Save. This is the workspace's webhook URL.
  2. In the same row's menu, choose Copy secret to copy the workspace's signing secret. If the page says no secret is configured, choose Regenerate secret to create one.
  3. Open each agent whose calls you want and, in its settings, turn on Post Conversation Webhook. Leave the URL field blank to use the workspace URL, or enter a URL for that agent alone.
  4. Back on the Webhooks page, choose Send Test Event and check that your server receives it.

You can also turn the webhook on for an agent through the API:

curl -X PUT "https://api.telvana.com/agents/<agentId>/settings" \
  -H "x-api-key: $TELVANA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "hasPostConversationWebhook": true }'

An agent's own URL takes precedence over the workspace URL. Every request is signed with the workspace secret, whichever URL it goes to.

The request

POST /telvana/webhook HTTP/1.1
Content-Type: application/json
X-Webhook-Signature: <HMAC-SHA256 of the body, as hex>

{
  "conversationId": "clx8y9z1a2b3c4d5e6f7g8h9i",
  "dateInitiated": "2026-10-06T17:04:11.000Z",
  "agentId": "jm1u1cawc7dka1x84mrjvb4y",
  "workspaceId": "ws_abc123",
  "direction": "OUTBOUND",
  "agentPhoneNumber": "+15551234567",
  "humanPhoneNumber": "+15557654321",
  "duration": 48,
  "cost": 12,
  "summary": "Dana confirmed the 2:30 PM pickup.",
  "sentiment": "POSITIVE",
  "disposition": "COMPLETED",
  "tags": ["appointment_booked"],
  "variables": { "firstName": "Dana", "pickupTime": "2:30 PM" },
  "transcript": "AI: Hi Dana, ...",
  "recordingUrl": "https://..."
}

The values are illustrative. duration is in seconds and cost in cents. disposition is one of COMPLETED, VOICEMAIL, TRANSFERRED, APPOINTMENT_BOOKED, SCHEDULED_CALLBACK, ABANDONED or FAILED. A call that never connected arrives with FAILED and an empty transcript and summary, and it has no recordingUrl when there is no recording. The recording link expires an hour after the webhook is sent. Every field is listed in the webhook payload reference.

The test event from the portal has the same shape, with "event": "test" and a conversationId that starts with test_.

Check the signature

X-Webhook-Signature is the HMAC-SHA256 of the raw request body, keyed with your workspace's signing secret and written as lowercase hex. Use the whole secret as the key, whsec_ prefix included.

Compute the HMAC over the body exactly as it arrived, before any JSON parsing, and compare the two values in constant time. Reject the request if they differ or the header is missing.

Node.js with Express:

import crypto from "node:crypto";
import express from "express";

const app = express();

app.post(
  "/telvana/webhook",
  express.raw({ type: "application/json" }),
  (req, res) => {
    const expected = crypto
      .createHmac("sha256", process.env.TELVANA_WEBHOOK_SECRET)
      .update(req.body)
      .digest("hex");
    const received = req.get("X-Webhook-Signature") ?? "";
    const valid =
      received.length === expected.length &&
      crypto.timingSafeEqual(Buffer.from(received), Buffer.from(expected));
    if (!valid) return res.sendStatus(401);

    const call = JSON.parse(req.body.toString("utf8"));
    // Save the call, keyed by call.conversationId, and process it afterwards.
    res.sendStatus(200);
  },
);

Python with Flask:

import hashlib
import hmac
import os

from flask import Flask, abort, request

app = Flask(__name__)


@app.post("/telvana/webhook")
def telvana_webhook():
    expected = hmac.new(
        os.environ["TELVANA_WEBHOOK_SECRET"].encode(),
        request.get_data(),
        hashlib.sha256,
    ).hexdigest()
    received = request.headers.get("X-Webhook-Signature", "")
    if not hmac.compare_digest(received, expected):
        abort(401)

    call = request.get_json()
    # Save the call, keyed by call["conversationId"], and process it afterwards.
    return "", 200

Delivery

Telvana sends each webhook once. It waits up to 10 seconds for your response, and does not send it again after a timeout, a connection error or an error status. So:

  • Answer with a 2xx quickly. Save the payload and do slow work, such as CRM updates, after you've responded.
  • Reconcile on a schedule. List recent conversations and fetch any conversationId you don't have, which also covers time your endpoint was down.

The URL has to be reachable from the public internet. Telvana won't send to private, loopback or link-local addresses. Use HTTPS so the call details are encrypted in transit.

Rotating the secret

Regenerate secret on the Webhooks page replaces the secret at once, and the next webhook is signed with the new one. To rotate without dropping results, have your server accept either the old or the new secret, regenerate, update your server's secret, then remove the old one.