# Voice Agent Platform Backend

> A runnable multi-tenant backend: provision tenants under `<orgId>`.sip.firstbyte.fast, mint scoped keys, publish agents, and drive calls per tenant.

You are the platform. Your customers sign up, bring their own numbers and
provider keys, and configure their own agents. You bill them for usage. All of
this runs on top of FirstByte Voice. This recipe is the backend that makes a
tenant real. It does four things:

1. It **provisions** the tenant's endpoints and data isolation.
2. It **mints a scoped key** for that tenant.
3. It **publishes** the tenant's agent config.
4. It **drives calls** with the tenant's own key.

Everything keys off one stable identifier: the **org id**. The per-tenant SIP
realm (`<orgId>.sip.firstbyte.fast`), the analytics row policy, sealed
credentials, and the usage account are all keyed on it. A customer can
therefore rename their workspace without a move of any infrastructure. For the
conceptual model behind this code, read
[Building voice agent platforms](/modalities/voice/build/agent-platforms) first.

## Two planes, two credentials

> **WARNING:**
> This backend uses **two different credentials**. A mix-up between them is the
> one mistake that breaks tenancy:
> 
> - A **platform control-plane credential** provisions tenants and mints keys. It
>   is privileged: it can create resources for any org. Keep it in your backend's
>   secret store. **Never** ship it to a browser. **Never** hand it to a
>   customer.
> - A **per-tenant scoped key** drives one tenant's calls. It resolves to exactly
>   one org id. The control plane refuses it (`403`) outside that tenant's
>   scope. This is the key that you give a customer (or embed in a short-lived
>   browser token).

The control plane lives at `portal.firstbyte.fast`. Per-tenant call and audio
traffic goes through the SDK. The SDK talks to `engine.firstbyte.fast` and the media
relay. Provisioning steps are **idempotent**. A re-run of the bootstrap
backfills anything that was missed. Onboarding is therefore safe to retry.

## 1. A thin control-plane client

Provisioning is not in the SDK. It is a privileged control-plane surface that
you call from your backend. This small wrapper posts to the control-plane API
with your platform credential. Every helper below is a real provisioning route.

  
```ts
// control-plane.ts — privileged; runs server-side only.
import { randomBytes } from "node:crypto";

export class ControlPlane {
  constructor(
    private readonly host = "https://portal.firstbyte.fast",
    private readonly adminKey = process.env.CLUTCHCALL_API_KEY!, // platform-admin credential
  ) {}

  private async rpc<T>(proc: string, input: unknown): Promise<T> {
    const res = await fetch(`${this.host}/api/trpc/${proc}`, {
      method: "POST",
      headers: {
        "content-type": "application/json",
        "authorization": `Bearer ${this.adminKey}`,
      },
      body: JSON.stringify(input),
    });
    if (!res.ok) throw new Error(`${proc} ${res.status}: ${(await res.text()).slice(0, 200)}`);
    const body = await res.json() as { result?: { data?: T }; error?: { message?: string } };
    if (body.error) throw new Error(body.error.message ?? `${proc} failed`);
    return body.result!.data as T;
  }

  /** Create the org's endpoints + data isolation. Idempotent. */
  bootstrap(orgId: string, adminToken: string) {
    return this.rpc<{ ok: true; provisioned: unknown }>("admin.bootstrapOrg", {
      orgId, adminToken,
    });
  }

  /**
   * Register a scoped key for one tenant. You generate the token value; the
   * control plane binds it to `orgId` with the given scopes. Returns nothing
   * secret — you already hold the token, so hand it to the tenant now.
   */
  async mintTenantKey(orgId: string, scopes: string[]): Promise<string> {
    const token = "sk_" + randomBytes(24).toString("hex");
    await this.rpc<{ ok: true }>("admin.publishServiceAccount", {
      orgId, token, scopes, isGlobalAdmin: false,
    });
    return token;
  }

  revokeTenantKey(orgId: string, token: string) {
    return this.rpc<{ ok: true }>("admin.revokeServiceAccount", { orgId, token });
  }

  /** Point the tenant's SIP trunk at their realm. */
  addTrunk(orgId: string, trunk: Record<string, unknown>) {
    return this.rpc<{ ok: true }>("admin.addTrunk", { orgId, trunk });
  }

  /** Map a DID to a trunk + agent for inbound routing. */
  upsertNumber(orgId: string, number: Record<string, unknown>) {
    return this.rpc<{ ok: true }>("admin.upsertPhoneNumber", { orgId, number });
  }

  /** Seal a tenant's provider key (BYO OpenAI / Deepgram / ElevenLabs / …). */
  setProviderCredentials(orgId: string, provider: string, payload: Record<string, string>) {
    return this.rpc<{ ok: true }>("admin.setProviderCredentials", { orgId, provider, payload });
  }

  /** Publish a saved agent's pipeline config to the engine. */
  hydrateAgent(orgId: string, agentId: string) {
    return this.rpc<{ ok: true; ready: boolean; missing_providers: string[] }>(
      "admin.hydrateAgent", { orgId, agentId },
    );
  }
}
```
  
  
```python
# control_plane.py — privileged; runs server-side only.
import os, secrets, httpx

class ControlPlane:
    def __init__(self, host="https://portal.firstbyte.fast", admin_key=None):
        self.host = host
        self.admin_key = admin_key or os.environ["CLUTCHCALL_API_KEY"]  # platform-admin credential
        self._http = httpx.Client(timeout=15)

    def _rpc(self, proc: str, input: dict):
        res = self._http.post(
            f"{self.host}/api/trpc/{proc}",
            headers={"authorization": f"Bearer {self.admin_key}"},
            json=input,
        )
        res.raise_for_status()
        body = res.json()
        if body.get("error"):
            raise RuntimeError(body["error"].get("message", f"{proc} failed"))
        return body["result"]["data"]

    def bootstrap(self, org_id: str, admin_token: str):
        return self._rpc("admin.bootstrapOrg", {"orgId": org_id, "adminToken": admin_token})

    def mint_tenant_key(self, org_id: str, scopes: list[str]) -> str:
        token = "sk_" + secrets.token_hex(24)
        self._rpc("admin.publishServiceAccount", {
            "orgId": org_id, "token": token, "scopes": scopes, "isGlobalAdmin": False,
        })
        return token

    def revoke_tenant_key(self, org_id: str, token: str):
        return self._rpc("admin.revokeServiceAccount", {"orgId": org_id, "token": token})

    def add_trunk(self, org_id: str, trunk: dict):
        return self._rpc("admin.addTrunk", {"orgId": org_id, "trunk": trunk})

    def upsert_number(self, org_id: str, number: dict):
        return self._rpc("admin.upsertPhoneNumber", {"orgId": org_id, "number": number})

    def set_provider_credentials(self, org_id: str, provider: str, payload: dict):
        return self._rpc("admin.setProviderCredentials",
                         {"orgId": org_id, "provider": provider, "payload": payload})

    def hydrate_agent(self, org_id: str, agent_id: str):
        return self._rpc("admin.hydrateAgent", {"orgId": org_id, "agentId": agent_id})
```
  

