You want an inbound line where an AI agent picks up, looks things up to actually resolve the caller’s problem, and — when it can’t, or when the caller asks for a person — hands off to a human with context. The whole time, one call_sid addresses the call, so recordings, reports, and QoS keyed on it stay intact across the handoff. This recipe wires the full path:
1

Route the number to an AI agent

Inbound calls on your DID land on a server-side agent — no code in your process answers the phone.
2

Give the agent a tool to resolve the query

A custom HTTP tool (lookup_order) lets the model read your systems and answer for real.
3

Escalate to a human with context

The built-in route_to_skill tool drops the live caller into an ACD skill queue with a one-line summary screen-pop. The runtime stamps the call_sid onto the handoff.
4

Accept and bridge — same sid throughout

A human accepts; the caller is bridged; the call_sid never changes.
The agent runs server-side, inside TeleQuick Voice. You configure it and its tools once; you don’t hold an audio bridge in your process for this pattern. Your code shows up in two places only: the backend your lookup_order tool calls, and (optionally) the control-plane calls that observe or steer the call.

1. Route the number to an AI agent

Inbound routing is control-plane configuration, not SDK code: a DID (or a whole trunk) binds to an agent_id, and calls that land on it inherit that agent. Set this on the number in the console, or provision it when you buy the number — see Inbound calls and Number provisioning. Once the DID points at your agent, an inbound call is answered by the engine (early-media SDP, then a 200), the audio bridge is wired end-to-end, and the agent starts talking. Nothing in your process has to be online for the call to be answered.

2. Define the agent and its resolve-the-query tool

An agent is a small orchestration graph (its DAG). It has a system prompt, a speech pipeline, and a set of tools the model may call mid-conversation. Author it in the console or push it over the control-plane API — there is no SDK wrapper for the DAG itself (see Agent DAGs and Tool calling). Here the agent is a single streaming (speech-to-speech) node with one custom tool, lookup_order, plus the implicit call-control toolset every voice agent gets for free:
Agent config (support-bot)
{
  "id": "support-bot",
  "version": 3,
  "entry": "converse",
  "nodes": {
    "converse": {
      "type": "REALTIME",
      "provider": "openai",
      "model": "gpt-4o-realtime-preview",
      "voice": "alloy",
      "system_prompt": "You are the support line for Acme. Greet the caller and ask how you can help. When they mention an order, call lookup_order with the order id and read back the status. If you cannot resolve the issue, or the caller asks for a person, call route_to_skill with skill_code \"SUPPORT\" and a one-line summary of what they need. Never invent an order status.",
      "tools": ["lookup_order"]
    }
  }
}
The lookup_order tool is an HTTP tool: you declare its name, description, and an OpenAPI-style parameter schema, and point it at an endpoint you host. The model fills the parameters; the runtime makes the request and feeds the JSON response back into the turn.
Tool: lookup_order (http)
{
  "kind": "http",
  "name": "lookup_order",
  "description": "Look up an order by its id and return status and ETA.",
  "silent": false,
  "spec": {
    "method": "GET",
    "url": "https://orders.example.com/v1/orders/{{order_id}}",
    "headers": { "authorization": "Bearer ${ORDERS_API_TOKEN}" },
    "parameters": {
      "type": "object",
      "properties": {
        "order_id": { "type": "string", "description": "The caller's order id, digits only." }
      },
      "required": ["order_id"]
    }
  }
}
The endpoint behind it is ordinary code you already know how to write. Return a compact JSON object — the model reads it back to the caller:
// orders.example.com — the backend your lookup_order tool calls.
import { Hono } from "hono";

const app = new Hono();

app.get("/v1/orders/:id", async (c) => {
  const id = c.req.param("id");
  const order = await db.orders.findById(id);        // your system of record
  if (!order) return c.json({ found: false }, 404);
  return c.json({
    found: true,
    order_id: id,
    status: order.status,                            // "shipped" | "processing" | …
    eta: order.etaIso ?? null,
    carrier: order.carrier ?? null,
  });
});

export default app;
Keep tool responses small and literal. The model narrates them out loud, so a tight {status, eta, carrier} reads back far better than a full order document. Use MCP tools instead of raw HTTP when you already expose a Model Context Protocol server — see Tool calling.

3. Escalate to a human — with context

