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 is available on every plan, including Explorer — there is no plan gate on any endpoint. Your prepaid balance is what limits usage.
- 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. Every key must be given at least one scope, and grants only what is listed on it β there is no implicit βall scopesβ state. A request missing the required scope returns 403 forbidden.
| Scope | Grants |
|---|---|
| agents:read | List and read agents |
| agents:write | Create and update agents β name, voice, personality, prompts |
| calls:read | List/read calls, transcripts, recordings; read context |
| calls:write | Initiate/end calls; write/delete context |
| calls:listen | Mint listen-only tokens for live in-progress calls (LiveKit pipeline) |
| calls:takeover | Silence the AI agent and speak to the caller directly on a live outbound call |
| 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 |
| campaigns:write | Create, add targets to, start, pause, resume and cancel bulk outbound campaigns |
| tenants:write | Provision client businesses under your account, each with its own API key (partners only) |
| email:read | Voice-agent email: sender status, templates, variable catalogue, previews, emails on a call |
| email:write | Connect / verify / sign in a sender mailbox, edit templates and allow-lists, test-send, unsubscribe |
Plan requirements
There are none. Every /api/v1 endpoint is available on every plan, Explorer included.
What differs between plans is the included-minutes allowance (the per-minute rate is the same on every plan) — not what the API can do. Calls draw on included minutes first, then prepaid wallet credit. An account with neither cannot place a call: it fails for lack of funds, not for being on the wrong plan.
Authorisation is by scope, not by plan — see Scopes.
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 | API key lacks the scope required by the endpoint |
| 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
1,000 requests per hour, per API key — configurable per 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": "Sally", "status": "active",
"voice": "eve", "language": "en-AU",
"voice_engine": "deepgram", "deepgram_voice": "aura-2-hyperion-en",
"brand_name": "McGrath Parramatta",
"greeting_owner_name": "Joe Wehbe",
"welcome_greeting_mode": "agent_script",
"outbound_ai_disclosure": true,
"outbound_recording_notice": true,
"humour_level": 10, "empathy_level": 80,
"formality_level": 50, "energy_level": 60, "directness_level": 60,
"system_prompt": "You answer calls for a real estate office.",
"created_at": "2026-06-25T10:30:00.000000Z",
"updated_at": "2026-06-25T10:30:00.000000Z"
}
brand_name is the business the agent names on a call — it overrides your account name, so one account can run agents presenting different businesses. greeting_owner_name is whose assistant it says it is: an outbound call opens “Hi, this is {name}, {owner}’s AI assistant, calling from {brand_name}.” Put the person’s name in greeting_owner_name, not in name.
/api/v1/agents
agents:write
Create agent
name and voice are required; everything else takes a default. Returns 201 with the same shape as the GET above.
curl -X POST https://audelo.ai/api/v1/agents \
-H "Authorization: Bearer cgk_..." \
-H "Content-Type: application/json" \
-d '{"name":"Sally","voice":"eve","brand_name":"McGrath Parramatta",
"greeting_owner_name":"Joe Wehbe","humour_level":10}'
/api/v1/agents/{id}
agents:write
Update agent
Partial — only the fields in your body change. Returns the full agent.
Writable: name, brand_name, greeting_owner_name, outbound_identity_question, inbound_recording_notice, outbound_recording_notice, welcome_greeting_mode, welcome_greeting_custom_text, the five personality sliders, system_prompt, business_context, outbound_instructions, website_url, voice, deepgram_voice, language, voice_runtime, gpt_live_voice.
call_scripts is read-only. The agent's named call scripts as {key, name, default}, empty for most agents.
humour_level is 0–30. The other four sliders are 0–100; a higher value is a 422.
inbound_recording_notice and outbound_recording_notice drop only the spoken sentence on that direction. Off does not stop recording. They default on. outbound_ai_disclosure is readable but not writable and stays on.
Every change snapshots a version you can roll back to from the dashboard. Consecutive edits from the same key within two minutes coalesce into one version, so an autosaving editor does not fill the history with a row per keystroke. That is version history, not request volume — your key’s hourly rate limit still counts every PATCH (1,000/hour by default), so debounce for that.
curl -X PATCH https://audelo.ai/api/v1/agents/12 \
-H "Authorization: Bearer cgk_..." \
-H "Content-Type: application/json" \
-d '{"greeting_owner_name":"Joe Wehbe","empathy_level":85}'
/api/v1/voices
agents:read
Voices & languages
Every legal value for the voice-shaped fields above. Read this before creating an agent — voice is required and is validated against it. Returns voices, deepgram_voices, voice_runtimes, gpt_live_voices and languages.
languages lists only what we can actually serve — a voice AND speech-to-text. A language we can speak but not hear is worse than one we decline: the agent replies fluently and mishears every answer. The list is therefore narrower than the set of codes the API will accept, and it widens as speech models are added. Pick from here.
Legacy regional codes (en-AU, fr-FR…) still save, so agents created before 2026-05-19 keep working, but they are not listed — use the base code for anything new.
curl https://audelo.ai/api/v1/voices \ -H "Authorization: Bearer cgk_..."
Partners
For partners who resell Audelo and create businesses on their customers’ behalf. If you manage a single account, you do not need this.
/api/v1/partner/tenants
tenants:write
Provision a client business
Creates a client business underneath your account and returns an API key scoped to it. Call this once when your customer signs up and store the key against them — they never log into Audelo. Only name is required.
The parent is taken from your API key and cannot be set in the body. There is no parent_tenant_id field; sending one is ignored.
A key cannot grant what it does not hold. Omit scopes and the new key inherits yours; name them and each must already be on your key. tenants:write is never passed on, so a business you provision cannot provision businesses of its own.
Their minutes are billed to you. The response carries parent_billed: true — the business can take calls from its first minute with no balance and no card, because its usage is invoiced to you rather than charged to it. Every minute is still metered against it, and that is what the invoice is built from.
By default the business gets no login. Pass owner_email for a real dashboard login; the address must not already exist on any account. api_key.key is returned once. Rate limit: 60 calls/hour, separate from your general key limit.
curl -X POST https://audelo.ai/api/v1/partner/tenants \
-H "Authorization: Bearer cgk_..." \
-H "Content-Type: application/json" \
-d '{"name":"Barton Dental",
"scopes":["agents:read","agents:write","calls:read","calls:write"]}'
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
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
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
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
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.
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
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
Get context
Returns key, value, expires_at. Expired or missing keys return 404.
/api/v1/context/{key}
calls:write
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 the first month's rental 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.
Voice-agent email
A voice agent emails the caller something they asked for, from the business's own mailbox. Transactional only β a promotional template never sends except as a test to its own sender. Blanks only β the voice agent fills catalogue blanks (one line, capped, no links) and never writes a subject or body. Each voice agent sends only the templates on its allow-list. Google / Microsoft 365 sign-in runs on your own OAuth app. No credential appears in any response.
/api/v1/agents/{id}/email/sender
email:read
The voice agent's sender: method, address, status (none | pending | verified)
/api/v1/agents/{id}/email/smtp
email:write
Connect by mail-server settings; a 6-digit code is emailed to the mailbox
/api/v1/agents/{id}/email/smtp/confirm
email:write
{ "code" } β verifies the sender
/api/v1/agents/{id}/email/smtp/resend-code
email:write
Send a fresh code
/api/v1/agents/{id}/email/oauth/{google|microsoft}
email:write
Start a mailbox sign-in on your own OAuth app; returns auth_url
/api/v1/email/oauth/{google|microsoft}/complete
email:write
{ "state", "code" } relayed from your redirect page
/api/v1/agents/{id}/email/sender
email:write
Disconnect the sender (turns email off)
/api/v1/agents/{id}/email/enabled
email:write
{ "enabled" } β refused unless the sender is verified
/api/v1/email-templates/variables
email:read
The fixed variable catalogue: who fills each blank, and caps
/api/v1/email-templates
email:read
Account templates
/api/v1/email-templates
email:write
name, category (transactional | promotional), trigger_description, subject, body_html
/api/v1/email-templates/{id}
email:read
One template, with the voice agents allowed to send it
/api/v1/email-templates/{id}
email:write
Partial update β reaches live calls immediately
/api/v1/email-templates/{id}
email:write
Delete; removed from every allow-list
/api/v1/email-templates/{id}/preview
email:read
Render subject + HTML + text with sample values; sends nothing
/api/v1/email-templates/{id}/test-send
email:write
{ agent_id, recipient_email } β promotional only to the sender's own address
/api/v1/agents/{id}/email-templates
email:read
The voice agent's allow-list
/api/v1/agents/{id}/email-templates
email:write
{ "template_ids": [...] } replaces it
/api/v1/calls/{id}/emails
email:read
What was emailed on a call
/api/v1/email/unsubscribe/lookup
email:read
{ "token" } β business name + masked address, for your unsubscribe page
/api/v1/email/unsubscribe
email:write
{ "token" } β record the opt-out (idempotent)
/api/v1/email/branding
email:read
Logo, brand colour and footer applied around every email
/api/v1/email/branding
email:write
Replaces all three; a field left out is cleared
/api/v1/agents/{id}/email/signature
email:read
The sender’s signature: name, email, title, mobile, photo
/api/v1/agents/{id}/email/signature
email:write
{ title, mobile, photo_url } β fills {{signature}}
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 or takes a callback request | booking_id, call_id, agent_id, customer_name, customer_phone, customer_email, starts_at, ends_at, status, kind (appointment | callback: a callback is status pending, ends_at null), calendar_event_id |
| lead.captured | A demo/callback request, sent once when the call ends. preferred_callback_time is the caller's words; preferred_callback_status is resolved, date_only, vague, contradictory, past, unresolvable or empty | call_id, agent_id, caller_name, caller_phone, caller_email, preferred_callback_time, preferred_callback_at, preferred_callback_date, preferred_callback_status, 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 |
| email.sent | A voice-agent email was accepted by the mail server | email_id, call_id, agent_id, template_id, recipient, status, source, sent_at |
| email.failed | The mail server refused it | email_id, call_id, agent_id, template_id, recipient, status, source, failed_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 an account, choose Starter or higher, and generate an API key from the dashboard.
Get StartedBuilding with an AI tool? Point it at audelo.ai/llms.txt for a machine-readable version of this reference.