## 2. Onboard a tenant

One call to `bootstrap` fans out the per-tenant resources: the DNS labels
(`<orgId>.sip` / `<orgId>.webrtc` / `<orgId>`) that resolve to the engine, the
isolated analytics role with a row policy pinned to `tenant_id`, the usage
account, and the tenant's entitlement. Then you do five things:

1. Register the tenant's trunk.
2. Map their DIDs.
3. Seal any bring-your-own provider keys.
4. Publish their agent.
5. Mint the scoped key that their backend will use.

  1. **Bootstrap the org**
Creates endpoints + data isolation, keyed on the org id.
  2. **Register trunk + numbers**
Point their SIP trunk at their realm. Map DIDs to it.
  3. **Seal provider keys**
Store BYO keys sealed under the org. This step is optional; tenants can inherit your keys.
  4. **Publish the agent**
Hydrate the saved agent config to the engine.
  5. **Mint a scoped key**
Hand the tenant a key bound to their org id.

  
```ts
import { ControlPlane } from "./control-plane";

const cp = new ControlPlane();

export async function onboardTenant(orgId: string, opts: {
  trunkId: string;
  sipHost: string;          // carrier SIP host
  did: string;              // E.164 the tenant answers on
  agentId: string;          // an agent the customer already saved
  openaiKey?: string;       // optional BYO provider key
}): Promise<{ orgId: string; tenantKey: string }> {
  // 1. Endpoints + isolation. Idempotent — safe to re-run on retry.
  const adminToken = "at_" + crypto.randomUUID().replace(/-/g, "");
  await cp.bootstrap(orgId, adminToken);

  // 2. Trunk + DID. The engine derives tenant_id from the INVITE's DNS label,
  //    so the trunk just needs to target this org's realm.
  await cp.addTrunk(orgId, {
    trunk_id: opts.trunkId,
    direction: "inbound",
    realm: "external",
    external_sip_ip: opts.sipHost,
    media_mode: "proxy",
    inbound_rule: "ai_bidirectional_stream",
  });
  await cp.upsertNumber(orgId, {
    e164: opts.did,
    trunk_id: opts.trunkId,
    agent_id: opts.agentId,
    provider: "manual",
  });

  // 3. BYO provider key (sealed under the org). Skip to inherit the platform's.
  if (opts.openaiKey) {
    await cp.setProviderCredentials(orgId, "openai", { api_key: opts.openaiKey });
  }

  // 4. Publish the agent's pipeline config to the engine.
  const pub = await cp.hydrateAgent(orgId, opts.agentId);
  if (!pub.ready) {
    console.warn(`agent ${opts.agentId} not ready — missing:`, pub.missing_providers);
  }

  // 5. Scoped key for the tenant's backend. Least privilege.
  const tenantKey = await cp.mintTenantKey(orgId, ["voice:read", "voice:write"]);
  return { orgId, tenantKey };
}
```
  
  
```python
import uuid
from control_plane import ControlPlane

cp = ControlPlane()

def onboard_tenant(org_id: str, *, trunk_id: str, sip_host: str, did: str,
                   agent_id: str, openai_key: str | None = None) -> dict:
    # 1. Endpoints + isolation. Idempotent — safe to re-run on retry.
    admin_token = "at_" + uuid.uuid4().hex
    cp.bootstrap(org_id, admin_token)

    # 2. Trunk + DID.
    cp.add_trunk(org_id, {
        "trunk_id": trunk_id, "direction": "inbound", "realm": "external",
        "external_sip_ip": sip_host, "media_mode": "proxy",
        "inbound_rule": "ai_bidirectional_stream",
    })
    cp.upsert_number(org_id, {
        "e164": did, "trunk_id": trunk_id, "agent_id": agent_id, "provider": "manual",
    })

    # 3. BYO provider key (optional).
    if openai_key:
        cp.set_provider_credentials(org_id, "openai", {"api_key": openai_key})

    # 4. Publish the agent config.
    pub = cp.hydrate_agent(org_id, agent_id)
    if not pub["ready"]:
        print(f"agent {agent_id} not ready — missing:", pub["missing_providers"])

    # 5. Scoped key for the tenant's backend.
    tenant_key = cp.mint_tenant_key(org_id, ["voice:read", "voice:write"])
    return {"org_id": org_id, "tenant_key": tenant_key}
```
  

