--- title: "Call Data API Overview" description: "Build your own product on Voicetta call data. Receive every completed call as a signed webhook, or read call history over a REST API." publishedAt: "2026-08-22" modifiedAt: "2026-08-22" category: "Getting Started" tags: "Voicetta, API, webhook, call data, integration, white label" navOrder: "10" canonical: "https://voicetta.com/developers/overview" --- # Call Data API Overview Voicetta answers the phone. This API hands you the result. Every completed voice call becomes a single structured object: who called, what was said, what the caller wanted, what happened, how long it took, and a link to the audio. You decide what to do with it — show it in your own dashboard, file it against a customer record, trigger a workflow, or bill on it. ## What you can build The shape of the data is deliberately close to what a call-review screen needs, because that is the most common thing people build with it. - **A white-label call dashboard.** Voicetta runs the voice agent; your product is what your client logs into. They never see our branding. - **A CRM or PMS timeline.** Attach the transcript and outcome to the record for the person who called. - **Workflow automation.** Route on `analysis.outcome` or `analysis.intent` — escalate the unhappy calls, auto-close the resolved ones. - **Quality reporting.** Evaluation verdicts and latency measurements per call, aggregated however you like. ## Push, pull, or both There are two ways to get the same call object, and most integrations end up using both. | | Webhook (push) | Read API (pull) | |---|---|---| | Who starts it | We POST to your URL | You send a request | | When | Seconds after post-call processing finishes | Whenever you ask | | Good for | Live updates, the primary integration | Backfilling history, catching up after downtime | | You need | A public HTTPS endpoint | An API key | Start with the webhook: it is the path that keeps your data current without polling. Add the read API when you need history from before you integrated, or to recover deliveries your server missed while it was down. The payload is the same either way, so one handler function covers both. ## The base URL ``` https://api.voicetta.com/v1 ``` Every read API request goes here, authenticated with a bearer token. The webhook goes the other direction: to a URL you own. ## Getting access 1. In the Voicetta dashboard, open **Settings → Developers**. 2. Create an **API key** for reading, or a **webhook endpoint** for receiving. You can have both. 3. Choose which blocks of call data each one may use — field groups for the webhook control what we push; field groups on the API key control what it may pull. 4. Ask us to enable delivery for your workspace. Both API keys and webhook signing secrets are shown once, at creation, and stored only as a hash. If you lose one, rotate it rather than asking us to retrieve it — we cannot. ## When the data arrives The `call.completed` event fires when post-call processing finishes, not when the caller hangs up. That is a deliberate few-seconds delay: by the time you receive the event, the transcript, summary, evaluations, and recording are final. You never have to poll for a field that has not been filled in yet, and you never have to reconcile a partial call with a later, more complete version of the same call. ## What "version 1" promises The payload carries `"version": 1`, and every read API path starts with `/v1`. Within version 1 we will only **add** fields. We will not rename a field, remove one, change a type, or change the meaning of an existing value. Anything that would break a working integration gets a new version number and a new `X-Voicetta-Payload-Version`, and version 1 keeps working. Two consequences worth designing for: - **Ignore fields you do not recognise.** A new field appearing is normal and is not a reason to reject a payload. - **Do not treat a missing key as an error.** Null values are omitted rather than sent as `null`, so a key you saw yesterday may be absent today simply because that call had no value for it. ## Reading these docs as an LLM Every page here has a Markdown twin, served as plain text: - This page: `/developers/overview.md` - All developer pages as one file: `/developers.md` - The OpenAPI 3.1 spec for the read API: [`/open-api`](/open-api) The spec is generated from the running API rather than written by hand, so it cannot drift from what the endpoints actually accept and return. Point a code generator at it, or paste `/developers.md` into a coding agent and let it write the handler. ## Related pages - [Quickstart](/developers/quickstart) — a working delivery in about ten minutes - [Webhook reference](/developers/webhooks) — the full payload, headers, and signature verification - [Read API reference](/developers/api-reference) — endpoints, parameters, pagination, and errors - [n8n and no-code](/developers/n8n) — receiving calls without writing a server --- --- title: "Quickstart" description: "See a real Voicetta call payload in under ten minutes, then write the handler. Includes the two rules that decide whether your integration behaves." 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 The fastest way to understand this API is to look at one real payload from your own workspace before you write any code. ## Step 1: See a real payload, without writing anything Do not start by building an endpoint. Start by pointing a throwaway URL at us and reading what arrives. 1. Open a request bin — [webhook.site](https://webhook.site), [RequestBin](https://requestbin.com), or an `ngrok` tunnel to your laptop. Copy the URL it gives you. 2. In the Voicetta dashboard, go to **Settings → Developers** and create a webhook endpoint with that URL. 3. Tick the blocks of data you want. The defaults are a good starting point. 4. Press **Send test event**. We post your most recent real call to that URL and show you the status code it returned. The request bin shows you the exact headers and body. Two things to notice while you have it open: - The payload is **one JSON object per call**, not a stream of events you have to stitch together. - Null fields are **absent**, not `null`. That is the rule that catches most people later, so it is worth seeing it now. **Send test event** does not touch the delivery log or the retry schedule, so you can press it as often as you like while you are finding your footing. ## Step 2: Read one call over the API Now the other direction. Create an **API key** in the same settings page — tick the field groups it may read (same checklist as the webhook), then copy the key immediately. It is shown once. ```bash curl -H "Authorization: Bearer vk_live_your_key_here" \ "https://api.voicetta.com/v1/calls?limit=1" ``` You get back 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" } ``` Narrow it to the blocks you actually want — but only within what the key was granted: ```bash curl -H "Authorization: Bearer vk_live_your_key_here" \ "https://api.voicetta.com/v1/calls?limit=1&fields=transcript" ``` Requesting a block the key does not have returns `403`. Omit `fields` to receive everything the key may read. If you got a `401`, the key is revoked or the `Bearer ` prefix is missing. Those are the only two causes. ## Step 3: Write the handler Now replace the request bin with your own endpoint. The smallest correct handler in Express: ```js app.post('/voicetta/calls', express.json(), (req, res) => { // Acknowledge first. Everything else happens after this line. res.sendStatus(200); const deliveryId = req.get('X-Voicetta-Delivery-Id'); if (alreadyProcessed(deliveryId)) { return; } enqueue(deliveryId, req.body.call); }); ``` That is the whole shape. Add signature verification next — see [the webhook reference](/developers/webhooks#verifying-that-it-is-really-us), which needs the raw body rather than parsed JSON. ## The two rules that decide whether this works Everything else in these docs is detail. These two are not. ### Acknowledge before you process Reply `200` as soon as the request arrives, then do your work. Write the payload to a queue or a table and return; do not insert into six tables, call three APIs, and then reply. If we do not get a response in time we assume the delivery failed and retry it. So slow processing does not just make things slow — it manufactures duplicate deliveries, and eventually marks deliveries as failed for calls your server actually received every single time. ### Assume you will receive the same call twice This is a property of every at-least-once delivery system, not a bug to be reported. If your `200` is lost on the way back to us, we retry a delivery you already handled perfectly. So make the handler idempotent. Dedupe on `X-Voicetta-Delivery-Id`, which is unique per attempt, or on `call_id` if you would rather key on the call itself and treat a repeat as an update. A handler that follows both rules is boring and reliable. One that follows neither will look fine in testing and produce duplicates in production. ## Step 4: Go live 1. Confirm your endpoint is reachable from the public internet over HTTPS. We reject private and loopback addresses, so `localhost` and `10.x` URLs will never work — use a tunnel while developing. 2. Press **Send test event** again and confirm you see a `200`. 3. Ask us to enable delivery for your workspace. 4. Watch the delivery log for the first few real calls. Test calls are never delivered to production endpoints, so your first real delivery will be a real call from a real person. ## Related pages - [Overview](/developers/overview) — what the API is and how the pieces fit - [Webhook reference](/developers/webhooks) — full payload, headers, signatures, and retries - [Read API reference](/developers/api-reference) — every endpoint and parameter - [n8n and no-code](/developers/n8n) — the same integration without a server --- --- title: "Webhook Reference" description: "The call.completed event: full payload, the ten field groups, every header we send, HMAC signature verification in Node and Python, and the retry ladder." publishedAt: "2026-08-22" modifiedAt: "2026-08-22" category: "Reference" tags: "Voicetta, webhook, HMAC, signature, retries, payload, call.completed" navOrder: "30" canonical: "https://voicetta.com/developers/webhooks" --- # Webhook Reference We POST every completed call to a URL you own. This page is the complete description of that request. ## The event There is one event type: **`call.completed`**. It fires when post-call processing finishes, which is a few seconds after the caller hangs up. Everything in the payload is final at that point — transcript, summary, evaluations, costs, and recording. Two things it does not cover: - **Only voice calls.** SMS and WhatsApp text conversations do not fire this event. - **Never test calls.** Calls you make from the dashboard's test panel are excluded, so your production pipeline only ever sees real traffic. ## Payload ```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" } } } ``` ### How to read it - **`event_id` is the call id.** A retry of the same call carries the same `event_id`, which makes it a usable idempotency key if you would rather not track delivery ids. - **Null fields are omitted, not sent as `null`.** Treat a missing key as "no value", never as an error. - **Whole blocks are absent when you switch them off**, and also when a call genuinely has no data for them. - **`recording.download_url` is a Voicetta endpoint, not a storage URL.** It does not expire. Requesting it redirects to a short-lived signed URL, so the link stays valid inside a payload you stored months ago. See [the read API reference](/developers/api-reference#get-v1-calls-call-id-recording). ## Choosing what you receive Ten blocks, each switchable per endpoint in **Settings → Developers**. Only the blocks you tick are sent. | Block | What is in it | Default | |---|---|---| | `call` | Call id, workspace, assistant, direction, duration, timestamps, end reason | Always sent | | `customer` | Caller name, phone, email, language, country | On | | `transcript` | Turn-by-turn transcript | On | | `analysis` | Summary, intent, outcome, sentiment | On | | `evaluations` | Evaluation verdicts and reasoning | On | | `recording` | Recording status and download link | On | | `costs` | Cost breakdown and the amount charged | Off | | `performance_metrics` | Speech, language model, and voice latency | Off | | `config_snapshot` | Assistant configuration at the time of the call | Off | | `follow_ups` | Recovery and disconnect follow-up state | Off | `call` cannot be switched off. Without identity and timing, the rest of the payload cannot be interpreted. Switch off anything you do not store. It is less data crossing the network, less to secure on your side, and less to explain in your own privacy documentation. `costs` in particular is off by default because it exposes what the call cost to run. The read API uses the same blocks on each **API key** — see [Choosing what you can read](/developers/api-reference#choosing-what-you-can-read). Webhook and key permissions are configured separately. ## Headers | Header | Value | |---|---| | `X-Voicetta-Event` | `call.completed` | | `X-Voicetta-Delivery-Id` | Unique per delivery **attempt**. Use this to detect repeats. | | `X-Voicetta-Payload-Version` | `1` | | `X-Voicetta-Signature` | `t=,v1=` | Plus any custom headers you configure on the endpoint. ## Verifying that it is really us Your endpoint is a public URL, so anything on the internet can post to it. There are two ways to establish that a request came from Voicetta. The signature is always sent, so you can start with headers and add signature verification later without any coordination from us. ### Option A: a custom header Add a header such as `X-Api-Key: ` in **Settings → Developers**. We attach it to every delivery and you check it on arrival. This is the right choice for no-code tools: in n8n it maps directly onto the Webhook node's Header Auth credential, with nothing to implement. Header values are encrypted at rest and never shown again after you save them. ### Option B: HMAC signature verification Compute HMAC-SHA256 over `.` using your signing secret, and compare the result to the `v1` value in `X-Voicetta-Signature`. Two details decide whether this works: 1. **Use the raw request body, byte for byte.** If your framework parses the JSON and you re-serialise it to hash it, key order and whitespace change and the digest will not match. This is by far the most common cause of "the signature never matches". 2. **Check that the timestamp is recent.** Five minutes is a reasonable window. Without this check, a request captured once can be replayed against you at any point in the future. Compare with a constant-time function, not `==`. **Node (Express):** ```js const crypto = require('crypto'); // express.raw, not express.json — you need the exact bytes. 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); } // Acknowledge first, then process. res.sendStatus(200); enqueue(JSON.parse(req.body.toString('utf8'))); }); ``` **Python (FastAPI):** ```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() # raw bytes, before any JSON parsing 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 at any time in the dashboard. The previous secret stops working immediately — there is no overlap window. So either update your side first if you can tolerate a brief period where old deliveries fail, or rotate during a quiet hour. ## Retries and duplicates Any response outside `2xx`, and any timeout, counts as a failure. We retry up to **six attempts**, doubling the delay each time. | Attempt | Delay after the previous one | |---|---| | 2 | 1 minute | | 3 | 2 minutes | | 4 | 4 minutes | | 5 | 8 minutes | | 6 | 16 minutes | After the sixth attempt the delivery is marked failed and we stop trying. Nothing is lost: retry it by hand from the delivery log, or fetch the call from [the read API](/developers/api-reference). Each attempt carries a **new** `X-Voicetta-Delivery-Id` and the **same** `event_id`. Dedupe on whichever matches your model — delivery id to process each attempt at most once, `event_id` to treat repeats as updates to the same call. ## About the timeout You can set the timeout between **1 and 10 seconds**. The default is 10, and 10 is almost always the right answer. It is tempting to lower it, on the theory that a tighter timeout means faster delivery. It does not work that way. A successful delivery finishes when your server answers, so shortening the timeout makes nothing faster — it only moves the point at which we give up and retry. What a tight timeout does do is fire on ordinary conditions: DNS and TLS setup on a cold connection, a large transcript upload, a serverless cold start. Each of those produces a duplicate delivery and a false failure for a call you handled perfectly. Leave it at 10 and make your endpoint reply immediately. Replying immediately is the thing that actually makes deliveries fast. ## Troubleshooting | Symptom | Likely cause | |---|---| | The signature never matches | You are hashing re-serialised JSON instead of the raw body, or omitting the `.` prefix | | The same call arrives repeatedly | Your `200` is arriving late or after the timeout. Acknowledge before processing | | Deliveries time out | Processing inline. Move it after the response | | Everything is `failed` in the log | Endpoint unreachable from the internet, a non-2xx status, or a private or loopback host, which we reject | | A block is missing from the payload | It is switched off, or that call has no data for it | | A field you expected is missing | It was null. Null fields are omitted rather than sent as `null` | ## Related pages - [Quickstart](/developers/quickstart) — see a real payload before you write code - [Read API reference](/developers/api-reference) — pull the same call objects on demand - [Overview](/developers/overview) — how push and pull fit together - [n8n and no-code](/developers/n8n) — receiving deliveries without a server --- --- title: "Read API Reference" description: "Every endpoint of the Voicetta call data API: bearer authentication, parameters, cursor pagination, error codes, rate limits, and a backfill recipe." publishedAt: "2026-08-22" modifiedAt: "2026-08-22" category: "Reference" tags: "Voicetta, REST API, OpenAPI, pagination, authentication, rate limits" navOrder: "40" canonical: "https://voicetta.com/developers/api-reference" --- # Read API Reference The read API returns the same call objects the webhook pushes, on demand. Use it to backfill history, to recover deliveries your server missed, or as your only integration if you would rather poll than run an endpoint. Base URL: ``` https://api.voicetta.com/v1 ``` The machine-readable OpenAPI 3.1 document is at [`/open-api`](/open-api). It is generated from the running API, so it is always an accurate description of these endpoints — point a client generator at it rather than writing request types by hand. ## Authentication Every request needs an API key from **Settings → Developers**, sent as a bearer token: ``` Authorization: Bearer vk_live_xxxxxxxxxxxxxxxxxxxxxxxx ``` Keys are shown once, at creation, and stored only as a hash. If you lose one, revoke it and create another. When you create a key, choose which **field groups** it may read — the same ten blocks as the [outbound webhook](/developers/webhooks#choosing-what-you-receive). Call metadata (`call`) is always included. You can change an active key's permissions in the dashboard without rotating the key value. A key is scoped to exactly one workspace. There is no parameter that widens that scope: a request can only ever return calls belonging to the workspace that owns the key. If you operate several workspaces, you hold several keys. ## Choosing what you can read Each API key carries its own allowlist. A request may only return field groups granted on that key. | Block | What is in it | Default on new keys | |---|---|---| | `call` | Call id, workspace, assistant, direction, duration, timestamps, end reason | Always sent | | `customer` | Caller name, phone, email, language, country | On | | `transcript` | Turn-by-turn transcript | On | | `analysis` | Summary, intent, outcome, sentiment | On | | `evaluations` | Evaluation verdicts and reasoning | On | | `recording` | Recording status and download link | On | | `costs` | Cost breakdown and the amount charged | Off | | `performance_metrics` | Speech, language model, and voice latency | Off | | `config_snapshot` | Assistant configuration at the time of the call | Off | | `follow_ups` | Recovery and disconnect follow-up state | Off | The webhook endpoint and the read API key are configured independently: what we **push** to your URL and what a key may **pull** do not have to match. Give a dashboard key only `customer` and `analysis` if that is all it displays; give your webhook everything you archive. Use the optional `fields` query parameter to narrow a response **within** the key's allowlist — for example, a key allowed to read `customer`, `transcript`, and `analysis` can request `fields=transcript` and receive only the transcript block. Requesting a block the key was not granted returns `403`. ## `GET /v1/calls` A page of calls, newest first. | Parameter | Type | Meaning | |---|---|---| | `cursor` | string | The `next_cursor` from the previous page | | `limit` | integer | 1–100. Default 25 | | `from` | ISO 8601 | Only calls that ended at or after this time | | `to` | ISO 8601 | Only calls that ended at or before this time | | `assistant_id` | UUID | Only calls handled by this assistant | | `include_test` | boolean | Include test calls. Default false | | `fields` | string | Comma-separated blocks, e.g. `call,transcript`. Must be a subset of the key's allowlist. Defaults to everything the key may read | ```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" ``` ```json { "calls": [ { "call_id": "9f8b1c4e-6d2a-4b7f-8e21-3c5a7d9e0f11" } ], "has_more": true, "next_cursor": "MjAyNi0wOC0yMFQxMDowMDowMCswMDowMHw5ZjhiMWM0ZQ" } ``` By default you receive every block the key was granted — not every block that exists. Omit `fields` when you want the full allowlist; add it when you want less. `call` is always included, whether or not you name it in `fields`. ### Pagination ```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. **Loop on `has_more`, not on `next_cursor`.** `has_more` is always present. `next_cursor` is absent on the last page, because null fields are omitted throughout this API. 2. **Cursors are opaque.** Pass them back byte for byte. They encode both a timestamp and a call id, which is what keeps paging stable while new calls arrive at the top of the list. Do not decode or construct one. 3. **Do not use `limit` for pagination.** There is no offset parameter, deliberately: offsets skip or repeat rows when the underlying list shifts under you. ## `GET /v1/calls/{call_id}` One call, in the same shape as the webhook payload's `call` object. ```bash curl -H "Authorization: Bearer vk_live_your_key_here" \ "https://api.voicetta.com/v1/calls/9f8b1c4e-6d2a-4b7f-8e21-3c5a7d9e0f11" ``` ```json { "call": { "call_id": "9f8b1c4e-6d2a-4b7f-8e21-3c5a7d9e0f11" } } ``` Also accepts `fields` within the key's allowlist. ## `GET /v1/calls/{call_id}/recording` A `302` redirect to a time-limited audio URL. Requires the `recording` field group on the API key. ```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" ``` Request it when you need to play or download the audio. **Do not store the URL it redirects to** — that one expires within the hour. This endpoint is what `recording.download_url` in the payload points at, and that indirection is the whole point: the link inside a payload you archived last year still works, because each request mints a fresh signed URL. Make sure your HTTP client follows redirects, or handles the `302` itself. Some clients do not by default. ## Errors Errors are JSON with a `detail` string. | Status | Meaning | What to do | |---|---|---| | `400` | Invalid cursor, or an unknown name in `fields` | Fix the request. Retrying will not help | | `401` | Missing, malformed, unknown, or revoked API key | Check the key and the `Bearer ` prefix | | `403` | The key is not permitted to access the requested field groups | Remove the block from `fields`, or grant it on the key in Settings → Developers | | `404` | No such call in this workspace, or no recording on the call | The id is wrong, belongs to another workspace, or the call has no audio | | `429` | Rate limited | Back off and retry | | `503` | The API is temporarily disabled | Retry with backoff | A `401` has exactly two causes in practice: the key was revoked, or the `Bearer ` prefix is missing. Note what `404` does **not** distinguish: a call id that does not exist and a call id belonging to someone else's workspace return the same response. That is intentional — the alternative would let anyone with a key confirm whether a given call id exists elsewhere in Voicetta. ## Rate limits | Limit | Scope | |---|---| | 1,000 requests per hour | Per endpoint, per caller | | 100 requests per second | Burst protection, per caller | Callers are identified by network address, so if several of your services share one outbound IP or NAT gateway, they share a bucket. Worth knowing before you fan out a backfill across a worker pool. Both limits are generous for normal use: 1,000 hourly requests at 100 calls per page is 100,000 calls an hour. If you are hitting them, you are almost certainly polling when you should be receiving [webhooks](/developers/webhooks). ## Backfilling history To load calls from before you integrated, or to catch up after your endpoint was down, page through `GET /v1/calls` with a `from` bound and feed the results into the same handler your webhook uses. Both produce identical call objects, so no separate code path is needed. ```bash # Everything since the start of August, oldest processed first. curl -H "Authorization: Bearer $KEY" \ "https://api.voicetta.com/v1/calls?limit=100&from=2026-08-01T00:00:00Z" ``` Two things make this safe to run more than once: - Your handler is already idempotent, because webhook retries required it. - `call_id` is stable, so a call fetched twice is recognisably the same call. If you are recovering from a specific outage, bound both ends with `from` and `to` rather than re-reading your whole history. ## Versioning Every path starts with `/v1`, and within v1 we only add fields — never rename, remove, retype, or change the meaning of one. See [the overview](/developers/overview) for what that guarantees and what it asks of your parser. ## Related pages - [Webhook reference](/developers/webhooks) — the push half of the same data - [Quickstart](/developers/quickstart) — your first request in a few minutes - [Overview](/developers/overview) — how push and pull fit together - [n8n and no-code](/developers/n8n) — calling this API from a workflow tool --- --- title: "n8n and No-code" description: "Receive Voicetta call data in n8n, Make, or Zapier without writing a server. The four settings that decide whether it works in production." 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 write a server to use this API. A webhook node in n8n, Make, or Zapier is a perfectly good receiver, and the call payload is a single flat-ish JSON object that these tools handle well. What you do need is four specific settings. Each of them looks optional and is not: the defaults are tuned for building a workflow interactively, and they are wrong for receiving production traffic. ## 1. Use the Production URL, not the Test URL The n8n Webhook node gives you two URLs. The **Test URL** only listens while the canvas is open and you have pressed *Listen for test event*. Close the tab and every delivery fails. Copy the **Production URL** into Voicetta, and make sure the workflow is **activated**. A deactivated workflow does not listen on its production URL either. This is the single most common reason a no-code integration works during setup and then stops. ## 2. Set Respond to "Immediately" In the Webhook node, set **Respond** to *Immediately*. The default is to respond when the last node finishes, which means our request stays open for as long as your whole workflow takes to run. If that is longer than the timeout, we conclude the delivery failed and retry it — so a workflow that is working correctly produces duplicate deliveries and, after six attempts, entries marked failed in your delivery log. *Immediately* sends the `200` first and runs the rest of the workflow afterwards. That is the [acknowledge-then-process rule](/developers/quickstart) that applies to coded handlers too; n8n just spells it as a dropdown. ## 3. Authenticate with Header Auth In Voicetta, add a custom header to the endpoint, for example `X-Api-Key` with a secret you generate. In n8n, set the Webhook node's **Authentication** to *Header Auth* and create a credential with the same header name and value. That is the whole thing. Requests without the header are rejected by n8n before your workflow runs. You can verify our HMAC signature instead, but it is not worth it here: it needs a Code node operating on the raw request bytes, and n8n has usually parsed the JSON by the time you can see it, which makes the digest fail. Header authentication gives you the property you actually want — only Voicetta can trigger this workflow — with nothing to get wrong. The signature is always sent, so you can move to it later if your setup changes. ## 4. Leave the timeout at 10 seconds No-code workers cold start. A tight timeout does not make delivery faster; it just guarantees spurious retries on any delivery that arrives while your worker is waking up. See [the timeout section of the webhook reference](/developers/webhooks#about-the-timeout) for why lowering it cannot help. ## Deduplicating Assume you will occasionally receive the same call twice. Every at-least-once delivery system does this, including ours. Two workable approaches in n8n: - **On the delivery id.** Use the `X-Voicetta-Delivery-Id` header as the key in a Remove Duplicates node, or check it against a table of ids you have already handled. Each retry has a new delivery id, so this processes every attempt at most once. - **On the call id.** Use `call_id` from the body, and treat a repeat as an update rather than an insert. Upserting into Airtable, Sheets, or a database keyed on `call_id` makes duplicates harmless by construction. The second is usually simpler in a no-code tool, because it needs no extra state: an upsert with the same key twice leaves one row. ## Reading data instead of receiving it If you would rather pull, an HTTP Request node against `GET /v1/calls` works the same way as anywhere else. Set the header: ``` Authorization: Bearer vk_live_your_key_here ``` Then loop while `has_more` is true, passing `next_cursor` back as `cursor`. Full parameter list in [the read API reference](/developers/api-reference). For most workflows the webhook is the better choice: you get each call once, seconds after it ends, with no schedule to tune and no window where you are polling an API that has nothing new to say. ## A minimal working flow 1. **Webhook** — Production URL, Respond: Immediately, Authentication: Header Auth. 2. **Remove Duplicates** or an upsert keyed on `call_id`. 3. **Set** — pull out the fields you care about: `call.call_id`, `call.analysis.summary`, `call.analysis.outcome`, `call.customer.phone`, `call.duration_seconds`. 4. Your destination — Airtable, Sheets, Slack, a CRM node, or an HTTP Request to your own system. Remember that null fields are omitted rather than sent as `null`, so reference nested fields defensively. A call with no transcript has no `transcript` key at all, not an empty array. ## Related pages - [Quickstart](/developers/quickstart) — test with a request bin before touching n8n - [Webhook reference](/developers/webhooks) — headers, payload, and retry behaviour - [Read API reference](/developers/api-reference) — parameters for the HTTP Request node - [Overview](/developers/overview) — what the API contains