Skip to content

Track Call Status

Overview

Voice API v2 gives you two complementary ways to know what is happening with a call:

  • Webhook events (recommended for real-time). Sinch pushes lifecycle events (call.incoming, call.answered, call.hangup, and so on) to your HTTPS endpoint as they happen. This is the right choice whenever you need to react to a call: update a dashboard, log an SLA timestamp, or write a CRM record the moment a call ends.
  • Polling endpoints (fallback / on-demand). GET /calls/{callId}, GET /sessions/{sessionId}, GET /calls (list & filter), and GET /batches/{batchId} (batch summary) let you read the platform's authoritative state on demand. Use them when you cannot run a public webhook receiver, for reconciliation and reporting jobs, or to back-fill state you may have missed.

If you can run a public endpoint, use webhooks. Reach for polling only when you can't, or for after-the-fact reporting.

Prerequisites

  • A Sinch account with API credentials.
  • A Sinch virtual phone number.
  • (Webhooks only) A publicly accessible HTTPS URL. ngrok works during development.
  • A way to start a call so there is something to track. The examples below start one for you; if you want to start calls separately, see Outbound Call.
  • A runtime for the code examples: Node.js 18+ (built-in fetch), or Python 3.9+ with requests, or PHP 8+ with the cURL extension.

Setup

All requests use HTTP Basic auth with your access key as username and secret as password. Instead of a config file, export the values into your shell so both the curl commands and the code examples can read them from the environment:

export PROJECT_ID='...'            # your project UUID
export KEY_ID='...'                # access key ID  (Basic auth username)
export KEY_SECRET='...'            # access key secret (Basic auth password)
export SERVICE_ID='...'            # your voice service UUID
export SINCH_NUMBER='+1...'        # your Sinch virtual number (E.164)
export DESTINATION_NUMBER='+1...'  # number to call (E.164)

Every curl below authenticates with -u "$KEY_ID:$KEY_SECRET". The Node.js, Python, and PHP examples read the same variables via process.env, os.environ, and getenv() respectively, so export them once in the terminal where you run the code.

Chicken-and-egg note. Tracking needs a sessionId (or callId) from a call that already exists. The tracking recipe below starts a call and captures its sessionId in one step. If you already started a call from Outbound Call, grab the sessionId from the POST /calls response and run export SESSION_ID=... before polling.

This is the recommended path. Run a small receiver, point your service at it, and start a call; Sinch streams the lifecycle to your terminal.

1. Run the receiver

The receiver below logs every webhook event with its CloudEvents headers and acknowledges correctly. It answers call.incoming with a short goodbye and acks every other event with commands: []. Use it to learn exactly what Sinch sends for your flows. Note that there is no accept command in v2: you return the call flow directly, typically starting with answer.

Pick one language, save the file, and run it.

Node.js

Save as server.mjs, then run npm install express and node server.mjs.

// server.mjs — minimal Sinch Voice v2 webhook receiver
import express from "express";

const app = express();
app.use(express.json());

app.post("/voice/events", (req, res) => {
  const ce = {
    type:        req.header("ce-type"),
    source:      req.header("ce-source"),
    id:          req.header("ce-id"),
    time:        req.header("ce-time"),
    specversion: req.header("ce-specversion"),
  };
  const { event, call } = req.body || {};
  console.log(JSON.stringify({ ce, event, call }, null, 2));

  if (event === "call.incoming") {
    // Return the call flow directly. events.onHangup is honored on the
    // response to call.incoming.
    return res.status(200).json({
      commands: [
        { command: "answer" },
        {
          command: "messages",
          messages: [
            { type: "SAY", say: { text: "Thank you for calling. Goodbye.", voiceName: "Emma" } },
          ],
          events: { onFinish: [{ command: "hangup" }] },
        },
      ],
      events: { onHangup: [{ command: "hangup" }] },
    });
  }

  return res.status(200).json({ commands: [] });
});

const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
  console.log(`Webhook receiver on :${PORT}`);
  console.log("Point callBehaviors.webhook.url -> https://<your-ngrok>/voice/events");
});

Python

Save as server.py, then run pip install flask and python server.py.

# server.py — minimal Sinch Voice v2 webhook receiver
import os
import json
from flask import Flask, request, jsonify

app = Flask(__name__)


