Place Calls
Each kind of call has its own endpoint. Pick the one that matches what should happen when the callee answers:
| Kind | Route | When the callee answers | Reply |
|---|---|---|---|
| Dial | POST /telephony/call/dial | Nothing plays — use the call to park or transfer it. | 202 queued |
| TTS | POST /telephony/call/tts | A message is spoken, then the call hangs up. | 202 queued |
| Agent | POST /telephony/call/agent | An AI agent talks with them. | 201 ringing |
| Survey | POST /telephony/call/survey | Questions are asked and the answers returned. | 202 or 201 |
| Flow | POST /telephony/call/flow | Not available yet — the call rings, but no flow runs. | 201 ringing |
| Batch | POST /telephony/call/batch | One TTS or dial call per number. | 202 queued |
Then track any of them with GET /telephony/call/{call_uuid}.
Your API key needs the voice.originate permission. A call to an internal extension (POST /telephony/call/extension) needs a signed-in user instead — an API key has no handset to ring.
Each endpoint accepts only its own fields
A field that belongs to another kind of call is refused with 400 and names the right endpoint — for example survey belongs to POST /telephony/call/survey. The older POST /telephony/call still works for dial, TTS, agent, flow and extension calls, but is deprecated and cannot run a survey.
Who is called, and from where
Every call to a number takes the same target fields:
| Field | Type | Notes |
|---|---|---|
destination | string | Required. The number to call, in E.164 (+66812345678). |
caller_id | string | Optional. One of your numbers to show the callee. |
caller_identity_id | string | Optional. A saved caller identity instead of caller_id. |
caller_extension_uuid | string | Optional. Call as this extension's number. |
trunk | string | Optional. Force a specific trunk. |
organization_id | string | Only with a user token. An API key already names its organization. |
Dial, TTS and batch also take mode: "queue" (default) waits for a free line; "instant" dials now or answers 429 when every line is busy.
Retry safely with Idempotency-Key
Send an Idempotency-Key header on every call you place. A retry with the same key returns the first call instead of dialing the callee a second time. The official SDKs send one for you and reuse it on their own retries.
curl -X POST "$ME_BASE/telephony/call/tts" \
-H "API-Key: $ME_API_KEY" \
-H "X-TenantID: $ME_TENANT" \
-H "Idempotency-Key: order-1042-reminder" \
-H "Content-Type: application/json" \
-d '{
"destination": "+66812345678",
"tts": { "text": "Your order 1042 is out for delivery.", "voice_id": "eve" }
}'{
"kind": "pstn",
"call": {
"call_uuid": "5d2c…",
"destination": "+66812345678",
"status": "queued",
"tts": { "provider": "xai", "voice_id": "eve", "characters": 36, "repeat": 0, "cached": false }
}
}call.tts reports how the message was rendered.
tts takes text, voice_id, and optionally provider, language and repeat (how many times it plays; 0 means once).
Connect an AI agent
{
"destination": "+66812345678",
"agent": { "id": "agent_support_th" },
"gather": {
"parameters": {
"type": "object",
"properties": { "order_id": { "type": "string" } },
"required": ["order_id"]
},
"callback_url": "https://example.com/hooks/order"
}
}agent.id names a saved agent (add config_mode: "draft" to use its draft). system_prompt, voice and language override that agent for this call only. gather is optional: the agent asks for the fields in parameters and returns them — see Getting the answers.
Run a survey
A survey calls the number, asks your questions, and returns the answers. It runs in one of two modes.
Keypad ("mode": "dtmf")
A voice reads each question and the callee answers by pressing one key.
{
"destination": "+66812345678",
"survey": {
"mode": "dtmf",
"survey_id": "csat_q3",
"greeting": "Thanks for calling us today.",
"questions": [
{ "key": "score", "prompt": "How satisfied were you? Press 1 to 5.", "min": 1, "max": 5 },
{ "key": "would_return", "prompt": "Would you use us again? Press 1 for no, 2 for yes.", "min": 1, "max": 2, "required": false }
],
"retry_prompt": "Please press a number from {min} to {max}.",
"closing": "Thank you. Goodbye.",
"tts": { "provider": "xai", "voice_id": "eve", "language": "en" },
"callback_url": "https://example.com/hooks/survey"
}
}| Field | Notes |
|---|---|
questions | Required. Up to 20, asked in order. Each takes one key between min and max (0–9). |
key | Lowercase letters, digits and _, starting with a letter. Names the answer. |
required | Default true. An unanswered required question ends the call; an optional one is skipped. |
retry_prompt | Said before a question is asked again after a key outside its range. {min} and {max} are filled in. Without it, the question is simply repeated. The callee gets 3 tries. |
survey_id | Groups the answers in your survey reports. Each answer is stored as <survey_id>.<key> — csat_q3.score — so every question keeps its own scale. Default telephony_survey. |
tts | The voice for every prompt: provider, voice_id, language, instructions. An unavailable provider is refused with 400 before the call is queued. |
A keypad survey is queued: the reply is 202 with status: "queued".
Put the better answer on the higher key
Survey reports count a score as satisfied when it is in the top two of its scale. For a yes/no question, make yes the higher key (1 no, 2 yes) so the report reads the way you expect.
Spoken ("mode": "agent", the default)
An AI agent asks for the fields in parameters by voice and hangs up once every required answer is valid. It rings now: the reply is 201.
{
"destination": "+66812345678",
"survey": {
"greeting": "Hi, this is a short survey from Acme.",
"parameters": {
"type": "object",
"properties": {
"rating": { "type": "integer", "x-parse": "integer" },
"comment": { "type": "string" }
},
"required": ["rating"]
},
"language": "th",
"callback_url": "https://example.com/hooks/survey"
}
}x-parse sets the format the agent returns for a field: text, integer, number, boolean, date, phone or email. Keypad-only fields (questions, closing, retry_prompt, survey_id, tts) are refused here.
Getting the answers
Answers from a gather or a survey reach you two ways, with the same body:
- Webhook — the
call.ai_gather.endedevent on your workspace webhooks, sent when an answered call ends, withstatusvalidorincomplete. callback_url— only when every required answer isvalid. It is signed:X-webhook-signatureis the base64 HMAC-SHA256 of the raw body, keyed withcall.gather.callback_secretfrom the place reply (or thecallback_secretyou sent).
{
"event": "call.ai_gather.ended",
"id": "evt_…",
"created_at": "2026-09-25T09:30:12Z",
"workspace_id": "org_…",
"gather_id": "…",
"session_uuid": "…",
"status": "valid",
"result": { "score": 4, "would_return": 2 }
}Match gather_id against call.gather.gather_id from the place reply.
Call many numbers
{
"destinations": ["+66811111111", "+66822222222"],
"tts": { "text": "The office is closed today.", "voice_id": "eve" }
}{ "queued": 2, "total": 2, "calls": [{ "destination": "+66811111111", "call": { "call_uuid": "…", "status": "queued" } }] }A number that could not be queued has an error in its item; the rest still go out.
Call status
curl "$ME_BASE/telephony/call/$CALL_UUID" \
-H "API-Key: $ME_API_KEY" -H "X-TenantID: $ME_TENANT"{ "call_uuid": "5d2c…", "room": "…", "destination": "+66812345678", "status": "completed", "handled_by_ai": false, "duration_secs": 42 }status moves through pending, ringing and in_progress, and ends as completed, no_answer, failed or canceled.
SDKs
res, err := c.Telephony.Survey(ctx, &sdk.SurveyCallRequest{
DialTarget: sdk.DialTarget{Destination: "+66812345678"},
Survey: &sdk.Survey{
Mode: sdk.SurveyModeDTMF,
SurveyID: "csat_q3",
Questions: []sdk.SurveyQuestion{{Key: "score", Prompt: "Press 1 to 5.", Min: 1, Max: 5}},
},
})const res = await me.telephony.survey({
destination: "+66812345678",
survey: { mode: "dtmf", survey_id: "csat_q3", questions: [{ key: "score", prompt: "Press 1 to 5.", min: 1, max: 5 }] },
});Every SDK has the same telephony service: dial, tts, agent, survey, flow, extension, batch and status.