> **NOTE:**
> `hydrateAgent` publishes an agent that your customer **already authored**
> (prompt, providers, voice, turn-detection, tools). The agent-authoring model is
> the pipeline config that each tenant edits. It lives in
> [runtime configuration](/modalities/voice/runtime/configuration).
> [BYO ASR/LLM/TTS](/modalities/voice/runtime/byo-asr-llm-tts) covers the
> provider-key resolution order (per-agent → per-tenant → platform fallback).

## 3. Drive calls with the tenant's key

Now switch planes. To place or manage a call **for** a tenant, instantiate the
SDK with **that tenant's** scoped key and org id. The org id is the boundary
that the control plane enforces on every request. One tenant's key can never
address another tenant's calls, agents, or audio.

  
```ts
import { Voice } from "clutchcall_connect/voice";

// Look up the tenant's key from your own store (you saved it at onboarding).
function voiceFor(orgId: string, tenantKey: string): Voice {
  return new Voice({
    baseUrl: "https://engine.firstbyte.fast",
    apiKey:  tenantKey,   // scoped to this org only
    orgId,
  });
}

// Place an outbound call on the tenant's trunk with their agent attached.
// The engine wires the audio bridge on answer — no bridge in your process.
export async function callForTenant(orgId: string, tenantKey: string, args: {
  to: string; from: string; trunkId: string; agent: string;
}) {
  const v = voiceFor(orgId, tenantKey);
  const call = await v.calls.originate({
    to: args.to,
    from: args.from,
    trunkId: args.trunkId,
    agent: args.agent,
    ringTimeoutSec: 25,
  });

  // Poll until it leaves the ringing states.
  for (;;) {
    const c = await v.calls.get({ sid: call.sid });
    if (c.status === "in_progress") return { sid: call.sid, status: c.status };
    if (c.status === "failed" || c.status === "no_answer") {
      return { sid: call.sid, status: c.status };
    }
    await new Promise((r) => setTimeout(r, 500));
  }
}
```
  
  
```python
import time
from clutchcall.voice import Voice

def voice_for(org_id: str, tenant_key: str) -> Voice:
    return Voice(base_url="https://engine.firstbyte.fast", api_key=tenant_key, org_id=org_id)

def call_for_tenant(org_id, tenant_key, *, to, from_, trunk_id, agent):
    v = voice_for(org_id, tenant_key)
    call = v.calls.originate(
        to=to, from_=from_, trunk_id=trunk_id, agent=agent, ring_timeout_sec=25,
    )
    while True:
        c = v.calls.get(sid=call.sid)
        if c.status == "in_progress":
            return {"sid": call.sid, "status": c.status}
        if c.status in ("failed", "no_answer"):
            return {"sid": call.sid, "status": c.status}
        time.sleep(0.5)
```
  

