Two things make a CTI app more than a remote control: it can decide where calls go, and it can see what calls are doing. This page covers both — registering your app as a route point so the engine asks you for a destination on each inbound call, and subscribing the CSTA event stream so you observe the real call-state changes your verbs (and everyone else’s) produce. Both build on a connected CTI leg. If you have not connected yet, start with the CTI overview — you need a session authenticated with the routing scope for routeRegister, and any authenticated CTI leg can subscribe the event stream.

Verbs change state; events report it

The CSTA verb API is request/reply: you send a verb (holdCall, singleStepTransfer, makeCall, …) and the engine replies on the same correlated line to say it accepted the request. That reply is an acknowledgement, not the outcome. The call actually going on hold, connecting, or clearing is an asynchronous state change, and it is reported to you as an event on the cti/<tenant> stream — the same way it is reported to every other observer of that tenant.
Treat the verb reply as “the engine took my request” and the event as “the call reached this state.” A holdCall reply tells you the hold was accepted; the matching held event tells you the call is now held. Drive your app state off the events, not the verb replies.
This split is why the event stream matters even for a pure call-control app: transfers complete, callers hang up, and consult legs answer without any verb of yours, and only the event stream tells you.

The CSTA event stream

CSTA events ride a durable per-tenant MoQT track, cti/<tenant>, that your SDK subscribes directly over the same connection as the rest of the SDK. It is durable, so a briefly disconnected client resumes without losing the events it missed. Every observer of a tenant sees the same ordered stream. Each event names a kind and carries the identifiers you need to correlate it back to a call and connection:
Event kindCSTA meaningFires when
deliveredDeliveredA call is offered/ringing at a device.
establishedEstablishedA call is answered and the media path is up.
heldHeldA connection is placed on hold (holdCall).
retrievedRetrievedA held connection is taken back (retrieveCall).
transferredTransferredA transfer completes and the call re-homes to the destination.
clearedConnection ClearedA connection (or the whole call) is released/hung up.
failedFailedA call attempt or operation could not complete.
Each event carries at least the callId it concerns so you can correlate it to the verb you issued or the route you selected; events also carry a CSTA cause so you can distinguish, for example, a normal cleared from an abandoned one. The exact field layout is documented in the CTI overview; the examples below key only off kind and callId, which are always present.

Register as a route point

A route point (a CSTA route dialog) inverts control: instead of you telling the engine what to do with a known call, the engine asks you where an inbound call should go, and waits for your answer. This is how you implement ACD-style third-party routing — skills-based selection, data-directed routing, custom queueing — in your own app while the engine owns the media and signalling. You claim that role with routeRegister, which needs the routing scope:
timeoutMs
number
required
Per-request decision deadline. When the engine hands you a routing request, you have this long to select a destination. If you do not answer in time, the engine stops waiting and applies its default handling for that call so a slow or stuck route point never wedges an inbound call.
ttlSec
number
required
Lease duration for the registration itself. The engine treats your app as the route point for this many seconds; renew by calling routeRegister again before it expires. If your app dies without renewing, the lease lapses and routing falls back to default handling instead of hanging on a dead route point.
Once registered, routing requests for the tenant arrive on the same cti/<tenant> event stream you already subscribe. For each request you inspect the call (caller, dialed number, attached data) and answer with the destination — an agent, a queue/skill, a number, or a trunk leg. Reject or let it time out to fall through to default handling.
A route point is a live decision loop, not fire-and-forget. Keep the registration renewed within ttlSec, and answer every request within timeoutMs. A route point that stalls silently degrades every inbound call to default routing until its lease expires.
The wire verb that returns your chosen destination is part of the CSTA route dialog and is documented with the full verb schema in the CTI overview. The loop below shows where that reply goes; substitute the exact select verb and its argument shape from the reference for your SDK version.

Subscribe and run the route loop