@app.post("/voice/events")
def voice_events():
    ce = {
        "type":        request.headers.get("ce-type"),
        "source":      request.headers.get("ce-source"),
        "id":          request.headers.get("ce-id"),
        "time":        request.headers.get("ce-time"),
        "specversion": request.headers.get("ce-specversion"),
    }
    body = request.get_json(silent=True) or {}
    event = body.get("event")
    call = body.get("call")
    print(json.dumps({"ce": ce, "event": event, "call": call}, indent=2))

    if event == "call.incoming":
        # Return the call flow directly; events.onHangup is honored here.
        return jsonify({
            "commands": [
                {"command": "answer"},
                {
                    "command": "messages",
                    "messages": [
                        {"type": "SAY", "say": {"text": "Thank you for calling. Goodbye.", "voiceName": "Emma"}}
                    ],
                    "events": {"onFinish": [{"command": "hangup"}]},
                },
            ],
            "events": {"onHangup": [{"command": "hangup"}]},
        })

    return jsonify({"commands": []})


if __name__ == "__main__":
    port = int(os.environ.get("PORT", 3000))
    print(f"Webhook receiver on :{port}")
    print("Point callBehaviors.webhook.url -> https://<your-ngrok>/voice/events")
    app.run(port=port)

PHP

Save as server.php, then run php -S 0.0.0.0:3000 server.php.

<?php
// server.php — minimal Sinch Voice v2 webhook receiver

$path = parse_url($_SERVER["REQUEST_URI"], PHP_URL_PATH);
if ($_SERVER["REQUEST_METHOD"] !== "POST" || $path !== "/voice/events") {
    http_response_code(404);
    exit;
}

// Custom headers arrive as HTTP_CE_* in $_SERVER.
$ce = [
    "type"        => $_SERVER["HTTP_CE_TYPE"] ?? null,
    "source"      => $_SERVER["HTTP_CE_SOURCE"] ?? null,
    "id"          => $_SERVER["HTTP_CE_ID"] ?? null,
    "time"        => $_SERVER["HTTP_CE_TIME"] ?? null,
    "specversion" => $_SERVER["HTTP_CE_SPECVERSION"] ?? null,
];

$body  = json_decode(file_get_contents("php://input"), true) ?: [];
$event = $body["event"] ?? null;
$call  = $body["call"] ?? null;
error_log(json_encode(["ce" => $ce, "event" => $event, "call" => $call], JSON_PRETTY_PRINT));

header("Content-Type: application/json");

if ($event === "call.incoming") {
    // Return the call flow directly; events.onHangup is honored here.
    echo json_encode([
        "commands" => [
            ["command" => "answer"],
            [
                "command"  => "messages",
                "messages" => [
                    ["type" => "SAY", "say" => ["text" => "Thank you for calling. Goodbye.", "voiceName" => "Emma"]],
                ],
                "events" => ["onFinish" => [["command" => "hangup"]]],
            ],
        ],
        "events" => ["onHangup" => [["command" => "hangup"]]],
    ]);
    exit;
}

echo json_encode(["commands" => []]);

Once the receiver is running, expose it during development:

ngrok http 3000    # in another terminal

2. Point your service at it

Configure the service's callBehaviors.webhook.url to https://<your-ngrok>/voice/events. A dial whose events property is omitted falls back to this service-level webhook for its lifecycle events.

3. Start a call and watch the events arrive

Start an outbound call, or place an inbound call to $SINCH_NUMBER. You'll see events like:

{ "event": "call.answered",
  "call": { "callId": "01ARZ...", "sessionId": "01BX5...", "callResult": "IN_PROGRESS" } }
{ "event": "call.hangup",
  "call": { "callId": "01ARZ...", "sessionId": "01BX5...", "callResult": "COMPLETED", "callReason": "CALLEE_HANGUP" } }

When call.hangup arrives with a final callResult, the call is done. That's your signal to write the outcome to your CRM, dashboard, or log.

Events you'll receive

EventWhen
call.incomingInbound call arrived on a service with webhook behaviour. Respond with the call-flow commands directly (typically starting with answer).
call.answeredOutbound call answered by the recipient.
call.busyOutbound call recipient was busy.
call.rejectedOutbound call rejected by the recipient.
call.timeoutOutbound call not answered within dialTimeoutDurationSeconds.
call.hangupCall disconnected.
call.failedCall could not be set up.
call.webhook.<name>Dynamic event triggered by a webhook SVAML command (webhookName is prepended with call.webhook.).

