API responses
What comes back from the call APIs, the live-call stream and the agent's tools — the shapes that are not a plain REST resource. Each section links to the page that documents the request.
Building an app? The app-key endpoints (/api/app-*) and the window.telenow.* bridge have their own page: App API responses.
Conventions
- REST answers are
{ "success": true, "data": … }. A refusal is an HTTP error status with{ "success": false, "error": "…" }. A few refusals add a field, such asmaxCharson a note that is too large. - Casing differs by endpoint. Call-history rows are
snake_case. The analysis object, live context notes and the live-stream envelope arecamelCase. Every example below shows the real casing. - Times are RFC 3339 in UTC, for example
2026-07-29T09:21:47.115900Z. - Tool results are not REST. A tool's result goes to the model as the tool's output, with no
{ success, data }wrapper. A failing tool returns{ "error": "…" }inside a successful call, so the model can read it and recover. See Tool results.
The post-call analysis object
Post-call analysis stores one analysis per call.
| Where | What you get |
|---|---|
GET /api/orgs/{orgId}/analysis/result/{sessionId} | data is the object below, or null when the call has no analysis yet. See Analytics & usage API → Post-call analysis |
call.analyzed webhook | The same results in a different shape. See The webhook's shape |
App API (/api/app-calls) | See App API responses |
{
"sessionId": "9f1c2b8e-4a3d-4f26-9b71-0c5ad83e1f42",
"orgId": "c81b0f47-2e6a-4d3b-8f19-5a7c6e2d40b1",
"agentId": "3d5a1f90-7c2e-4b18-a6d4-91e2f0c74b35",
"status": "done",
"summary": "The customer rescheduled a dental cleaning from Tue 29 Jul to Tue 4 Aug, 11:00. An SMS confirmation was promised.",
"sentiment": "positive",
"sentimentScore": 0.62,
"disposition": "appointment_rescheduled",
"actionItems": ["Send SMS confirmation for 2026-08-04 11:00"],
"customData": { "patient_id": "PT-4471", "branch": "Indiranagar" },
"evidence": {
"sentiment": "that works perfectly, thank you",
"disposition": "let's move it to next Tuesday instead"
},
"qa": [
{ "key": "verified_identity", "met": true, "evidence": "Can you confirm your date of birth?" },
{ "key": "offered_reminder", "met": false, "evidence": "" }
],
"objections": ["morning slots are hard for me"],
"score": 88,
"coaching": [
{ "issue": "Never offered the SMS reminder opt-in", "suggestion": "Offer the reminder before closing the call", "severity": "low" }
],
"hallucinations": [],
"cx": { "rating": "good", "friction": ["repeated the new date twice"], "highlights": ["reschedule took under 30 seconds"] },
"topics": ["appointment", "rescheduling"],
"keywords": ["dental cleaning", "Tuesday", "SMS confirmation"],
"agentWords": 142,
"customerWords": 88,
"agentTurns": 9,
"customerTurns": 8,
"model": "openai/gpt-4o-mini",
"error": null,
"createdAt": "2026-07-29T09:21:47.115900Z",
"updatedAt": "2026-07-29T09:21:52.884301Z"
}
| Field | Type | Meaning |
|---|---|---|
sessionId, orgId, agentId | uuid | The call, its organization and its agent. agentId can be null |
status | string | The analysis, not the call: pending, done or failed |
summary | string | null | A short recap, written for the agent's owner |
sentiment | string | null | positive, neutral or negative |
sentimentScore | number | null | −1 to 1 |
disposition | string | null | The analysis' outcome label, such as appointment_rescheduled. The call row has its own disposition (the carrier result: answered, no-answer, busy, failed); the two are unrelated |
actionItems | string[] | Follow-ups found in the conversation |
customData | object | Your custom fields, keyed by their key. A call placed with its own fields (per-call settings) has those here too, beside the agent's |
evidence | object | Verbatim transcript quotes, keyed by the field or judgment they back |
qa | array | One { key, met, evidence } per QA rubric criterion |
objections | string[] | Concerns the caller raised |
score | integer | null | The judge's quality score, 0–100 |
coaching | array | { issue, suggestion, severity }; severity is low, medium, high or null |
hallucinations | array | { claim, why } |
cx | object | { rating, friction, highlights }; rating is excellent, good, fair, poor or null |
topics, keywords | string[] | Tags for reporting |
agentWords, customerWords, agentTurns, customerTurns | integer | Talk-ratio counts, counted from the transcript (no model) |
model | string | null | The provider/model that produced this analysis. If the agent's chosen analysis model failed and the platform model analysed the call instead, this names the platform model |
error | string | null | Why a failed analysis failed |
createdAt, updatedAt | timestamp |
- Empty, never missing. Every key is always present. Arrays default to
[]and objects to{}.summary,sentiment,sentimentScore,disposition,scoreandmodelcan benull. Apendingorfailedanalysis has every result field empty. - Parts you switched off stay empty. A part unticked under What the analysis includes comes back
null,[]or{}. With the QA judge off,cxis{ "rating": null, "friction": [], "highlights": [] }.qais[]when the agent has no QA rubric. - Check
customDatabefore you read it. A field the transcript doesn't answer should benull, but nothing reconciles the model's answer against your configured keys: a key can be missing, andtypeis a hint, not a conversion. - Scrubbed. The free-text fields, and the string values in
customData,evidenceandqa, are scrubbed with the call's PII policy. See What is and is not redacted. - Off by default. Analysis is switched on per agent. A call whose agent has it off has no analysis, and
dataisnull.
The call.analyzed webhook has a different shape
The webhook carries the same results, arranged differently:
| REST object (above) | call.analyzed webhook | |
|---|---|---|
| Talk-ratio counts | Flat: agentWords, customerWords, agentTurns, customerTurns | Nested under analysis.talkRatio |
sessionId | In the object | At the top level, beside agentId, orgId, identifier and occurredAt |
status, error, createdAt, updatedAt | In the object | Not sent |
| PII the call captured | Not included | piiCollected at the top level: presence only, never the value |
The webhook fires once per call, and only for a finished analysis — never for a failed one. Full payload: Webhook events → call.analyzed.
An installed app's event handlers get a third, smaller shape: a flat object with summary, sentiment, disposition, custom (not customData), topics and keywords, plus caller_number, session_id and agentId. See App API responses.
Live call stream
Watch a live call as it happens: what the caller and the agent say, flow steps, interruptions, keypresses, silence check-ins and the notes the agent receives. The socket is read-only and ignores anything you send.
To hear the call instead, use live listen-in (/ws/monitor, owners and admins only).
1. Get a ticket
POST /api/orgs/{orgId}/calls/{id}/stream-ticket
Any member of the organization can ask.
{
"success": true,
"data": {
"ticket": "5b81a3c74f6e4d2ab0973e1c8f4d2266",
"wsUrl": "wss://app.telenow.ai/ws/live-call-stream?ticket=5b81a3c74f6e4d2ab0973e1c8f4d2266"
}
}
wsUrlalready carries the ticket. Open it as given: on a fleet of servers it can point at the server holding the call rather than the host you asked.- The ticket works once, within 30 seconds.
404: no such call in your organization.409: the call is not live.
Apps get the same answer from POST /api/app-calls/{sessionId}/stream-ticket (App API responses).
2. Read the frames
Each event is one JSON text frame: { "topic", "sessionId", "data" }. The envelope is camelCase; everything inside data is snake_case.
{"topic":"call.transcript_partial","sessionId":"9f1c2b8e-4a3d-4f26-9b71-0c5ad83e1f42","data":{"corr":"t3:n=greet:g=1","text":"i need to move my tues"}}
{"topic":"call.turn","sessionId":"9f1c2b8e-4a3d-4f26-9b71-0c5ad83e1f42","data":{"corr":"t3:n=greet:g=1","text":"I need to move my Tuesday cleaning."}}
{"topic":"call.assistant_turn","sessionId":"9f1c2b8e-4a3d-4f26-9b71-0c5ad83e1f42","data":{"corr":"t3:n=greet:g=1","text":"Of course — I can move that for you.","engine":"cascade"}}
{"topic":"call.node_entered","sessionId":"9f1c2b8e-4a3d-4f26-9b71-0c5ad83e1f42","data":{"corr":"t4:n=reschedule:g=2","node_id":"reschedule","node_name":"Reschedule","node_kind":"conversation","node_generation":2,"llm_model":"gpt-4o-mini","tts_provider":"telenow","tts_voice":"aditi","entry_message":"Let me pull up your appointment.","duration_ms":31}}
{"topic":"call.barge_in","sessionId":"9f1c2b8e-4a3d-4f26-9b71-0c5ad83e1f42","data":{"corr":"t4:n=reschedule:g=2","played_ms":820,"turn_audio_ms":3400}}
{"topic":"call.dtmf","sessionId":"9f1c2b8e-4a3d-4f26-9b71-0c5ad83e1f42","data":{"corr":"t5:n=reschedule:g=2","digit":"2"}}
{"topic":"call.silence","sessionId":"9f1c2b8e-4a3d-4f26-9b71-0c5ad83e1f42","data":{"corr":"t6:n=reschedule:g=2","text":"Are you still there?","llm_generated":false}}
{"topic":"call.context_note","sessionId":"9f1c2b8e-4a3d-4f26-9b71-0c5ad83e1f42","data":{"note_id":"0b6e2f4a-…","key":"payment","source":"server","actor":"key:7c1d…","chars":43,"respond":"when_idle","delivery":"speaking_now","replaced":false,"retry":false,"pruned":0,"text":"Payment of ₹4,500 received. UPI ref 41234."}}
| Topic | data | Sent when |
|---|---|---|
call.transcript_partial | text, corr | The caller is still speaking. The text changes as recognition settles |
call.turn | text, corr | The caller finished a turn |
call.assistant_turn | text, engine (cascade or s2s), corr | The agent replied. Once per reply, never per sentence, filler or opener |
call.node_entered | node_id, node_name, node_kind, node_generation, llm_model, tts_provider, tts_voice, entry_message, duration_ms, corr | A voice call entered a flow step. The step's prompt is never sent |
call.barge_in | played_ms, turn_audio_ms, corr | The caller interrupted the agent |
call.dtmf | digit, corr | The caller pressed a key |
call.silence | text, llm_generated, corr | The agent checked in after a silence |
call.context_note | note_id, key, source, actor, chars, respond, delivery, replaced, retry, pruned, text | A live context note reached the agent |
- Ignore topics you don't know. These are all the topics today, but new ones are added:
call.assistant_turnarrived in September 2026, andcall.context_noteafter it. A client written for an older list must skip what it doesn't recognise. corris a diagnostic label (t{turn}:n={step}:g={generation}). Don't build logic on it.call.context_note:sourceisserver(your systems) orclient(the caller's app).actorsays who sent it:key:<API key id>,user:<user id>orapp:<app id>. It isnullfor the caller's app.deliveryis where the note stood when it arrived (see Mid-call notes and activity).replacedistruewhen it replaced an earlier note with the same key.retryistruewhen it repeated a note the agent already had.prunedcounts older notes dropped to make room.charsis the note's length.
- Scrubbed. Every payload is scrubbed with the call's PII policy, and credential fields are removed.
- No heartbeats. A reader that falls behind skips the frames it missed; the socket stays open.
Softphone live-assist frames
On a manual (softphone) call with live assist on, call.transcript_partial and call.turn carry { text, speaker } instead: no corr, and speaker is member (your team member's side) or remote (the other party).
{"topic":"call.turn","sessionId":"9f1c2b8e-4a3d-4f26-9b71-0c5ad83e1f42","data":{"text":"So the renewal quote is eighteen thousand.","speaker":"member"}}
{"topic":"call.turn","sessionId":"9f1c2b8e-4a3d-4f26-9b71-0c5ad83e1f42","data":{"text":"That's higher than last year.","speaker":"remote"}}
Branch on whether speaker is present. Blank text is never sent.
3. When the socket closes
The socket closes with a WebSocket close frame, never a JSON error:
| Code | Reason | Meaning |
|---|---|---|
1000 | call ended | The call is over |
1008 | missing ticket | The URL had no ticket |
1008 | invalid or expired ticket | The ticket was used already, or is older than 30 seconds |
1008 | call is not live | The call ended before you connected |
Mid-call notes and activity
The requests are documented in Sessions & calls API → Tell the agent something mid‑call. What comes back:
A note was taken: 202
{ "success": true, "data": { "noteId": "0b6e2f4a-…", "key": "payment", "delivery": "next_turn" } }
keyechoes yours, ornullwithout one.deliveryisnext_turn,speaking_noworheld(see Live context notes → When the agent sees a note).
A note was refused
{ "success": false, "error": "too_large", "maxChars": 412 }
error is a stable code: empty, invalid_respond, not_live, no_agent, engine_unsupported, too_large, owner_unknown or owner_unreachable. Only too_large adds maxChars: how many characters (Unicode code points) of this note would fit right now. A 503 (owner_unknown, owner_unreachable) carries a Retry-After header. A 404 carries a message (Session not found), not a code. The status for each code is in the request reference.
Activity: 200
{ "success": true, "data": { "nextCheckinInMs": 20000 } }
nextCheckinInMs is how long until the agent would speak up unprompted. null means nothing is armed right now.
Replies on the call's WebSocket
The caller's app sends contextual_update and user_activity on the web-call WebSocket. Every frame gets exactly one reply, in the order sent:
{ "event": "context_ack", "key": "cart", "noteId": "0b6e2f4a-…", "delivery": "next_turn" }
{ "event": "context_rejected", "key": "cart", "reason": "too_large", "maxChars": 120 }
{ "event": "activity_ack", "nextCheckinInMs": 20000 }
{ "event": "activity_rejected", "reason": "not_live" }
reason uses the REST codes, plus disabled (the agent doesn't take notes from the caller's app) and rate_limited. A frame sent before start is answered not_live.
The call's notes: GET /api/sessions/{id}/context-notes
{
"success": true,
"data": [
{
"id": "0b6e2f4a-…",
"noteKey": "payment",
"body": "Payment of ₹4,500 received. UPI ref 41234.",
"source": "server",
"actor": "key:7c1d…",
"respond": "when_idle",
"delivery": "speaking_now",
"createdAt": "2026-07-29T09:18:02.114Z"
}
]
}
- Oldest first. Works during the call and after it.
deliveryis kept up to date. Awhen_idlenote moves tospeaking_nowonce the agent speaks up about it. A note dropped to make room for newer ones becomesdropped, and staysdropped.sourceandactorare as on the live stream.bodyandnoteKeyare stored with the call's PII policy applied.
Tool results
When the agent calls a tool, the tool's result goes back to the model as the tool's output. Its shape decides what the agent can say next.
What the model receives
Telenow passes your tool's JSON to the model as compact text (no spaces), unchanged. There is no { success, data } wrapper. How the text is wrapped depends on the model family. The same booking result, as each one receives it:
OpenAI and OpenAI-compatible models: a tool message.
{
"role": "tool",
"content": "{\"ok\":true,\"id\":\"7d3f9a1e-2c48-4b6a-9f10-5e8b2d4c7a63\",\"record\":{\"patient_name\":\"Asha Rao\"}}",
"tool_call_id": "call_9f2bA1",
"name": "book_appointment"
}
Anthropic (including Bedrock): a tool_result block in a user message. Results from one turn share one message, and there is no name.
{
"role": "user",
"content": [
{ "type": "tool_result", "tool_use_id": "toolu_01A9f2bA1", "content": "{\"ok\":true,\"id\":\"7d3f9a1e-2c48-4b6a-9f10-5e8b2d4c7a63\",\"record\":{\"patient_name\":\"Asha Rao\"}}" }
]
}
Gemini: a functionResponse part. A JSON object passes through as the response; anything else becomes { "result": … }.
{
"role": "user",
"parts": [
{ "functionResponse": { "name": "book_appointment", "response": { "ok": true, "id": "7d3f9a1e-2c48-4b6a-9f10-5e8b2d4c7a63", "record": { "patient_name": "Asha Rao" } } } }
]
}
OpenAI Realtime (speech-to-speech): a function_call_output item, with no role and no name.
{
"type": "conversation.item.create",
"item": { "type": "function_call_output", "call_id": "call_9f2bA1", "output": "{\"ok\":true,\"id\":\"7d3f9a1e-2c48-4b6a-9f10-5e8b2d4c7a63\",\"record\":{\"patient_name\":\"Asha Rao\"}}" }
}
Gemini Live (speech-to-speech): the result is always nested under response.result.
{
"toolResponse": {
"functionResponses": [
{ "id": "call_9f2bA1", "name": "book_appointment", "response": { "result": { "ok": true, "id": "7d3f9a1e-2c48-4b6a-9f10-5e8b2d4c7a63", "record": { "patient_name": "Asha Rao" } } } }
]
}
}
- A result that is not an object reaches the model as that literal text. A bare array arrives as
[…], andnullasnull. - How Telenow classifies a result in
tool.invokedand the call's tool log: a top-levelerrorkey makes iterror; anything else issuccess. So{ "ok": false, "error": "…" }counts as an error, and{ "results": [] }as a success. - Casing is whatever the tool returns. The built-in tools below mix
snake_caseandcamelCase. - A tool that fails returns
{ "error": "…" }, delivered the same way:
{ "role": "tool", "tool_call_id": "call_9f2bA1", "name": "book_appointment", "content": "{\"error\":\"…\"}" }
Confirm before executing
For a tool with requiresConfirmation, and for every payment.* connector action, the first call does not run the tool. The model gets this instead:
{
"status": "confirmation_required",
"instruction": "Do NOT treat this as done. Tell the caller you're about to send a payment link for ₹1499 to +919876543210, and ask them to confirm. Only after they clearly say yes, call this tool again with the same details. If they decline or change anything, do not proceed."
}
- After the caller agrees, the model calls the tool again with the same details, and the tool runs.
- The first call is recorded with status
confirmation_pending. If the details changed between the read-back and the caller's yes, the agent reads the new details back instead of running the tool; that call is recordedconfirmation_superseded. - The read-back uses only the tool's arguments, never secrets.
See Tools → Confirm before executing.
Built-in tools
Built-in tools answer with a status word plus a few keys of their own. They never use ok, and a failure is { "error": "…" }.
| Tool | Result | Notes |
|---|---|---|
end_call | { "status": "ending" } | The goodbye plays first; ending means the hang-up is on its way |
navigate (flow) | { "status": "navigating", "to": "node_collect_details" } | to is the step's id, not its label. Failure: navigate: unknown target |
transfer | { "status": "transferring", "to": "+912261234567" } | to is the number actually dialled: the first of the destination's numbers that is not on the Do-Not-Call list. Failures: transfer tool has no destination configured, all transfer destinations are on the do-not-call list, or the carrier's own error |
| Agent handoff | { "status": "handed_off", "mode": "transfer", "toAgent": "b41e77c2-…" } | mode is transfer or connect; toAgent is the target agent's id. An unknown destination lists the valid ones: unknown handoff destination "billing" — valid destinations: Sales, Support. With none configured: agent handoff tool has no target agent configured |
adjust_volume | { "status": "adjusted", "volume_percent": 141, "at_limit": false, "note": "volume changed. Do not announce the change or narrate this tool; just repeat your last point so they can judge it for themselves." } | volume_percent runs from 50 to 200 (100 is normal). At a limit, at_limit is true and note says so: already at maximum volume — if the caller still can't hear, suggest they raise their phone or speaker volume, or already at minimum volume |
set_language | { "status": "language_set", "language": "Hindi", "note": "…" } | language is the resolved name, not what the model sent. Failure: unrecognized language 'klingon' — retry with a plain language name like 'hindi', 'telugu', 'english' |
opt_out | { "status": "opted_out", "note": "recorded — the caller will not be contacted again. Confirm that plainly, then close politely." } | Takes no arguments: the number comes from the call. Its failures say plainly that nothing was recorded |
| Dataset lookup | { "result": [ … ], "row_count": 1 } | See below |
| Follow-up scheduling | { "status": "scheduled", "at": "2026-07-29T17:00:00+05:30", "numberEnding": "2345" } | See below |
noteis an instruction to the model, not text to show anyone. Afteradjust_volume,set_languageandopt_outthe model carries on and speaks.end_call,navigate,transferand a handoff end the model's turn. It sees the result but gets no further turn.
Dataset lookup (a structured knowledge base). result changes shape with the question, so read it together with row_count:
| Question | result | row_count |
|---|---|---|
| Matching rows | An array of rows; every cell value is a string. At most 20 rows | The number of rows |
| One total, such as a count | A number: { "result": 143, "row_count": null } | null |
| Totals per group | [ { "group": "Mumbai", "value": 42 } ] | The number of groups |
Failures: could not understand the lookup arguments: … (a malformed filter never turns into an unfiltered read), unknown column: <name>, unsupported op: <op>, <func> needs a numeric column; '<col>' is not numeric, and could not run that lookup on the dataset for any database error. The database's own message is never shown to the model.
Follow-up scheduling.
numberEndingis the last four digits. The full number is never given to the model.atkeeps the caller-local offset the model sent, so the agent reads back the right local time.- In a simulation the result is
{ "status": "simulated", "at": "…", "note": "simulation — no real follow-up call was scheduled" }and nothing is saved. - Failures are sentences for the model, for example:
follow-up calls only work on phone calls, and this is a web session — politely tell the caller you cannot schedule an automatic call-back from this channelthis call already has 3 pending follow-ups — do not schedule morethis number already has 2 pending follow-ups — do not schedule moreinvalid `at` value "5pm" — send an ISO 8601 date-time WITH a timezone offset (e.g. 2026-07-04T17:00:00+05:30), or use `in_minutes`
Unknown, unavailable and skipped tools
| Case | What the model receives |
|---|---|
The model names a tool the agent doesn't have (recorded as not_found) | { "error": "unknown tool: book_appointmnt" } |
| Speech-to-speech: the tool belongs to another step of the flow | { "error": "this tool is not available at this step: 'refund_order' belongs to a different step of this conversation. Use only the tools listed in your current instructions, or navigate to the step that offers it." } |
| Speech-to-speech: a kind of tool that can't run on speech-to-speech calls | { "error": "tool '<name>' (native kind None) is not available on speech-to-speech calls" } |
| An earlier tool in the same turn already ended, transferred or handed off the call | { "error": "skipped: an earlier tool in this turn already took over the call" } |
| The call is already ending, and the tool is a transfer or a handoff | { "error": "skipped: the call is already ending" } |
| A built-in tool of a kind this server doesn't know | { "error": "unsupported native tool kind: Some(\"voicemail\")" } |
Skipped tools still answer, so every tool call the model made gets a result.
In a flow's tool step
A flow's tool step doesn't give the result to a model. It copies the result's top-level text, number and true/false values into the call's variables, and adds _tool_status:
{ "_tool_status": "ok", "ok": true, "id": "7d3f9a1e-2c48-4b6a-9f10-5e8b2d4c7a63" }
- Objects and arrays are dropped. A step that returns
{ "results": [ … ] }adds no variable at all. A step that returns{ "ok": true, "id": …, "record": { … } }adds onlyokandid. _tool_statusisokwhenever the tool answered, including a soft refusal such as{ "ok": false, "error": "…" }. That refusal takes the step's success edge, with its text in anerrorvariable. Only a hard failure sets_tool_statustoerror. Branch onokorerrorwhen a tool can refuse.- In a simulation the step doesn't run, and
_tool_statusisok.
The tool.invoked webhook and the call trace
The tool.invoked webhook reports each tool call as a camelCase object: arguments and result as JSON (not text), status and latencyMs. status is success, error, not_found, abandoned, confirmation_pending or confirmation_superseded. result keeps the tool's own casing, so one payload can mix both.
When a call is traced (debug trace on), the call's trace records the same tool call as a tool.call event, in a different, snake_case shape:
{
"type": "tool.call",
"seq": 42,
"t_ms": 8137,
"corr": "t3:n=book_slot:g=1",
"id": "call_9XkQm2ZrT4",
"name": "book_appointment",
"kind": "connector",
"args": { "patient_name": "Asha Rao", "slot": "2026-08-03T10:30:00+05:30" },
"result": { "ok": true, "id": "7d3f9a1e-2c48-4b6a-9f10-5e8b2d4c7a63", "record": { "status": "booked" } },
"status": "success",
"error": null,
"latency_ms": 214,
"http": [
{
"kind": "connector",
"method": "POST",
"url": "https://api.clinic-crm.example/v1/appointments",
"request": {
"headers": { "authorization": "Bearer ***", "content-type": "application/json" },
"body": { "patient_name": "Asha Rao", "slot": "2026-08-03T10:30:00+05:30" },
"body_format": "json"
},
"response": {
"status": 201,
"headers": { "content-type": "application/json", "x-request-id": "c4f2" },
"body": { "ok": true, "id": "7d3f9a1e-2c48-4b6a-9f10-5e8b2d4c7a63", "record": { "status": "booked" } },
"body_format": "json",
"bytes": 96
},
"error": null,
"duration_ms": 209
}
]
}
| Field | Meaning |
|---|---|
type, seq, t_ms | The event's place in the call's timeline |
id | The model's tool-call id (the webhook doesn't carry it) |
kind | connector, mcp, app, native or tool |
args, result | The arguments, and the result as the model received it |
status, error, latency_ms | error is a text the webhook doesn't carry |
http | Every HTTP request the tool made (below) |
A flow's tool step records flow_tool.call instead (corr, name, kind, status, latency_ms, args, result, http, saved, unresolved; no id). A bundled tool records one tool.step per step (composite, step, name, kind, status, args, result, latency_ms, http).
http: the requests on the wire. Every HTTP request the tool made, in order, as it left Telenow and as it was answered.
resultis what the model received, after Telenow reshaped the reply (a connector's response pick, an MCP tool's content unwrapped).http[].response.bodyis the vendor's actual reply, before that.- How many entries:
- one for a webhook or a cURL-import tool;
- one per request for a connector (a spreadsheet lookup may page);
- three for an MCP tool (
initialize,notifications/initialized,tools/call, each named inlabel); []for a tool that made no HTTP request (a database source, a built-in messaging action, a built-in tool).
response: nullwitherrorset is a transport failure (DNS, TLS, timeout); the request half is still recorded. With noerror, it is a request whose answer was never awaited.- The key is absent on traces recorded before this capture existed. Absent means "unknown", never "none".
- Masking:
- Credentials are masked when captured (
Authorization: Bearer ***,?key=***,apikey=***in a form body, a password in the URL). - Bodies are read as JSON or a form, so the trace's own key-name redaction and PII scrub reach inside them.
- A body over 24 KB is kept as an 8 KB text preview, with
truncated: true.
- Credentials are masked when captured (
- Only on traced calls. With debug trace off nothing is captured and nothing changes.
Event samples: GET /api/v1/events/sample
The most recent real payload of one webhook event in your organization, or a canned one when there is none yet. Automation platforms use it to show sample fields. Authenticate with an API key.
GET /api/v1/events/sample?type=whatsapp.message.status
{
"event_type": "whatsapp.message.status",
"is_real": true,
"samples": [
{
"event": "whatsapp.message.status",
"channelId": "c41d9a02-6b58-4e17-9f83-2a7d0e5c1b64",
"channelPhone": "+14155550100",
"wamid": "wamid.HBgMOTE5ODEyMzQ1Njc4FQIAERgSN0YzQjRDNUQ2RTdGODA5MQA=",
"status": "delivered",
"recipient": "+919812345678",
"timestamp": "2026-07-29T09:14:07+00:00",
"errors": null,
"pricing": { "category": "utility", "billable": true, "type": "regular" },
"conversation": { "id": "a1b2c3d4e5f60718", "origin": { "type": "utility" } }
}
]
}
- Not wrapped in
{ success, data }. The answer's own keys are snake_case (event_type,is_real); the sample inside is camelCase. is_real: trueis your organization's latest real delivery.falseis a canned sample with visibly fake values.- The query parameter is
type(orevent). An unknown event type is a400that lists the valid ones. - Samples come from your webhook endpoints' deliveries only, never from an installed app's. So for
call.analyzedandcall.endedthis is the webhook shape, not what an app receives.
Related
- Sessions & calls API: placing calls, mid-call control, call history.
- Webhook events reference: every webhook payload.
- Tools & function calling: defining tools.
- App API responses: the app-key endpoints and the
window.telenow.*bridge.