Threll.ai
Guides

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:

POST /v1/accounts/{accountId}/phone-calls
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 queued
call = 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 queued

Useful optional fields on the request:

contextstring
Free-text background the worker uses to steer the conversation — who the customer is, why you're calling, what outcome you want.
customerobject
A person record (type is required: customer or lead). Pass externalId to link the call back to your CRM.
callScriptFileNamesstring[]
Pin specific outbound call scripts for this call. List the available scripts with GET …/documents/outbound-call-scripts.
scheduledAtstring · ISO 8601
Schedule the call for later instead of dialing immediately. Scheduled calls re-resolve their worker configuration when they actually run (see the call.worker_request webhook).

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:

400 — error response
{
  "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 typeWhat it means
TELEPHONY_INTEGRATION_NOT_SETUPNo telephony integration on the account. Set one up in the platform before dialing out.
TELEPHONY_INTEGRATION_NOT_ADDED_TO_WORKERThe account has telephony, but this worker isn't assigned to it.
TELEPHONY_TRANSPORT_NOT_SUPPORTEDThe call resolved to a transport type that can't carry it. Check the worker's integration assignment.
TELEPHONY_THRELL_PROVIDER_UNAVAILABLEThe Threll voice provider is temporarily unavailable. Retry later.
CALL_SCRIPT_NOT_CONFIGUREDThe 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:

scheduled → queued → in_progress → completed

Fetch the call at any time to check where it is:

GET /v1/accounts/{accountId}/phone-calls/{phoneCallId}
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:

nextActionstring
The follow-up action determined from call analysis — one of the next_actions keys defined in your worker config, or undetermined.
dimensionsobject
Structured values extracted from the conversation, keyed by the dimension keys in your worker config — e.g. {"interested_product": "Product A", "expressed_urgency": true}.
durationnumber · seconds
Call length, alongside startedAt and endedAt timestamps.

Fetch the transcript and audio

After post-processing completes, two more endpoints become useful:

Transcript & audio
# 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 recording
transcript = 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
Transcript — response
[
  {
    "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."
  }
]

Next steps