Request format

Delivery uses CloudEvents binary mode. Each request looks like:

POST /voice/events
ce-specversion: 1.0
ce-type:        com.sinch.voice.call.control.v1
ce-source:      projects/<projectId>/services/<serviceId>
ce-id:          <uuid>
ce-time:        2026-04-01T12:00:00Z
content-type:   application/json

{ "event": "call.answered",
  "call": { "callId": "...", "sessionId": "...", "callResult": "IN_PROGRESS" } }

Webhook contract

  • Sinch enforces a 5-second response timeout. Slow responses may affect call quality.
  • Webhook requests are blocking: execution of the call flow pauses until the endpoint responds, and the next command runs once a response has been received.
  • Webhook URLs are configured on the service via the Services API or the Dashboard. The webhook SVAML command carries its own url and fallbackUrl for mid-call events.

What counts as a failure

A delivery attempt has failed when:

  • The endpoint returns a non-successful status code.
  • The endpoint does not respond within the 5-second timeout.
  • Returned body does not contain proper SVAML commands. Because the call flow is blocked while waiting, a delivery that ends with a failure disconnects the call. This happens when the primary URL fails and no fallbackUrl is configured, or when both the primary URL and the fallbackUrl fail.

Failover algorithm

This section is the authoritative description of how the webhook URL is selected. When a fallbackUrl is configured:

  1. Requests are sent to the primary url.
  2. If a request to the primary URL fails, that same event is re-sent immediately to the fallbackUrl. The primary URL continues to be used for subsequent requests.
  3. After several consecutive failures (exact number may vary) of the primary URL, the primary URL is bypassed and requests are sent only to the fallbackUrl. This avoids incurring the timeout on every request.
  4. While the primary URL is bypassed it is retried once every 60 seconds. The interval is counted from the last failure of the primary URL that triggers the bypass. Each unsuccessful retry restarts the 60-second clock. As soon as a retry succeeds the primary URL is restored and processing returns to step 1. If no fallbackUrl is configured, a failed request is not retried and the event is not delivered.

Delivery guarantees

Because a failed request is re-sent to the fallback URL, the same event can be delivered more than once — for example when the primary endpoint received and processed the request but responded too slowly. Webhook handlers should be idempotent and deduplicate on the ce-id and ce-source header pair.

Suppressing webhooks per command

When you embed inline events on a dial, those events are handled directly and no webhook fires for them:

  • No events key means all lifecycle events go to the service webhook.
  • events: {} (empty object) means no webhook fires and no inline handling occurs. Use sparingly; it's an easy trap that silently drops events.

Fallback / on-demand: polling

Use polling when you can't run a public webhook endpoint, for reconciliation and reporting, or to recover state you may have missed. Polling reads the platform's authoritative state, so it's also the source of truth for post-call reporting.

The curl snippets below show the raw request and response for each endpoint. The same Basic-auth pattern (KEY_ID / KEY_SECRET) used in the code recipe at the end applies if you call these from Node.js, Python, or PHP.

Fetch one call by id

curl -u "$KEY_ID:$KEY_SECRET" \
  "https://voice.api.sinch.com/v2/projects/$PROJECT_ID/calls/$CALL_ID" | jq

Returns a call resource:

{
  "callId":          "01ARZ3NDEKTSV4RRFFQ69G5FAA",
  "projectId":       "...",
  "serviceId":       "...",
  "sessionId":       "01BX5ZZKBKACTAV9WEVGEMMVRB",
  "direction":       "OUTBOUND",
  "originationType": "SERVER",
  "callType":        "PHONE",
  "callResult":      "COMPLETED",
  "callReason":      "CALLEE_HANGUP",
  "startTime":       "2025-02-10T09:00:00Z",
  "answerTime":      "2025-02-10T09:00:05Z",
  "endTime":         "2025-02-10T09:00:47Z",
  "callDurationSeconds": 42,
  "from": { "type": "PHONE", "phone": { "number": "+15551234567" } },
  "to":   { "type": "PHONE", "phone": { "number": "+15559876543" } },
  "callRate": { "currencyCode": "USD", "amount": "0.0123" },
  "callResourceUrl": "https://voice.api.sinch.com/v2/projects/.../calls/01ARZ3..."
}

The key state field is callResult (verified against the spec; these are the only valid values):

