Schedule an AI assistant call via REST — one POST, one webhook, one transcript.
Your CRM, ticketing tool, marketing automation or backend job sends one HTTP POST. The CodeB platform whitelists the target number, places a SIP call through your configured trunk, and attaches a Live Voice AI session that follows your system prompt. When the call ends, your webhook receiver gets a signed POST with the outcome and a transcript filename you can pull on demand. End to end — under twenty lines of code in any language.
What you will build
A four-step integration: mint a token, POST a call, receive a webhook on completion, fetch the transcript. The diagram below shows where each component lives. Your code only ever talks to one host (your CodeB tenant) over plain HTTPS — no SIP, no WebRTC, no media plumbing on your side.
Prerequisites
A CodeB tenant
Any host you control: https://phone.yourtenant.com. Self-host or run on a CodeB-operated tenant. Multi-tenant isolation is per-domain.
A Bearer token
Either an OIDC access token (from /oidc.ashx/token) or an ak_-prefixed API key minted in the tenant admin. Either works on every endpoint on this page.
A configured SIP trunk
Already there if you can place / receive normal calls. The platform picks the trunk via your outbound routing rules — no per-call trunk choice needed.
A speech engine API key
Bring your own Voice AI engine key. You bring your own key; the platform never stores it across calls.
Recipient consent
Outbound AI calling is regulated. Confirm the target opted in (TCPA / GDPR / ePrivacy / local rules). The platform is a transport — consent is on you.
A webhook receiver (optional)
A public HTTPS endpoint that accepts POST. If you skip it, the call still happens — you just poll GET /v1/calls/{id} instead.
Step 1 — Mint a Bearer token
Skip this step if you already have an ak_-prefixed API key from the tenant admin
— just use it directly as Authorization: Bearer ak_yourkeyhere. For interactive
flows, exchange a username/password for a short-lived OIDC access token:
# Exchange username + password for a short-lived OIDC access token. TOKEN=$(curl -s -X POST "https://phone.yourtenant.com/oidc.ashx/token" \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "grant_type=password&username=alice&password=YOUR_PASSWORD&scope=openid" \ | python3 -c "import sys,json;print(json.load(sys.stdin)['access_token'])") echo "$TOKEN"
import requests r = requests.post( "https://phone.yourtenant.com/oidc.ashx/token", data={ "grant_type": "password", "username": "alice", "password": "YOUR_PASSWORD", "scope": "openid", }, timeout=10, ) r.raise_for_status() token = r.json()["access_token"] print(token)
const body = new URLSearchParams({ grant_type: "password", username: "alice", password: "YOUR_PASSWORD", scope: "openid", }); const r = await fetch("https://phone.yourtenant.com/oidc.ashx/token", { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body, }); if (!r.ok) throw new Error(`token ${r.status}`); const { access_token } = await r.json(); console.log(access_token);
ak_-prefixed API key once in the tenant admin and
skip the token dance entirely. It is the same Bearer header on every call.
Step 2 — POST the call
Send a single JSON body to POST /api.ashx/v1/calls with the destination number,
the AI's instructions, and where to email the transcript. The response is immediate; the
actual dial happens out of band.
Request fields
| Field | Type | Required | Notes |
|---|---|---|---|
phone | string | yes | E.164, e.g. +4915157610183. Numbers are normalised before whitelist insertion. |
displayName | string | no | Caller-ID label / SIP From-display. Defaults to phone. |
email | string | yes | Recipient for transcript + outcome email when the call ends. |
systemPrompt | string | yes | Plain-text instructions to the AI, or a slug of a saved tenant prompt (e.g. reminder). |
apiKey | string | yes | Voice AI engine key. Used per call — not retained. |
model | string | no | Engine model id. Defaults to the tenant configuration. |
voice | string | no | e.g. Aoede, Charon. Default Aoede. |
language | string | no | BCP-47 tag, e.g. en-US, de-DE. Default en-US. |
maxSeconds | int | no | Hard cap, 10–3600. Default 300. |
retries | int | no | 0–10. How many times to redial on no-answer / busy. Default 0. |
retryDelayMinutes | int | no | 1–1440. Gap between retry attempts. Default 5. |
scheduleAtUtc | string | no | ISO-8601 UTC. If omitted, dial immediately. Use for future-time campaigns. |
Example — appointment reminder
curl -X POST https://phone.yourtenant.com/api.ashx/v1/calls \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "phone": "+4915157610183", "displayName": "Reminder", "email": "ops@yourcompany.example", "systemPrompt": "You are calling Alex to confirm the dentist appointment tomorrow at 14:00. Be brief and warm. If they answer YES, say goodbye. If NO, offer to reschedule for the same time next week.", "apiKey": "YOUR_AI_API_KEY", "voice": "Aoede", "language": "en-US", "maxSeconds": 180, "retries": 2, "retryDelayMinutes": 10 }'
import requests, os TOKEN = os.environ["CODEB_TOKEN"] payload = { "phone": "+4915157610183", "displayName": "Reminder", "email": "ops@yourcompany.example", "systemPrompt": ( "You are calling Alex to confirm the dentist appointment tomorrow " "at 14:00. Be brief and warm. If they answer YES, say goodbye. " "If NO, offer to reschedule for the same time next week." ), "apiKey": os.environ["AI_API_KEY"], "voice": "Aoede", "language": "en-US", "maxSeconds": 180, "retries": 2, "retryDelayMinutes": 10, } r = requests.post( "https://phone.yourtenant.com/api.ashx/v1/calls", headers={"Authorization": f"Bearer {TOKEN}"}, json=payload, timeout=15, ) r.raise_for_status() call_id = r.json()["callId"] print("queued", call_id)
const token = process.env.CODEB_TOKEN; const apiKey = process.env.AI_API_KEY; const r = await fetch("https://phone.yourtenant.com/api.ashx/v1/calls", { method: "POST", headers: { "Authorization": `Bearer ${token}`, "Content-Type": "application/json", }, body: JSON.stringify({ phone: "+4915157610183", displayName: "Reminder", email: "ops@yourcompany.example", systemPrompt: "You are calling Alex to confirm the dentist appointment tomorrow at 14:00. Be brief and warm. If YES, say goodbye. If NO, offer to reschedule for the same time next week.", apiKey, voice: "Aoede", language: "en-US", maxSeconds: 180, retries: 2, retryDelayMinutes: 10, }), }); if (!r.ok) throw new Error(`POST ${r.status}: ${await r.text()}`); const { callId } = await r.json(); console.log("queued", callId);
Response
{
"ok": true,
"callId": "oac-0123456789ab",
"tenant": "phone.yourtenant.com",
"whitelistAdded": true,
"whitelistError": null,
"bridgeReply": "{...}"
}
Persist the callId on your record. You will see it again on the webhook,
in GET /v1/calls/{id}, and in the transcript filename
(outbound-ai-YYYYMMDD-HHMMSS-<callId>.txt).
Step 3 — Receive the webhook
Within seconds of the call ending, your subscribed webhook URL gets two HMAC-signed POSTs:
outbound-ai.finished with the outcome, and (if recording or transcription was
enabled) transcript.saved with the path. Verify the signature first,
always.
Verifying X-CodeB-Signature
import hmac, hashlib, os from flask import Flask, request, abort WEBHOOK_SECRET = os.environ["CODEB_WEBHOOK_SECRET"].encode() app = Flask(__name__) def verify(raw_body: bytes, sig_header: str) -> bool: expected = hmac.new(WEBHOOK_SECRET, raw_body, hashlib.sha256).hexdigest() return hmac.compare_digest(expected, (sig_header or "").strip()) @app.post("/codeb/webhook") def hook(): raw = request.get_data() # raw bytes, NOT json() sig = request.headers.get("X-CodeB-Signature", "") if not verify(raw, sig): abort(401) evt = request.get_json() # now safe to parse if evt["type"] == "outbound-ai.finished": update_crm(evt["data"]["callId"], evt["data"]["status"]) return ("", 204)
import express from "express"; import crypto from "node:crypto"; const SECRET = process.env.CODEB_WEBHOOK_SECRET; const app = express(); // raw body required for HMAC; capture before json() parses it app.use("/codeb/webhook", express.raw({ type: "application/json" })); app.post("/codeb/webhook", (req, res) => { const sig = (req.headers["x-codeb-signature"] || "").toString().trim(); const expected = crypto.createHmac("sha256", SECRET).update(req.body).digest("hex"); if (!crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) { return res.status(401).end(); } const evt = JSON.parse(req.body.toString()); if (evt.type === "outbound-ai.finished") { updateCrm(evt.data.callId, evt.data.status); } res.status(204).end(); });
What the webhook body looks like
{
"type": "outbound-ai.finished",
"tenant": "phone.yourtenant.com",
"timestamp": "2026-06-23T12:02:31.000Z",
"data": {
"callId": "oac-0123456789ab",
"phone": "+4915157610183",
"displayName": "Reminder",
"status": "ended-success",
"endedReason": "finished",
"durationSec": 145,
"dispatchedAtUtc": "2026-06-23T12:00:00.000Z",
"answeredAtUtc": "2026-06-23T12:00:05.500Z",
"endedAtUtc": "2026-06-23T12:02:30.000Z",
"transcriptPath": "outbound-ai-20260623-120000-oac-0123456789ab.txt",
"attempt": 1,
"retriesLeft": 1
}
}
GET /api.ashx/v1/calls/{id} until status is one of
ended-success, ended-failed-final, or cancelled.
Cheap, no infrastructure, no signature dance.
Step 4 — Fetch the transcript
For analytics, follow-up generation, or compliance archiving, pull the transcript when the
webhook arrives. The platform produces two artefacts per call: a plain-text transcript
(.txt) and a structured JSON sidecar (.json) with per-turn
timestamps, role labels, and tool-call records.
curl -H "Authorization: Bearer $TOKEN" \ "https://phone.yourtenant.com/api.ashx/v1/transcripts/outbound-ai-20260623-120000-oac-0123456789ab.txt"
Sample transcript (plain text, one line per turn):
[12:00:06] AI: Hello, this is your reminder service. Am I speaking with Alex?
[12:00:09] USER: Yes, this is Alex.
[12:00:11] AI: Just a quick reminder about your dentist appointment tomorrow at 14:00.
Will you still be able to make it?
[12:00:17] USER: Yes, I'll be there.
[12:00:19] AI: Wonderful. Have a great day. Goodbye.
Want recordings too? Enable per-vnum or per-trunk audio recording in the admin and a stereo
WAV lands at App_Data/<tenant>/recordings/YYYY/MM/DD/ alongside a JSON
sidecar with the same metadata.
See REST API reference for
the full transcripts + recordings endpoints.
Common patterns
Appointment reminders
Your scheduling tool fires the POST 24h before each booking. AI confirms or reschedules. Outcome lands back in your booking record via webhook.
Lead qualification
Marketing automation fires the POST seconds after form submission. AI greets, qualifies on a script, and (with permission) transfers to a sales rep mid-call.
Payment / dunning reminders
Billing job batches up overdue accounts each morning, POSTs one call per debtor. Tone-controlled by your system prompt. Transcript archived for the audit trail.
Service follow-up surveys
Ticketing tool POSTs an hour after ticket close. AI asks three structured questions. JSON transcript feeds your NPS dashboard.
Internal staff notifications
Oncall / outage tooling POSTs to your engineer's number. AI reads the alert, confirms acknowledgement, hangs up. Cheap, language-agnostic, no app to install.
Multi-language campaigns
Set language per call (en-US, de-DE, fr-FR, es-ES, …). Same endpoint, same flow, voice and prompt swap automatically.
Hospitality — sensors trigger calls
Short-let noise meter or smoke detector trips → sensor platform POSTs to your webhook → AI voice agent dials the guest in seconds, names the apartment, cites the threshold, asks them to act. Worked example on hospitality.html.
Cancel, list, inspect
Three more endpoints round out the lifecycle:
| Verb + path | What it does |
|---|---|
GET /api.ashx/v1/calls | List active + recent calls. Filter by status: scheduled,in-flight,ended-success etc. Paginate with limit + offset. |
GET /api.ashx/v1/calls/{id} | One call — status, outcome, transcript path, attempt counter, retry budget. |
POST /api.ashx/v1/calls/{id}/hangup | Cancel a scheduled call or terminate an in-flight one. Idempotent. Use when the contact replied via another channel before the AI dialed them. |
Full request/response shape with every field is on the REST API reference.
Security & compliance
- Bearer auth on every call. OIDC access tokens or
ak_-prefixed API keys. No anonymous endpoints, no IP allowlists required for the REST surface. - HMAC-signed webhooks.
X-CodeB-Signatureis HMAC-SHA256 of the raw body with your subscription secret. Verify before parsing. Rotate the secret at any time from the admin. - Per-tenant isolation. Every API key, token, transcript, recording and CDR is scoped to the tenant host that issued it. Cross-tenant reads are blocked at the dispatcher.
- EU-hosted, self-hostable. Run on your own infrastructure or on an EU-region tenant. No US transfer for call signalling, media, transcripts or recordings.
- Recipient consent. Outbound AI calling is regulated in most jurisdictions (TCPA in the US, GDPR / ePrivacy in the EU, plus local rules). The platform places the call you ask it to; consent capture is your responsibility.
- Auditable. Every call leaves a CDR (caller / callee / trunk / outcome / duration) and an optional transcript + recording for compliance retention.
Ready to ship outbound AI in an afternoon?
Spin up a free CodeB tenant, drop in your speech-engine API key, fire the first POST. Full reference + every endpoint is on the API hub.