event field that identifies which lifecycle event fired. Treat the body as a discriminated union and branch on that field.
string
Discriminator. One of
session.ended, call.transferred, call.ended, session.finalized, recording.ready.string
The Pathors session ID.
string
ISO 8601 timestamp of when the event was emitted.
session.ended
Fires when the conversation has ended. For call sessions, the call’s final status and duration may not be available yet at this moment.
string
Pathway node the agent was on when the conversation ended.
number
Length of
messages — the number of user/assistant turns delivered in this payload.array
Conversation transcript as
{ role, content, timestamp? } objects, in turn order. role is user or assistant — tool calls/results and system prompts are stripped. timestamp is the ISO 8601 time of the turn, present when available. For finalized voice calls this is the agent-authoritative conversation the caller actually heard.object
Variables extracted across the session, keyed by name. Besides agent-extracted values, this carries the variables injected at session start:
startAt / endAt, call metadata (fromNumber, toNumber), and — for SIP calls on a phone number with header capture configured — the values of captured custom SIP headers, keyed by the attribute name configured for each header (e.g. an X-Campaign-Id header captured as campaignId appears as extractedVariables.campaignId).string
deprecated
Legacy alias for
event from before multi-event support existed; Will be removed in a future version — new receivers should branch on event.call.transferred
Fires the moment a transfer destination answers, mid-call. Call sessions only.
What it is for: getting the conversation and the call’s context at hand-off time instead of after the call ends. Nothing in this payload is exclusive to it — the transcript, the caller’s number, the captured SIP headers all arrive again in session.ended / session.finalized. What differs is when. A system routing a caller to a human needs that context while the human is picking up, not once they have hung up.
How much earlier depends on how the transfer was carried out:
On top of that,
session.ended waits for post-session variable extraction (an LLM call) before it is emitted. This event waits for nothing: the agent reports it the instant the destination answers.
It is the only event delivered while the call is still up.
Successful transfers only. A declined or failed attempt (busy, rejected, no answer, destination not on the allowlist, no outbound trunk for the line) does not fire this event — the caller is still with the agent, so no hand-off happened, and the call’s own end-of-call events already describe the outcome.
Web calls fire too, with callType: "web". A web transfer is simulated: the agent says its hand-off line and ends the call — there is no SIP leg and no human receives the caller. That is deliberate, so the web preview can exercise a transfer webhook end to end, which means you should check callType before acting on a hand-off (paging a human, opening a ticket).
What it cannot carry: anything that only exists once the call is over — the final call status, the duration, the recording, and the post-session extracted variables. The call log row is not written yet at this point. Subscribe to session.finalized for those, and correlate the two by sessionId.
string
ID of the project that owns this agent.
string
Always
transferred — the event only fires on success. Same field name and vocabulary as call.ended’s callStatus, so you read the call’s state the same way whichever event you got.string
phone (a real SIP hand-off; the destination answered) or web (the web preview’s simulated hand-off; no human received the caller). See Call type.number
Length of
messages.array
The conversation up to the hand-off, in exactly the same shape as
session.ended’s messages ({ role, content, timestamp? }, user/assistant turns only). The human picking up the call can read what the caller already said.object
The call’s telephony identity — see The sip block. Omitted entirely for non-telephony sessions.
Call type
call.transferred, call.ended and session.finalized each carry callType, describing which surface the call came in on:
phone— a real telephony call over SIP.web— a browser call. A real conversation, but with no SIP leg.
session.finalized ever fires for those.
The distinction matters most on call.transferred: a web transfer is simulated, so check callType before acting on a hand-off.
The sip block
call.transferred, call.ended and session.finalized each carry a sip object describing the call’s telephony identity — the two call events because it is theirs, and session.finalized because it aggregates the call side (it already carries callStatus / callDuration / recordingUrl).
session.ended does not: it is purely session-scoped and fires for text channels too, where a telephony block could never apply.
It is omitted entirely when the session has no telephony identity (a text channel, or a session created before the field existed), so 'sip' in payload discriminates.
Those values also remain flattened into extractedVariables exactly as before — nothing was removed, and existing receivers need no change. What the block solves is different: from the payload alone you cannot tell whether extractedVariables.fromNumber is the caller’s number or a variable the agent happened to extract under that name, and captured custom SIP headers sit mixed in with everything else, so a receiver has to already know which attribute names were configured in order to pick them out.
string
Caller’s number (E.164 where the trunk provides it). Omitted when the trunk did not supply it — a
sip block can carry only headers.string
The dialed line — your number for inbound, the callee for outbound. Omitted on the same terms as
fromNumber.string
Call SID, on Twilio-backed lines only.
object
Custom SIP headers captured for this number, keyed by the configured attribute name (e.g.
{ "campaignId": "42" }). A configured header that the call did not carry is absent rather than empty. Sessions created through the legacy /config + /session pair never learn the capture config, so this is {} for them.call.ended
Fires when the call has ended. Call sessions only — text sessions never produce this event.
string
The project that owns the agent.
string
Final domain status. One of
userHangup, agentHangup, transferred, voicemail, errorTransferred, busy, userNoAnswer, userRejected, invalidNumber, sipTrunkFailure, agentNoAnswer, error, unknown, Ended.number
Call duration in seconds.
0 for non-traffic outcomes (busy, no-answer, invalid number).object
The call’s telephony identity — see The sip block.
recording.ready
Fires after a call’s recording finishes processing — on both success and failure. Call sessions only; text sessions never produce this event.
Recording processing gates session.finalized for call sessions: finalization waits until the recording succeeds or fails (with a timeout backstop), and the resulting recordingUrl also rides the session.finalized payload. Subscribe to recording.ready when you want the recording signal on its own — it is the only event that tells you a recording failed — otherwise session.finalized already carries the URL.
string
The project that owns the agent.
boolean
Whether the recording was processed and stored successfully. When
false, recordingUrl is omitted.string
Temporary download URL for the composite audio file. Present only when
success is true. The link is short-lived (valid for ~15 minutes) — download the file promptly. Once it expires there is currently no API to re-fetch it; download the recording from the call log in the Pathors dashboard instead.session.finalized
Fires once everything is done for this session — for call sessions that includes recording processing, so the payload merges the data you would otherwise piece together from session.ended, call.ended, and recording.ready, and your receiver only has to handle one event.
For text sessions, the call fields (callStatus, callDuration) are omitted entirely — not set to null. Branch on whether they’re present to discriminate call vs text:
string
The project that owns the agent.
string
Pathway node the agent was on when the conversation ended.
number
Length of
messages, same as session.ended’s messagesCount.array
Conversation transcript, same shape and filtering as
session.ended’s messages.object
Same as
session.ended’s extractedVariables — including captured custom SIP header values when the phone number has header capture configured.string
Call sessions only. Same as
call.ended’s callStatus. Omitted for text sessions.number
Call sessions only. Same as
call.ended’s callDuration. Omitted for text sessions.object
Call sessions only. The call’s telephony identity — see The sip block. Omitted for text sessions, like the other call-side fields.
string
Call sessions only. Temporary download URL for the composite audio file, same as
recording.ready’s recordingUrl. Present when the call was recorded and processing succeeded; omitted for text sessions and failed recordings. Short-lived (~15 minutes) — download promptly.Implementation example
A receiver that handles every event withevent as the discriminator:
Ordering
session.finalized is always last for a given session: it fires only after every other expected step has completed — session.ended for text sessions; session.ended, call.ended, and recording.ready for call sessions.
Typical order for a call session:
call.transferred(transferred calls only, and delivered mid-call)session.endedcall.endedrecording.readysession.finalized
call.transferred takes no part in finalization: a call that never transfers still finalizes normally instead of waiting for an event that will never arrive.
For text sessions, session.ended and session.finalized arrive back-to-back.
The individual steps (session.ended, call.ended, recording.ready) are not strictly ordered with respect to each other in edge cases (e.g. hard hangup before the agent reaches its final node), and HTTP delivery of recording.ready can race the session.finalized request it unblocked. What you can rely on: when session.finalized arrives, recording processing has finished, and a successful recording’s URL is in that payload.