# OpenPhonex — full docs + blog corpus Agent-native telephony: real phone numbers, calls, and SMS an AI agent runs on its own over MCP. This file is the entire OpenPhonex documentation and blog in one pull. The machine-readable API lives at: - OpenAPI: https://openphonex.com/control-plane/openapi.json - API llms-full.txt: https://openphonex.com/control-plane/llms-full.txt - MCP endpoint: POST https://openphonex.com/control-plane/mcp Each page below is also available as raw markdown at .md. --- # Documentation # Authentication & keys URL: /docs/authentication Scoped tai_ keys, Bearer auth, scope clamping and fail-closed semantics, org isolation, and rotate/revoke. Every agent request authenticates with one scoped **`tai_` API key** passed as a Bearer token. The key is also your permission boundary — it decides which tools run and which organization they run against. Numbers, compliance, billing, and provider operations are never on the key; the agent gets capability tools and nothing else. ## Bearer auth [#bearer-auth] Pass the key on every call to `POST /mcp` (or the REST API): ```http Authorization: Bearer tai_your_scoped_key ``` Over the OpenClaw connector the key lives in the mcporter `env` block as `OPENPHONEX_API_KEY`; the stdio bridge attaches it to each JSON-RPC call. It is a secret — never commit it, never put it in `SKILL.md`. ## Minting a key [#minting-a-key] ```bash curl -s -X POST "$OPENPHONEX_BASE_URL/v1/api-keys" \ -H "Authorization: Bearer $OPENPHONEX_ADMIN_TOKEN" \ -H "Content-Type: application/json" \ -d '{"organization_id":"org_...", "name":"OpenClaw agent", "scopes":["account:read","coverage:read","agents:write", "trial_calls:write","calls:read","recordings:read"]}' ``` The response returns `token` **once**. Store it immediately — it is never retrievable again. ## Scope clamping (cannot self-escalate) [#scope-clamping-cannot-self-escalate] Requested scopes are **clamped server-side** to what your admin credential already holds. A key can never grant itself more than its minter has. Control- plane scopes — `api_keys:write`, `wallet:write`, `operator:*`, `worker_jobs:write`, `provider_mutations:*` — are intentionally omitted from agent keys. The agent gets tools, never the control plane. ## Fail-closed semantics [#fail-closed-semantics] `tools/list` returns every tool, but a `tools/call` for a tool whose `required_scopes` are not on your key fails with `missing MCP scopes: X`. There is no partial or best-effort execution — unauthorized calls fail closed. Mint only the scopes you need. For a cautious agent, drop `sms:send`, `calls:write`, and `numbers:write` and run a read + trial-only key: `account:read`, `coverage:read`, `agents:write`, `trial_calls:write`, `calls:read`, `recordings:read`. ## Org isolation [#org-isolation] Every call runs pinned to the key's organization. A tool argument whose `organization_id` differs from the key's org is rejected with `MCP organization mismatch`. You cannot read or write another tenant. ## Rotate / revoke [#rotate--revoke] Mint a new key, swap the mcporter `env`, then revoke the old one: ```bash curl -s -X POST "$OPENPHONEX_BASE_URL/v1/api-keys/{key_id}/revoke" \ -H "Authorization: Bearer $OPENPHONEX_ADMIN_TOKEN" ``` The bridge is stateless, so there is no downtime on the OpenPhonex side. See the full [scope catalog](/docs/scopes) for every scope and the tool it gates. --- # Deployment modes URL: /docs/deployment-modes Fully managed cloud vs managed numbers with a self-hosted gateway — who provides the brain, and where the media runs. Every deployment answers two questions: **who provides the brain** (the LLM), and **where does the media run** (the gateway that carries call audio). OpenPhonex's two MVP launch paths differ only on the second. OpenPhonex deployment modes: fully managed cloud versus managed numbers with a self-hosted gateway ## `fully_managed_cloud` [#fully_managed_cloud] OpenPhonex Cloud runs everything: the API, the hosted **media gateway**, the DIDWW/provider account, numbers, credits, the compliance workflow, and abuse controls. You bring an agent and a scoped key; the phone side is entirely managed. This is the fastest path to a live line. ## `managed_numbers_self_hosted_gateway` [#managed_numbers_self_hosted_gateway] You run the **media gateway** yourself (Docker/media locally), while OpenPhonex Cloud still manages the API account, DIDWW/provider numbers, billing, the compliance workflow, and abuse controls. Use this when call media must stay in your own network but you still want OpenPhonex to own numbers, compliance, and provider operations. Every number and gateway response includes `provider_account_id` and `provider_account`, so you can always tell managed DIDWW, customer-BYO DIDWW, self-managed provider, and sandbox accounts apart. A fully self-hosted / bring-your-own-provider path (your own DIDWW or a future provider account) is designed into the same open-source stack, but it is a later path — not one of the two MVP launch modes. ## Picking a mode [#picking-a-mode] | | `fully_managed_cloud` | `managed_numbers_self_hosted_gateway` | | --------------------------- | --------------------- | ------------------------------------- | | API account | OpenPhonex | OpenPhonex | | Numbers & provider ops | OpenPhonex | OpenPhonex | | Billing & compliance | OpenPhonex | OpenPhonex | | Media gateway | OpenPhonex (hosted) | **You** (Docker/media local) | | Fastest to live | Yes | — | | Media stays in your network | — | Yes | To scaffold the self-hosted path, the API returns an install plan: `GET /v1/self-hosted/install-plan?deployment_model_id=managed_numbers_self_hosted_gateway` lists the customer/operator steps, required env names, the compose command, and the doctor command. --- # Introduction URL: /docs OpenPhonex is agent-native telephony — real phone numbers, calls, and SMS an AI agent runs on its own over MCP. OpenPhonex is **agent-native telephony**: real phone numbers, calls, and SMS that an AI agent can run on its own. Because every capability is exposed over **MCP** and described in **llms.txt**, any MCP-capable agent already knows how to use it — no bespoke integration required. You are basically done. Point your client at `POST /mcp`, pass a scoped key as a Bearer token, and the OpenPhonex tools show up in `tools/list`. ## What is OpenPhonex? [#what-is-openphonex] OpenPhonex gives your agents the phone side of the world: search number coverage, wire an agent to a number, place a trial or outbound call, read recordings and transcripts, and send SMS — all behind one scoped API key that is also your permission boundary. Numbers, compliance, billing, and provider operations stay on the OpenPhonex side; your agent gets the capability tools and nothing else. ## How your agent connects [#how-your-agent-connects] There is one universal path and two convenience wrappers around it. All three end at the same scoped tools. ## Quickstart [#quickstart] Three steps from zero to your agent placing a trial call. This uses the OpenClaw connector as the worked example; any MCP client follows the same shape. ### Install the connector [#install-the-connector] Drop the OpenPhonex skill into your agent and register the MCP server it ships with. ```bash # copy the connector into your agent checkout cp -R connectors/openclaw skills/openphonex # register the hosted MCP endpoint (mcporter) npx mcporter add openphonex --url https://api.openphonex.com/mcp ``` ### Mint a scoped key [#mint-a-scoped-key] One org key with agent-facing scopes only. Control-plane scopes are omitted and clamped server-side, so the key cannot self-escalate. ```bash curl -X POST "$OPENPHONEX_BASE_URL/v1/api-keys" \ -H "Authorization: Bearer $ADMIN_TOKEN" \ -d '{"name":"my agent", "scopes":["coverage:read","agents:write", "trial_calls:write","calls:read", "recordings:read"]}' # -> returns a tai_… key ONCE. Store it now. ``` ### Your agent calls the tools [#your-agent-calls-the-tools] Set the key in the MCP server env, restart, and ask your agent in plain language. It picks the right tool from `tools/list`. ```text you › search GB number coverage, then place a trial call # the agent runs, over MCP: → get_coverage(country="GB") → create_trial_call(...) # returns a dial-in PIN ✓ transcript + cost summarized back to you ``` Live outbound calls and SMS are policy-gated: the agent evaluates `evaluate_outbound_policy` and surfaces the decision and cost before anything leaves the machine. Scopes fail closed. ## Next steps [#next-steps] --- # Install the connector URL: /docs/install-connector The OpenClaw connector in detail — skill, stdio bridge, mcporter registration, the remote-MCP shortcut, and the offline smoke test. The OpenClaw connector is the reference way to give an agent OpenPhonex tools. It is two parts that work together: an OpenClaw **skill** (a `SKILL.md` playbook that makes the capability discoverable) and an **mcporter MCP registration** that points at OpenPhonex Cloud's hosted `POST /mcp`. The MCP entry runs a thin local **stdio bridge** that forwards each JSON-RPC call to `/mcp` with your scoped `tai_` key as a Bearer token. You need both — the skill makes it discoverable, the MCP entry makes it callable. Not billing, provider ordering, operator/compliance actions, or access to any other tenant. Numbers, compliance, billing, and provider operations stay in OpenPhonex Cloud. The scoped key is your permission boundary: capabilities you were not granted fail closed, and every call is pinned to your organization server-side. `request_number` creates a *request* — a live number still requires an OpenPhonex operator. ## 1. Install the skill [#1-install-the-skill] Copy this directory into your OpenClaw checkout as `skills/openphonex/` and make the bridge scripts executable. ```bash cp -R connectors/openclaw /path/to/openclaw/skills/openphonex cd /path/to/openclaw chmod +x skills/openphonex/bin/openphonex-mcp-bridge.mjs \ skills/openphonex/bin/openphonex_mcp_bridge.py \ skills/openphonex/bin/openphonex-mint-key.sh ``` ## 2. Register the MCP server [#2-register-the-mcp-server] Merge the `openphonex` block from `mcporter.template.json` into your `config/mcporter.json` under `mcpServers`, alongside any existing entries. ```json { "mcpServers": { "openphonex": { "command": "node", "args": ["skills/openphonex/bin/openphonex-mcp-bridge.mjs"], "env": { "OPENPHONEX_BASE_URL": "https://api.openphonex.com", "OPENPHONEX_API_KEY": "tai_your_scoped_key", "OPENPHONEX_ORGANIZATION_ID": "org_your_org" } } } } ``` The `OPENPHONEX_API_KEY` is a **secret** — keep it in the mcporter `env` block (or an env-var reference), never in `SKILL.md`, and git-ignore it the same way you would `.env`. ### Prefer the Python bridge? [#prefer-the-python-bridge] Swap the command — the wire protocol is identical. ```json "command": "python3", "args": ["skills/openphonex/bin/openphonex_mcp_bridge.py"] ``` ### Remote-MCP shortcut [#remote-mcp-shortcut] If your mcporter build supports remote MCP directly, you can skip the local bridge entirely. ```json "openphonex": { "url": "https://api.openphonex.com/mcp", "headers": { "Authorization": "Bearer tai_your_scoped_key" } } ``` The stdio bridge is the guaranteed-portable default because it matches the mcporter schema already in use. Restart OpenClaw / re-run mcporter so it picks up the new server; `tools/list` will then show the OpenPhonex tools. ## 3. Verify [#3-verify] Run the offline smoke test (no live Cloud needed) from the OpenPhonex repo. ```bash PYTHONPATH=src python -m unittest discover -s connectors/openclaw/tests # or PYTHONPATH=src python -m pytest connectors/openclaw/tests ``` Then a live-ish check with the agent: * *"search coverage for GB and read the coverage matrix"* → `get_coverage` * *"create a shared-number trial"* → `create_trial_call` * *"show me the transcript for call X"* → `get_call_transcript` ## Troubleshooting [#troubleshooting] | Symptom | Cause | Fix | | ----------------------------------- | ---------------------------------------------------------- | ---------------------------------------------------------------------------- | | `missing MCP scopes: X` | The key lacks scope `X`. | Re-mint with the needed scope (within what your admin credential holds). | | `MCP organization mismatch` | A tool arg's `organization_id` differs from the key's org. | Pass your own `organization_id` (prefill from `OPENPHONEX_ORGANIZATION_ID`). | | `invalid or revoked API key` | The key was rotated/revoked, or is wrong. | Mint a fresh key and update the mcporter `env`. | | `bridge transport error` | The bridge can't reach `OPENPHONEX_BASE_URL`. | Check the URL, network egress, and that the instance is reachable. | | No OpenPhonex tools in `tools/list` | mcporter didn't pick up the new server. | Confirm the merge into `config/mcporter.json` and restart OpenClaw. | --- # llms-full.txt URL: /docs/llms-full The whole docs + blog corpus as one plain-text file, cross-linked to the API surface. **`/llms-full.txt`** is the entire documentation and blog corpus concatenated into one plain-text file — every page's processed markdown, in reading order, with a header block that cross-links the API surface. It is the single-pull way to give an agent everything OpenPhonex's docs know. ## When to use which [#when-to-use-which] * **`/llms.txt`** — an index. Cheap to fetch; the agent follows links to the pages it needs. * **`/llms-full.txt`** — the whole corpus in one request. Use it to prime an agent with all of the docs at once. * **`/docs/.md`** — one page's raw markdown, when the agent already knows which page it wants. ## Cross-linked to the API [#cross-linked-to-the-api] The header of `/llms-full.txt` links the API's own agent surface — `/control-plane/openapi.json`, `/control-plane/llms-full.txt`, and `POST /control-plane/mcp` — so a single pull of the docs corpus also points the agent at the machine-readable API. On-brand for "works with any AI". --- # llms.txt URL: /docs/llms The site's llms.txt index — a titled map of every doc and blog page plus the API surface. OpenPhonex publishes an **llms.txt** index at the site root: a titled, linked map of every documentation and blog page, plus links to the API surface. It follows the [llms.txt](https://llmstxt.org) convention so an agent can discover the whole corpus from one file. ## What is in it [#what-is-in-it] * Every **docs** page, grouped by section, as a titled link. * Every **blog** post as a titled link. * The **API** surface: `/control-plane/openapi.json`, `/control-plane/llms.txt`, and `POST /control-plane/mcp`. ## Per-page markdown [#per-page-markdown] Every page on this site is also available as raw markdown by appending `.md` to its URL — for example `/docs/quickstart.md` or `/blog/launch.md`. These dotted URLs are public and locale-neutral, so an agent can fetch a single page's clean markdown without scraping HTML. This site's `/llms.txt` describes the **docs/blog** corpus. The API serves its own `/control-plane/llms.txt` describing the **API**. They are different paths and cross-link each other — pull both for the complete picture. --- # MCP overview URL: /docs/mcp The hosted MCP endpoint — POST /mcp, tools/list schemas with required_scopes, and prefix scopes. OpenPhonex speaks **MCP** (Model Context Protocol). Any MCP-capable client points at one endpoint, passes a scoped key, and the OpenPhonex tools appear in `tools/list` — ready to call. This is the universal path; the [OpenClaw connector](/docs/install-connector) and the [REST API](/docs/rest-endpoints) are convenience wrappers around the same tools. ## The endpoint [#the-endpoint] ```http POST https://api.openphonex.com/mcp Authorization: Bearer tai_your_scoped_key ``` Standard JSON-RPC: `tools/list` to discover, `tools/call` to invoke. ## tools/list carries scopes [#toolslist-carries-scopes] Each tool in `tools/list` advertises its `required_scopes`. `tools/list` returns **all** tools regardless of your key, but a `tools/call` for a tool whose scopes are not on your key fails closed with `missing MCP scopes: X`. This lets an agent see the full surface while still being held to least privilege. ## Prefix scopes [#prefix-scopes] Some scopes are grouped by prefix — for example `sms:*` covers both `sms:read` and `sms:send`. Grant the specific leaf scope for least privilege, or the prefix when an agent legitimately needs the whole group. This page replaces the old `/docs/agents` stub. Its MCP-endpoint, OpenAPI, and scoped-key material now lives here and in [Authentication & keys](/docs/authentication). --- # OpenAPI URL: /docs/openapi The live, machine-readable OpenAPI spec for the OpenPhonex API. The OpenPhonex API ships a live **OpenAPI 3** document. It is the machine-readable source of truth for every REST endpoint, request/response schema, and auth requirement — generated from the running service, so it never drifts from the docs. ## Open the spec [#open-the-spec] Point any OpenAPI-aware client at it — Swagger UI, Postman, an SDK generator, or an agent that reads OpenAPI directly. The same scoped `tai_` Bearer key authenticates every operation. `/control-plane/openapi.json` is the **API** surface. The [Agent surface](/docs/llms) pages describe the **docs/blog** corpus — this site's `/llms.txt`, `/llms-full.txt`, and the `.md` on every page. An agent can pull both to get the full picture. --- # Policy & outbound URL: /docs/policy-and-outbound How policy-gated outbound works — evaluate first, authorize, then place. Blocked categories and the broker example. Live outbound calls and SMS never leave the machine unchecked. OpenPhonex makes the agent **evaluate policy first**, surface the decision and cost, and only then authorize and place. Scopes fail closed, and the policy engine blocks whole categories of outbound regardless of scope. ## The outbound sequence [#the-outbound-sequence] ### Evaluate [#evaluate] ```text → evaluate_outbound_policy(...) # policy:evaluate ``` Returns the policy decision and cost estimate **before** anything leaves the machine. This is a read — it changes nothing. ### Get explicit human approval [#get-explicit-human-approval] Show the decision and cost to a human and wait for a clear yes. Never place a live call or send a live SMS on an implied approval. ### Authorize and place [#authorize-and-place] ```text → authorize_outbound(...) # policy:evaluate → create_outbound_call(...) # calls:write ``` ## Blocked categories [#blocked-categories] The policy engine blocks outbound in these categories regardless of scope: healthcare, political, emergency, debt collection, robocalling, mass outreach, telemarketing, lead generation, scraped / listed-contact broker campaigns, spoofing, and cold sales. A Czech contact-broker campaign that dials a purchased list is a scraped/listed-contact broker category — `evaluate_outbound_policy` returns a blocked decision, and `create_outbound_call` fails closed. The agent should report the block and stop, not retry. --- # Quickstart URL: /docs/quickstart The five-minute path — install the OpenClaw connector, mint a scoped key, and have your agent place a trial call. This is the full five-minute path from an empty checkout to your agent placing a trial call over MCP. The worked example uses the OpenClaw connector; any MCP-capable client follows the same three moves — install, mint, call. ## Prerequisites [#prerequisites] * An **OpenPhonex organization** and a credential that can mint keys — a dashboard session token (`opsess_…`) or a bootstrap key that holds `api_keys:write`. * **Node 18+** (for the `.mjs` bridge) or Python 3.8+ (for the `.py` bridge). * An agent host that speaks MCP — an OpenClaw checkout with `config/mcporter.json`, or any other MCP client. ### Install the connector [#install-the-connector] Copy the connector into your agent checkout and register the hosted MCP endpoint. ```bash cp -R connectors/openclaw skills/openphonex npx mcporter add openphonex --url https://api.openphonex.com/mcp ``` Full detail — the stdio bridge, the `.mjs` vs `.py` choice, and the remote-MCP shortcut — is in [Install the connector](/docs/install-connector). ### Mint a scoped key [#mint-a-scoped-key] Mint one org key with the agent scope set. Control-plane scopes (`api_keys:write`, `wallet:write`, `operator:*`, `provider_mutations:*`) are intentionally omitted, and requested scopes are clamped server-side to what your admin credential already holds — the key cannot self-escalate. ```bash curl -s -X POST "$OPENPHONEX_BASE_URL/v1/api-keys" \ -H "Authorization: Bearer $OPENPHONEX_ADMIN_TOKEN" \ -H "Content-Type: application/json" \ -d '{"organization_id":"org_...", "name":"OpenClaw agent", "scopes":["account:read","coverage:read","agents:write", "trial_calls:write","numbers:read","numbers:write", "calls:read","calls:write","recordings:read", "sms:read","sms:send","policy:evaluate"]}' ``` The response returns `token` (a `tai_…` key) **once** — store it immediately, it is never retrievable again. For a cautious tier, drop `sms:send`, `calls:write`, and `numbers:write` for a read + trial-only key. See [Authentication & keys](/docs/authentication) for the full scope model. ### Wire the key into the MCP server [#wire-the-key-into-the-mcp-server] Set the three env vars on the `openphonex` MCP server and restart your agent so `tools/list` picks up the tools. ```json { "mcpServers": { "openphonex": { "command": "node", "args": ["skills/openphonex/bin/openphonex-mcp-bridge.mjs"], "env": { "OPENPHONEX_BASE_URL": "https://api.openphonex.com", "OPENPHONEX_API_KEY": "tai_your_scoped_key", "OPENPHONEX_ORGANIZATION_ID": "org_your_org" } } } } ``` ### Your agent calls the tools [#your-agent-calls-the-tools] Ask the agent in plain language — it selects the right tool from `tools/list`. ```text you › search GB number coverage, then place a trial call → get_coverage(country="GB") # coverage:read → create_trial_call(...) # trial_calls:write — returns a dial-in PIN ✓ transcript + cost summarized back to you ``` Live outbound calls and SMS are policy-gated. Evaluate `evaluate_outbound_policy` first, show the decision and cost, and wait for an explicit human yes. Scopes fail closed — a `tools/call` for a tool whose `required_scopes` are not on your key returns `missing MCP scopes`. ## Next steps [#next-steps] --- # Read call evidence URL: /docs/read-call-evidence Recordings, waveform peaks, transcript turns, summaries, and retention/disclosure state for every call. Every call — trial or production — attaches evidence to the session. An agent reads it back with the call and recording tools. ## The evidence tools [#the-evidence-tools] ```text → list_calls(...) # calls:read → get_call(call_id="cl_...") # calls:read — status + recording state → get_call_recording(call_id="cl_...") # recordings:read — metadata + disclosure → get_call_transcript(call_id="cl_...") # recordings:read — transcript turns ``` ## What a recording contains [#what-a-recording-contains] A `recording` is the call's audio metadata plus: * **Waveform peaks** — for rendering a scrubbable waveform. * **Transcript turns** — speaker-attributed turns of the conversation. * **Retention / disclosure state** — how long the audio is kept and whether the required call disclosure was made. `get_call` returns the call status and recording state; `get_call_recording` returns recording metadata and disclosure state; `get_call_transcript` returns the transcript turns. Recording reads require the `recordings:read` scope, which is distinct from `calls:read` so you can grant transcript access without call metadata, or vice versa. Recording retention runs on the OpenPhonex side (dry-run or execute). The disclosure state on each recording tells you whether the call was announced as recorded — surface it when you summarize a call back to a human. --- # REST endpoints URL: /docs/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](/docs/openapi) is the machine-readable source of truth; this is the human map. ## Agents [#agents] | Method | Path | Purpose | | ------ | ----------------------------- | -------------------------------------------------------------- | | `POST` | `/v1/agents` | Create an agent (`external_audio`, `external_text`, `hosted`). | | `POST` | `/v1/agents/{id}/voice-stack` | Set the STT/LLM/TTS stack. | ## Calls [#calls] | Method | Path | Purpose | | ------ | -------------------- | -------------------------------------- | | `POST` | `/v1/trial-calls` | Create a shared-number trial call PIN. | | `POST` | `/v1/calls/outbound` | Place a policy-gated outbound call. | | `GET` | `/v1/calls` | List calls. | | `GET` | `/v1/calls/{id}` | Call status + recording state. | ## Numbers [#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. | ## Messages [#messages] | Method | Path | Purpose | | ------ | ------------------- | ------------------------ | | `GET` | `/v1/conversations` | List SMS conversations. | | `POST` | `/v1/messages` | Send a policy-gated SMS. | ## Wallet & usage [#wallet--usage] | Method | Path | Purpose | | ------ | ---------------------- | -------------------------------------- | | `GET` | `/v1/account/overview` | Wallet, agents, numbers, usage rollup. | REST and MCP share the same scoped keys and the same fail-closed scope checks. See [Authentication & keys](/docs/authentication). --- # Scopes & permissions URL: /docs/scopes The scope catalog, prefix scopes, and fail-closed semantics that make a key a permission boundary. A scope is the unit of permission on a `tai_` key. The key is the boundary: tools whose `required_scopes` are not present fail closed. Grant the fewest scopes an agent needs. ## Scope catalog [#scope-catalog] | Scope | Grants | | ------------------- | ---------------------------------------------------------------- | | `account:read` | Workspace overview — wallet, agents, numbers, calls, SMS, usage. | | `coverage:read` | Coverage, number catalog, agent catalog, requirements. | | `agents:write` | Create and configure agents; set voice stacks. | | `trial_calls:write` | Create shared-number trial calls. | | `numbers:read` | List numbers, read number rules, list phone servers. | | `numbers:write` | Request numbers, update rules, create/test phone servers. | | `calls:read` | List calls, read call status. | | `calls:write` | Place policy-gated outbound calls. | | `recordings:read` | Read recording metadata, disclosure, and transcripts. | | `sms:read` | Read SMS conversations. | | `sms:send` | Send policy-gated SMS. | | `policy:evaluate` | Evaluate outbound policy and authorize outbound. | | `onboarding:read` | Read the workspace risk envelope. | | `onboarding:write` | Request risk-envelope changes. | ## Prefix scopes [#prefix-scopes] A prefix scope covers every leaf under it. `sms:*` grants both `sms:read` and `sms:send`; grant the leaf for least privilege, the prefix when the whole group is genuinely needed. ## Control-plane scopes are off-limits [#control-plane-scopes-are-off-limits] Agent keys never carry `api_keys:write`, `wallet:write`, `operator:*`, `worker_jobs:write`, or `provider_mutations:*`. Requested scopes are clamped server-side to what the minting credential holds, so a key cannot self-escalate. ## Fail-closed [#fail-closed] A `tools/call` for a tool whose `required_scopes` are missing returns `missing MCP scopes: X` and does nothing. There is no best-effort fallback. Every call is also pinned to the key's organization — a mismatched `organization_id` is rejected with `MCP organization mismatch`. See [Authentication & keys](/docs/authentication) for minting, rotating, and revoking keys. --- # Send SMS URL: /docs/send-sms Read SMS conversations and send policy-gated messages from your numbers. OpenPhonex exposes SMS as two scoped capabilities: reading conversation history and sending messages. Sending is gated separately from reading so a read-only agent can follow a thread without ever being able to send. ## Read conversations [#read-conversations] ```text → list_conversations(...) # sms:read ``` Connects SMS history to the same customer workspace as calls and numbers. ## Send a message [#send-a-message] ```text → send_sms(from_phone_number_id="pn_...", to_phone_number="+1555...", body="...") # sms:send ``` `send_sms` requires the `sms:send` scope and is subject to the same outbound policy as calls — blocked categories (telemarketing, mass outreach, lead generation, and more) fail closed. Evaluate policy and get explicit human approval before sending live. See [Policy & outbound](/docs/policy-and-outbound). --- # Tool reference URL: /docs/tool-reference Every OpenPhonex MCP tool with its required scope, generated from the server's TOOL_SCOPES. Every tool the agent can call, with the scope it requires. Scopes are authoritative on the server (`TOOL_SCOPES` in `src/telephony_ai/mcp.py`) — treat the live `tools/list` on your instance as canonical. ## Account [#account] | Tool | Scope | Description | | ------------------ | -------------- | ----------------------------------------------------------------------------- | | `account_overview` | `account:read` | Wallet, agents, numbers, calls, SMS conversations, and usage for a workspace. | ## Coverage & catalog [#coverage--catalog] | Tool | Scope | Description | | --------------------------- | --------------- | --------------------------------------------------------------------------------------- | | `get_coverage` | `coverage:read` | DIDWW-supported countries with voice, SMS, registration, and onboarding speed. | | `get_number_catalog` | `coverage:read` | Number SKUs with setup/monthly price, capabilities, and onboarding terms. | | `get_agent_catalog` | `coverage:read` | Selectable STT/LLM/TTS providers with cost, latency, languages, streaming, runnability. | | `check_number_requirements` | `coverage:read` | Country and number-type onboarding requirements in plain terms. | ## Agents [#agents] | Tool | Scope | Description | | ------------------------ | -------------- | ---------------------------------------------------------------------------------- | | `create_agent` | `agents:write` | Create a first-class agent in `external_audio`, `external_text`, or `hosted` mode. | | `connect_external_agent` | `agents:write` | Shortcut for an `external_audio` agent with webhook + audio-stream URLs. | | `set_agent_voice_stack` | `agents:write` | Set transcriber/model/voice, first message, system prompt, language. | ## Trial calls [#trial-calls] | Tool | Scope | Description | | ------------------- | ------------------- | ------------------------------------------------------------ | | `create_trial_call` | `trial_calls:write` | Create a shared-number trial call PIN for a connected agent. | ## Numbers [#numbers] | Tool | Scope | Description | | --------------------- | --------------- | ------------------------------------------------------------------------ | | `list_numbers` | `numbers:read` | List phone numbers and registration status. | | `get_number_rules` | `numbers:read` | Get customer-facing number rules for one phone number. | | `list_phone_servers` | `numbers:read` | List customer phone servers for self-hosted media/calling. | | `request_number` | `numbers:write` | Create a managed number request (live ordering needs operator approval). | | `update_number_rules` | `numbers:write` | Update customer-facing number rules for one phone number. | | `create_phone_server` | `numbers:write` | Create a customer phone server (dry-run verify before assigning). | | `test_phone_server` | `numbers:write` | Dry-run verify a customer phone server before assigning a number. | ## Calls & recordings [#calls--recordings] | Tool | Scope | Description | | ---------------------- | ----------------- | -------------------------------------------- | | `list_calls` | `calls:read` | List calls for a workspace. | | `get_call` | `calls:read` | Get call status and recording state. | | `get_call_recording` | `recordings:read` | Get recording metadata and disclosure state. | | `get_call_transcript` | `recordings:read` | Get transcript turns. | | `create_outbound_call` | `calls:write` | Place a policy-gated outbound call. | ## SMS [#sms] | Tool | Scope | Description | | -------------------- | ---------- | --------------------------------------- | | `list_conversations` | `sms:read` | List SMS conversations for a workspace. | | `send_sms` | `sms:send` | Send a policy-gated SMS message. | ## Policy & onboarding [#policy--onboarding] | Tool | Scope | Description | | -------------------------- | ------------------ | -------------------------------------------------------------------- | | `evaluate_outbound_policy` | `policy:evaluate` | Return the outbound policy decision and cost before anything leaves. | | `authorize_outbound` | `policy:evaluate` | Authorize a policy-approved outbound action. | | `get_risk_envelope` | `onboarding:read` | Read the workspace risk envelope (caps, gates). | | `request_risk_envelope` | `onboarding:write` | Request a risk-envelope change. | See [Scopes & permissions](/docs/scopes) for the full scope catalog and fail-closed semantics. --- # Trial calls URL: /docs/trial-calls Place a shared-number trial call to hear a connected agent end to end, with no number provisioning. A **trial call** is the fastest way to hear a connected agent on a real phone line. It uses a shared OpenPhonex number and a dial-in PIN, so you can validate an agent before requesting or assigning a number of your own. ## Place a trial call [#place-a-trial-call] ```text you › create a shared-number trial for my agent → create_trial_call(agent_id="ag_...") # trial_calls:write ✓ returns a dial-in number + PIN ``` `create_trial_call` returns a PIN you dial into from any phone. The agent answers on the shared number, and the call produces the same evidence as a production call — recording, waveform, transcript, and cost. ## After the call [#after-the-call] Read what happened with the call-evidence tools: ```text → list_calls(...) # calls:read → get_call(call_id="cl_...") # calls:read — status + recording state → get_call_transcript(...) # recordings:read ``` Trial calls need only `trial_calls:write` (and `agents:write` to have connected the agent). You do not need a provisioned number, `numbers:write`, or operator approval to run one. See [Read call evidence](/docs/read-call-evidence) for recordings, transcripts, and retention detail. --- # Webhooks URL: /docs/webhooks Call-lifecycle and DIDWW SMS delivery callbacks — where your call and SMS events land. Webhooks deliver call-lifecycle and SMS events to your endpoint so your agent can react without polling. Configure the destination in the dashboard; the recent event stream is visible under **Webhooks** in the app. ## Call lifecycle [#call-lifecycle] Call events fire as a call moves through its lifecycle — ringing, answered, recording available, transcript ready, ended — each carrying the call id so you can fetch full [evidence](/docs/read-call-evidence) with `get_call` / `get_call_transcript`. ## SMS delivery (DIDWW callbacks) [#sms-delivery-didww-callbacks] Inbound SMS and delivery-status callbacks arrive from DIDWW and are normalized into conversation events. Read the thread with `list_conversations` (`sms:read`). Treat webhook bodies as signals to fetch authoritative state over MCP/REST rather than the source of truth. The call and recording tools always return the current, scope-checked view. --- # Wire an agent to a number URL: /docs/wire-an-agent Connect an external or hosted agent, set its voice stack, and answer real calls. An OpenPhonex **agent** is the phone-side configuration that decides how a call is heard, thought about, and spoken. There are three modes: * `external_audio` — your own audio agent, connected over a webhook + audio stream URL. Use `connect_external_agent` for the common case. * `external_text` — your own text agent; OpenPhonex handles STT/TTS around it. * `hosted` — OpenPhonex runs the whole STT/LLM/TTS stack for you. ## 1. Create the agent [#1-create-the-agent] ```text you › connect my audio agent at wss://my-agent.example/stream → connect_external_agent(...) # agents:write ``` `create_agent` is the full form (mode, first message, system prompt, voice stack). `connect_external_agent` is the shortcut for an `external_audio` agent with webhook and audio-stream URLs. ## 2. Choose a voice stack [#2-choose-a-voice-stack] For hosted or external-text agents, pick STT / LLM / TTS providers from the catalog. `get_agent_catalog` lists selectable providers with cost, latency, languages, streaming, and runnability; filter by component and language (e.g. `language=et` surfaces the single Estonian TTS). ```text → get_agent_catalog(component="tts", language="et") # coverage:read → set_agent_voice_stack(agent_id="ag_...", ...) # agents:write ``` `set_agent_voice_stack` validates your selection against the catalog and the Estonian `eleven_v3`-only TTS guard. ## 3. Point a number at it [#3-point-a-number-at-it] Number rules decide what happens when someone calls a number and which agents can use it to place calls. Update them with `update_number_rules` (`numbers:write`); read them with `get_number_rules` (`numbers:read`). Before assigning a live number, place a shared-number [trial call](/docs/trial-calls) to hear the agent end to end — no number provisioning required. --- # Blog # OpenPhonex: agent-native telephony is live URL: /blog/launch Real phone numbers, calls, and SMS an AI agent can run on its own — over MCP, described in llms.txt. Here is what shipped and how it deploys. Agents are ready. Phone infrastructure was still the hard part — until now. **OpenPhonex is live**: real phone numbers, calls, and SMS that an AI agent can run on its own, exposed over **MCP** and described in **llms.txt** so any MCP-capable agent already knows how to use it. Getting a working phone number is one step. Getting calls to route, recordings to capture, transcripts to index, SMS to activate, outbound to pass review, and the whole stack to run in production is the part that used to take months. OpenPhonex makes the full call lifecycle programmable behind one scoped API key. ## Two ways to deploy at MVP [#two-ways-to-deploy-at-mvp] OpenPhonex deployment modes Every deployment answers two questions: who provides the brain, and where the media runs. The two launch paths differ only on the second: * **`fully_managed_cloud`** — OpenPhonex Cloud runs the API, the hosted media gateway, the provider account, numbers, credits, compliance, and abuse controls. The fastest path to a live line. * **`managed_numbers_self_hosted_gateway`** — you run the media gateway (Docker locally) while OpenPhonex Cloud still manages the API account, numbers, billing, compliance, and provider operations. Use it when call media must stay in your own network. Full detail is in [Deployment modes](/docs/deployment-modes). ## The MCP + llms.txt story [#the-mcp--llmstxt-story] Because every capability is an MCP tool with a `required_scopes` contract, and because the whole surface is described in `llms.txt`, there is no bespoke integration to write. Point an MCP client at `POST /mcp`, pass a scoped `tai_` key, and the OpenPhonex tools show up in `tools/list`. Prefer REST? The same key authenticates a plain JSON API, and the [OpenAPI spec](/docs/openapi) is generated from the running service. ## Safe by default [#safe-by-default] Live outbound calls and SMS are policy-gated: the agent evaluates `evaluate_outbound_policy`, surfaces the decision and cost, and waits for an explicit human yes. Scopes fail closed, and whole categories of outbound — telemarketing, mass outreach, broker campaigns — are blocked regardless of scope. ## Get started [#get-started] The [Quickstart](/docs/quickstart) takes you from an empty checkout to your agent placing a trial call in about five minutes: install the connector, mint a scoped key, and ask your agent in plain language. --- # Your agent already knows how to use OpenPhonex URL: /blog/works-with-any-ai The MCP + llms-full.txt thesis — why an agent-native telephony API needs no bespoke integration. Most APIs assume a human will read the docs and write an integration. An agent-native API assumes the opposite: the agent reads the surface and calls it directly. OpenPhonex is built for the second world. ## MCP is the integration [#mcp-is-the-integration] Every OpenPhonex capability is an **MCP tool** with a declared `required_scopes` contract. An agent points its MCP client at `POST /mcp`, passes a scoped `tai_` key as a Bearer token, and the tools appear in `tools/list` — coverage search, agent wiring, trial calls, call evidence, SMS, policy-gated outbound. There is no SDK to install per language, no glue code to maintain. The [MCP overview](/docs/mcp) has the shape; the [tool reference](/docs/tool-reference) has every tool and its scope. ## llms-full.txt is the manual [#llms-fulltxt-is-the-manual] Point an agent at [`/llms-full.txt`](/llms-full.txt) and it pulls the entire docs and blog corpus in one request — every page's processed markdown, cross-linked to the API's own `/control-plane/openapi.json` and `/control-plane/llms.txt`. Every page is also available as raw markdown by appending `.md` to its URL, so an agent can fetch exactly the page it needs without scraping HTML. ## Fail-closed keeps it safe [#fail-closed-keeps-it-safe] "Works with any AI" does not mean "does anything". The scoped key is the permission boundary: a `tools/call` for a tool whose scopes are not on the key fails closed with `missing MCP scopes`, every call is pinned to the key's organization, and live outbound is policy-gated. An agent gets exactly the capabilities it was granted — and nothing else. ## Try it [#try-it] The [Quickstart](/docs/quickstart) is three moves — install, mint, call — and about five minutes to your agent placing a trial call over MCP.