zoxaAI
Homepage
API ReferenceExamples

Outbound dialer — end-to-end

Complete recipe — agent + outbound call + lifecycle webhook + transcript fetch.

A production-ready outbound flow. Stand up an agent once, dial customers on demand, react to lifecycle events via webhook, fetch transcript when the call ends.

This is the most common production pattern — the foundation of sales bots, appointment reminders, surveys, follow-ups.

Create the agent

curl -X POST https://dashboard.zoxa.ai/api/v1/agents \
  -H "X-API-Key: zsk_..." \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Order follow-up bot",
    "systemPrompt": "You are calling {{customerName}} about order {{orderId}}. Ask if it arrived on time and whether they have any feedback. Keep replies short.",
    "greeting": { "firstMessages": ["Hi {{customerName}}, I'm calling about your recent order {{orderId}}. Quick question — did it arrive on time?"] },
    "llm": { "provider": "openai", "model": "gpt-5.4-mini", "temperature": 0.7 },
    "tts": { "provider": "elevenlabs" },
    "stt": { "provider": "soniox", "interruptionMinWords": 2 },
    "tools": [
      { "type": "endCall", "name": "end_call", "config": { "messageType": "custom", "customMessages": ["Thanks for your time. Have a great day!"] } }
    ],
    "maxCallDurationS": 240,
    "webhook": {
      "url": "https://your.app/zoxa/webhook",
      "enableSummarization": true
    }
  }'

Note agent.uuid from the response — that's what you'll pass to POST /call.

Place an outbound call

curl -X POST https://dashboard.zoxa.ai/api/v1/call \
  -H "X-API-Key: zsk_..." \
  -H "Content-Type: application/json" \
  -d '{
    "type": "outbound",
    "callConfig": {
      "provider": "twilio",
      "phoneNumber": "+14155550100",
      "auth": { "accountSid": "AC...", "authToken": "..." }
    },
    "toNumber": "+14155559999",
    "agentId": "<agent.uuid>",
    "contextVariables": {
      "customerName": "Aman",
      "orderId":      "ORD-42"
    }
  }'

Response:

{ "callId": "call_4e4e571f8c9b3cf8e99d", "status": "initiated" }

Handle lifecycle webhooks

zoxaAI POSTs to the agent's webhook.url as the call progresses. The event name is in the body's event field (not a header), and only X-Zoxa-Signature is set on the request. Verify every payload before trusting it. See Webhook events for full payload shapes.

import crypto from "node:crypto";
import express from "express";

const WEBHOOK_SECRET = process.env.ZOXA_WEBHOOK_SECRET;  // 64-char hex; not exposed by the API

const app = express();
app.use(express.raw({ type: "application/json" }));      // need raw bytes for HMAC verify

app.post("/zoxa/webhook", (req, res) => {
  const expected = crypto
    .createHmac("sha256", WEBHOOK_SECRET)
    .update(req.body)                                    // raw bytes — don't re-serialize
    .digest("hex");
  const got = req.header("X-Zoxa-Signature") ?? "";
  if (!crypto.timingSafeEqual(Buffer.from(expected, "hex"), Buffer.from(got, "hex"))) {
    return res.status(401).end();
  }

  const payload = JSON.parse(req.body.toString());
  switch (payload.event) {
    case "call.started":
      console.log(`Call ${payload.callId} started (${payload.transport})`);
      break;
    case "call.ended":
      // Cheap, fast — terminal state known. Transcript / recording not yet ready.
      console.log(`Ended: ${payload.callId} reason=${payload.endedReason} duration=${payload.duration}s`);
      break;
    case "call.completed":
      // Final event after post-processing — includes transcript, recordingUrl, summary, usage.
      console.log(`Completed: ${payload.callId}`);
      console.log("Summary:",   payload.summary);
      console.log("Recording:", payload.recordingUrl);
      // hand off to your downstream pipeline here
      break;
  }
  res.status(200).end();                                 // no retries — your 2xx is final
});

app.listen(3000);
import hashlib, hmac, json
from fastapi import FastAPI, Header, HTTPException, Request

WEBHOOK_SECRET = "..."   # 64-char hex; not exposed by the API

app = FastAPI()

@app.post("/zoxa/webhook")
async def webhook(request: Request, x_zoxa_signature: str = Header("")):
    raw = await request.body()
    expected = hmac.new(WEBHOOK_SECRET.encode(), raw, hashlib.sha256).hexdigest()
    if not hmac.compare_digest(expected, x_zoxa_signature):
        raise HTTPException(401)

    payload = json.loads(raw)
    event = payload.get("event")

    if event == "call.started":
        print(payload["callId"], payload["transport"])
    elif event == "call.ended":
        print(payload["callId"], payload["endedReason"], payload["duration"])
    elif event == "call.completed":
        print(payload["callId"], payload.get("summary"))
        print("Recording:", payload.get("recordingUrl"))
    return {"ok": True}

No retries

Webhook delivery is fire-and-forget with a 10-second timeout. If your endpoint is down, the event is lost. For state you must not miss, treat webhooks as a notification and re-fetch from GET /calls/{callId} as the source of truth.

(Optional) Fetch the transcript

If you didn't enable summarization, or you need the full transcript / recording, hit GET /calls/{callId} once the call is terminal.

curl https://dashboard.zoxa.ai/api/v1/calls/call_4e4e571f8c9b3cf8e99d \
  -H "X-API-Key: zsk_..." | jq '{
    connectionStatus,
    endedReason,
    durationSeconds,
    cost,
    transcript:   .transcript.url,
    recording:    .recordings[0].url,
    summary:      .analysis.summary,
    perTurn:      .turns
  }'

Signed URLs expire in ~1 hour. Re-fetch the call detail to get fresh URLs.

Why this shape works in production

ConcernHow this flow handles it
Customer changes order id between callscontextVariables per request — same agent, different context.
Need to react to outcomes quicklyWebhook is push, fires within a second of the call ending.
Need to backfill history if webhook handler was downGET /calls?call_source=api_outbound&from_date=... lists every call you placed.
Want to scale to thousands of callsWrap this in a campaign — concurrency, retry, scheduling, and circuit-breaker built in.

On this page