The pattern is: connect and authenticate the CTI leg (see the overview), subscribe the cti/<tenant> track, call routeRegister once to claim the route point, then loop over events — answering routing requests and reacting to call-state changes. Frames are the correlated <verb>\t<args-json> lines described in the overview.
import { CtiSession } from "@telequick/sdk/cti";

// A connected, authenticated CTI leg (see the CTI overview).
// Token must carry the `routing` scope for routeRegister.
const cti = await CtiSession.connect({
  host: "engine.telequick.dev",
  token: ctiToken,        // short-lived, control-plane-minted, scopes: ["routing"]
  tenant: workspaceId,
});

// 1) Subscribe the per-tenant CSTA event stream.
const events = cti.subscribeEvents(); // async iterator over cti/<tenant>

// 2) Claim the route point. Renew before the lease expires.
await cti.send("routeRegister", { timeoutMs: 4000, ttlSec: 300 });
const renew = setInterval(
  () => cti.send("routeRegister", { timeoutMs: 4000, ttlSec: 300 }),
  240_000, // renew comfortably inside ttlSec
);

// 3) One loop for routing decisions AND state observation.
for await (const ev of events) {
  switch (ev.kind) {
    case "route": {
      // The engine is asking where this call should go.
      const dest = await pickDestination(ev); // your ACD logic
      // Answer with the destination on the same correlated request.
      await cti.selectRoute(ev, { destination: dest });
      break;
    }
    case "delivered":
      console.log("ringing", ev.callId);
      break;
    case "established":
      console.log("answered", ev.callId);
      break;
    case "held":
    case "retrieved":
    case "transferred":
      console.log(ev.kind, ev.callId);
      break;
    case "cleared":
      console.log("hung up", ev.callId, ev.cause);
      break;
    case "failed":
      console.warn("failed", ev.callId, ev.cause);
      break;
  }
}

clearInterval(renew);
from telequick.cti import CtiSession

# A connected, authenticated CTI leg (see the CTI overview).
# Token must carry the `routing` scope for routeRegister.
cti = await CtiSession.connect(
    host="engine.telequick.dev",
    token=cti_token,        # short-lived, control-plane-minted, scopes: ["routing"]
    tenant=workspace_id,
)

# 1) Subscribe the per-tenant CSTA event stream.
events = cti.subscribe_events()  # async iterator over cti/<tenant>

# 2) Claim the route point, then renew before the lease expires.
await cti.send("routeRegister", {"timeoutMs": 4000, "ttlSec": 300})

async def renew():
    while True:
        await asyncio.sleep(240)  # comfortably inside ttlSec
        await cti.send("routeRegister", {"timeoutMs": 4000, "ttlSec": 300})

asyncio.create_task(renew())

# 3) One loop for routing decisions AND state observation.
async for ev in events:
    if ev.kind == "route":
        dest = await pick_destination(ev)          # your ACD logic
        await cti.select_route(ev, destination=dest)
    elif ev.kind == "delivered":
        print("ringing", ev.call_id)
    elif ev.kind == "established":
        print("answered", ev.call_id)
    elif ev.kind in ("held", "retrieved", "transferred"):
        print(ev.kind, ev.call_id)
    elif ev.kind == "cleared":
        print("hung up", ev.call_id, ev.cause)
    elif ev.kind == "failed":
        print("failed", ev.call_id, ev.cause)
subscribeEvents / subscribe_events and selectRoute / select_route above stand in for your SDK version’s exact names; the parts that are fixed by the protocol are the routeRegister verb, its { timeoutMs, ttlSec } arguments, the cti/<tenant> track, and the event kinds. Confirm the SDK method names against the CTI reference before you ship.

CTI (CSTA) overview

Connect, authenticate, scopes, and the full event/verb schema.

CSTA call control

The verbs that change call state — make, hold/retrieve, transfer, consult, clear.

CSTA agent state

Log agents in and out and set ready / not-ready so routing has targets.

High availability

Routing and CTI control keep resolving through a node failure.