ValueMeaningKind
QUEUEDQueued for initiation.transitional
INITIATEDConnecting; recipient has not yet answered.transitional
IN_PROGRESSAnswered and in progress.transitional
COMPLETEDAnswered and ended.final
REJECTEDRejected by the recipient.final
NO_ANSWERNot answered by the recipient.final
CANCELCancelled.final
BUSYRecipient was busy.final
FAILEDCould not be completed.final

For ended calls, callReason explains why (verified against the spec; these are the only valid values): OK, NOT_AVAILABLE, CALLER_HANGUP, CALLEE_HANGUP, MANAGER_HANGUP, DID_NOT_FOUND, INVALID_SCRIPT, UNKNOWN_PRODUCT, NO_MORE_ROUTES, ERROR.

Fetch a session (all legs)

A session can contain multiple call legs: the original call plus any nested dials or transfers. To inspect them together:

curl -u "$KEY_ID:$KEY_SECRET" \
  "https://voice.api.sinch.com/v2/projects/$PROJECT_ID/sessions/$SESSION_ID" | jq
{
  "sessionId":  "01BX5ZZKBKACTAV9WEVGEMMVRB",
  "projectId":  "...",
  "serviceId":  "...",
  "state":      "COMPLETED",
  "createTime": "2025-02-10T09:00:00Z",
  "updateTime": "2025-02-10T09:00:47Z",
  "endTime":    "2025-02-10T09:00:47Z",
  "calls": [
    { "callId": "...", "callResult": "COMPLETED", "callReason": "CALLEE_HANGUP" },
    { "callId": "...", "callResult": "NO_ANSWER" }
  ]
}

The session-level state (sessionStates, verified against the spec) is one of:

ValueMeaningKind
QUEUEDQueued; no calls initiated yet.transitional
IN_PROGRESSAt least one call initiated or received.transitional
COMPLETEDSession completed; no more events.final

COMPLETED is the only final state, so that's your stop condition when polling. Inspect calls[] for per-leg results; this is the right view for hunt or forwarding flows where the question is "which leg won?".

Field note. Each entry in calls[] is a call resource, which carries callId, callResult, callReason, callDurationSeconds, answerTime, and similar. It does not carry callName. That property exists only on the dial command you sent and as the path segment in GET /sessions/{sessionId}/calls/{callName}. Don't expect callName in this response.

List & filter calls

GET /v2/projects/{projectId}/calls accepts query parameters:

ParameterUse
serviceIdRestrict to one service.
from, toE.164 number filters.
callTypePHONE, SIP, or STREAM.
callResultOne of the callResult enum values above.
callReasonOne of the callReason enum values above.
startTimeInclude calls that started at or after this RFC 3339 timestamp.
endTimeInclude calls that ended before this RFC 3339 timestamp (exclusive).
pageSize, pageNumberPage size and 1-based page number.

Example, listing all FAILED calls since midnight UTC:

curl -u "$KEY_ID:$KEY_SECRET" \
  "https://voice.api.sinch.com/v2/projects/$PROJECT_ID/calls?callResult=FAILED&startTime=$(date -u +%Y-%m-%dT00:00:00Z)&pageSize=50" \
  | jq '.calls[] | {callId, callResult, callReason, from: .from.phone.number, to: .to.phone.number}'

Pagination uses the standard RFC 8288 Link header (relations such as first, prev, next, last):

Link: <.../calls?pageNumber=2&pageSize=50>; rel="next",
      <.../calls?pageNumber=4&pageSize=50>; rel="last"

Batch summary

For batch operations:

curl -u "$KEY_ID:$KEY_SECRET" \
  "https://voice.api.sinch.com/v2/projects/$PROJECT_ID/batches/$BATCH_ID" | jq

The response includes counts (callCount, completed, failed, inProgress, queued) and a per-session callSessions[] array, each entry having an id and a status of QUEUED, IN_PROGRESS, or COMPLETED.

Recipe: start a call and poll to completion (bounded)

This recipe starts an outbound call, captures its sessionId, then polls the session until it reaches the COMPLETED final state. It uses a bounded number of attempts and backoff so it can never loop forever, and it stops immediately on the terminal state. Note that callName is not returned by the API, so the final output does not include it.

Pick one language and run it in the same shell where you exported the variables.

Node.js

Save as track-session.mjs, then run node track-session.mjs (Node.js 18+).

// track-session.mjs — start an outbound call, then poll to completion.
import { randomUUID } from "node:crypto";

