Threll.ai
Guides

End-to-End Integration: Outbound

A complete workflow that combines the REST API and webhooks: start an outbound call, let the worker query your systems mid-conversation, stream the transcript live, and sync the results into your CRM when the call ends.

The scenario: your sales worker Harald calls a lead. During the call he looks up the lead's account in your CRM through an external tool. When the call completes, you fetch the analysis, transcript, and recording, and write everything back to the CRM. Receiving calls instead? See End-to-End Integration: Inbound.

Anatomy of one call

Here's every HTTP exchange for a single outbound call. Blue dots are sync events (Threll waits for your response); green dots are async notifications.

POST/v1/accounts/…/phone-calls

You start the call via the REST API. Response: the call in queued status.

call.worker_requestsync

Right before dialing, Threll asks your endpoint for worker configuration overrides — your chance to inject fresh context.

call.status_updateasync

Lifecycle changes stream in as the call connects: ringing, then in progress.

call.transcriptasync

Transcript turns arrive as they're committed — one event per turn, for both speakers.

call.tool_callsync

Harald invokes your external lookup_account tool. The voice session pauses until you return the result.

call.endedasync

The final event. No further events follow — now fetch the results over REST.

GET…/phone-calls/{id} · …/transcript · …/audio/url

You pull the analysis, full transcript, and recording URL, and sync them to your CRM.

Step 1 — Set up

Step 2 — Handle the events

One endpoint handles all five events. Sync events return data; async events are acknowledged and processed in the background:

server.js — webhook handler
const express = require('express');
const crypto = require('crypto');
const app = express();

const SECRET = process.env.THRELL_WEBHOOK_SECRET; // whsec_...

function verify(rawBody, header) {
  const expected = crypto.createHmac('sha256', SECRET).update(rawBody).digest('hex');
  const a = Buffer.from(header || '', 'utf8');
  const b = Buffer.from(expected, 'utf8');
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
  if (!verify(req.body, req.get('X-Threll-Signature'))) {
    return res.status(401).end();
  }
  const { type, data } = JSON.parse(req.body);

  switch (type) {
    // ── Sync: Threll waits for these responses ──────────────
    case 'call.worker_request':
      // Inject fresh context just before the call starts
      return res.json({
        context: crm.latestNotesFor(data.customer.number),
      });

    case 'call.tool_call':
      if (data.name === 'lookup_account') {
        const account = crm.findAccount(data.arguments.accountNumber);
        return res.json({ result: account ?? { error: 'not found' } });
      }
      return res.json({ result: { error: `unknown tool ${data.name}` } });

    // ── Async: acknowledge fast, process in background ──────
    case 'call.transcript':
      res.status(200).end();
      if (data.isFinal) liveFeed.push(data.callId, data.role, data.text);
      return;

    case 'call.status_update':
      res.status(200).end();
      crm.updateCallStatus(data.callId, data.status);
      return;

    case 'call.ended':
      res.status(200).end();
      finalizeCall(data.callId); // Step 4
      return;

    default:
      return res.status(200).end();
  }
});

app.listen(3000);

Step 3 — Start the call

Start the outbound call
const call = await threll('/v1/accounts/9c3e8f02-7a14-4b62-bc59-1d8e5fa3027b/phone-calls', {
  method: 'POST',
  body: JSON.stringify({
    workerId: '2b7d9a4c-6e3f-48a1-9c5d-8e0b4f2a1c8d',          // Harald
    phoneNumber: '+15555550100',
    context: 'Qualify this inbound demo request. Goal: book a meeting.',
    customer: {
      type: 'lead',
      firstName: 'Kari',
      lastName: 'Nordmann',
      externalId: 'crm-8841',
    },
  }),
});
console.log('started', call.id); // events for call.id now flow to /webhook
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": "Qualify this inbound demo request. Goal: book a meeting.",
    "customer": { "type": "lead", "firstName": "Kari", "externalId": "crm-8841" }
  }'

During the call, your handler from Step 2 does the live work: it answers the call.worker_request, serves the lookup_account tool call, and streams transcript turns into your live feed.

Step 4 — Finalize when the call ends

call.ended is your signal to collect results. Post-processing may still be running when it arrives, so check postProcessingStatus before reading the transcript:

finalizeCall — pull results, sync to CRM
async function finalizeCall(callId) {
  const base = `/v1/accounts/9c3e8f02-7a14-4b62-bc59-1d8e5fa3027b/phone-calls/${callId}`;

  // Wait for post-processing (transcript + analysis) to finish
  let call = await threll(base);
  while (call.postProcessingStatus === 'in_progress' || call.postProcessingStatus === 'none') {
    await new Promise(r => setTimeout(r, 5000));
    call = await threll(base);
  }

  if (call.postProcessingStatus === 'failed') {
    return crm.flagForReview(callId, 'post-processing failed');
  }

  const [transcript, audio] = await Promise.all([
    threll(`${base}/transcript`),
    threll(`${base}/audio/url`),
  ]);

  await crm.logCall({
    externalId: call.customer?.externalId,
    duration: call.duration,
    nextAction: call.nextAction,        // e.g. 'book_meeting'
    dimensions: call.dimensions,        // e.g. { budget_range: '10000-50000' }
    transcript: transcript.map(s => `[${s.speaker}] ${s.text}`).join('\n'),
    recordingUrl: audio.url,            // short-lived — download promptly
  });
}

The audio URL is a temporary download link. If you need the recording long-term, download it when you receive the URL rather than storing the link.

Production checklist

Reference

Reference: REST API · Webhooks