Speko Docs
Build

Build a phone agent

Inbound and outbound PSTN calls, phone-number routing, lifecycle webhooks, reports, and transfers.

Speko can run the same voice agent over browser sessions and PSTN calls. Phone calls use the platform-hosted LiveKit SIP path: Speko creates or receives the call leg, dispatches the agent worker, records the room, persists transcripts and events, and finalizes a call report after hangup.

Use this guide when you want a receptionist, appointment setter, callback workflow, or support line that can place calls, answer calls, and transfer callers to a human.

Phone surfaces

NeedAPISDK
Place an outbound phone callPOST /v1/sessions/phonespeko.voice.dial()
Buy or import numbers/v1/phone-numbers/*speko.phoneNumbers
Route inbound callsagentId or dispatchMetadataTemplate on a phone numberspeko.phoneNumbers.update()
Inspect calls and reports/v1/calls/{id}speko.calls.get()
Read lifecycle events/v1/calls/{id}/eventsspeko.calls.events()
Transfer live calls/v1/calls/{id}/transfers/*speko.calls.blindTransfer() / speko.calls.warmTransfer()

1. Create an agent

Phone calls work best with a persisted agent because the phone-number row can hydrate its prompt, routing intent, provider preferences, speech settings, tools, and lifecycle webhooks every time a call starts.

curl -X POST https://api.speko.dev/v1/agents \
  -H "Authorization: Bearer $SPEKO_API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{
    "name": "Front desk",
    "systemPrompt": "You are the front-desk receptionist. Greet callers, collect their name and reason for calling, answer common questions, and transfer urgent calls.",
    "intent": { "language": "en-US", "optimizeFor": "latency" },
    "webhooks": {
      "preCall": { "url": "https://example.com/speko/pre-call" },
      "status": { "url": "https://example.com/speko/status" },
      "postCall": { "url": "https://example.com/speko/post-call" }
    }
  }'

Lifecycle webhooks are Standard Webhooks-signed with the organization webhook secret. The legacy per-webhook secret field is still accepted for older clients, but new integrations should verify the org-level secret.

2. Add a phone number

You can use a platform-managed number or register a customer-owned SIP trunk number.

Managed number search and purchase is currently the US-number path. It requires phone-number business verification and sufficient credits:

const options = await speko.phoneNumbers.searchAvailable({ areaCode: '415' });

const number = await speko.phoneNumbers.create({
  e164: options[0]!.e164,
  direction: 'both',
  agentId: 'agent_123',
  label: 'Main line',
});

For numbers you already own, import the SIP trunk instead:

await speko.phoneNumbers.importSipTrunk({
  e164: '+442071234567',
  sipConnectionInstallationId: '00000000-0000-4000-8000-000000000010',
  direction: 'both',
  agentId: 'agent_123',
  label: 'London front desk',
});

direction controls whether the number can be used for inbound calls, outbound calls, or both. Inbound calls require either an agentId or a dispatchMetadataTemplate.

3. Place outbound calls

Use POST /v1/sessions/phone or speko.voice.dial() for outbound PSTN calls. The response returns the Speko session id, room name, resolved caller ID, and SIP participant handle.

const call = await speko.voice.dial({
  to: '+12015551234',
  from: '+12015550199',
  agentId: 'agent_123',
  firstMessage: 'Hi, this is Ava from Acme. Is now still a good time?',
  telephony: {
    region: 'us-east',
    amd: { mode: 'agent', timeoutSeconds: 8 },
  },
  metadata: {
    campaignId: 'renewal-q2',
    leadId: 'lead_456',
  },
});

console.log(call.sessionId, call.status);

agentId is required unless you pass an ad hoc intent. Per-call fields such as systemPrompt, firstMessage, voice, llm, ttsOptions, sttOptions, constraints, and metadata override or extend the agent defaults for that call.

4. Receive inbound calls

When a call arrives on a registered number, Speko matches the dialed E.164 number, checks direction, creates a voice session, hydrates the linked agent, dispatches the worker, and bridges the caller into the LiveKit room.

For forwarded calls, Speko attempts to preserve the original forwarding source. It looks at Telnyx payload fields such as forwarded_from, original_to, and redirecting_number, plus SIP headers such as Diversion and History-Info. The normalized value is exposed as:

  • forwardedFromNumber in session metadata.
  • forwarded_from_number in pre-call webhook payloads.
  • call events and reports through the session metadata.

Use dispatchMetadataTemplate when you need static metadata or a legacy template alongside the linked agent:

await speko.phoneNumbers.update('pn_123', {
  agentId: 'agent_123',
  dispatchMetadataTemplate: {
    tenant: 'acme',
    line: 'front_desk',
    caller: '{{callerNumber}}',
    dialedNumber: '{{dialedNumber}}',
    forwardedFromNumber: '{{forwardedFromNumber}}',
  },
});

The agent fields are canonical. If agentId and dispatchMetadataTemplate set the same pipeline key, the persisted agent wins and the template fills the gaps.

5. Customize calls before answer

Configure an agent preCall webhook when your application needs to look up the caller, choose a greeting, inject account context, or override the call pipeline before the worker starts speaking.

Speko sends:

{
  "type": "call.pre_call",
  "call_id": "session_123",
  "session_id": "session_123",
  "organization_id": "org_123",
  "direction": "inbound",
  "to": "+12015550199",
  "from": "+12015551234",
  "dialed_number": "+12015550199",
  "forwarded_from_number": null,
  "phone_number_id": "pn_123",
  "call_control_id": "sip_participant_123",
  "pipeline_config": {},
  "metadata": {}
}

Return any supported pipeline overrides at the top level or under pipelineConfig / pipeline_config:

{
  "firstMessage": "Hi Maya, thanks for calling Acme.",
  "systemPrompt": "The caller is Maya Chen, a priority customer. Keep responses brief.",
  "metadata": {
    "customerId": "cus_123",
    "plan": "enterprise"
  }
}

Supported pre-call override keys include intent, constraints, voice, systemPrompt, firstMessage, llm, ttsOptions, sttOptions, backgroundAudio, tools, pronunciation and text replacement rules, and idle reprompts.

6. Track lifecycle and reports

Speko records durable lifecycle events for LiveKit, Telnyx, Speko status updates, SIP cause codes, and transfer attempts.

const call = await speko.calls.get('session_123');
const { events } = await speko.calls.events('session_123');
const report = await speko.calls.report('session_123');

Agent status webhooks receive call progress and failure information:

{
  "type": "call.status",
  "call_id": "session_123",
  "event_type": "call.hangup",
  "provider": "telnyx",
  "status": "ended",
  "failure_cause": null,
  "sip_status_code": 200,
  "sip_status": "OK",
  "metadata": {}
}

After hangup, Speko finalizes a report with transcript entries, summary, outcome, structured data, cost breakdown, artifacts, metadata, and any scheduled callback created by analysis. The agent postCall webhook receives the same report payload with type: "call.report". Failed post-call deliveries are retried, and you can force rerun or retry with speko.calls.finalizeReport(callId, { forceAnalysis: true, retryWebhook: true }).

7. Transfer callers

Use the Calls API for operator-driven transfers, or add the built-in transfer_call tool to let the agent transfer based on conversation context.

await speko.calls.blindTransfer('session_123', {
  to: '+12015554321',
  ringingTimeout: 25,
  metadata: { reason: 'billing escalation' },
});

const transfer = await speko.calls.warmTransfer('session_123', {
  from: '+12015550199',
  destinations: [
    { to: '+12015551234', label: 'Front desk' },
    { to: '+12015554321', label: 'Overflow' },
  ],
  screeningPrompt: 'Confirm the recipient can help before bridging.',
  fallback: { strategy: 'take_message' },
  voicemailDetection: { mode: 'agent', timeoutSeconds: 10 },
});

Warm transfer starts a consultation leg first. Complete the transfer when the screened recipient accepts, or cancel with tryNext: true to continue through the destination list.

Next

On this page