const BASE = `https://voice.api.sinch.com/v2/projects/${process.env.PROJECT_ID}`;
const auth =
  "Basic " + Buffer.from(`${process.env.KEY_ID}:${process.env.KEY_SECRET}`).toString("base64");
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));

const body = {
  commands: [
    {
      command: "dial",
      callName: "tracked",
      from: { type: "PHONE", phone: { number: process.env.SINCH_NUMBER } },
      to:   { type: "PHONE", phone: { number: process.env.DESTINATION_NUMBER } },
      dialTimeoutDurationSeconds: 30,
      events: {
        onAnswer: [
          {
            command: "messages",
            messages: [
              { type: "SAY", say: { text: "This call is being tracked. Goodbye.", voiceName: "Emma" } },
            ],
            events: { onFinish: [{ command: "hangup" }] },
          },
        ],
        onHangup: [{ command: "hangup" }],
      },
    },
  ],
};

// 1. Start the call. Inline events play a short message and hang up.
const startRes = await fetch(`${BASE}/calls`, {
  method: "POST",
  headers: {
    Authorization: auth,
    "Content-Type": "application/json",
    "Idempotency-Key": randomUUID(),
  },
  body: JSON.stringify(body),
});
const { sessionId } = await startRes.json();
if (!sessionId) throw new Error("No sessionId in response");
console.log(`Started sessionId=${sessionId}`);

// 2. Poll until COMPLETED (the only final session state), bounded with backoff.
let delay = 2000, maxDelay = 15000, attempt = 0, maxAttempts = 40, state;
while (attempt < maxAttempts) {
  const res = await fetch(`${BASE}/sessions/${sessionId}`, { headers: { Authorization: auth } });
  ({ state } = await res.json());
  console.log(`state=${state}`);
  if (state === "COMPLETED") break; // terminal state -> stop
  attempt++;
  await sleep(delay);
  delay = Math.min(delay * 2, maxDelay);
}

// 3. Print per-leg results (callName is NOT returned by the API).
const finalRes = await fetch(`${BASE}/sessions/${sessionId}`, { headers: { Authorization: auth } });
const { calls = [] } = await finalRes.json();
for (const c of calls) {
  const { callId, callResult, callReason, callDurationSeconds, answerTime } = c;
  console.log(JSON.stringify({ callId, callResult, callReason, callDurationSeconds, answerTime }));
}

Python

Save as track_session.py, then run pip install requests and python track_session.py.

# track_session.py — start an outbound call, then poll to completion.
import os
import sys
import time
import json
import uuid
import requests

BASE = f"https://voice.api.sinch.com/v2/projects/{os.environ['PROJECT_ID']}"
AUTH = (os.environ["KEY_ID"], os.environ["KEY_SECRET"])

body = {
    "commands": [
        {
            "command": "dial",
            "callName": "tracked",
            "from": {"type": "PHONE", "phone": {"number": os.environ["SINCH_NUMBER"]}},
            "to":   {"type": "PHONE", "phone": {"number": os.environ["DESTINATION_NUMBER"]}},
            "dialTimeoutDurationSeconds": 30,
            "events": {
                "onAnswer": [
                    {
                        "command": "messages",
                        "messages": [
                            {"type": "SAY", "say": {"text": "This call is being tracked. Goodbye.", "voiceName": "Emma"}}
                        ],
                        "events": {"onFinish": [{"command": "hangup"}]},
                    }
                ],
                "onHangup": [{"command": "hangup"}],
            },
        }
    ]
}

# 1. Start the call. Inline events play a short message and hang up.
start = requests.post(
    f"{BASE}/calls",
    auth=AUTH,
    headers={"Content-Type": "application/json", "Idempotency-Key": str(uuid.uuid4())},
    json=body,
)
session_id = start.json().get("sessionId")
if not session_id:
    sys.exit(f"No sessionId in response: {start.text}")
print(f"Started sessionId={session_id}")

# 2. Poll until COMPLETED (the only final session state), bounded with backoff.
delay, max_delay, attempt, max_attempts = 2, 15, 0, 40
while attempt < max_attempts:
    state = requests.get(f"{BASE}/sessions/{session_id}", auth=AUTH).json().get("state")
    print(f"state={state}")
    if state == "COMPLETED":  # terminal state -> stop
        break
    attempt += 1
    time.sleep(delay)
    delay = min(delay * 2, max_delay)

