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.
fully_managed_cloud
OpenPhonex Cloud runs everything: the API, the hosted media gateway, the carrier/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.
Nothing here is provisioned per workspace. One hosted dialer places every managed
outbound call and one hosted runner drives every real-time Simulations Lab
session, for every workspace, and both read a job's workspace, project, agent,
caller number and destination from the call record itself. There is no worker to
run, no worker key to install and no gateway deployment to register before
POST /v1/calls works. What outbound still depends on is your
outbound authorization, not any setup of your own.
managed_numbers_self_hosted_gateway
You run the media gateway yourself (Docker/media locally), while OpenPhonex Cloud still manages the API account, carrier/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, customer-BYO,
self-managed provider, and sandbox accounts apart.
Credentials for a self-hosted gateway
An inbound self-hosted gateway holds one runtime credential: the
deployment-bound tgw_ token. Create it in the Gateway Tokens card after
the gateway deployment is registered. Its one-time reveal includes this exact
line; replace the blank OPENPHONEX_GATEWAY_API_TOKEN= assignment in the
gateway host's .env.livekit with the whole revealed line:
OPENPHONEX_GATEWAY_API_TOKEN=tgw_...That token performs the whole call path: the number-rules sync, the inbound
route, the resulting call's runtime-config read, and MCP tools/list schema
discovery. Each of those derives its organization server-side from the token
row — nothing the host sends can name a workspace — so a compromised gateway
reaches the workspace its own deployment belongs to and no further.
Two limits make that boundary real rather than nominal:
tools/listis the only MCP method a gateway token may call. It returns the static tool catalogue and reads no workspace row;tools/calland the resource methods stay refused, so a stolentgw_cannot send an SMS or read a recording through MCP.- The token's
kind_allowlistcarriescall.inbound_route. Mint one without that entry and the deployment cannot route calls at all — which is how you quarantine a suspect host without touching the rest of the workspace.
Set OPENPHONEX_ORGANIZATION_ID to the workspace that registered the gateway
and owns the managed DID; a mismatch with the token's own organization is
refused before the call is routed. The smoke test checks the token on both
surfaces a call needs it for (number-rules and tools/list). Neither is a DID
check: tools/list has no target number, so the first inbound route is what
proves the routing authority end to end.
The plaintext value is revealed only once and never appears in gateway-token list views. Revoke and replace an exposed value instead of expecting it to be shown again.
One-time bootstrap: two values
The open-source gateway ships selfhost-bootstrap.sh beside its compose file.
Run it before docker compose up with exactly two inputs — the control-plane
URL and one organization API key carrying gateways:write:
OPENPHONEX_CONTROL_PLANE_URL=https://api.openphonex.com \
OPENPHONEX_BOOTSTRAP_API_KEY=tai_... \
bash selfhost-bootstrap.shIt registers the deployment, mints the tgw_ token (with no expiry — an expiry
without auto-rotation only manufactures a lockout), resolves the organization,
picks the agent-routed number when there is exactly one, and generates the
LiveKit key pair into livekit.yaml, sip.yaml and .env.livekit
identically. The bootstrap key is read from the environment (or the terminal,
without echo) and is never written to the host, so retiring it is one
revoke in the dashboard and nothing else.
It deliberately does not derive your carrier's SIP signalling ranges: the control plane holds none, and a guessed allowlist produces a stack that reports healthy and never rings.
What a new gateway token carries
A token minted without an explicit scopes list carries only the two originator
scopes, gateways:report and calls:authorize. Both selfhost-bootstrap.sh and
the workspace's Gateway Tokens action request the full runtime set instead —
adding number-rules:read, worker_jobs:enqueue, artifacts:upload and
component_credentials:lease — so a gateway either of them sets up is complete.
A client that mints its own token must ask for every scope its host needs.
Tokens that already exist keep the scopes they were stored with.
Your gateway publishes no URL
A self-hosted gateway does not need a public hostname, a tunnel, or a TLS certificate for OpenPhonex's benefit. Your carrier reaches it over SIP, and the control plane never dials into it — every connection runs the other way, from your gateway out to the API. There is nothing to port-forward and no inbound address to register.
Registration reflects this: a deployment carries no public gateway URL and no
local status URL. Both fields were removed from the workspace form and from the
deployment resource. A public_url or local_status_url sent on
POST /v1/gateway-deployments or on a health report is still accepted, so an
older client keeps working, but it is ignored and never returned.
Outbound: which originator dials
A deployment's outbound_execution_mode decides how its outbound calls are
placed. Read it from the deployment (GET /v1/gateway-deployments/{gateway_id})
and change it with
PATCH /v1/gateway-deployments/{gateway_id}/outbound-execution-mode
(gateways:write). Only a self-hosted deployment may be local.
local | queue (the default) | |
|---|---|---|
| Who asks for the call | the gateway host itself | any client with an API key, through POST /v1/calls |
| Who dials it | the same gateway host | that host's --profile outbound worker, after claiming the job |
| Credentials on the host | the tgw_ token only | the tgw_ token and an organization-scoped tai_ key |
Both modes describe a gateway you run yourself. A workspace with no self-hosted deployment has nothing to choose here: its managed outbound calls are dialed by OpenPhonex's shared hosted dialer, which you neither run nor configure.
local exists because on a self-hosted box the requester and the dialer are the
same machine, so the shared job queue buys nothing and costs an
organization-scoped credential. The host calls
POST /v1/gateway-deployments/{gateway_id}/outbound-calls with its own tgw_
token; OpenPhonex runs the same admission a remote POST /v1/calls gets — the
outbound authorization decision, the kill switches and caps, the wallet reserve,
the call record — and answers with the admitted call plus a single-use
authorization bound to that one (gateway, organization, from-number, to-number,
call). Nothing on the host can reach media without it, and a blocked decision
returns no authorization at all.
Remote origination fails closed in local mode
Once a workspace has a local-mode deployment and no queue-mode deployment
left, POST /v1/calls for one of its numbers is refused with a message
naming the local flow, rather than queueing a job nothing would claim. The
refusal happens before the wallet reserve, so no money is held. Job claims are
per-organization, so one remaining queue-mode deployment keeps every project
working.
The organization-scoped worker key
OPENPHONEX_WORKER_API_TOKEN is an organization-scoped tai_ key. Inbound
calling no longer needs it — the change above is additive, so an existing
deployment that still sets one keeps working unchanged — and the only part of
the shipped stack that still reads it is the legacy queue outbound mode's
job-claiming worker, because claiming a call job somebody else requested is
worker_jobs:* authority over the whole workspace queue that a
deployment-bound token deliberately does not carry.
An inbound-only host, and a host in local outbound mode, therefore hold no
organization-wide credential at all.
Later: fully self-hosted / BYO provider
A fully self-hosted / bring-your-own-provider path (your own carrier 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
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.
Webhooks
Call-lifecycle and carrier SMS delivery callbacks — where your call and SMS events land.
How OpenPhonex handles provider languages and voices
How provider-reported inventories become searchable, what OpenPhonex still validates for live calls, and what happens when a provider changes its catalog.