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 instead.
Base URL#
https://api.receptiva.aiEvery 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 is generated from it.
Authentication#
Send an API key as a bearer token on every request:
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. 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. |
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 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: thenext_cursorvalue 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.
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.
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:
{
"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.