> ## Documentation Index
> Fetch the complete documentation index at: https://docs.pathors.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhook Payloads

> 各事件投遞到你 endpoint 的 JSON payload。由最頂層 `event` 欄位區分。

每個 payload 開頭都有 `event` 欄位指出觸發的事件種類。把 body 當 discriminated union、用這個欄位分流。

```ts theme={null}
type LifecycleWebhookPayload =
  | SessionEndedPayload
  | CallEndedPayload
  | SessionFinalizedPayload
  | RecordingReadyPayload;
```

每個事件都有的共通欄位：

<ResponseField name="event" type="string">
  Discriminator。`session.ended` / `call.ended` / `session.finalized` / `recording.ready` 之一。
</ResponseField>

<ResponseField name="sessionId" type="string">
  Pathors session ID。
</ResponseField>

<ResponseField name="timestamp" type="string">
  事件 emit 時的 ISO 8601 timestamp。
</ResponseField>

## `session.ended`

對話結束時觸發。對 call session，通話的最終狀態跟時長此時可能還沒就緒。

<ResponseField name="currentNodeId" type="string">
  對話結束時 agent 停在的 pathway node。
</ResponseField>

<ResponseField name="messagesCount" type="number">
  `messages` 的長度 —— 這個 payload 送出的 user/assistant 對話輪數。
</ResponseField>

<ResponseField name="messages" type="array">
  對話逐字稿，依對話順序排列的 `{ role, content, timestamp? }` 物件。`role` 為 `user` 或 `assistant` —— tool call/result 跟 system prompt 都會被濾掉。`timestamp` 是該輪對話的 ISO 8601 時間，有記錄到才會出現。語音通話 finalize 後，這裡是 agent-authoritative 的對話內容（使用者實際聽到的版本）。
</ResponseField>

<ResponseField name="extractedVariables" type="object">
  這個 session 提取出來的變數，以名稱為 key。除了 agent 對話中提取的值之外，也包含 session 建立時注入的變數：`startAt` / `endAt`、通話 metadata（`fromNumber`、`toNumber`），以及 —— 若該電話號碼有設定 **header capture** 的 SIP 通話 —— 被擷取的 custom SIP header 值，以各 header 設定的 attribute 名稱為 key（例如 `X-Campaign-Id` header 設定擷取為 `campaignId`，就會出現在 `extractedVariables.campaignId`）。
</ResponseField>

<ResponseField name="reason" type="string" deprecated>
  `event` 在多事件支援之前的 legacy 別名。未來版本會移除 —— 新 receiver 請改用 `event`。
</ResponseField>

```json theme={null}
{
  "event": "session.ended",
  "sessionId": "2c4f9a13-7e6b-4d8a-9f25-c81e3a7b6d04",
  "timestamp": "2026-05-03T08:42:11.512Z",
  "currentNodeId": "ask_intention",
  "messagesCount": 2,
  "messages": [
    { "role": "assistant", "content": "您好，這裡是 Pathors，需要什麼協助？", "timestamp": "2026-05-03T08:40:52.101Z" },
    { "role": "user", "content": "我想預約 demo。", "timestamp": "2026-05-03T08:41:03.870Z" }
  ],
  "extractedVariables": {
    "name": "Nancy",
    "intention": "book a demo"
  },
  "reason": "session_ended"
}
```

## `call.ended`

通話結束時觸發。**Call session 限定** —— text session 不會收到這個事件。

<ResponseField name="projectId" type="string">
  擁有此 agent 的 project ID。
</ResponseField>

<ResponseField name="callStatus" type="string">
  最終 domain status。可能值：`userHangup`、`agentHangup`、`transferred`、`voicemail`、`errorTransferred`、`busy`、`userNoAnswer`、`userRejected`、`invalidNumber`、`sipTrunkFailure`、`agentNoAnswer`、`error`、`unknown`、`Ended`。
</ResponseField>

<ResponseField name="callDuration" type="number">
  通話秒數。對於不計費的結束狀態（busy、no-answer、無效號碼）為 `0`。
</ResponseField>

```json theme={null}
{
  "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
}
```

## `recording.ready`

通話錄音處理完成後觸發 —— **成功與失敗都會觸發**。**Call session 限定**，text session 不會收到。

對 call session 來說，錄音處理**會 gate `session.finalized`**：finalization 會等到錄音成功或失敗才發生（有 timeout backstop），成功的 `recordingUrl` 也會一併放進 `session.finalized` 的 payload。想單獨收到錄音訊號才需要訂閱 `recording.ready` —— 它是唯一會告訴你錄音**失敗**的事件；否則 `session.finalized` 已經帶了 URL。

<ResponseField name="projectId" type="string">
  擁有此 agent 的 project ID。
</ResponseField>

<ResponseField name="success" type="boolean">
  錄音是否成功處理並存檔。為 `false` 時不帶 `recordingUrl`。
</ResponseField>

