REST endpoints
The key REST endpoints — agents, web/trial calls, numbers, calls, messages, and wallet/usage.
No MCP client? Every capability is also a plain JSON REST endpoint under /v1.
Authenticate with the same scoped tai_ key as a Bearer token. The
OpenAPI spec is the machine-readable source of truth; this is
the human map.
Building a customer application?
Start with Customer applications, then connect live data through Integrations. The Generated API reference lists the current customer SDK operation set from the canonical OpenAPI snapshot.
Agents
| Method | Path | Purpose |
|---|---|---|
POST | /v1/agents | Create an agent (external_audio, external_text, hosted). |
GET | /v1/workspace-context | Discover the key's own organization and a bounded project page. |
GET PATCH | /v1/agents/{id}/voice-stack | Read or set the hosted agent's STT/LLM/TTS stack. |
GET | /v1/agents/{id}/workflow | Read a draft/revision and bounded published-version summary before editing or publishing. |
Bootstrap and configure a hosted agent
Start a machine client with GET /v1/workspace-context and account:read.
The bearer key decides the organization; this route does not accept an
organization_id selector. It returns only the safe organization id/name plus
a stable, explicitly bounded page of project id/name/environment values. Use
the returned page metadata to continue discovery. It also works for a new
workspace with no agents.
For a hosted agent, GET /v1/agents/{id}/voice-stack needs agents:read and
PATCH /v1/agents/{id}/voice-stack needs agents:write. Select platform-managed
STT, model, and TTS components by their catalog selectors. For BYOK, first
create the credential through the separate write-only hosted-AI credential API,
then supply only that existing credential reference in
component_credential_bindings. Voice-stack reads never return a secret,
ciphertext, credential-binding ID, or provider credential; unsupported
component, language, and credential combinations are rejected on update.
Voice settings belong to the voice-stack route
PATCH /v1/agents/{id} edits the agent's own fields: its name, status, webhook
and audio-stream URLs, prompt, first message, mode, tools, metadata and
recording flag. It does not save voice settings.
These six names belong to PATCH /v1/agents/{id}/voice-stack (MCP tool
set_agent_voice_stack):
| Setting | What it configures |
|---|---|
turn_taking | interruption sensitivity, endpointing, and the automatic call limits |
voice_polish | end-of-call phrase, delivery profile, acknowledgements, tool-wait |
audio_environment | the pinned ambience preset |
follow_caller | the automatic multilingual policy |
languages | the selected language set |
language_profiles | per-locale voice, transcriber and greeting overrides |
Sending any of them to PATCH /v1/agents/{id} returns 400 naming the route
that does save them. Until this release the same request returned 200 and
silently discarded the setting, so a limit you believed was saved was not, and
the call then ran on the platform default. If you have code that sets
turn_taking through the plain agent route and appears to succeed, it has never
been taking effect; move it to the voice-stack route.
The MCP tool update_agent is the same route and behaves the same way: it is
documented as applying "the same generic PATCH fields and validation as
PATCH /v1/agents/{agent_id}", and from this release that includes the refusal.
An agent script that sets turn_taking through update_agent now gets an error
where it used to get a success it should never have had.
Unrelated fields your client happens to send are still accepted and ignored, as before. Only these six recognised-but-misplaced names are refused — they are not "unknown fields", so REST's no-op compatibility for genuinely unknown keys does not cover them.
A voice selection must name an exact voice
PATCH /v1/agents/{id}/voice-stack returns 400 if you send a TTS provider
and model without a voice_id, whenever that provider has voices to choose
from. The message asks you to choose an exact provider voice for the selected
TTS model.
Until this release such a save could return 200 while storing a stack with no
voice. Nothing reported a problem until the agent was called, and the call then
failed before the caller heard anything. Refusing the save is how you find out
at the moment you make the change rather than on the next live call.
Two things are unchanged. Saving only a transcriber, with no voice selection in the request, is unaffected. And editing an older agent whose stored voice predates this rule still works: that repair path resolves the provider's own default voice rather than refusing your edit.
Before a workflow PATCH or publish, use
GET /v1/agents/{id}/workflow?version_limit=20&version_offset=0 with
agents:read. It gives you the editable draft/revision, validation, safe
canvas layout, current published-version metadata, and a newest-first bounded
version page to compare. An existing agent with no draft returns
workflow_state: "not_created"; the read never creates a blank workflow,
compiles, repairs, calls AI/providers, or writes state.
The Generated API reference and live OpenAPI document carry the exact request and response contracts.
Audio environments
| Method | Path | Purpose |
|---|---|---|
GET | /v1/audio-environments | Read the immutable, versioned licensed ambience catalogue for the workspace. |
GET /v1/audio-environments needs account:read. It is a local catalogue
read: it does not contact a voice provider, call a carrier, change a selected
environment, or return audio bytes. See Audio environments
for the outgoing-ambience and caller-input contracts. A 503 means the
packaged catalogue failed its integrity check; retry rather than inventing a
preset or substituting another version.
Calls
| Method | Path | Purpose |
|---|---|---|
POST | /v1/trial-calls | Create an offline fixture with synthetic_only: true; not a live phone trial. |
POST | /v1/calls/outbound | Place a policy-gated outbound call. |
GET | /v1/calls | List calls. |
GET | /v1/calls/{id} | Call status + recording state. |
GET | /v1/calls/stats | Call volume, outcome, talk time and spend over one or more date windows. |
What a call-stats window counts
GET /v1/calls/stats needs calls:read. Each window you ask for comes back
with since and until plus five integers:
| Field | What it counts |
|---|---|
calls | Every call created in the window, including ones a policy blocked and ones that were never billable. |
answered | Calls somebody was on: completed, or answered, in progress, or with media started right now. Never a call the platform merely accepted. |
failed | Calls that ended with nobody on them: failed, busy, no answer, canceled, or refused by a policy or live-call gate. |
talk_seconds | Total talk time across the window's recorded calls. |
spend_minor | The wallet reservation estimate made when each call was admitted, in the wallet's minor units. This is not settled billing. |
answered and failed do not have to add up to calls. A call that is still
dialing or still queued is in neither, and a call waiting on operator review is
in neither. Read the two figures the endpoint gives you; do not derive one from
calls minus the other.
Numbers
| Method | Path | Purpose |
|---|---|---|
GET | /v1/coverage | Country coverage matrix. |
POST | /v1/number-requests | Request a managed number (operator-approved). |
GET | /v1/numbers | List numbers and registration status. |
GET PUT | /v1/numbers/{id}/rules | Read / update number rules. |
How long a regulated number takes to activate
Some countries require the carrier to approve a registration before a number carries traffic. Two fields describe that wait, and both are deliberately silent rather than guessing.
activation_estimate, on the requirements for a country and number type, is not
always a duration, and must be checked before it is displayed as one. It holds a
duration only where the number needs a carrier registration and the carrier
publishes an activation time for that market: that published figure, passed
through unchanged, usually measured in weeks. Where registration is required and
the carrier publishes nothing — Estonia among them — it is empty, because
OpenPhonex does not substitute a figure of its own.
The other two values are routing states, not times:
instant_after_payment_and_policy_checks— the number needs no carrier registration at all.manual_review— whether it needs one is not yet established.
Either describes the market, never your particular request.
The headline on GET /v1/end-users/{end_user_profile_id}/registration-status
says how many of that profile's number registrations OpenPhonex has submitted to
the carrier. It is empty before anything has been submitted, and empty while a
refusal or a request for more information is outstanding, because that is what
needs your attention instead. The per-number headline on
GET /v1/numbers/{number_id}/activation reports the same submission for one
number and adds the carrier's published activation time when that market has one.
An empty headline is not a missing value: it means nothing has been submitted
that it could report. For the stage a request has actually reached, read
regulatory_status on the number, and blocking_items on the activation
endpoint for what it is waiting on.
next_action on the activation endpoint is held to the same rule. It repeats the
first blocking item when there is one. With nothing blocking it tells you to wait
for carrier approval only when the registration really is open at the carrier;
where nothing has been sent it says so, and where a registration exists but is
not showing as open it says that instead of guessing either way.
Messages
| Method | Path | Purpose |
|---|---|---|
GET | /v1/conversations | List SMS conversations. |
POST | /v1/messages | Send a policy-gated SMS. |
Capabilities
Outbound calls and SMS authorize against the workspace's capabilities record. Machine clients can inspect that record, manage optional destination restrictions, or request higher limits with these commands. See Policy & outbound for what each field means.
| Method | Path | Purpose |
|---|---|---|
GET | /v1/organizations/{organization_id}/capabilities | The record, its calling rules, the selectable profiles, and live usage against its caps. |
POST | /v1/organizations/{organization_id}/capabilities | Request or replace the selection. profiles is required, and so is a destination choice: all_countries: true or a countries allowlist. |
POST | /v1/organizations/{organization_id}/capabilities/accept-policy | Accept a policy version without changing what the record authorizes. |
POST | /v1/organizations/{organization_id}/capabilities/destination-restrictions | Save optional customer narrowing with the current destination_revision; countries: null removes only this extra restriction. |
POST | /v1/organizations/{organization_id}/capabilities/limit-requests | Request higher limits with a reason; does not change caps, prices or wallet funds. |
For an explicit profile-selection request, the destination choice is
all_countries: true (every destination
except sanctioned countries) or countries naming at least one ISO 3166-1
alpha-2 code. A request with neither, with an empty list, or with an entry
that is not a two-letter code once normalized answers 400, names the entry,
and writes nothing. The check is on shape, not on membership of the assigned
ISO list, so a well-formed but unassigned code is stored as sent and then
matches no destination — see
Destination controls.
Wallet & usage
| Method | Path | Purpose |
|---|---|---|
GET | /v1/account/overview | Wallet, agents, numbers, usage rollup. |
The wallet's purchased availability and browser-only promotional credit are separate. See Billing and browser credit before treating a balance as available phone spending.
Keys & scopes are identical to MCP
REST and MCP share the same scoped keys and the same fail-closed scope checks. See Authentication & keys.