Inbound calls need no per-call code at all. You mapped the DID to the agent at
onboarding. The engine routes an INVITE to `<orgId>.sip.firstbyte.fast` by its
leftmost DNS label to the tenant. It resolves the call to the agent. It answers
the call. Your backend gets involved only for outbound calls, transfers, or
supervision.

## 4. Wire it to an HTTP surface

Wrap the two functions above in two routes. That gives you a complete
onboarding-and-dial backend. Guard the provisioning route with **your own**
staff auth, because it holds the platform credential. Scope the dial route to
the authenticated tenant.

  
```ts
import { serve } from "@hono/node-server";
import { Hono } from "hono";
import { onboardTenant, callForTenant } from "./platform";
import { lookupTenantKey } from "./store"; // your own tenant→key store

const app = new Hono();

// Privileged: your staff / signup flow only.
app.post("/tenants", async (c) => {
  const body = await c.req.json();
  const { orgId, tenantKey } = await onboardTenant(body.orgId, body);
  // Persist tenantKey in YOUR store; return an id, not the raw key, to the UI.
  await lookupTenantKey.save(orgId, tenantKey);
  return c.json({ orgId, provisioned: true });
});

// Per-tenant: authenticate the caller as a member of :orgId first (your auth).
app.post("/tenants/:orgId/calls", async (c) => {
  const orgId = c.req.param("orgId");
  const key = await lookupTenantKey.get(orgId);
  if (!key) return c.json({ error: "unknown tenant" }, 404);
  const result = await callForTenant(orgId, key, await c.req.json());
  return c.json(result);
});

serve({ fetch: app.fetch, port: 8787 });
```
  
  
```python
from fastapi import FastAPI, HTTPException
from platform_backend import onboard_tenant, call_for_tenant
from store import save_tenant_key, get_tenant_key  # your own store

app = FastAPI()

@app.post("/tenants")            # privileged: staff / signup only
def create_tenant(body: dict):
    result = onboard_tenant(body["orgId"], **body)
    save_tenant_key(result["org_id"], result["tenant_key"])
    return {"orgId": result["org_id"], "provisioned": True}

@app.post("/tenants/{org_id}/calls")   # authenticate the member first
def place_call(org_id: str, body: dict):
    key = get_tenant_key(org_id)
    if not key:
        raise HTTPException(404, "unknown tenant")
    return call_for_tenant(org_id, key, **body)
```
  

## Least-privilege scopes

Mint the **narrowest** scope set that each consumer needs. Scopes are
`product:action` strings that the control plane checks per endpoint. The
control plane refuses (`403`) a request that does not have the required scope.

- **`voice:read`** (`scope`) — Read call state and history: status polls, listing, reports. Give this scope   to a dashboard or a read-only integration.
- **`voice:write`** (`scope`) — Originate, transfer, and hang up calls. Attach agents. This is the scope   that a tenant's call-driving backend needs.
- **`voice:*`** (`scope`) — All voice actions. Use it rarely. Prefer the two explicit scopes above.

> **TIP:**
> For a customer-facing dashboard, do not embed a long-lived tenant key in the
> browser. Mint a **short-lived** token per browser session, with only the scopes
> that the session needs. The supervisor-attach and browser-media legs already
> work this way. Revoke with `revokeTenantKey` when a customer offboards or
> rotates keys.

## Metering is automatic

You do not write metering code. The engine attributes usage per tenant as
calls run. On each teardown, it emits a CDR (Q.850 cause + duration, keyed by
`call_sid`) rated against the tenant's usage account. It also accounts media
bytes per tenant per modality into a per-minute rollup. Both are keyed on the
same org id. You can therefore read a tenant's spend and bandwidth the moment
their first call clears. See
[dashboards](/modalities/voice/observability/dashboards) and
[call traces](/modalities/voice/observability/call-traces).

## Related

  - **[Building voice agent platforms](/modalities/voice/build/agent-platforms)** — The concepts behind this code: tenancy, isolation, and metering.
  - **[Runtime configuration](/modalities/voice/runtime/configuration)** — The per-agent pipeline config each tenant authors.
  - **[SIP trunking](/modalities/voice/transport-telephony/sip-trunking)** — Register a tenant's trunk against their realm.
  - **[Contact-center platform](/modalities/voice/build/contact-center-platforms)** — Add ACD, skills, and supervisor tooling on top of tenancy.
