2026-09-05 The first real calls have been placed: five ended normally, six failed. Here is what broke.

Developers

Everything the console does, the API does.

One REST surface with an OpenAPI document, scoped keys, webhooks, an MCP server, your own storage bucket and a tenant boundary that is enforced in more than one place.

§01 API

Two hundred and twenty-one documented paths.

The API is not a subset kept alongside the interface. The console is built on the same models and the same policies, so an assistant, a call, a queue or a campaign is the same object whether you reach it through a screen or through a request.

Routes are grouped by domain, each in its own file, and mounted under one versioned prefix behind key authentication and tenant resolution.

The route groups above are read from the routes directory when this page renders.[1]

Route groups 13

  • /v1/analytics
  • /v1/assistants
  • /v1/billing
  • /v1/calls
  • /v1/campaigns
  • /v1/contact-center
  • /v1/knowledge
  • /v1/mcp
  • /v1/platform
  • /v1/squads
  • /v1/telephony
  • /v1/tools
  • /v1/webhooks
API conventions.
openapiA committed OpenAPI document with 221 paths, versioned by date. Generated server SDKs are not shipped yet — the status page says so.
throttleRate limited to 300 requests a minute, keyed on the API key rather than on the IP, because several tenants can share an egress address and one noisy customer must not throttle another.
errorsA single error renderer, so a failure has the same shape whichever domain produced it.
§02 Keys

A key is a credential with a blast radius.

Keys are scoped, rotatable and tenant-bound. Rotation issues a new secret without invalidating the key record, so the thing you are rotating is the secret rather than every integration that used it.

Tenant resolution happens before route-model binding, which sounds like plumbing and is not: with the ordering the other way round, a request for another organization resolved a real record before any policy ran. That was found, fixed globally, and has a regression test.

API key and request handling.
creation Created in the console or through the API. The secret is shown once.
scopes Per-domain read and write scopes, and destructive operations separated from writes.
rotation A rotate action on the key itself, so the integration keeps its identity and its audit trail.
rate limits Keyed on the API key. Unauthenticated requests fall back to the IP at a lower limit, which is the only identifier available before the key resolves.
tenant binding Resolved from the key, then applied as a global query scope, before any model is bound.
unauthenticated An unauthenticated API request gets a JSON error, not a redirect to a login page it cannot follow.
§03 Integration

Three ways in, and one way out.

Webhooks push call lifecycle and event data to your endpoints, with delivery records you can inspect and redeliver rather than a fire-and-forget POST you have to trust.

CallAgent is also an MCP server. It is mounted with the same key authentication and tenant resolution as the REST API, deliberately: an MCP client is another API consumer, not a privileged one.

The voice gateway talks to the control plane over its own internal channel with a shared token, and that channel is not tenant-scoped because it serves every organization at once. It is separated from the public API for exactly that reason.

Integration surfaces
POST /mcpCallAgent exposed as an MCP server, behind API-key auth, tenant resolution and the same rate limit as the REST API.
/v1/webhooksYour endpoints, with the events you want and a signing secret.
deliveriesEvery attempt recorded with its response, and a redeliver action.
/api/internal/v1The gateway channel. Shared-token authenticated, not tenant-scoped, and never exposed as customer API.

Webhook retries run on a scheduled command every minute; a delivery that failed is a record with an attempt count, not a lost event.[2]

§04 Storage

Recordings can live in your bucket, not ours.

Call recordings and artefacts are the part of this system most likely to be governed by somebody else's policy. So the destination is configurable per organization, and the platform bucket is one option among five rather than the only one.

Files are served through signed URLs that expire after 15 minutes. A driver whose adapter package is not installed is never offered in the interface at all, because a backend you can select and cannot use is worse than one that is absent — you would find out at the first recording.

Storage drivers 8

  • platform
  • s3
  • s3_sts
  • r2
  • azure_blob
  • gcs
  • supabase
  • s3_compatible

The driver list is a real capability check at runtime, not a feature flag.[3]

§05 Isolation

One organization cannot see another.

Every tenant-owned model carries an organization scope applied globally, and the current organization is bound by the panel, by the API key middleware, and explicitly inside queued jobs — because a job that runs an hour later has no request to inherit it from.

On top of that, roles and permissions, an audit log, an identity-provider integration and OAuth for the connections that need it.

Tenancy, access control and identity.
isolation A global query scope on every tenant-owned model, plus policies, plus tenant resolution before binding.
roles 10 roles over a catalogue of 71 permissions, from owner down to operator, member and analyst.
audit log Who changed what, as records rather than as log lines, readable in the console.
SCIM Provisioning under its own bearer token, deliberately a different credential class from API keys: a token that can suspend people has to be revocable on its own.
OAuth Callback routes that carry a session, so state and PKCE can be validated, for the integrations that require it.
jobs Queued work binds its tenant explicitly instead of inheriting one, which is the only way a job an hour later is scoped correctly.
§06 Money

Cost accounting that reconciles.

Credits move through an append-only ledger. Balance is the sum of the ledger rather than a column somebody updates, so the two cannot disagree, and there is a reconcile command that proves it.

A call is admitted only if the wallet can plausibly cover 60 seconds of it, and the reservation is released by a reaper if the process holding it dies. Otherwise a crash mid-call silently strands the customer's credit.

Stripe paths run the real SDK against fakes in this build. No live charge has been made.[4]

Billing and cost accounting.
ledger Append-only credit entries. Nothing is edited, so an audit is a read.
reservations Held while a call is in progress, released on completion, and reaped on a schedule if the holder disappears.
admission A call that the wallet cannot plausibly cover does not start, which is kinder than cutting it off halfway.
usage events Per-call, per-stage, per-provider usage recorded as events, which is what per-call cost is computed from.
pricing Effective-dated prices and products, so a price change does not retroactively rewrite last month.
invoices Invoices with a download, plus auto-reload and a dunning schedule for accounts that lapse.

Notes

  1. 1 Route groups: routes/api/v1/, listed from the filesystem. OpenAPI document: docs/api/openapi.json, 221 paths.
  2. 2 Mount points and their middleware: bootstrap/app.php. Rate limiters: app/Providers/AppServiceProvider.php.
  3. 3 Storage drivers and the availability check: app/Enums/Storage/StorageDriver.php. Signed URL lifetime: config/callagent.php.
  4. 4 Admission threshold: config/callagent.php, key billing.admission_minimum_seconds. Permissions: app/Services/Rbac/Permissions.php. Roles: app/Services/Rbac/Roles.php.