Voicetta
    Voicetta.

    Voicetta API

    Webhook Reference

    Receive Voicetta conversation events in your backend. Covers the event types, payloads, headers, signatures, retries, and delivery behavior.

    Webhook Reference

    Voicetta sends conversation events to your backend as signed webhooks.

    Use webhooks to keep your product in sync with what is happening across voice, SMS, and WhatsApp.

    Each delivery contains one event and the data behind it.

    The events

    EventSubjectFires
    call.completedOne voice callWhen post-call processing finishes
    message.receivedOne inbound SMS or WhatsApp message15–30 seconds after arrival
    message.sentOne outbound SMS or WhatsApp message15–30 seconds after sending
    thread.completedOne finished SMS or WhatsApp conversationWhen the conversation is graded

    Choose the events you want for each webhook endpoint in Settings → Developers.

    The envelope

    Every event has the same structure:

    json
    { "event": "call.completed", "event_id": "9f8b1c4e-6d2a-4b7f-8e21-3c5a7d9e0f11", "version": 1, "occurred_at": "2026-08-20T10:03:00+00:00", "call": {} }

    The event tells you what happened.

    The object named after the event contains the data:

    • call
    • message
    • thread

    event_id is stable across retries.

    call.completed

    This event fires after post-call processing is complete.

    The transcript, analysis, evaluations, costs, and recording are ready at this point.

    json
    { "event": "call.completed", "event_id": "9f8b1c4e-6d2a-4b7f-8e21-3c5a7d9e0f11", "version": 1, "occurred_at": "2026-08-20T10:03:00+00:00", "call": { "call_id": "9f8b1c4e-6d2a-4b7f-8e21-3c5a7d9e0f11", "workspace_id": "2a1f8e7d-4c3b-4a29-9f18-6d5e4c3b2a19", "assistant_id": "7c6b5a49-3e2d-4c1b-8a97-5f4e3d2c1b09", "assistant_name": "Front Desk", "call_type": "inbound", "direction": "inbound", "status": "ended", "duration_seconds": 180, "started_at": "2026-08-20T10:00:00+00:00", "ended_at": "2026-08-20T10:03:00+00:00", "end_reason": "caller_hung_up", "is_test": false, "customer": { "customer_id": "5d4c3b2a-1f09-4e8d-7c6b-5a493e2d1c0b", "name": "Jane Caller", "phone": "+15551234567", "email": "[email protected]", "language": "en", "country": "US" }, "transcript": [ { "role": "assistant", "content": "Good morning, how can I help?" }, { "role": "user", "content": "I'd like to book a room for Friday." } ], "analysis": { "summary": "Caller asked about availability for Friday and booked a double room.", "intent": "booking_enquiry", "outcome": "booked", "sentiment": "positive" }, "evaluations": [ { "evaluation_id": "3e2d1c0b-5a49-4f8e-9d7c-6b5a493e2d1c", "title": "Confirmed the booking reference", "result": "pass", "initial_result": "pass", "rationale": "The agent read the reference back to the caller.", "recovered": false } ], "recording": { "status": "complete", "available": true, "download_url": "https://api.voicetta.com/v1/calls/9f8b1c4e-6d2a-4b7f-8e21-3c5a7d9e0f11/recording" } } }

    Reading the payload

    • event_id is the call id.
    • Retries keep the same event_id.
    • Missing fields mean there was no value.
    • Optional blocks can be disabled in your webhook settings.
    • recording.download_url points to a Voicetta recording endpoint. The endpoint creates a short-lived signed URL when accessed.

    Choosing what you receive

    You can choose which data blocks each webhook sends.

    BlockContainsDefaultEvents
    callRecord id, workspace, assistant, timestampsAlwaysAll
    customerName, phone, email, language, countryOnAll
    costsCost breakdownOffAll
    transcriptTurn-by-turn transcriptOnCalls, threads
    analysisSummary, intent, outcome, sentimentOnCalls, threads
    evaluationsEvaluation verdicts and reasoningOnCalls, threads
    follow_upsRecovery and follow-up stateOffCalls, threads
    recordingRecording status and download linkOnCalls
    performance_metricsSpeech, model, and voice latencyOffCalls
    config_snapshotAssistant configuration at call timeOffCalls

    A block that does not apply to an event is omitted.

    For example, messages do not contain recordings or call latency.

    Only request or store the data your product actually needs.

    Headers

    Every webhook includes:

    HeaderValue
    X-Voicetta-EventEvent name
    X-Voicetta-Delivery-IdUnique id for this delivery attempt
    X-Voicetta-Payload-Version1
    X-Voicetta-SignatureHMAC signature

    You can also configure custom headers.

    Verifying Voicetta requests

    Your endpoint is public, so you should verify incoming requests.

    You have two options.

    Option A: Custom header

    Add a secret header in Settings → Developers.

    For example:

    text
    X-Api-Key: your-secret

    Voicetta sends the header with every delivery.

    This works well with no-code tools such as n8n.

    Option B: HMAC signature

    Voicetta sends:

    text
    X-Voicetta-Signature: t=<unix seconds>,v1=<hex hmac-sha256>

    Calculate HMAC-SHA256 over:

    text
    <timestamp>.<raw request body>

    using your webhook signing secret.

    Always use the raw request body.

    Do not parse and re-serialize the JSON before calculating the signature.

    Also reject signatures older than a reasonable window, such as five minutes.

    Node

    js
    const crypto = require('crypto'); app.post( '/voicetta/calls', express.raw({ type: 'application/json' }), (req, res) => { const header = req.get('X-Voicetta-Signature') || ''; const parts = Object.fromEntries( header.split(',').map((p) => p.split('=')) ); const timestamp = Number(parts.t); if ( !Number.isFinite(timestamp) || Math.abs(Date.now() / 1000 - timestamp) > 300 ) { return res.sendStatus(400); } const expected = crypto .createHmac( 'sha256', process.env.VOICETTA_SIGNING_SECRET ) .update(`${timestamp}.`) .update(req.body) .digest('hex'); const provided = Buffer.from(parts.v1 || '', 'utf8'); const computed = Buffer.from(expected, 'utf8'); if ( provided.length !== computed.length || !crypto.timingSafeEqual(provided, computed) ) { return res.sendStatus(401); } res.sendStatus(200); enqueue(JSON.parse(req.body.toString('utf8'))); } );

    Python

    python
    import hashlib import hmac import os import time from fastapi import BackgroundTasks, HTTPException, Request SECRET = os.environ["VOICETTA_SIGNING_SECRET"] TOLERANCE_SECONDS = 300 @app.post("/voicetta/calls") async def receive_call( request: Request, background: BackgroundTasks ): body = await request.body() parts = dict( part.split("=", 1) for part in request.headers .get("X-Voicetta-Signature", "") .split(",") ) try: timestamp = int(parts["t"]) provided = parts["v1"] except (KeyError, ValueError): raise HTTPException( status_code=400, detail="Malformed signature" ) if abs(time.time() - timestamp) > TOLERANCE_SECONDS: raise HTTPException( status_code=400, detail="Stale signature" ) expected = hmac.new( SECRET.encode(), f"{timestamp}.".encode() + body, hashlib.sha256 ).hexdigest() if not hmac.compare_digest(expected, provided): raise HTTPException( status_code=401, detail="Bad signature" ) background.add_task( process_call, await request.json() ) return {"received": True}

    Rotating the signing secret

    You can rotate the secret from the dashboard.

    The old secret stops working immediately.

    Update your backend first if possible, then rotate the secret.

    Retries

    If your endpoint returns anything outside 2xx, or does not respond in time, Voicetta retries the delivery.

    There are up to six attempts.

    AttemptDelay
    21 minute
    32 minutes
    44 minutes
    58 minutes
    616 minutes

    After the final attempt, the delivery is marked as failed.

    You can retry it manually from the delivery log or fetch the same record through the REST API.

    Each retry has a new delivery id but the same event_id.

    What your integration should assume

    At-least-once delivery

    The same event can arrive more than once.

    Deduplicate it.

    Not real-time

    Voice events arrive a few seconds after the call.

    Text events usually arrive within 15–30 seconds.

    This is suitable for activity feeds and workflows, not for building a live chat transport.

    Not ordered

    Events can arrive out of order.

    Use occurred_at when you need chronological ordering.

    Do not rely on arrival order.

    Timeout

    Webhook timeout can be set between 1 and 10 seconds.

    The default is 10 seconds.

    Keep it at 10 unless you have a specific reason to change it.

    More importantly: respond quickly.

    Do your processing after the 200.

    Troubleshooting

    ProblemLikely cause
    Signature does not matchYou are hashing parsed JSON instead of the raw body
    Same event arrives repeatedlyYour response is too slow
    Deliveries time outProcessing is happening before the response
    Everything is failedEndpoint is unreachable, private, or returning non-2xx
    A block is missingIt is disabled or does not apply to that event
    A field is missingThe value is not present
    Event never arrivesIt is not enabled for that webhook
    Old events are missingWebhook events are not retroactive