<ResponseField name="recordingUrl" type="string">
  合併後音檔的臨時下載連結。只有 `success` 為 `true` 時才有。連結為短期有效（約 15 分鐘）—— 請儘快下載。過期後目前沒有 API 可重新取得，請改從 Pathors 後台的通話記錄下載錄音。
</ResponseField>

**成功範例：**

```json theme={null}
{
  "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?..."
}
```

**失敗範例：**

```json theme={null}
{
  "event": "recording.ready",
  "sessionId": "2c4f9a13-7e6b-4d8a-9f25-c81e3a7b6d04",
  "projectId": "8f3e2c91-4a7b-4d6e-a23c-9b1f5e8d4c20",
  "timestamp": "2026-05-03T08:42:30.114Z",
  "success": false
}
```

## `session.finalized`

這個 session 的所有事情都做完時觸發 —— 對 call session 來說**包含錄音處理**。Payload 已經把原本要從 `session.ended`、`call.ended`、`recording.ready` 各自抓出來的資料合併到同一個 body，receiver 只需處理這一個事件。

對 text session，call 相關欄位（`callStatus`、`callDuration`）**整個被省略**（不是設成 `null`）。用欄位有沒有來區分 call vs text：

```ts theme={null}
if ("callStatus" in payload) {
  // call session — payload.callStatus、.callDuration 都有
} else {
  // text session — call 相關欄位不存在
}
```

<ResponseField name="projectId" type="string">
  擁有此 agent 的 project ID。
</ResponseField>

<ResponseField name="currentNodeId" type="string">
  對話結束時 agent 停在的 pathway node。
</ResponseField>

<ResponseField name="messagesCount" type="number">
  `messages` 的長度，同 `session.ended` 的 `messagesCount`。
</ResponseField>

<ResponseField name="messages" type="array">
  對話逐字稿，格式與過濾規則同 `session.ended` 的 `messages`。
</ResponseField>

<ResponseField name="extractedVariables" type="object">
  同 `session.ended` 的 `extractedVariables` —— 電話號碼有設定 header capture 時，一樣包含擷取到的 custom SIP header 值。
</ResponseField>

<ResponseField name="callStatus" type="string">
  *Call session 限定。* 同 `call.ended` 的 `callStatus`。Text session 被省略。
</ResponseField>

<ResponseField name="callDuration" type="number">
  *Call session 限定。* 同 `call.ended` 的 `callDuration`。Text session 被省略。
</ResponseField>

<ResponseField name="recordingUrl" type="string">
  *Call session 限定。* 合併後音檔的臨時下載連結，同 `recording.ready` 的 `recordingUrl`。有錄音且處理成功時才有；text session 與錄音失敗時省略。連結為短期有效（約 15 分鐘）—— 請儘快下載。
</ResponseField>

**Call session 範例：**

```json theme={null}
{
  "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": 2,
  "messages": [
    { "role": "assistant", "content": "您好，這裡是 Pathors，需要什麼協助？", "timestamp": "2026-05-03T08:40:52.101Z" },
    { "role": "user", "content": "我想預約 demo。", "timestamp": "2026-05-03T08:41:03.870Z" }
  ],
  "extractedVariables": {
    "name": "Nancy",
    "intention": "book a demo"
  },
  "callStatus": "userHangup",
  "callDuration": 78,
  "recordingUrl": "https://recordings.pathors.com/recordings/projects/8f3e2c91.../sessions/2c4f9a13.../audio.ogg?..."
}
```

**Text session 範例：**

```json theme={null}
{
  "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": 2,
  "messages": [
    { "role": "user", "content": "嗨，我是 Nancy。", "timestamp": "2026-05-03T08:47:41.512Z" },
    { "role": "assistant", "content": "Nancy 您好！", "timestamp": "2026-05-03T08:47:43.006Z" }
  ],
  "extractedVariables": { "name": "Nancy" }
}
```

## 實作範例

用 `event` 欄位 dispatch 各事件的 receiver：

```ts theme={null}
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 "recording.ready":
      if (payload.success && payload.recordingUrl) {
        await onRecordingReady(payload); // 趁連結過期前抓檔
      }
      break;

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

    default:
      // 未知事件 — 接收後忽略，這樣未來 Pathors 加新事件時你的 receiver
      // 不會壞掉。
      break;
  }

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

## 順序

`session.finalized` 對某個 session 一定**最後**到：它只在所有預期的步驟都完成後才觸發 —— text session 只等 `session.ended`；call session 等 `session.ended`、`call.ended` **加上** `recording.ready`。

Call session 的典型順序：

1. `session.ended`
2. `call.ended`
3. `recording.ready`
4. `session.finalized`

Text session 則是 `session.ended` 跟 `session.finalized` 緊接著到。

個別步驟（`session.ended`、`call.ended`、`recording.ready`）彼此之間在 edge case（例如使用者直接掛斷在 agent 還沒走到結尾節點時）**沒有嚴格順序**，而且 `recording.ready` 的 HTTP 送達可能跟它解鎖的 `session.finalized` 請求互相 race。可以依賴的是：收到 `session.finalized` 時錄音處理必定已結束，成功的錄音 URL 就在該 payload 裡。
