Working with the REST API
How to start an outbound call, handle configuration errors, follow the call lifecycle, and fetch the transcript and audio once the call completes.
This guide assumes you've finished Getting Started and have a threll() helper or session with your API key.
Start an outbound call
Create a phone call by POSTing to the phone-calls collection. The only required field is workerId — but you'll almost always pass a phone number and some context for the conversation:
curl -X POST https://api.threll.io/v1/accounts/9c3e8f02-7a14-4b62-bc59-1d8e5fa3027b/phone-calls \
-H "x-api-key: $THRELL_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"workerId": "2b7d9a4c-6e3f-48a1-9c5d-8e0b4f2a1c8d",
"phoneNumber": "+15555550100",
"context": "Follow up on the demo request from the website.",
"customer": {
"type": "lead",
"firstName": "Kari",
"lastName": "Nordmann",
"externalId": "crm-8841"
}
}'const call = await threll('/v1/accounts/9c3e8f02-7a14-4b62-bc59-1d8e5fa3027b/phone-calls', {
method: 'POST',
body: JSON.stringify({
workerId: '2b7d9a4c-6e3f-48a1-9c5d-8e0b4f2a1c8d',
phoneNumber: '+15555550100',
context: 'Follow up on the demo request from the website.',
customer: {
type: 'lead',
firstName: 'Kari',
lastName: 'Nordmann',
externalId: 'crm-8841',
},
}),
});
console.log(call.id, call.status); // 5f1a3e7b-2c8d-49a6-b0e4-7d9f3c5a1b2e queuedcall = session.post(
f"{BASE}/v1/accounts/9c3e8f02-7a14-4b62-bc59-1d8e5fa3027b/phone-calls",
json={
"workerId": "2b7d9a4c-6e3f-48a1-9c5d-8e0b4f2a1c8d",
"phoneNumber": "+15555550100",
"context": "Follow up on the demo request from the website.",
"customer": {
"type": "lead",
"firstName": "Kari",
"lastName": "Nordmann",
"externalId": "crm-8841",
},
},
).json()
print(call["id"], call["status"]) # 5f1a3e7b-2c8d-49a6-b0e4-7d9f3c5a1b2e queuedUseful optional fields on the request:
type is required: customer or lead). Pass externalId to link the call back to your CRM.Handle configuration errors
Creating a call returns 400 when the account or worker isn't ready to dial. The body is a structured error with a stable type you can branch on:
{
"statusCode": 400,
"type": "TELEPHONY_INTEGRATION_NOT_SETUP",
"localizedKey": "telephony.telephonyIntegrationNotSetup",
"details": "Telephony integration must be set up on the account before making outbound calls"
}| Error type | What it means |
|---|---|
TELEPHONY_INTEGRATION_NOT_SETUP | No telephony integration on the account. Set one up in the platform before dialing out. |
TELEPHONY_INTEGRATION_NOT_ADDED_TO_WORKER | The account has telephony, but this worker isn't assigned to it. |
TELEPHONY_TRANSPORT_NOT_SUPPORTED | The call resolved to a transport type that can't carry it. Check the worker's integration assignment. |
TELEPHONY_THRELL_PROVIDER_UNAVAILABLE | The Threll voice provider is temporarily unavailable. Retry later. |
CALL_SCRIPT_NOT_CONFIGURED | The worker has no call script configured for outbound calls. |
Follow the call lifecycle
A phone call moves through four statuses, with a separate post-processing status for transcript generation:
Fetch the call at any time to check where it is:
curl https://api.threll.io/v1/accounts/9c3e8f02-7a14-4b62-bc59-1d8e5fa3027b/phone-calls/5f1a3e7b-2c8d-49a6-b0e4-7d9f3c5a1b2e \
-H "x-api-key: $THRELL_API_KEY"const call = await threll('/v1/accounts/9c3e8f02-7a14-4b62-bc59-1d8e5fa3027b/phone-calls/5f1a3e7b-2c8d-49a6-b0e4-7d9f3c5a1b2e');
if (call.status === 'completed' && call.postProcessingStatus === 'completed') {
console.log(call.duration, call.nextAction, call.dimensions);
}call = session.get(
f"{BASE}/v1/accounts/9c3e8f02-7a14-4b62-bc59-1d8e5fa3027b/phone-calls/5f1a3e7b-2c8d-49a6-b0e4-7d9f3c5a1b2e"
).json()
if call["status"] == "completed" and call["postProcessingStatus"] == "completed":
print(call["duration"], call["nextAction"], call["dimensions"])Polling works, but webhooks are better: subscribe to call.status_update and call.ended and Threll pushes every transition to you. See How Webhooks Work.
Once post-processing finishes, the call carries the analysis results:
next_actions keys defined in your worker config, or undetermined.{"interested_product": "Product A", "expressed_urgency": true}.startedAt and endedAt timestamps.Fetch the transcript and audio
After post-processing completes, two more endpoints become useful:
# Full transcript, segment by segment
curl https://api.threll.io/v1/accounts/9c3e8f02-7a14-4b62-bc59-1d8e5fa3027b/phone-calls/5f1a3e7b-2c8d-49a6-b0e4-7d9f3c5a1b2e/transcript \
-H "x-api-key: $THRELL_API_KEY"
# Short-lived download URL for the recording
curl https://api.threll.io/v1/accounts/9c3e8f02-7a14-4b62-bc59-1d8e5fa3027b/phone-calls/5f1a3e7b-2c8d-49a6-b0e4-7d9f3c5a1b2e/audio/url \
-H "x-api-key: $THRELL_API_KEY"const transcript = await threll(
'/v1/accounts/9c3e8f02-7a14-4b62-bc59-1d8e5fa3027b/phone-calls/5f1a3e7b-2c8d-49a6-b0e4-7d9f3c5a1b2e/transcript'
);
for (const seg of transcript) {
console.log(`[${seg.speaker}] ${seg.text}`);
}
const audio = await threll(
'/v1/accounts/9c3e8f02-7a14-4b62-bc59-1d8e5fa3027b/phone-calls/5f1a3e7b-2c8d-49a6-b0e4-7d9f3c5a1b2e/audio/url'
);
// audio.url is a temporary download link for the recordingtranscript = session.get(
f"{BASE}/v1/accounts/9c3e8f02-7a14-4b62-bc59-1d8e5fa3027b/phone-calls/5f1a3e7b-2c8d-49a6-b0e4-7d9f3c5a1b2e/transcript"
).json()
for seg in transcript:
print(f"[{seg['speaker']}] {seg['text']}")
audio = session.get(
f"{BASE}/v1/accounts/9c3e8f02-7a14-4b62-bc59-1d8e5fa3027b/phone-calls/5f1a3e7b-2c8d-49a6-b0e4-7d9f3c5a1b2e/audio/url"
).json()
# audio["url"] is a temporary download link for the recording[
{
"timestamps": { "from": "00:00:02", "to": "00:00:06" },
"speaker": "agent",
"text": "Hi, this is Harald calling from Acme Corp about your demo request."
},
{
"timestamps": { "from": "00:00:07", "to": "00:00:11" },
"speaker": "user",
"text": "Oh hi, yes — I was hoping to see the enterprise plan."
}
]