---
title: REST API overview
description: How to call the Receptiva REST API from your own systems, covering the base URL, API key authentication, pagination, errors, rate limits and versioning.
sidebarTitle: Overview
section: api
order: 10
updated: 2026-10-06
---

The REST API gives your own systems, such as a case-management system or a reporting script, the same calls and leads your firm sees in the Receptiva dashboard. It is for servers. For Claude and ChatGPT, use the [MCP connector](/docs) instead.

## Base URL

```
https://api.receptiva.ai
```

Every path starts with the version, `/v1`, so the calls list is `https://api.receptiva.ai/v1/calls`. Paths on these pages and in the API reference are written the same way, for example `GET /v1/calls`.

Requests and responses are JSON over HTTPS. A machine-readable description of every endpoint is at `https://api.receptiva.ai/v1/openapi.json` (OpenAPI 3.1, no key needed), and the [API reference](/docs/api-reference) is generated from it.

## Authentication

Send an API key as a bearer token on every request:

```bash
curl https://api.receptiva.ai/v1/calls \
  -H "Authorization: Bearer $RECEPTIVA_API_KEY"
```

Your firm's owner creates keys in the Receptiva dashboard under **Integrations**. See [API keys](/docs/api-keys). A key belongs to one firm, so there is no firm parameter. Keep keys on a server. Do not put one in a web page, a mobile app or a URL.

## Endpoints

| Endpoint | What it returns | Permission |
| --- | --- | --- |
| `GET /v1/account` | Where the firm stands: plan, phone number, trial and the one next step. Also the firm's stable `org.id` and the calling key's own name, permissions and expiry (`api_key`). | `org:read` |
| `GET /v1/receptionist` | The receptionist's live settings | `receptionist:read` |
| `GET /v1/calls` | Calls, newest first, each with structured details (people, company, claim and case numbers). `caller_number` finds one caller's calls; `reference` finds the calls that quoted a claim or case number. | `calls:read` |
| `GET /v1/calls/{call_id}` | One call, with how it was routed and, after a transfer, a summary of the conversation with your staff member (`untrusted.conversation_summary`) | `calls:read` |
| `GET /v1/calls/{call_id}/transcript` | The call's transcript, turn by turn: the intake, and after a transfer the briefing and the conversation with your staff member. `segment` returns one part. Pages by `start_turn` and `limit` (see below). | `calls:transcript` |
| `GET /v1/calls/{call_id}/recording` | The call's audio recording | `calls:recording` |
| `POST /v1/calls/{call_id}/recording-link` | A link to the recording that works for one hour | `calls:recording` |
| `GET /v1/recordings/{token}` | The audio behind a recording link. No key needed: the link is the credential. | none |
| `GET /v1/leads` | New-client leads, newest first. `caller_number` finds one caller's leads; `external_id` finds the lead behind a record in your own system. | `leads:read` |
| `GET /v1/leads/{lead_id}` | One lead with its full intake | `leads:narrative` |
| `PATCH /v1/leads/{lead_id}` | Sets a lead's status, your own id for it (`external_id`) and a note on its status (`status_reason`), any of the three. Send `expected_status` with `status` to refuse the change if the lead has moved on. | `leads:write` |
| `GET /v1/events` | Your firm's events, oldest first. Pass `after` to read the ones that came after an event you already have. See [Webhooks](/docs/webhooks). | `webhooks:read` |
| `GET /v1/events/{event_id}` | One event | `webhooks:read` |
| `GET /v1/webhook-endpoints` | Your webhook endpoints and their health. Never a signing secret. | `webhooks:read` |
| `GET /v1/webhook-deliveries` | Webhook deliveries, newest first, with each attempt | `webhooks:read` |
| `POST /v1/webhook-endpoints/{endpoint_id}/test` | Sends a test event to one endpoint | `webhooks:test` |
| `POST /v1/webhook-deliveries/{delivery_id}/redeliver` | Sends a delivery's event to its endpoint again | `webhooks:test` |
| `GET /v1/health` | `{"ok": true}`. No key needed. | none |

The [API reference](/docs/api-reference) lists every parameter and response field.

## Pagination

List endpoints return the newest items first and take two query parameters:

- `limit`: how many items to return, from 1 to 100. The default is 20.
- `cursor`: the `next_cursor` value from the previous page.

The transcript is not a list and pages by turn, not by cursor: `GET /v1/calls/{call_id}/transcript` takes `start_turn` (the turn to start from, default 0) and an optional `limit` (at most 500 turns). Without `limit` it returns every turn from `start_turn` to the end; with it, at most `limit` turns, and `has_more` plus `next_start_turn` say where the next page starts. It takes no `cursor`; a request that sends one is a `400`. Its one filter is `segment` (`intake`, `briefing` or `conversation`), which returns one part of the call; `start_turn` then counts that part's turns. See [Transcripts](/docs/sync-guide#transcripts).

Each list response carries `has_more` and `next_cursor`. Keep requesting with the new cursor until `has_more` is `false`. A cursor is opaque: pass it back unchanged, and use it only on the endpoint that issued it.

Send every filter again with the cursor, unchanged. A cursor marks a position in the list; it does not remember the filters, so a cursor from a request with `updated_since` replayed without it returns a different set.

