--- title: "Voicetta API" description: "Build your own product on top of Voicetta. Use voice, SMS and WhatsApp through one communication backend, with webhooks and a REST API for everything that happens." publishedAt: "2026-08-22" modifiedAt: "2026-08-22" category: "Getting Started" tags: "Voicetta, API, webhook, voice, SMS, WhatsApp, integration, white label, communication" navOrder: "10" canonical: "https://voicetta.com/developers/overview" --- # Build on Voicetta Voicetta handles customer communication across voice, SMS, and WhatsApp. You build the product around it. Use the Voicetta API to power your own dashboard, CRM, white-label product, workflow, or customer experience. You can receive events through webhooks, or read the same data through the REST API. ## What Voicetta handles Voicetta takes care of the communication layer. That includes: * voice calls * SMS * WhatsApp * transcripts * conversation analysis * outcomes and intents * evaluations * recordings * follow-ups * conversation history Your product decides what happens with that information. Store it in your database. Show it in your own UI. Add it to a CRM. Trigger a workflow. Build reports. Or build an entirely new product around it. ## What you can build ### A white-label product Voicetta runs the communication and AI agents. Your customers use your product, your UI, and your branding. They do not need to know that Voicetta is behind it. ### Your own communication dashboard Build your own inbox, conversation timeline, activity feed, reporting, or customer view across voice, SMS, and WhatsApp. ### CRM or PMS integrations Push calls, messages, transcripts, outcomes, and customer information into the systems your team already uses. ### Workflow automation Trigger actions based on what happened in a conversation. For example: * create a lead after an inbound call * notify a sales rep * update a booking * escalate an unhappy customer * send a follow-up * mark a conversation as resolved ### Your own AI product Use Voicetta as the communication layer behind a product you are building. You do not need to build the telephony, messaging, conversation storage, transcripts, recordings, or conversation evaluation yourself. ## The API There are three main resources. | Resource | Event | API | |---|---|---| | Voice call | `call.completed` | `/v1/calls` | | SMS or WhatsApp message | `message.received`, `message.sent` | `/v1/messages` | | Completed text conversation | `thread.completed` | `/v1/threads` | Messages and completed threads are linked through `thread_id`. Messages tell you what is happening. Threads tell you how the conversation ended and how it was evaluated. ## Webhooks or REST API You can use webhooks, the REST API, or both. | | Webhook | REST API | |---|---|---| | Who starts it | Voicetta sends the event | You make the request | | Best for | Keeping your product up to date | History, backfills, recovery | | Authentication | Signed webhook | API key | | Endpoint | Your HTTPS URL | `https://api.voicetta.com/v1` | For most integrations, start with webhooks. Use the REST API when you need historical data or need to recover something your backend missed. The same underlying objects are available through both, so you can use one data model. ## Getting access 1. Open **Settings → Developers** in Voicetta. 2. Create an API key, a webhook endpoint, or both. 3. Choose the events and data you want. 4. Start building. API keys and webhook secrets are shown once when created. If you lose one, rotate it and create a new one. ## When events arrive Events are sent when the relevant data is ready. | Event | Fires | Typical delay | |---|---|---| | `call.completed` | Post-call processing is finished | A few seconds after hang-up | | `message.received` | A customer message is received | 15–30 seconds | | `message.sent` | Voicetta sends a message | 15–30 seconds | | `thread.completed` | A text conversation is closed and graded | About 30 minutes after the last message | A text conversation is different from a message. The message event tells you what is happening now. The thread event tells you how the conversation went after it is finished. ## Version 1 All API paths use `/v1`. Within version 1, we only add fields. We do not rename, remove, retype, or change the meaning of existing fields. Breaking changes get a new API version. Design your integration to: * ignore fields it does not recognize * treat missing fields as normal * switch on `event` * ignore events it does not recognize instead of failing New events and fields should not break an existing integration. ## For AI coding tools Every developer page has a Markdown version. * This page: `/developers/overview.md` * All developer docs: `/developers.md` * OpenAPI 3.1 spec: `/open-api` The OpenAPI spec is generated from the running API. You can point a code generator at it or give `/developers.md` to your coding agent. ## Related pages * [Quickstart](/developers/quickstart) * [Webhook Reference](/developers/webhooks) * [Message Events](/developers/message-events) * [Thread Events](/developers/thread-events) * [Read API Reference](/developers/api-reference) * [n8n and No-code](/developers/n8n) --- --- title: "Quickstart" description: "Connect your product to Voicetta in a few minutes. Receive a real conversation event, handle it in your backend, and read the same data through the API." publishedAt: "2026-08-22" modifiedAt: "2026-08-22" category: "Getting Started" tags: "Voicetta, quickstart, webhook, API key, curl, integration" navOrder: "20" canonical: "https://voicetta.com/developers/quickstart" --- # Quickstart Connect Voicetta to your product in a few minutes. The basic flow is: **Customer → Voicetta → your backend → your product** Voicetta handles the conversation. Your backend receives the event and decides what to do with it. ## Step 1: See a real event Before writing code, send a test event to a temporary endpoint. 1. Open [webhook.site](https://webhook.site), RequestBin, or use an `ngrok` tunnel. 2. Copy the URL. 3. In Voicetta, open **Settings → Developers**. 4. Create a webhook endpoint using that URL. 5. Choose the events you want. 6. Click **Send test event**. You will see the exact HTTP request, headers, and JSON payload. A Voicetta event is one JSON object. The basic shape is: ```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 underneath it contains the data. ## Step 2: Read data through the API Create an API key in **Settings → Developers**. ```bash curl -H "Authorization: Bearer vk_live_your_key_here" \ "https://api.voicetta.com/v1/calls?limit=1" ``` You get a page of calls: ```json { "calls": [ { "call_id": "9f8b1c4e-6d2a-4b7f-8e21-3c5a7d9e0f11", "workspace_id": "2a1f8e7d-4c3b-4a29-9f18-6d5e4c3b2a19", "call_type": "inbound", "direction": "inbound", "status": "ended", "duration_seconds": 180, "analysis": { "summary": "Caller booked a double room for Friday." } } ], "has_more": true, "next_cursor": "MjAyNi0wOC0yMFQxMDowMDowMCswMDowMHw5ZjhiMWM0ZQ" } ``` You can request only the fields your application needs: ```bash curl -H "Authorization: Bearer vk_live_your_key_here" \ "https://api.voicetta.com/v1/calls?limit=1&fields=transcript" ``` Requesting a field group your key does not have returns `403`. A missing field is not an error. Fields with no value are omitted. The other collections work the same way: ```bash curl -H "Authorization: Bearer vk_live_your_key_here" \ "https://api.voicetta.com/v1/messages?limit=1" curl -H "Authorization: Bearer vk_live_your_key_here" \ "https://api.voicetta.com/v1/threads?limit=1" ``` One API key can read all three collections. ## Step 3: Handle webhook events Replace the temporary URL with your own endpoint. A minimal Express handler: ```js const HANDLERS = { 'call.completed': (body) => store('call', body.call), 'message.received': (body) => store('message', body.message), 'message.sent': (body) => store('message', body.message), 'thread.completed': (body) => store('thread', body.thread), }; app.post('/voicetta/events', express.json(), (req, res) => { res.sendStatus(200); const deliveryId = req.get('X-Voicetta-Delivery-Id'); if (alreadyProcessed(deliveryId)) { return; } const handler = HANDLERS[req.body.event]; if (!handler) { return; } enqueue(deliveryId, () => handler(req.body)); }); ``` The important part is the shape: 1. receive the event 2. acknowledge it 3. deduplicate it 4. dispatch based on `event` 5. process it in the background Add signature verification before going to production. See [Webhook Reference](/developers/webhooks#verifying-voicetta-requests). ## The two rules that matter ### Acknowledge first Return `200` quickly. Do not wait for database writes, API calls, or other processing before responding. Put the event into a queue or store it, return `200`, and process it afterward. If the response takes too long, Voicetta will retry the delivery. ### Expect duplicates Webhook delivery is at-least-once. A network problem can cause the same event to arrive more than once. Make your handler idempotent. Use `X-Voicetta-Delivery-Id` to identify delivery attempts, or use `event_id` when you want to treat repeated deliveries as the same conversation event. ## Step 4: Go live 1. Make sure your endpoint is publicly reachable over HTTPS. 2. Test the endpoint with **Send test event**. 3. Confirm that your server returns `200`. 4. Add signature verification. 5. Watch the delivery log. 6. Start receiving real traffic. Enabling a webhook event is not retroactive. If you need older conversations, use the REST API. ## Related pages * [Overview](/developers/overview) * [Webhook Reference](/developers/webhooks) * [Message Events](/developers/message-events) * [Thread Events](/developers/thread-events) * [Read API Reference](/developers/api-reference) * [n8n and No-code](/developers/n8n) --- --- title: "Webhook Reference" description: "Receive Voicetta conversation events in your backend. Covers the event types, payloads, headers, signatures, retries, and delivery behavior." publishedAt: "2026-08-22" modifiedAt: "2026-08-22" category: "Reference" tags: "Voicetta, webhook, HMAC, signature, retries, payload, call.completed, events" navOrder: "30" canonical: "https://voicetta.com/developers/webhooks" --- # 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 | Event | Subject | Fires | |---|---|---| | `call.completed` | One voice call | When post-call processing finishes | | `message.received` | One inbound SMS or WhatsApp message | 15–30 seconds after arrival | | `message.sent` | One outbound SMS or WhatsApp message | 15–30 seconds after sending | | `thread.completed` | One finished SMS or WhatsApp conversation | When 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": "jane@example.com", "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. | Block | Contains | Default | Events | |---|---|---|---| | `call` | Record id, workspace, assistant, timestamps | Always | All | | `customer` | Name, phone, email, language, country | On | All | | `costs` | Cost breakdown | Off | All | | `transcript` | Turn-by-turn transcript | On | Calls, threads | | `analysis` | Summary, intent, outcome, sentiment | On | Calls, threads | | `evaluations` | Evaluation verdicts and reasoning | On | Calls, threads | | `follow_ups` | Recovery and follow-up state | Off | Calls, threads | | `recording` | Recording status and download link | On | Calls | | `performance_metrics` | Speech, model, and voice latency | Off | Calls | | `config_snapshot` | Assistant configuration at call time | Off | Calls | 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: | Header | Value | |---|---| | `X-Voicetta-Event` | Event name | | `X-Voicetta-Delivery-Id` | Unique id for this delivery attempt | | `X-Voicetta-Payload-Version` | `1` | | `X-Voicetta-Signature` | HMAC 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=,v1= ``` Calculate HMAC-SHA256 over: ```text . ``` 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. | Attempt | Delay | |---|---| | 2 | 1 minute | | 3 | 2 minutes | | 4 | 4 minutes | | 5 | 8 minutes | | 6 | 16 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 | Problem | Likely cause | |---|---| | Signature does not match | You are hashing parsed JSON instead of the raw body | | Same event arrives repeatedly | Your response is too slow | | Deliveries time out | Processing is happening before the response | | Everything is failed | Endpoint is unreachable, private, or returning non-2xx | | A block is missing | It is disabled or does not apply to that event | | A field is missing | The value is not present | | Event never arrives | It is not enabled for that webhook | | Old events are missing | Webhook events are not retroactive | ## Related pages * [Quickstart](/developers/quickstart) * [Message Events](/developers/message-events) * [Thread Events](/developers/thread-events) * [Read API Reference](/developers/api-reference) * [n8n and No-code](/developers/n8n) --- --- title: "Message Events" description: "Receive SMS and WhatsApp activity from Voicetta. Use message events for live communication data and thread events for the finished conversation." publishedAt: "2026-08-22" modifiedAt: "2026-08-22" category: "Reference" tags: "Voicetta, SMS, WhatsApp, webhook, message.received, message.sent, text messages" navOrder: "32" canonical: "https://voicetta.com/developers/message-events" --- # Message Events Voicetta sends SMS and WhatsApp activity through two events. Use them when your product needs to know what is happening in a conversation. | Event | Fires for | |---|---| | `message.received` | A customer message and the reply generated by Voicetta | | `message.sent` | A message Voicetta sends without an incoming customer message | For the finished conversation and its evaluation, use [`thread.completed`](/developers/thread-events). ## Why there are two events An incoming message and an outgoing message are different events. When a customer sends a message, Voicetta creates a `message.received` event. If the assistant replies in the same turn, that reply is included in the `reply` field. When Voicetta starts the conversation itself, it creates a `message.sent` event. That can happen when the voice assistant sends a confirmation during a call or when a follow-up is sent. ## `message.received` ```json { "event": "message.received", "event_id": "b71e9d02-4a6c-4f18-9d3e-7c2b5a4e8f31", "version": 1, "occurred_at": "2026-08-20T14:12:00+00:00", "message": { "message_id": "b71e9d02-4a6c-4f18-9d3e-7c2b5a4e8f31", "workspace_id": "2a1f8e7d-4c3b-4a29-9f18-6d5e4c3b2a19", "assistant_id": "7c6b5a49-3e2d-4c1b-8a97-5f4e3d2c1b09", "assistant_name": "Front Desk", "channel": "whatsapp", "direction": "inbound", "thread_id": "c82fa013-5b7d-4029-8e4f-8d3c6b5f9a42", "source": "inbound", "occurred_at": "2026-08-20T14:12:00+00:00", "content": "Do you have a room free on Friday?", "provider_message_id": "SM8f3c1a2b4d5e6f70", "reply": { "content": "Yes, we have a double available. Shall I hold it?", "provider_message_id": "SM1a2b3c4d5e6f7080", "timestamp": "2026-08-20T14:12:04+00:00" }, "customer": { "customer_id": "5d4c3b2a-1f09-4e8d-7c6b-5a493e2d1c0b", "name": "Jane Texter", "phone": "+15551234567" } } } ``` ## `message.sent` ```json { "event": "message.sent", "event_id": "d93ba124-6e8f-4130-9a5f-9e4d7c6b0a53", "version": 1, "occurred_at": "2026-08-20T10:03:12+00:00", "message": { "message_id": "d93ba124-6e8f-4130-9a5f-9e4d7c6b0a53", "workspace_id": "2a1f8e7d-4c3b-4a29-9f18-6d5e4c3b2a19", "assistant_id": "7c6b5a49-3e2d-4c1b-8a97-5f4e3d2c1b09", "assistant_name": "Front Desk", "channel": "sms", "direction": "outbound", "thread_id": "e04cb235-7f90-4241-8b60-af5e8d7c1b64", "source": "voice_tool", "occurred_at": "2026-08-20T10:03:12+00:00", "content": "Your booking reference is BK-4471. See you Friday.", "provider_message_id": "SM2b3c4d5e6f708190", "customer": { "customer_id": "5d4c3b2a-1f09-4e8d-7c6b-5a493e2d1c0b", "phone": "+15551234567" } } } ``` ## Reading the fields * `content` is the message text. * `channel` is `sms` or `whatsapp`. * `direction` is `inbound` or `outbound`. * `thread_id` identifies the conversation. * `provider_message_id` is the carrier's message id when available. * `reply` appears on `message.received` when the assistant replies in the same turn. ## `source` | Value | Meaning | |---|---| | `inbound` | Customer started the conversation | | `voice_tool` | Voice assistant sent the message during a call | | `evaluation_follow_up` | Recovery follow-up after evaluation | | `test` | Dashboard test message | Test messages are not delivered to production webhooks or returned by the read API. Treat unknown future source values as unknown origins, not errors. ## Timing Messages are usually delivered within 15–30 seconds. They are not guaranteed to arrive in order. If order matters, sort by `occurred_at`. ## Costs If you enable the `costs` block, messages can include an indicative message cost. This is not the billed amount. Use your invoice as the source of truth for billing. ## What messages do not contain A single message does not contain: * a conversation summary * evaluation results * a recording * call latency * assistant configuration Use `thread.completed` for the finished conversation. ## Reading messages through the API ```text GET /v1/messages ``` You can filter by date, assistant, channel, direction, and `thread_id`. Use the API when you need history or when you need to recover messages your webhook did not receive. ## Related pages * [Webhook Reference](/developers/webhooks) * [Thread Events](/developers/thread-events) * [Read API Reference](/developers/api-reference) * [Quickstart](/developers/quickstart) --- --- title: "Thread Events" description: "Get the finished SMS and WhatsApp conversation, including transcript, outcome, evaluations, and follow-up history." publishedAt: "2026-08-22" modifiedAt: "2026-08-22" category: "Reference" tags: "Voicetta, thread.completed, SMS, WhatsApp, evaluations, close_reason, webhook" navOrder: "34" canonical: "https://voicetta.com/developers/thread-events" --- # Thread Events A `thread.completed` event represents a finished SMS or WhatsApp conversation. Messages tell you what happened during the conversation. The thread tells you how the conversation ended. It can contain: * the transcript * summary * intent * outcome * sentiment * evaluation results * follow-ups * cost information ## When it fires A thread is not completed when the last message arrives. Voicetta waits for the conversation to become inactive before grading it. The default inactivity period is 30 minutes. If a recovery follow-up is needed, the conversation can stay open longer. Expect `thread.completed` roughly 30 minutes after the conversation goes quiet, unless a follow-up extends it. If you need the live conversation, use message events. ## Payload ```json { "event": "thread.completed", "event_id": "c82fa013-5b7d-4029-8e4f-8d3c6b5f9a42", "version": 1, "occurred_at": "2026-08-20T14:45:00+00:00", "thread": { "thread_id": "c82fa013-5b7d-4029-8e4f-8d3c6b5f9a42", "workspace_id": "2a1f8e7d-4c3b-4a29-9f18-6d5e4c3b2a19", "assistant_id": "7c6b5a49-3e2d-4c1b-8a97-5f4e3d2c1b09", "assistant_name": "Front Desk", "channel": "whatsapp", "close_reason": "graded", "message_count": 6, "started_at": "2026-08-20T14:12:00+00:00", "last_message_at": "2026-08-20T14:19:00+00:00", "completed_at": "2026-08-20T14:45:00+00:00", "anchor_message_id": "b71e9d02-4a6c-4f18-9d3e-7c2b5a4e8f31", "customer": { "customer_id": "5d4c3b2a-1f09-4e8d-7c6b-5a493e2d1c0b", "name": "Jane Texter", "phone": "+15551234567" }, "transcript": [ { "role": "user", "content": "Do you have a room free on Friday?" }, { "role": "assistant", "content": "Yes, we have a double available." } ], "analysis": { "summary": "Customer asked about Friday availability and held a double room.", "intent": "booking_enquiry", "outcome": "booked", "sentiment": "positive" }, "evaluations": [ { "evaluation_id": "3e2d1c0b-5a49-4f8e-9d7c-6b5a493e2d1c", "title": "Offered a booking reference", "result": "pass", "initial_result": "fail", "rationale": "The reference was given after the follow-up.", "recovered": true } ], "costs": { "currency": "USD", "total": "0.0300", "charged_message_count": 3 }, "follow_ups": [ { "status": "sent", "channel": "whatsapp", "sent_at": "2026-08-20T14:40:00+00:00" } ] } } ``` ## Reading the fields * `thread_id` is the conversation id. * `thread_id` is also present on message events. * `anchor_message_id` identifies the message associated with the grading. * `completed_at` is when grading finished. * `last_message_at` is when the conversation went quiet. * `message_count` is the number of messages in the transcript. * `evaluations[].recovered` tells you whether a failed criterion was fixed by a follow-up. ## Costs `costs.total` is an indicative value based on message usage. It is not the amount charged on your invoice. Use your invoice for billing. ## `close_reason` Always read `close_reason`. Not every completed thread has evaluation results. | Reason | Meaning | Graded? | |---|---|---| | `graded` | Normal completed conversation | Yes | | `no_reply_after_follow_up` | Recovery follow-up was not answered | Yes | | `customer_opted_out` | Customer opted out | Partly | | `no_enabled_evaluations` | No evaluations were enabled | No | | `empty_transcript` | Nothing could be graded | No | | `grading_failed` | Grading failed | No | | `follow_up_failed` | Recovery follow-up failed | No | | `follow_up_daily_cap` | Daily follow-up limit was reached | No | | `follow_up_no_criteria` | Nothing needed recovery | Partly | | `follow_up_no_customer_phone` | No phone number was available | No | | `follow_up_missing_openai_key` | Follow-up generation was not configured | No | | `follow_up_send_ambiguous` | Follow-up delivery could not be confirmed | No | | `follow_up_skipped:` | Follow-up was skipped | No | Do not treat every thread as graded. For quality reporting, use `graded` and `no_reply_after_follow_up` as your graded population. Treat the other close reasons as not graded. ## Why ungraded threads are still delivered A conversation happened even if grading failed. We still send it. That gives your product a complete picture of communication instead of silently hiding conversations because evaluation did not run. ## What threads do not contain Threads do not contain: * recordings * voice latency * voice configuration * `is_test` Those fields only apply to voice calls. ## Reading threads through the API ```text GET /v1/threads ``` You can filter by date, assistant, channel, and `close_reason`. Use: ```text close_reason=graded ``` when you only want conversations that produced evaluation results. ## Related pages * [Message Events](/developers/message-events) * [Webhook Reference](/developers/webhooks) * [Read API Reference](/developers/api-reference) * [Overview](/developers/overview) --- --- title: "Read API Reference" description: "Read Voicetta calls, messages, and completed conversations through the REST API. Includes authentication, pagination, filters, errors, rate limits, and backfills." publishedAt: "2026-08-22" modifiedAt: "2026-08-22" category: "Reference" tags: "Voicetta, REST API, OpenAPI, pagination, authentication, rate limits, messages, threads" navOrder: "40" canonical: "https://voicetta.com/developers/api-reference" --- # Read API Reference The REST API gives your product access to the same data that Voicetta sends through webhooks. Use it when you need: * conversation history * backfills * recovery after downtime * individual records * reports * an integration without webhooks ## Base URL ```text https://api.voicetta.com/v1 ``` ## Collections | Collection | Contains | Webhook event | |---|---|---| | `/v1/calls` | Voice calls | `call.completed` | | `/v1/messages` | SMS and WhatsApp messages | `message.received`, `message.sent` | | `/v1/threads` | Completed SMS and WhatsApp conversations | `thread.completed` | The same underlying objects are available through the API and webhooks. ## Authentication Every request requires an API key. Create one in **Settings → Developers**. Send it as a bearer token: ```text Authorization: Bearer vk_live_xxxxxxxxxxxxxxxxxxxxxxxx ``` Keys are shown once. If you lose a key, revoke it and create another one. Each key belongs to one workspace. ## Choosing what you can read API keys have their own field permissions. | Block | Contains | Default | Collections | |---|---|---|---| | `call` | Record id, workspace, assistant, timestamps | Always | All | | `customer` | Name, phone, email, language, country | On | All | | `costs` | Cost breakdown | Off | All | | `transcript` | Turn-by-turn transcript | On | Calls, threads | | `analysis` | Summary, intent, outcome, sentiment | On | Calls, threads | | `evaluations` | Evaluation results | On | Calls, threads | | `follow_ups` | Follow-up state | Off | Calls, threads | | `recording` | Recording status and link | On | Calls | | `performance_metrics` | Speech, model, and voice latency | Off | Calls | | `config_snapshot` | Assistant configuration | Off | Calls | You can further narrow a request with `fields`. For example: ```text ?fields=transcript ``` Requesting a field your key is not allowed to read returns `403`. ## `GET /v1/calls` Returns voice calls, newest first. ### Parameters | Parameter | Type | Meaning | |---|---|---| | `cursor` | string | Cursor from the previous page | | `limit` | integer | 1–100, default 25 | | `from` | ISO 8601 | Calls ending after this time | | `to` | ISO 8601 | Calls ending before this time | | `assistant_id` | UUID | Filter by assistant | | `include_test` | boolean | Include test calls | | `fields` | string | Requested field blocks | Example: ```bash curl -H "Authorization: Bearer vk_live_your_key_here" \ "https://api.voicetta.com/v1/calls?limit=50&from=2026-08-01T00:00:00Z&fields=transcript" ``` Response: ```json { "calls": [ { "call_id": "9f8b1c4e-6d2a-4b7f-8e21-3c5a7d9e0f11" } ], "has_more": true, "next_cursor": "MjAyNi0wOC0yMFQxMDowMDowMCswMDowMHw5ZjhiMWM0ZQ" } ``` ## Pagination Use cursor pagination. Do not use offsets. ```bash cursor="" while :; do page=$(curl -sH "Authorization: Bearer $KEY" \ "https://api.voicetta.com/v1/calls?limit=100&cursor=$cursor") echo "$page" | jq -c '.calls[]' [ "$(echo "$page" | jq -r '.has_more')" = "true" ] || break cursor=$(echo "$page" | jq -r '.next_cursor') done ``` Three rules: 1. Stop when `has_more` is false. 2. Treat cursors as opaque strings. 3. Do not construct or modify cursors. ## `GET /v1/calls/{call_id}` Returns one call. ```bash curl -H "Authorization: Bearer vk_live_your_key_here" \ "https://api.voicetta.com/v1/calls/9f8b1c4e-6d2a-4b7f-8e21-3c5a7d9e0f11" ``` Response: ```json { "call": { "call_id": "9f8b1c4e-6d2a-4b7f-8e21-3c5a7d9e0f11" } } ``` You can use `fields` here too. ## `GET /v1/calls/{call_id}/recording` Returns a redirect to the audio file. ```bash curl -L -o call.mp3 \ -H "Authorization: Bearer vk_live_your_key_here" \ "https://api.voicetta.com/v1/calls/9f8b1c4e-6d2a-4b7f-8e21-3c5a7d9e0f11/recording" ``` The redirect URL is temporary. Do not store the redirected URL. The `recording.download_url` in an archived Voicetta payload remains useful because it points to this stable Voicetta endpoint. ## `GET /v1/messages` Returns SMS and WhatsApp messages, newest first. ### Parameters | Parameter | Type | Meaning | |---|---|---| | `cursor` | string | Cursor from the previous page | | `limit` | integer | 1–100, default 25 | | `from` | ISO 8601 | Messages after this time | | `to` | ISO 8601 | Messages before this time | | `assistant_id` | UUID | Filter by assistant | | `channel` | string | `sms` or `whatsapp` | | `direction` | string | `inbound` or `outbound` | | `thread_id` | UUID | Filter by conversation | | `fields` | string | Requested field blocks | Example: ```bash curl -H "Authorization: Bearer vk_live_your_key_here" \ "https://api.voicetta.com/v1/messages?channel=whatsapp&limit=50" ``` ## `GET /v1/messages/{message_id}` Returns one message. ```bash curl -H "Authorization: Bearer vk_live_your_key_here" \ "https://api.voicetta.com/v1/messages/b71e9d02-4a6c-4f18-9d3e-7c2b5a4e8f31" ``` ## `GET /v1/threads` Returns completed SMS and WhatsApp conversations. ### Parameters | Parameter | Type | Meaning | |---|---|---| | `cursor` | string | Cursor from previous page | | `limit` | integer | 1–100, default 25 | | `from` | ISO 8601 | Threads completed after this time | | `to` | ISO 8601 | Threads completed before this time | | `assistant_id` | UUID | Filter by assistant | | `channel` | string | `sms` or `whatsapp` | | `close_reason` | string | Filter by close reason | | `fields` | string | Requested field blocks | Example: ```bash curl -H "Authorization: Bearer vk_live_your_key_here" \ "https://api.voicetta.com/v1/threads?close_reason=graded&limit=50" ``` ## `GET /v1/threads/{thread_id}` Returns one completed conversation. ```bash curl -H "Authorization: Bearer vk_live_your_key_here" \ "https://api.voicetta.com/v1/threads/c82fa013-5b7d-4029-8e4f-8d3c6b5f9a42" ``` ## Errors The API uses standard HTTP status codes. | Status | Meaning | |---|---| | `400` | Invalid request | | `401` | Missing or invalid API key | | `403` | Your key does not have access to the requested data | | `404` | Record does not exist | | `429` | Rate limit exceeded | | `500` | Server error | Error responses include a JSON body explaining the problem. ## Rate limits | Limit | Scope | |---|---| | 1,000 requests per hour | Per endpoint, per caller | | 100 requests per second | Burst protection | If you are regularly hitting the hourly limit, use webhooks instead of polling. ## Backfilling history The REST API is also how you load data from before your webhook was enabled. For example: ```bash curl -H "Authorization: Bearer $KEY" \ "https://api.voicetta.com/v1/calls?limit=100&from=2026-08-01T00:00:00Z" ``` Feed the results through the same processing logic as your webhook. The objects are the same. Make your handler idempotent so running a backfill more than once is safe. For a specific recovery window, use both `from` and `to`. ## Versioning All API paths use `/v1`. Within version 1, existing fields are stable. We add fields without breaking existing integrations. Breaking changes get a new version. The OpenAPI specification is available at: ```text /open-api ``` ## Related pages * [Overview](/developers/overview) * [Quickstart](/developers/quickstart) * [Webhook Reference](/developers/webhooks) * [Message Events](/developers/message-events) * [Thread Events](/developers/thread-events) * [n8n and No-code](/developers/n8n) --- --- title: "n8n and No-code" description: "Connect Voicetta to n8n, Make, or Zapier without building a server." publishedAt: "2026-08-22" modifiedAt: "2026-08-22" category: "No-code" tags: "Voicetta, n8n, no-code, Make, Zapier, webhook, automation" navOrder: "50" canonical: "https://voicetta.com/developers/n8n" --- # n8n and No-code You do not need to build a backend to start using Voicetta events. n8n, Make, and Zapier can receive Voicetta webhooks directly. For a simple workflow, the setup is: **Voicetta → webhook → automation → your tools** ## 1. Use the production webhook URL Create the webhook endpoint in: **Settings → Developers** Use the URL provided by your production workflow. During development, you can use a temporary webhook URL such as webhook.site or an ngrok tunnel. ## 2. Respond immediately Your webhook should return `200` as soon as it receives the event. Do not make your workflow wait for long-running actions before responding. If the response takes too long, Voicetta will retry the event. ## 3. Authenticate You can use a custom header. For example: ```text X-Api-Key: your-secret ``` Configure the same value in your automation platform. For custom server integrations, HMAC verification is recommended. ## 4. Keep the timeout at 10 seconds The default timeout is 10 seconds. Leave it there unless you have a specific reason to change it. The important thing is to acknowledge the webhook quickly. ## 5. Branch on `event` If one webhook receives multiple events, branch on: ```text event ``` For example: ```text call.completed message.received message.sent thread.completed ``` Each event contains the corresponding object: ```text call message thread ``` ## Deduplicating Webhook delivery is at-least-once. The same event can arrive more than once. Use: ```text X-Voicetta-Delivery-Id ``` to identify delivery attempts. Or use: ```text event_id ``` to identify the underlying conversation record. Your workflow should be safe to run twice. ## Reading data through the API Use the REST API when you need: * historical conversations * data from before the webhook was enabled * recovery after downtime * reports * a specific call, message, or thread Base URL: ```text https://api.voicetta.com/v1 ``` ## A minimal workflow A simple n8n flow can look like: ```text Voicetta Webhook ↓ Check event ↓ Store conversation ↓ Update CRM ↓ Notify team ``` Or: ```text Voicetta Webhook ↓ message.received ↓ Check intent ↓ Create lead / Update CRM / Notify team ``` Voicetta provides the communication layer. Your automation decides what happens next. ## Related pages * [Overview](/developers/overview) * [Quickstart](/developers/quickstart) * [Webhook Reference](/developers/webhooks) * [Message Events](/developers/message-events) * [Thread Events](/developers/thread-events) * [Read API Reference](/developers/api-reference)