--- 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