You do not author the escalation tool. route_to_skill is part of the implicit call-control toolset every voice agent carries. Unlike a blind SIP transfer that hands the caller away, it keeps the caller on our leg, pauses the AI’s audio, plays hold music, and drops the caller into the ACD ring for a skill. When a human answers, the caller is bridged. The model calls it with two fields:
skill_code
string
required
The department to queue to, e.g. SUPPORT, BILLING, HC_TRIAGE. Match the department the caller asked for.
summary
string
A one-line reason the caller needs a human. This is the screen-pop the receiving agent sees before they take the call.
Everything else the human needs is stamped on by the runtime — you never pass the sid or the caller’s number by hand. When the model invokes the tool, the runtime posts to the control-plane ACD picker:
POST https://engine.telequick.dev/api/acd/queue (emitted by the runtime)
{
  "tenant_id":       "org_abc",
  "agent_config_id": "support-bot",
  "skill_code":      "SUPPORT",
  "summary":         "Order 88231 shows delivered but caller never received it",
  "caller_ani":      "+15551234567",
  "source_call_id":  "call_9f2c…"        // ← the preserved call_sid, injected for you
}
The picker selects the most-idle human with the SUPPORT skill (or queues the request if none is online) and returns the chosen agent so the model can say “connecting you to Ashwin now.” The source_call_id is the same call_sid the AI leg has been using; the summary becomes the human’s screen-pop.

Accepting the offer

When the offer rings on a human agent’s desktop, the desktop confirms it by hitting the accept endpoint. That clears the offer marker, stops the ring, and bridges the caller — on the same call_sid:
// Agent-desktop "Accept" button → clears the offer, bridges the caller.
async function acceptCall(callSid: string, agentJwt: string) {
  const res = await fetch("https://engine.telequick.dev/api/acd/accept", {
    method: "POST",
    headers: {
      "content-type": "application/json",
      "authorization": `Bearer ${agentJwt}`,   // the signed-in agent
    },
    body: JSON.stringify({ call_id: callSid }),
  });
  if (!res.ok) throw new Error(`accept failed: ${res.status}`);
  return res.json() as Promise<{ status: "accepted"; call_id: string }>;
}
The context that survives the handoff today is the one-line summary screen-pop, plus the skill, the caller’s number, and the preserved call_sid. A richer structured payload — full transcript, verified identity, collected fields rendered as a screen-pop card — is a product-level capability on the roadmap, not something the SDK ships wired yet. Build the human-facing card off the summary + source_call_id for now.
Human agents can be browser softphones or real SIP desk/softphones — the accept and bridge are the same either way. For the routing model (skills, VDNs, picker algorithms) see PBX & ACD; for the mechanics of the live audio handoff see AI ↔ human handoff.

4. The sid is the throughline

Because route_to_skill keeps the caller on our leg, the call_sid is stable from the first ring through the human conversation. That means your control-plane code can observe or steer the same call at any point with the TeleQuick Voice SDK:
import { Voice } from "@telequick/sdk/voice";

const v = new Voice({
  baseUrl: "https://engine.telequick.dev",
  apiKey:  process.env.TELEQUICK_CREDENTIALS!,
  orgId:   "org_abc",
});

// Read the live call by the same sid the agent has been using.
const call = await v.calls.get({ sid: callSid });
console.log(call.status);            // "in_progress" through the handoff

// Alternative escalation: instead of letting the model queue to a skill,
// your app can transfer the live call itself — the sid is preserved either way.
await call.transfer({ agent: "senior-support" });   // re-attach to another agent
// …or forward to an on-call phone via SIP REFER:
await call.transfer({ to: "+15557654321" });
There are two escalation styles here, and they compose:
  • Model-driven (route_to_skill) — the agent decides, mid-conversation, that it needs a human and queues to a skill with a summary. Best when the AI owns the judgment call.
  • App-driven (call.transfer) — your backend decides and either re-attaches the call to a different agent or REFERs it to a phone number. Best when an external signal (a supervisor, a business rule) forces the handoff.
Either way the call_sid is unchanged, so the recording (both legs), the segmented call report (AI segment vs human segment), and the per-call QoS all stay keyed to one call.
On a PSTN leg there is no browser echo-cancellation, so tune barge-in for the AI segment — see Turn detection. This only matters while the AI agent holds the line; once a human is bridged, turn detection is out of the path.

AI ↔ human handoff

How the live audio actually moves from the AI leg to the human.

Tool calling

HTTP, MCP, client, and the built-in telephony tools in full.

PBX & ACD

Skills, VDNs, and the picker that chooses the human.

Handoffs

The concept model behind AI→human, transfer, and re-attach.

Transfer to a human

The one-call transfer snippet, on its own.

Outbound sales agent

The outbound sibling of this recipe.