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 as maxChars on 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 are camelCase. 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.

WhereWhat 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 webhookThe 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"
}
FieldTypeMeaning
sessionId, orgId, agentIduuidThe call, its organization and its agent. agentId can be null
statusstringThe analysis, not the call: pending, done or failed
summarystring | nullA short recap, written for the agent's owner
sentimentstring | nullpositive, neutral or negative
sentimentScorenumber | null−1 to 1
dispositionstring | nullThe 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
actionItemsstring[]Follow-ups found in the conversation
customDataobjectYour custom fields, keyed by their key. A call placed with its own fields (per-call settings) has those here too, beside the agent's
evidenceobjectVerbatim transcript quotes, keyed by the field or judgment they back
qaarrayOne { key, met, evidence } per QA rubric criterion
objectionsstring[]Concerns the caller raised
scoreinteger | nullThe judge's quality score, 0–100
coachingarray{ issue, suggestion, severity }; severity is low, medium, high or null
hallucinationsarray{ claim, why }
cxobject{ rating, friction, highlights }; rating is excellent, good, fair, poor or null
topics, keywordsstring[]Tags for reporting
agentWords, customerWords, agentTurns, customerTurnsintegerTalk-ratio counts, counted from the transcript (no model)
modelstring | nullThe 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
errorstring | nullWhy a failed analysis failed
createdAt, updatedAttimestamp
  • Empty, never missing. Every key is always present. Arrays default to [] and objects to {}. summary, sentiment, sentimentScore, disposition, score and model can be null. A pending or failed analysis 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, cx is { "rating": null, "friction": [], "highlights": [] }. qa is [] when the agent has no QA rubric.
  • Check customData before you read it. A field the transcript doesn't answer should be null, but nothing reconciles the model's answer against your configured keys: a key can be missing, and type is a hint, not a conversion.
  • Scrubbed. The free-text fields, and the string values in customData, evidence and qa, 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 data is null.

The call.analyzed webhook has a different shape

The webhook carries the same results, arranged differently:

REST object (above)call.analyzed webhook
Talk-ratio countsFlat: agentWords, customerWords, agentTurns, customerTurnsNested under analysis.talkRatio
sessionIdIn the objectAt the top level, beside agentId, orgId, identifier and occurredAt
status, error, createdAt, updatedAtIn the objectNot sent
PII the call capturedNot includedpiiCollected 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"
  }
}
  • wsUrl already 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."}}
TopicdataSent when
call.transcript_partialtext, corrThe caller is still speaking. The text changes as recognition settles
call.turntext, corrThe caller finished a turn
call.assistant_turntext, engine (cascade or s2s), corrThe agent replied. Once per reply, never per sentence, filler or opener
call.node_enterednode_id, node_name, node_kind, node_generation, llm_model, tts_provider, tts_voice, entry_message, duration_ms, corrA voice call entered a flow step. The step's prompt is never sent
call.barge_inplayed_ms, turn_audio_ms, corrThe caller interrupted the agent
call.dtmfdigit, corrThe caller pressed a key
call.silencetext, llm_generated, corrThe agent checked in after a silence
call.context_notenote_id, key, source, actor, chars, respond, delivery, replaced, retry, pruned, textA 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_turn arrived in September 2026, and call.context_note after it. A client written for an older list must skip what it doesn't recognise.
  • corr is a diagnostic label (t{turn}:n={step}:g={generation}). Don't build logic on it.
  • call.context_note:
    • source is server (your systems) or client (the caller's app).
    • actor says who sent it: key:<API key id>, user:<user id> or app:<app id>. It is null for the caller's app.
    • delivery is where the note stood when it arrived (see Mid-call notes and activity).
    • replaced is true when it replaced an earlier note with the same key.
    • retry is true when it repeated a note the agent already had.
    • pruned counts older notes dropped to make room.
    • chars is 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:

CodeReasonMeaning
1000call endedThe call is over
1008missing ticketThe URL had no ticket
1008invalid or expired ticketThe ticket was used already, or is older than 30 seconds
1008call is not liveThe 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" } }

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.
  • delivery is kept up to date. A when_idle note moves to speaking_now once the agent speaks up about it. A note dropped to make room for newer ones becomes dropped, and stays dropped.
  • source and actor are as on the live stream.
  • body and noteKey are 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 […], and null as null.
  • How Telenow classifies a result in tool.invoked and the call's tool log: a top-level error key makes it error; anything else is success. 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_case and camelCase.
  • 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 recorded confirmation_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": "…" }.

ToolResultNotes
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
  • note is an instruction to the model, not text to show anyone. After adjust_volume, set_language and opt_out the model carries on and speaks.
  • end_call, navigate, transfer and 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:

Questionresultrow_count
Matching rowsAn array of rows; every cell value is a string. At most 20 rowsThe number of rows
One total, such as a countA 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.

  • numberEnding is the last four digits. The full number is never given to the model.
  • at keeps 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 channel
    • this call already has 3 pending follow-ups — do not schedule more
    • this number already has 2 pending follow-ups — do not schedule more
    • invalid `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

CaseWhat 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 only ok and id.
  • _tool_status is ok whenever the tool answered, including a soft refusal such as { "ok": false, "error": "…" }. That refusal takes the step's success edge, with its text in an error variable. Only a hard failure sets _tool_status to error. Branch on ok or error when a tool can refuse.
  • In a simulation the step doesn't run, and _tool_status is ok.

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
    }
  ]
}
FieldMeaning
type, seq, t_msThe event's place in the call's timeline
idThe model's tool-call id (the webhook doesn't carry it)
kindconnector, mcp, app, native or tool
args, resultThe arguments, and the result as the model received it
status, error, latency_mserror is a text the webhook doesn't carry
httpEvery 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.

  • result is what the model received, after Telenow reshaped the reply (a connector's response pick, an MCP tool's content unwrapped). http[].response.body is 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 in label);
    • [] for a tool that made no HTTP request (a database source, a built-in messaging action, a built-in tool).
  • response: null with error set is a transport failure (DNS, TLS, timeout); the request half is still recorded. With no error, 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.
  • 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: true is your organization's latest real delivery. false is a canned sample with visibly fake values.
  • The query parameter is type (or event). An unknown event type is a 400 that lists the valid ones.
  • Samples come from your webhook endpoints' deliveries only, never from an installed app's. So for call.analyzed and call.ended this is the webhook shape, not what an app receives.