Skip to main content
Each payload starts with an event field that identifies which lifecycle event fired. Treat the body as a discriminated union and branch on that field.
type LifecycleWebhookPayload =
  | SessionEndedPayload
  | CallEndedPayload
  | SessionFinalizedPayload
  | RecordingReadyPayload;
Common fields on every event:
event
string
Discriminator. One of session.ended, call.ended, session.finalized, recording.ready.
sessionId
string
The Pathors session ID.
timestamp
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.
currentNodeId
string
Pathway node the agent was on when the conversation ended.
messagesCount
number
Total messages exchanged in the session.
extractedVariables
object
Variables extracted across the session, keyed by name.
reason
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.
{
  "event": "session.ended",
  "sessionId": "2c4f9a13-7e6b-4d8a-9f25-c81e3a7b6d04",
  "timestamp": "2026-05-03T08:42:11.512Z",
  "currentNodeId": "ask_intention",
  "messagesCount": 8,
  "extractedVariables": {
    "name": "Nancy",
    "intention": "book a demo"
  },
  "reason": "session_ended"
}

call.ended

Fires when the call has ended. Call sessions only — text sessions never produce this event.
projectId
string
The project that owns the agent.
callStatus
string
Final domain status. One of userHangup, agentHangup, transferred, voicemail, errorTransferred, busy, userNoAnswer, userRejected, invalidNumber, sipTrunkFailure, agentNoAnswer, error, unknown, Ended.
callDuration
number
Call duration in seconds. 0 for non-traffic outcomes (busy, no-answer, invalid number).
{
  "event": "call.ended",
  "sessionId": "2c4f9a13-7e6b-4d8a-9f25-c81e3a7b6d04",
  "projectId": "8f3e2c91-4a7b-4d6e-a23c-9b1f5e8d4c20",
  "timestamp": "2026-05-03T08:42:14.022Z",
  "callStatus": "userHangup",
  "callDuration": 78
}

session.finalized

Fires once everything is done for this session. The payload merges the data you would otherwise piece together from session.ended and call.ended, so 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:
if ("callStatus" in payload) {
  // call session — payload.callStatus, .callDuration are present
} else {
  // text session — call fields are absent
}
projectId
string
The project that owns the agent.
currentNodeId
string
Pathway node the agent was on when the conversation ended.
messagesCount
number
Total messages exchanged in the session.
extractedVariables
object
Variables extracted across the session.
callStatus
string
Call sessions only. Same as call.ended’s callStatus. Omitted for text sessions.
callDuration
number
Call sessions only. Same as call.ended’s callDuration. Omitted for text sessions.
Call session example:
{
  "event": "session.finalized",
  "sessionId": "2c4f9a13-7e6b-4d8a-9f25-c81e3a7b6d04",
  "projectId": "8f3e2c91-4a7b-4d6e-a23c-9b1f5e8d4c20",
  "timestamp": "2026-05-03T08:42:14.300Z",
  "currentNodeId": "ask_intention",
  "messagesCount": 8,
  "extractedVariables": {
    "name": "Nancy",
    "intention": "book a demo"
  },
  "callStatus": "userHangup",
  "callDuration": 78
}
Text session example:
{
  "event": "session.finalized",
  "sessionId": "5d8e1f2a-9c4b-4e7d-b3a8-6f9c2d5e8a01",
  "projectId": "8f3e2c91-4a7b-4d6e-a23c-9b1f5e8d4c20",
  "timestamp": "2026-05-03T08:48:15.288Z",
  "currentNodeId": "start",
  "messagesCount": 10,
  "extractedVariables": { "name": "Nancy" }
}

recording.ready

Fires after a call’s recording finishes processing — on both success and failure. Call sessions only; text sessions never produce this event. Unlike the lifecycle events, this one is independent of session.finalized — it does not gate finalization and can arrive either before or after session.finalized, depending on how long recording processing takes.
projectId
string
The project that owns the agent.
success
boolean
Whether the recording was processed and stored successfully. When false, recordingUrl is omitted.
recordingUrl
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.
Success example:
{
  "event": "recording.ready",
  "sessionId": "2c4f9a13-7e6b-4d8a-9f25-c81e3a7b6d04",
  "projectId": "8f3e2c91-4a7b-4d6e-a23c-9b1f5e8d4c20",
  "timestamp": "2026-05-03T08:42:30.114Z",
  "success": true,
  "recordingUrl": "https://recordings.pathors.com/recordings/projects/8f3e2c91.../sessions/2c4f9a13.../audio.ogg?..."
}
Failure example:
{
  "event": "recording.ready",
  "sessionId": "2c4f9a13-7e6b-4d8a-9f25-c81e3a7b6d04",
  "projectId": "8f3e2c91-4a7b-4d6e-a23c-9b1f5e8d4c20",
  "timestamp": "2026-05-03T08:42:30.114Z",
  "success": false
}

Implementation example

A receiver that handles every event with event as the discriminator:
import express from "express";

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

app.post("/pathors-webhook", async (req, res) => {
  const payload = req.body;

  switch (payload.event) {
    case "session.ended":
      await onSessionEnded(payload);
      break;

    case "call.ended":
      await onCallEnded(payload);
      break;

    case "session.finalized":
      if ("callStatus" in payload) {
        await onCallCompleted(payload);
      } else {
        await onTextCompleted(payload);
      }
      break;

    case "recording.ready":
      if (payload.success && payload.recordingUrl) {
        await onRecordingReady(payload); // fetch the URL before it expires
      }
      break;

    default:
      // Unknown event — accept and ignore so future Pathors event types
      // do not break your receiver.
      break;
  }

  res.status(200).json({ status: "ok" });
});

Ordering

Among the lifecycle events (session.ended, call.ended, session.finalized), session.finalized is always last for a given session. Typical order for a call session:
  1. session.ended
  2. call.ended
  3. session.finalized
For text sessions, session.ended and session.finalized arrive back-to-back. session.ended and call.ended are not strictly ordered with respect to each other in edge cases (e.g. hard hangup before the agent reaches its final node). recording.ready sits outside this order. It does not gate session.finalized and may arrive before or after it — do not assume the recording is ready just because session.finalized has fired.