# 3. Print per-leg results (callName is NOT returned by the API).
calls = requests.get(f"{BASE}/sessions/{session_id}", auth=AUTH).json().get("calls", [])
fields = ("callId", "callResult", "callReason", "callDurationSeconds", "answerTime")
for c in calls:
    print(json.dumps({k: c.get(k) for k in fields}))

PHP

Save as track-session.php, then run php track-session.php (PHP 8+ with cURL).

<?php
// track-session.php — start an outbound call, then poll to completion.

$base = "https://voice.api.sinch.com/v2/projects/" . getenv("PROJECT_ID");
$auth = getenv("KEY_ID") . ":" . getenv("KEY_SECRET");

function api(string $method, string $url, string $auth, ?array $body = null, array $extraHeaders = []): array {
    $ch = curl_init($url);
    curl_setopt_array($ch, [
        CURLOPT_CUSTOMREQUEST  => $method,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_USERPWD        => $auth,
        CURLOPT_HTTPHEADER     => array_merge(["Content-Type: application/json"], $extraHeaders),
    ]);
    if ($body !== null) {
        curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
    }
    $resp = curl_exec($ch);
    curl_close($ch);
    return json_decode($resp, true) ?: [];
}

$body = [
    "commands" => [[
        "command"  => "dial",
        "callName" => "tracked",
        "from" => ["type" => "PHONE", "phone" => ["number" => getenv("SINCH_NUMBER")]],
        "to"   => ["type" => "PHONE", "phone" => ["number" => getenv("DESTINATION_NUMBER")]],
        "dialTimeoutDurationSeconds" => 30,
        "events" => [
            "onAnswer" => [[
                "command"  => "messages",
                "messages" => [
                    ["type" => "SAY", "say" => ["text" => "This call is being tracked. Goodbye.", "voiceName" => "Emma"]],
                ],
                "events" => ["onFinish" => [["command" => "hangup"]]],
            ]],
            "onHangup" => [["command" => "hangup"]],
        ],
    ]],
];

// 1. Start the call. Inline events play a short message and hang up.
$idem = bin2hex(random_bytes(16));
$start = api("POST", "$base/calls", $auth, $body, ["Idempotency-Key: $idem"]);
$sessionId = $start["sessionId"] ?? null;
if (!$sessionId) {
    fwrite(STDERR, "No sessionId in response\n");
    exit(1);
}
echo "Started sessionId=$sessionId\n";

// 2. Poll until COMPLETED (the only final session state), bounded with backoff.
$delay = 2; $maxDelay = 15; $attempt = 0; $maxAttempts = 40;
while ($attempt < $maxAttempts) {
    $state = api("GET", "$base/sessions/$sessionId", $auth)["state"] ?? null;
    echo "state=$state\n";
    if ($state === "COMPLETED") break; // terminal state -> stop
    $attempt++;
    sleep($delay);
    $delay = min($delay * 2, $maxDelay);
}

// 3. Print per-leg results (callName is NOT returned by the API).
$calls = api("GET", "$base/sessions/$sessionId", $auth)["calls"] ?? [];
$fields = ["callId", "callResult", "callReason", "callDurationSeconds", "answerTime"];
foreach ($calls as $c) {
    echo json_encode(array_intersect_key($c, array_flip($fields))) . "\n";
}

What success looks like

Polling a healthy outbound call typically walks the session through QUEUED -> IN_PROGRESS -> COMPLETED, and step 3 prints something like:

{ "callId": "01ARZ...", "callResult": "COMPLETED", "callReason": "CALLEE_HANGUP", "callDurationSeconds": 42, "answerTime": "2025-02-10T09:00:05Z" }

If nobody answered, you'd instead see "callResult": "NO_ANSWER" (no callReason), and the session would still reach COMPLETED.

Real-life examples

  • CRM activity log: write each call's final callResult to the customer record when call.hangup fires.
  • Operational dashboards: poll GET /sessions/{sessionId} while a hunt is in progress to show which leg is currently ringing.
  • Reconciliation jobs: a nightly job lists GET /calls?startTime=...&endTime=...&callResult=FAILED and re-triggers failed reminders. This is also how you back-fill any webhooks you missed.
  • SLA reporting: log the delta between call.incoming and call.answered on the agent leg for every bridged call.
We'd love to hear from you!
Rate this content: