A runnable multi-tenant backend: provision tenants under <orgId>.sip.telequick.dev, 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, configure their own agents, and you bill them for usage — all on
top of TeleQuick Voice. This recipe is the backend that makes a tenant real:
it provisions the tenant’s endpoints and data isolation, mints a scoped
key for that tenant, publishes their agent config, and then drives
calls with the tenant’s own key.Everything keys off one stable identifier — the org id. The per-tenant SIP
realm (<orgId>.sip.telequick.dev), the analytics row policy, sealed
credentials, and the usage account are all keyed on it, so a customer can rename
their workspace without moving any infrastructure. For the conceptual model
behind this code, read
Building voice agent platforms first.
This backend uses two different credentials, and mixing them up 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 or hand it to a customer.
A per-tenant scoped key drives one tenant’s calls. It resolves to exactly
one org id and is refused (403) outside that tenant’s scope. This is the key
you give a customer (or embed in a short-lived browser token).
The control plane lives at portal.telequick.dev; per-tenant call and audio
traffic goes through the SDK, which talks to engine.telequick.dev and the media
relay. Provisioning steps are idempotent — re-running the bootstrap backfills
whatever was missed, so onboarding is safe to retry.
Provisioning isn’t in the SDK — it is a privileged control-plane surface 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.
TypeScript
Python
// control-plane.ts — privileged; runs server-side only.import { randomBytes } from "node:crypto";export class ControlPlane { constructor( private readonly host = "https://portal.telequick.dev", private readonly adminKey = process.env.TELEQUICK_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 }, ); }}
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 register the tenant’s trunk, map
their DIDs, seal any bring-your-own provider keys, publish their agent, and mint
the scoped key 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 (optional — they can inherit yours).
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.
TypeScript
Python
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 };}
hydrateAgent publishes an agent your customer already authored (prompt,
providers, voice, turn-detection, tools). The agent-authoring model — the
pipeline config each tenant edits — lives in
runtime configuration; provider-key
resolution order (per-agent → per-tenant → platform fallback) is covered in
BYO ASR/LLM/TTS.
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 the
control plane enforces on every request — one tenant’s key can never address
another tenant’s calls, agents, or audio.
TypeScript
Python
import { Voice } from "@telequick/sdk/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.telequick.dev", 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)); }}
import timefrom telequick.voice import Voicedef voice_for(org_id: str, tenant_key: str) -> Voice: return Voice(base_url="https://engine.telequick.dev", 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: because you mapped the DID to the
agent at onboarding, an INVITE to <orgId>.sip.telequick.dev is routed by its
leftmost DNS label to the tenant, resolved to the agent, and answered by the
engine. Your backend only gets involved for outbound, transfers, or supervision.
Wrapping the two functions above in a couple of routes gives you a complete
onboarding-and-dial backend. Guard the provisioning route with your own
staff auth — it holds the platform credential — and scope the dial route to the
authenticated tenant.
TypeScript
Python
import { serve } from "@hono/node-server";import { Hono } from "hono";import { onboardTenant, callForTenant } from "./platform";import { lookupTenantKey } from "./store"; // your own tenant→key storeconst 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 });
from fastapi import FastAPI, HTTPExceptionfrom platform_backend import onboard_tenant, call_for_tenantfrom store import save_tenant_key, get_tenant_key # your own storeapp = FastAPI()@app.post("/tenants") # privileged: staff / signup onlydef 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 firstdef 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)
Mint the narrowest scope set each consumer needs. Scopes are
product:action strings the control plane checks per endpoint; a request
missing the required scope is refused (403).
All voice actions. Use sparingly; prefer the two explicit scopes above.
For a customer-facing dashboard, don’t embed a long-lived tenant key in the
browser. Mint a short-lived token per browser session with only the scopes
that session needs — the supervisor-attach and browser-media legs already work
this way. Revoke with revokeTenantKey when a customer offboards or rotates.
You don’t 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, and it accounts media bytes
per tenant per modality into a per-minute rollup. Both are keyed on the same org
id, so a tenant’s spend and bandwidth are readable the moment their first call
clears. See dashboards and
call traces.