https://voice-api.relationsai.com/v1 with a rai_test_ key, which never places a real call. It works once the API is enabled for your workspace (private preview: support@relationsai.com). Your key stays in this tab's memory; live keys are refused.RelationsAI API
Place AI voice calls, run calling campaigns and manage contacts from your own systems.
The RelationsAI API lets your servers place outbound AI voice calls, run campaigns, keep contacts and the do-not-call list in sync, and receive events by webhook. It is a JSON-over-HTTPS REST API.
Private preview. The API is enabled per workspace. A workspace that has not been enabled gets
403 api_access_not_enabled on every request. Write to support@relationsai.com to request access.
Authentication
Every request sends an API key as a bearer token: Authorization: Bearer <key>. Keys are created in
the console (API & Developers) by a workspace admin and are shown once.
rai_test_...keys work in test mode: nothing is dialled, nothing is charged, and every object has"livemode": false.rai_live_...keys work in live mode: real calls to real people, charged to the workspace balance.- Older
sk_live_...keys are not accepted by this API (401 legacy_api_key_not_supported).
Each key carries scopes (for example calls:write). Each operation lists the scopes it needs
under x-required-scopes. A key can never do more than the member who created it is currently
allowed to do. Live keys are refused from browsers (requests with an Origin header).
Test mode and live mode
Test mode and live mode are fully separate. Calls, campaigns, recipients, contacts, do-not-call
entries, webhook endpoints, events and deliveries created with a test key exist only in test mode,
and the same for live. An id from the other mode is answered with 404 resource_not_found.
Agents and phone numbers are your workspace's real configuration in both modes; test mode also has
a built-in Sample agent and a sample number +12025550150, so you can try everything before
your own agent is ready.
In test mode the destination number chooses what happens to a call:
| Destination | Simulated result |
|---|---|
+12025550100 | Answered and completed, with transcript, recording and analysis |
+12025550101 | No answer |
+12025550102 | Busy |
+12025550103 | Failed (network error) |
+12025550104 | Answered by voicemail: no_response |
+12025550105 | Completed after a longer conversation |
+12025550106 | The person hangs up early: completed, short duration |
| any other number | Completed |
A test call reaches ended within about 20 seconds and settled within about 30 seconds.
Do-not-call entries and missing consent refuse calls in test mode exactly as in live mode.
Requests and responses
- Bodies are JSON with
Content-Type: application/json. Field names aresnake_case. Request bodies are strict: an unknown field is a400 validation_error, never silently ignored. - Ids are opaque strings with a type prefix (
call_,cmp_,ct_, ...). Treat them as opaque. - Timestamps are RFC 3339 in UTC, for example
2026-10-21T09:30:00.000Z. - Money is
{"amount": <integer minor units>, "currency": "INR"}, always what your workspace is charged. - Phone numbers are E.164 strings, for example
+919000000001. - Enums are open. New values can be added at any time; treat an unknown value as "other", never as success.
- Every response carries
X-Request-Id. Quote it to support. HEADis accepted whereverGETis and answers the same status and headers with no body.- An operation that documents no request body ignores a body sent with it (it is not read, so it is
never refused as
415,413orinvalid_json). - Responses carry no automatic
ETag; only campaigns and contacts have one (see Concurrency), and conditional GET headers (If-None-Match,If-Modified-Since) are ignored: a response is never304.
Pagination
List operations return {"object": "list", "data": [...], "has_more": bool, "next_cursor": string|null},
newest first. Pass limit (1 to 100, default 25) and, for the next page, cursor set to the
previous next_cursor with the same filters. Cursors are opaque and bound to the filters that
produced them.
Idempotency
The Idempotency-Key header is optional on every POST and DELETE: an extra safety for retries.
Without it every request runs, so a retry after a timeout or a lost response can repeat the action (place
a second call, start a campaign again); before retrying, check the state with a GET (for a call, list
calls by to and created_after). With it (1 to 255 printable ASCII characters, no spaces; a UUID v4 is
ideal), a retry is safe. Keys are scoped to your workspace and mode and kept for 24 hours.
What is stored under a key: a successful (2xx) answer, and the few errors whose documentation says the
outcome is unknown and the answer is kept (for example campaign_outcome_unknown). A retry with the
same key and the same request gets that stored answer again with Idempotent-Replayed: true (the
same data and status; request_id and X-Request-Id are the retry's own, and a stored retry: after
error carries its Retry-After). Any other error is not stored: a retry with the same key runs the
request again (each error page says what to check first). Reusing a key for a different request is
409 idempotency_key_reused. If the first attempt is still running you get
409 idempotency_key_in_progress with Retry-After. If you send keys, never generate a new key to
retry a request that may have succeeded: that is how a request runs twice.
Concurrency
Campaigns and contacts carry an ETag response header. PATCH on them requires If-Match with
that value (428 precondition_required if missing, 412 version_conflict if the resource changed).
If-Match: * matches any current version and applies the update without the concurrency check.
Errors
Every error has the same envelope: {"error": {"code", "message", "details", "request_id", "retry", "doc_url"}}. Branch on code. retry tells you what to do: no (fix the request or the
configuration), yes (retry with backoff: with the same Idempotency-Key if you sent one, otherwise after
checking the state with a GET) or after (wait
Retry-After seconds). Some errors can happen after work may have begun; their documentation says
so and tells you to retry with the same key and then check the resource state.
Rate limits
Limits apply per key and per workspace, separately in test and live mode, in three classes: read
(600 per minute per key, 1200 per workspace), write (120 per key, 240 per workspace) and dial
(placing calls and starting or resuming campaigns: 30 per minute per workspace). Requests that end
before they reach an operation (an unknown path, a refused body, a missing or invalid key, a workspace
without API access) are counted per client IP address: 300 per minute, after which every request
from that address answers 429 rate_limited until the window resets. Live mode also caps calls
placed through the API per UTC day (429 daily_call_limit_reached): each POST /v1/calls counts
one, a campaign start counts every recipient and is refused whole if they do not all fit, and a
resume counts nothing. Every response has RateLimit-Limit, RateLimit-Remaining and
RateLimit-Reset (for the tightest limit that applied); 429 and 503 responses have Retry-After.
Each operation's class is shown as x-rate-limit-class.
Asynchronous work
Calls and campaigns run after the request returns. POST /v1/calls answers 202 with a call in
queued status; follow it with webhooks (call.ended, call.settled) or by polling
GET /v1/calls/{call_id}. Transcripts, recordings and analysis become ready after the call ends,
usually within a minute and sometimes several minutes later; call.settled says everything,
including the charge, is final.
Versioning
The major version is in the path (/v1). Additive changes (new operations, fields, enum values,
event types, error codes) ship without advance notice and are recorded in the changelog. Breaking changes need a new major
version and at least six months' notice. Events carry api_version.
POST /v1/calls, when a campaign starts and when it resumes (the resume re-screens). A number added while a campaign is running is not removed from that run: pause and resume the campaign to re-screen it, or stop the campaign.Authorization: Bearer <key>. rai_test_ keys use test mode and rai_live_ keys use live mode.Call ended
Call settled
Campaign started
Campaign paused
Campaign resumed
Campaign stopped
Campaign completed
Campaign failed
Test event