The calls and leads lists also carry `as_of`: Receptiva's own clock when the list was read, a few seconds early on purpose. To poll for changes, pass the first page's `as_of` as the next poll's `updated_since` rather than reading your own clock. See [Keep another system in sync](/docs/sync-guide).

## Query parameters

Unknown query parameters are rejected with `400` and the code `invalid_request`, so a typo is caught at once. The error names the parameters that endpoint does accept, for example ``Unknown parameter `limit`. This endpoint accepts: `start_turn`, `segment`.`` Do not add a cache-buster such as `?_=123` to the query string. Responses are never cached (`Cache-Control: private, no-store`).

## Finding a caller by phone number

`GET /v1/calls` and `GET /v1/leads` take `caller_number`. Send E.164 (`+12105550147`) or a 10-digit US number in any common format (`(210) 555-0147`, `210-555-0147`). A number without `+` is read as a US number. Encode the `+` as `%2B` if you can; an unencoded `+` (which arrives as a space) is accepted too. Anything that cannot be a phone number is a `400 invalid_request`.

- On calls, it matches the number the call came from (`from`, the caller ID), not the callback number the caller gave.
- On leads, it matches the lead's callback number (`phone`), compared the same way however the caller said it.

## Caller text is data, not instructions

Anything said on a call, by the caller or your staff, or written from what was said, is returned under a field named `untrusted`: summaries, the conversation summary, names, transcript turns and intake details. If you pass API responses to an AI model, treat everything under `untrusted` as data and never as instructions.

A lead's `external_id` and `status_reason` are not caller text: they are what your firm set, so they sit beside `status`, not under `untrusted`.

## Errors

Errors use the problem details format (RFC 9457) with the content type `application/problem+json`:

```json
{
  "type": "about:blank",
  "title": "Not Found",
  "status": 404,
  "code": "not_found",
  "detail": "No call with that id for this firm.",
  "request_id": "..."
}
```

Use `code` in your own logic. `title` and `detail` are for people and may change. Every response also carries an `X-Request-Id` header. Include it when you email us about a request.

| Status | `code` | Meaning |
| --- | --- | --- |
| 400 | `invalid_request` | A parameter or the body is not valid. The `errors` array names each field. |
| 400 | `invalid_cursor` | The cursor is not one this endpoint issued. |
| 400 | `invalid_range` | `from` must be earlier than `to`. |
| 401 | `unauthorized` | The key is missing, mistyped, revoked or expired. |
| 403 | `insufficient_scope` | The key does not have the permission this endpoint needs. |
| 403 | `org_suspended` | The firm's account is suspended. |
| 403 | `forbidden` | The key may not make this change. |
| 404 | `not_found` | Nothing with that id belongs to this firm. |
| 404 | `transcript_unavailable` | Receptiva has no transcript for that call. |
| 404 | `no_recording` | That call has no recording. |
| 404 | `recording_unavailable` | The recording could not be read. |
| 404 | `invalid_link` | The recording link is wrong, expired or no longer allowed. |
| 409 | `not_configured` | The firm's receptionist is not set up yet. |
| 405 | `method_not_allowed` | `HEAD` is not supported. Use `GET`. |
| 409 | `status_conflict` | The lead's status is no longer the `expected_status` you sent, or it changed while the request ran. Nothing was changed; the answer's `lead` member is the lead as it is now. |
| 415 | `unsupported_media_type` | A request body was sent without `Content-Type: application/json`. |
| 429 | `rate_limited` | Too many requests. Wait for the time in `Retry-After`. |
| 500 | `internal_error` | Something went wrong on our side. Email us the request id. |
| 503 | `unavailable` | A temporary problem on our side. Retry after a short wait. |

A request for another firm's call or lead answers `not_found`, the same as an id that does not exist.

## Rate limits

Each key may make 60 requests per minute. Once a key is accepted, every response says where it stands:

| Header | Meaning |
| --- | --- |
| `RateLimit-Limit` | Requests allowed per minute |
| `RateLimit-Remaining` | Requests left in the current minute |
| `RateLimit-Reset` | Seconds until the count resets |

Over the limit, the API answers `429` with a `Retry-After` header in seconds. There is also a limit of 120 requests per minute per IP address, shared with the MCP connector.

## Timestamps

Every timestamp the API returns is ISO 8601 with an explicit offset. Most end in `+00:00`, for example `2026-10-03T00:24:58.248695+00:00`; a few, such as a recording link's `expires_at`, end in `Z`, for example `2026-10-03T01:24:58.000Z`. Both are valid ISO 8601 and both mean UTC, so parse them with an ISO 8601 parser rather than comparing them as text.

Timestamps you send, such as `from`, `to` and `updated_since`, need an offset too: `Z` or `±HH:MM`. In a URL a `+` means a space, so write a positive offset as `%2B`: `2026-10-02T14:00:00%2B02:00`. Using `Z` avoids the problem. If a `+` does arrive as a space in the offset, for example when you send a response's `updated_at` back unencoded, the API reads it as `+`. Anything else that is not a valid timestamp is rejected with `invalid_request`.

## Versioning

The version is in the path (`/v1`). Within a version, changes are additive: new endpoints, new optional parameters and new response fields. Write clients that ignore fields they do not know. A change that would break existing clients ships as a new version.

## Next

- [API keys](/docs/api-keys)
- [Keep another system in sync](/docs/sync-guide)
- [API reference](/docs/api-reference)
