Audelo API
A REST API to manage AI voice agents, place and control outbound calls, read transcripts and recordings, query analytics, store per-call context, and subscribe to webhooks β programmatically.
Authentication
Every request authenticates with an API key sent as a Bearer token in the Authorization header.
curl https://audelo.ai/api/v1/agents \ -H "Authorization: Bearer cgk_your_api_key" \ -H "Accept: application/json"
- Generate keys in the dashboard under API Keys (account admins only).
- The full key (
cgk_+ 48 characters) is shown once at creation β store it securely. Afterward only acgk_xxxxxxxxprefix is displayed. - API access requires a paid plan (Starter or higher). Free / Explorer accounts receive
403 plan_required. - Up to 20 active keys per account. Delete a key to revoke it, or set it inactive to disable.
Scopes
Each key is limited to the scopes you assign when creating it. A key created with no scopes has full access. A request missing the required scope returns 403 forbidden.
| Scope | Grants |
|---|---|
| agents:read | List and read agents |
| calls:read | List/read calls, transcripts, recordings; read context |
| calls:write | Initiate/end calls; write/delete context Pro |
| calls:listen | Mint listen-only tokens for live in-progress calls (LiveKit pipeline) Pro |
| calls:takeover | Silence the AI agent and speak to the caller directly on a live outbound call Pro |
| analytics:read | Read call and agent analytics |
| webhooks:manage | Create, update, delete, and test webhook endpoints |
| integrations:read | Read customer_lookup config (per-agent and business-wide) |
| integrations:write | Set the customer_lookup URL, toggle it, rotate signing secrets |
| sms:write | Send a transactional one-off SMS from a number you own |
| numbers:read | List, read, and search your phone numbers |
| numbers:write | Assign/unassign a number to an agent for inbound routing |
| numbers:buy | Purchase a brand-new number β spends wallet credit (requires numbers:read too) |
| campaigns:read | Campaign status, progress counts and per-target outcomes Pro |
| campaigns:write | Create, add targets to, start, pause, resume and cancel bulk outbound campaigns Pro |
Plan requirements
| Capability | Minimum plan |
|---|---|
| Any API access; read agents / calls / analytics; manage webhooks | Starter |
| Initiate / end calls; all context endpoints | Pro |
Base URL
https://audelo.ai/api/v1
All endpoints are relative to this base URL. All responses are application/json; timestamps are ISO-8601.
Official SDK
Building in Node or TypeScript? Use the official client instead of hand-rolling HTTP β it ships full type definitions, handles auth and errors, and includes webhook signature verification.
npm install @audelo/sdk
import { AudeloClient } from '@audelo/sdk';
const cg = new AudeloClient({ apiKey: process.env.AUDELO_KEY! });
const call = await cg.calls.initiate({
agent_id: 42,
phone_number: '+61400000000',
dial_pipeline: 'livekit',
});
Source, changelog and more examples: github.com/audelo-ai/audelo-sdk — MIT licensed, Node 18+. Every endpoint below is available on the client; no SDK is required to use this API.
Errors
Errors use a consistent envelope. request_id is unique per request β quote it when contacting support. errors appears only on validation failures.
{
"error": "validation_error",
"message": "The given data was invalid.",
"errors": { "phone_number": ["The phone number format is invalid."] },
"request_id": "f1c2a4e8-..."
}
| Code | error | Meaning |
|---|---|---|
| 401 | unauthorized | Missing, malformed, inactive, or expired key |
| 403 | forbidden / plan_required | Key lacks the scope, or plan too low |
| 404 | not_found | Resource missing or belongs to another account |
| 422 | validation_error | Invalid body or parameters |
| 429 | too_many_requests | Rate limit exceeded |
Rate limits
300 requests per minute, per API key. Exceeding the limit returns 429 too_many_requests.
Pagination
List endpoints (/agents, /calls) return a length-aware paginator. Page size is fixed at 50; navigate with ?page=N. Single-resource, transcript, recording, analytics, and context endpoints are not paginated.
{
"data": [ ... ],
"current_page": 1, "last_page": 3, "per_page": 50, "total": 142,
"next_page_url": "https://audelo.ai/api/v1/calls?page=2",
"prev_page_url": null
}
Agents
/api/v1/agents
agents:read
List agents
Returns your agents, paginated (50/page), ordered by name. Each row: id, name, status, voice, language, voice_engine, deepgram_voice, created_at, updated_at. voice_engine is "xai" or "deepgram" β which TTS engine actually renders this agent's calls (computed from its assigned phone number). voice is the legacy xAI voice id and is not consulted when voice_engine is "deepgram" β check deepgram_voice instead.
curl https://audelo.ai/api/v1/agents \ -H "Authorization: Bearer cgk_..."
/api/v1/agents/{id}
agents:read
Get agent
Returns one agent, or 404 if it isn't in your account.
{
"id": 12, "name": "Reception Bot", "status": "active",
"voice": "aria", "language": "en-AU",
"voice_engine": "deepgram", "deepgram_voice": "aura-2-hyperion-en",
"humour_level": 10, "empathy_level": 80,
"created_at": "2026-06-25T10:30:00.000000Z",
"updated_at": "2026-06-25T10:30:00.000000Z"
}
Calls
/api/v1/calls
calls:read
List calls
Paginated call records, newest first. Row fields: id, agent_id, from_e164, to_e164, direction, status, duration_sec, created_at. Filters: agent_id, status, from (date), to (date).
curl "https://audelo.ai/api/v1/calls?status=completed&agent_id=12" \ -H "Authorization: Bearer cgk_..."
/api/v1/calls/{id}
calls:read
Get call
One call, including the AI summary and recording link.
{
"id": 456, "agent": { "id": 12, "name": "Reception Bot" },
"from_e164": "+61412345678", "to_e164": "+61253009003",
"direction": "inbound", "status": "completed", "duration_sec": 92,
"summary": "Caller booked a consultation for Tuesday...",
"recording_url": "https://...", "created_at": "2026-06-25T10:30:00.000000Z"
}
/api/v1/calls/{id}/transcript
calls:read
Transcript
Ordered turns. timestamp is the offset in milliseconds from call start; speaker is caller or agent.
{
"call_id": 456,
"transcript": [
{ "timestamp": 0, "speaker": "agent", "text": "Hi, thanks for calling..." },
{ "timestamp": 3200, "speaker": "caller", "text": "I'd like to book an appointment" }
]
}
/api/v1/calls/{id}/recording
calls:read
Recording
Returns the recording link, or 404 if the call has no recording.
{ "call_id": 456, "recording_url": "https://...", "duration_sec": 92 }
/api/v1/calls/initiate
calls:write
Pro
Initiate an outbound call
Places an outbound call. The call runs through your DNC list, calling-hours, and max-concurrent caps. Returns 202 Accepted.
Two ways to get real-world data into the call: push it now via custom_data below (needs dial_pipeline: "livekit"), or let the agent pull it live mid-call via Customer lookup β works on any call, inbound or outbound.
| Field | Type | Required | Description |
|---|---|---|---|
| agent_id | int | yes | An agent in your account with a number assigned |
| phone_number | string | yes | E.164, e.g. +61412345678 |
| caller_name | string | no | The person's name β never a business name; use business_name for that |
| business_name | string | no | The business being called, for B2B outreach. custom_data.contact_name/custom_data.business_name are preferred over these top-level fields when both are present |
| callback_url | url | no | Optional per-call callback |
| dial_pipeline | string | no | "twilio" (default) or "livekit". Must be "livekit" for custom_data to actually reach the agent |
| custom_data | object | no | Per-call facts the agent speaks from during the call β amount owing, order id, appointment time, anything relevant. Only used when dial_pipeline is "livekit" |
| idempotency_key | string | no | β€64 chars; de-dupes retries for 24h |
curl -X POST https://audelo.ai/api/v1/calls/initiate \
-H "Authorization: Bearer cgk_..." \
-H "Content-Type: application/json" \
-d '{"agent_id":12,"phone_number":"+61412345678","caller_name":"Jane","dial_pipeline":"livekit","custom_data":{"amount_owing":"$412.50","invoice_number":"INV-2231"}}'
{ "call_id": "cgt_789", "agent_id": 12, "status": "queued",
"created_at": "2026-06-25T10:30:00+00:00" }
Note: call_id is a cgt_... placeholder for the queued call β it hasn't been dialed yet. Use it with /end, or poll GET /calls for the materialized record once it connects.
Note: the agent treats custom_data as data to relay, not instructions to follow β it will ignore anything in there that reads like a command, and won't invent values that aren't present.
Note: once the call actually connects and its webhooks start firing, they carry the internal numeric call_id, not this cgt_... token. Match a webhook back to this call via its data.outbound_target_id field (equal to this cgt_... value) instead of polling GET /calls/{id} in a race with delivery.
/api/v1/calls/{id}/end
calls:write
Pro
End a call
Hangs up an in-progress call. id may be a numeric call id or a cgt_... placeholder. Idempotent for already-ended calls. The final status is set by the carrier callback β poll GET /calls/{id} to confirm.
{ "call_id": "cgt_789", "status": "in-progress",
"hangup_requested_at": "2026-06-25T10:31:00+00:00" }
/api/v1/calls/{id}/listen
calls:listen
Pro
Listen to a live call
Mints a short-lived, room-scoped, listen-only LiveKit token for an in-progress call, so your own UI can play the live audio in-page (connect with livekit-client, subscribe to the audio tracks). The token cannot publish audio and expires after expires_in seconds β request a fresh one on the same endpoint when it does.
{ "call_id": 1850, "url": "wss://β¦livekit.cloud", "token": "eyJβ¦",
"room": "outbound-β¦", "identity": "api-7-1850", "expires_in": 600 }
Errors: 409 call_not_active (call already ended) Β· 409 unsupported_pipeline (classic-pipeline call β live listening is available for LiveKit-pipeline calls only) Β· 404 (not found / not yours).
Note: anyone your app hands this token to can hear the live call β your application's own authentication is the real gate on who gets to press Listen. Treat the token like a secret and never log it.
/api/v1/calls/{id}/takeover
calls:takeover
Pro
Take over a live call
Silences the AI agent on an in-progress outbound call β it stops speaking and stops responding to the caller for the remainder of the call β and mints a short-lived, room-scoped LiveKit token that can publish microphone audio, so a human supervisor speaks to the caller directly. Optional body {"handover_text": "β¦"} (β€200 chars): one sentence the agent speaks verbatim before going silent, e.g. a handover line β it also covers the connection gap while your microphone comes up.
{ "call_id": 1850, "url": "wss://β¦livekit.cloud", "token": "eyJβ¦",
"room": "outbound-β¦", "identity": "api-takeover-7-1850", "expires_in": 600 }
The token also subscribes, so you keep hearing the call on this connection β disconnect any listen connection once this one is up, or every track plays twice. If the supervisor disconnects, the agent stays silent and the call continues until it is hung up or ended via End call; re-POST for a fresh token to rejoin (the handover line is never replayed). There is no agent-resume in this build.
Errors: 409 call_not_active (call already ended) Β· 409 unsupported_pipeline (classic-pipeline call) Β· 409 takeover_unavailable (inbound call β take-over is outbound-only in this build) Β· 502 takeover_unavailable (could not start β the agent was not silenced) Β· 404 (not found / not yours).
Note: anyone holding this token can speak to your caller as your business. Gate the button behind your own admin authentication, treat the token like a secret, and use headphones β echo cancellation is on by default in browsers, but speakers + open mic risk feeding the caller's own voice back to them.
Context Store
A per-account key/value store for data your integration uses across calls. Keys match [A-Za-z0-9._:-]+ (β€191 chars); values are JSON. Pro required for all context endpoints.
Values stored here are not currently read by the live agent during a call. This is bookkeeping storage for your own integration β use it to stash state between webhook events, not as a way to get data spoken on a call. For that, use custom_data on Initiate call or Customer lookup below.
/api/v1/context/{key}
calls:write
Pro
Set context
Body: value (any JSON, required), ttl_seconds (optional). TTL defaults to 86400 (24h), max 2592000 (30 days). Returns 201.
{ "key": "customer:42", "expires_at": "2026-06-26T10:30:00+00:00", "ttl_seconds": 86400 }
/api/v1/context/{key}
calls:read
Pro
Get context
Returns key, value, expires_at. Expired or missing keys return 404.
/api/v1/context/{key}
calls:write
Pro
Delete context
Idempotent β returns 204 No Content whether or not the key existed.
Customer lookup (pull)
The other way to get real-world data into a call β instead of pushing it up front, let the agent pull it live, mid-call, from your own API. Works on any call direction, inbound or outbound β unlike custom_data, which only applies to calls your integration places.
Configure it from the dashboard (Agent β AI Tools β Customer lookup) or with the Integrations endpoints below β turn it on, give it a URL on your own server, and Audelo generates an HMAC secret. From then on, the agent can call the customer_lookup tool whenever it needs to identify who it's talking to.
your-endpoint-here
What Audelo sends you, mid-call
Audelo POSTs to your configured URL with an 8-second timeout, no redirects followed, and signs the body the same way as webhook deliveries:
POST https://yourapp.com/audelo/customer-lookup
User-Agent: Audelo-CustomerLookup/1.0
X-Audelo-Signature: sha256=<hmac>
Content-Type: application/json
{ "call_id": 8842, "tenant_id": 12, "from_e164": "+61412345678",
"to_e164": "+61478889900", "query": "invoice INV-2231" }
query is whatever the caller gave the agent to search on β a name, phone number, order or account reference. X-Audelo-Signature is HMAC-SHA256 over the raw JSON body using your agent's lookup secret β verify it exactly like a webhook delivery.
Respond with a JSON object of whatever facts the agent should know β it's relayed to the model the same way custom_data is, treated as data to speak from, never as instructions. Return an empty {} (or any non-2xx) if nothing matches; the agent falls back to offering to take a message.
Integrations
Configure customer_lookup through the API instead of the dashboard β the endpoints above describe what your server receives; these set it up. Resolution is agent-first: an agent's own URL always wins over the business default. Whichever level's URL wins, that level's secret signs the request. Neither GET below ever returns a secret value β secrets come back exactly once, from the two POST .../secret endpoints.
/api/v1/agents/{id}/integrations
integrations:read
Get agent config
resolved_url / resolved_source ("agent", "tenant", or null) reflect what will actually be called on the next customer_lookup invocation β accounting for the tenant-level fallback, not just this agent's own raw columns.
{
"agent_id": 12, "customer_lookup_enabled": true,
"customer_lookup_url": null, "has_secret": false,
"resolved_url": "https://api.yourcompany.com/lookup", "resolved_source": "tenant"
}
/api/v1/agents/{id}/integrations
integrations:write
Set agent config
Partial update β body: { customer_lookup_url?, customer_lookup_enabled? }. Omit a field to leave it unchanged; setting the URL alone does not touch enabled. The URL must resolve to a public address β loopback, private-network, link-local, and cloud-metadata hosts are rejected with 422, re-checked on every call, not just here.
curl -X PUT https://audelo.ai/api/v1/agents/12/integrations \
-H "Authorization: Bearer cgk_..." -H "Content-Type: application/json" \
-d '{"customer_lookup_url": "https://api.yourcompany.com/sales-lookup"}'
/api/v1/agents/{id}/integrations/secret
integrations:write
Rotate agent secret
Rotates this agent's own signing secret. Returns { agent_id, secret, message } β secret is shown exactly once; store it immediately.
/api/v1/integrations
integrations:read
Get business default
The business-wide default. { tenant_id, customer_lookup_url, has_secret } β any agent with customer_lookup_enabled and no URL of its own uses this URL/secret.
/api/v1/integrations
integrations:write
Set business default
Body: { customer_lookup_url } (required key; null clears it). Configure once and every agent with customer_lookup_enabled inherits this URL unless it has its own β the recommended setup for a single lookup endpoint shared across many agents.
curl -X PUT https://audelo.ai/api/v1/integrations \
-H "Authorization: Bearer cgk_..." -H "Content-Type: application/json" \
-d '{"customer_lookup_url": "https://api.yourcompany.com/lookup"}'
/api/v1/integrations/secret
integrations:write
Rotate business secret
Rotates the tenant-wide secret. Returns { tenant_id, secret, message, affected_agents } β affected_agents ([{id, name}]) lists every agent currently inheriting this tenant-level URL (enabled, no URL of its own) β exactly the agents whose requests you need to re-verify against the new secret. An agent with its own URL/secret is unaffected and not listed.
Analytics
/api/v1/analytics/calls
analytics:read
Call analytics
Aggregate call stats for a window (from, to dates; default last 30 days).
{
"period": { "from": "2026-05-26", "to": "2026-06-25" },
"calls": { "total_calls": 412, "total_duration_sec": 38400,
"avg_duration_sec": 93, "completed": 388 }
}
/api/v1/analytics/agents
analytics:read
Agent analytics
Per-agent breakdown for the window (from, to; default last 30 days).
{
"period": { "from": "2026-05-26", "to": "2026-06-25" },
"agents": [ { "id": 12, "name": "Reception Bot",
"total_calls": 250, "avg_duration_sec": 88 } ]
}
Phone Numbers
Full white-label provisioning: search available numbers, buy one, and assign it to an agent for inbound routing β or reassign/unassign a number you already own β all via the API. Buying is real wallet spend and needs numbers:buy in addition to numbers:read. None of the endpoints below require a specific plan.
/api/v1/numbers/countries
numbers:read
Purchasable countries
Countries currently purchasable (Phase 1 markets). Cheap, no auth-sensitive data.
/api/v1/numbers/available
numbers:read
Search available numbers
Search Twilio's inventory for numbers to buy. Query params: country_code (2-letter, default AU), type (local/mobile/toll_free), contains, locality, limit (max 50).
GET /api/v1/numbers/available?country_code=AU&type=local&locality=Sydney
If no inventory exists for that country/type, returns 200 with { numbers: [], warning } rather than an error.
/api/v1/numbers
numbers:read
numbers:buy
Buy a brand-new number
Body {"e164": "+61491570156", "country_code": "AU", "number_type": "local", "agent_id": 42} β e164 is the exact number from an available-number search; agent_id is optional and assigns the number immediately on purchase. Same wallet-credit gate as the dashboard (must have enough credit to cover at least one month's wholesale cost before Twilio is charged) and the same regulatory-bundle/address handling β never reimplemented here, only wrapped.
{ "id": 17, "e164": "+61491570156", "country_code": "AU",
"capabilities": { "voice": true, "sms": true },
"agent_id": 42, "agent_name": "Rebecca",
"created_at": "2026-05-07T18:42:11Z" }
Errors: 402 insufficient_credit (wallet balance can't cover it β required_cents/available_cents included) Β· 422 agent_tenant_mismatch Β· 422 purchase_failed (a required regulatory bundle isn't set up β the message explains) Β· 502 carrier_error (transient β retry).
/api/v1/numbers
numbers:read
List phone numbers
Paginated (50/page), this business's numbers only. Never includes the internal Twilio SID β that's an implementation detail, not something you need.
{ "data": [ { "id": 17, "e164": "+61491570156", "country_code": "AU",
"capabilities": { "voice": true, "sms": true },
"agent_id": 42, "agent_name": "Rebecca",
"created_at": "2026-05-07T18:42:11Z" } ] }
/api/v1/numbers/{id}
numbers:read
Get a single number
Same shape as one row of the list above. 404 if the id isn't in your account.
/api/v1/numbers/{id}/assign
numbers:write
Assign to an agent
Body {"agent_id": 42}. Inbound calls to this number now route to that agent. Re-assigning to the same agent it's already on is a harmless no-op.
{ "id": 17, "e164": "+61491570156", "country_code": "AU",
"capabilities": { "voice": true, "sms": true },
"agent_id": 42, "agent_name": "Rebecca",
"created_at": "2026-05-07T18:42:11Z" }
Errors: 404 (number not yours) Β· 422 agent_tenant_mismatch (agent_id isn't an agent in your account) Β· 409 number_already_assigned (already on a different agent β unassign it first) Β· 502 twilio_number_missing (the carrier record is gone β remove the number and add a new one) Β· 502 carrier_error (transient β retry).
/api/v1/numbers/{id}/unassign
numbers:write
Unassign from its agent
Clears inbound routing for this number β it stops routing to any agent until reassigned. No body required. A no-op if it wasn't assigned to begin with.
Webhooks
/api/v1/webhooks
webhooks:manage
Manage endpoints
Full CRUD for webhook subscriptions (max 20 per account):
| GET | /api/v1/webhooks | List endpoints |
| POST | /api/v1/webhooks | Create β returns the signing secret once |
| PUT | /api/v1/webhooks/{id} | Update url / events / active |
| DELETE | /api/v1/webhooks/{id} | Delete endpoint |
| POST | /api/v1/webhooks/{id}/test | Fire a synchronous test delivery |
Create β body: url (required, public HTTPS), description (optional), events (required array). Returns 201 including the signing secret (whsec_...) β shown once.
{ "id": 5, "url": "https://example.com/hooks/audelo",
"events": ["call.ended"], "secret": "whsec_...",
"is_active": true, "created_at": "..." }
Delivery & verification
Payload, signature, retries
When a subscribed event fires, Audelo sends an HTTP POST to your URL. Available events:
| Event | Fires when | data |
|---|---|---|
| call.started | A call connects | call_id, agent_id, from, to, direction, started_at, outbound_target_id |
| call.ended | A call completes | + status, duration_sec, ended_at, outbound_target_id |
| call.transcript | A transcript segment is finalized | call_id, speaker, text, outbound_target_id |
| booking.created | An agent books an appointment | booking_id, call_id, agent_id, customer_name, customer_phone, customer_email, starts_at, ends_at, status, calendar_event_id |
| lead.captured | An agent captures a demo/callback request | call_id, agent_id, caller_name, caller_phone, caller_email, preferred_callback_time, notes, source |
| call.summary.ready | The post-call summary finishes generating | call_id, agent_id, summary, sentiment, key_insights, action_items, duration_sec, started_at, ended_at |
| sms.received | An inbound SMS or MMS arrives | sms_log_id, from, to, body, type, agent_id, received_at |
Payload
{
"id": "wh_a1b2c3d4e5f6a7b8",
"event": "call.ended",
"timestamp": "2026-06-25T10:31:32.000000Z",
"data": { "call_id": 456, "agent_id": 12, "status": "completed",
"duration_sec": 92, "ended_at": "2026-06-25T10:31:32.000000Z",
"outbound_target_id": "cgt_789" }
}
outbound_target_id is the same cgt_... token Initiate call returned when you queued this call β match it against your own records instead of polling GET /calls/{id} to correlate a webhook back to the call you placed. It's null for inbound calls and any call not placed via /calls/initiate.
Headers
| X-Audelo-Signature | sha256=<hex> β HMAC-SHA256 of the raw body |
| X-Audelo-Event | The event name |
| X-Audelo-Delivery | Unique delivery id β dedupe on this |
Verify the signature β recompute HMAC-SHA256 over the raw request body with your whsec_ secret and compare (constant-time):
import hmac, hashlib
def verify(raw_body, header, secret):
expected = "sha256=" + hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, header)
Delivery: a 2xx marks success. Failures retry up to 5 times (backoff 5s, 30s, 5m, 30m, 2h). An endpoint auto-disables after 10 consecutive failures. The same event may arrive more than once β dedupe on X-Audelo-Delivery.
Ready to build?
Create a free account, upgrade to a paid plan, and generate an API key from the dashboard.
Get Started FreeBuilding with an AI tool? Point it at audelo.ai/llms.txt for a machine-readable version of this reference.