|
| 1 | +--- |
| 2 | +title: "Session channels" |
| 3 | +sidebarTitle: "Channels" |
| 4 | +description: "The raw HTTP endpoints behind a session's .in and .out streams: append records, read them over SSE, and drain them non-streaming." |
| 5 | +--- |
| 6 | + |
| 7 | +Every session has two durable streams: `.in` carries records from your clients to the task, `.out` carries records from the task back to your clients. The [`sessions` SDK](/ai-chat/sessions) wraps these as `session.in.*` and `session.out.*`. This page documents the underlying HTTP endpoints for callers that aren't using the TypeScript SDK. |
| 8 | + |
| 9 | +All channel endpoints live under `/realtime/v1/sessions/{session}/{io}`, where: |
| 10 | + |
| 11 | +- `{session}` is the session's friendly ID (`session_…`) or your `externalId`. One token authorizes both forms. |
| 12 | +- `{io}` is either `in` or `out`. |
| 13 | + |
| 14 | +Authorize requests with a secret key or a [session public token](/management/authentication#session-scopes). The token's scopes decide what you can do — see [Authorization](#authorization) below. |
| 15 | + |
| 16 | +## Append a record |
| 17 | + |
| 18 | +Append a single record to a channel. |
| 19 | + |
| 20 | +```bash Append to .in |
| 21 | +curl -X POST "https://api.trigger.dev/realtime/v1/sessions/{session}/in/append" \ |
| 22 | + -H "Authorization: Bearer $TRIGGER_TOKEN" \ |
| 23 | + -H "Content-Type: application/json" \ |
| 24 | + -H "X-Part-Id: 0f8c2b1e-..." \ |
| 25 | + --data '{"type":"user-message","text":"hello"}' |
| 26 | +``` |
| 27 | + |
| 28 | +The body is the raw record — any text up to 1MiB (records over the per-record cap return `413`). The response is `{ "ok": true }`. |
| 29 | + |
| 30 | +Set the `X-Part-Id` header to a unique value per record to make the append idempotent: replaying the same `X-Part-Id` does not duplicate the record. Appending to a closed or expired session returns `400`. |
| 31 | + |
| 32 | +<Warning> |
| 33 | + Appending to `.out` requires a **secret key**. A session public token (even one with |
| 34 | + `write:sessions`) can only append to `.in` — appending to `.out` with a public token returns |
| 35 | + `403`. The `.out` stream is the task's to write. |
| 36 | +</Warning> |
| 37 | + |
| 38 | +## Read a channel over SSE |
| 39 | + |
| 40 | +Subscribe to a channel as a Server-Sent Events stream. New records are delivered as they arrive. |
| 41 | + |
| 42 | +```bash Read .out |
| 43 | +curl -N "https://api.trigger.dev/realtime/v1/sessions/{session}/out" \ |
| 44 | + -H "Authorization: Bearer $TRIGGER_TOKEN" \ |
| 45 | + -H "Last-Event-ID: 42" \ |
| 46 | + -H "Timeout-Seconds: 60" |
| 47 | +``` |
| 48 | + |
| 49 | +| Header | Direction | Description | |
| 50 | +| --- | --- | --- | |
| 51 | +| `Last-Event-ID` | request | Resume after this sequence number. Set it to the last `id:` you received to pick up exactly where you left off after a disconnect. | |
| 52 | +| `Timeout-Seconds` | request | How long the server holds the stream open with no new records before closing, `1`–`600`. | |
| 53 | + |
| 54 | +Each SSE event carries: |
| 55 | + |
| 56 | +- `id:` — the record's sequence number. Use the most recent one as `Last-Event-ID` to resume. |
| 57 | +- `data:` — a JSON record `{ "data": <record>, "id": <id> }`. For `.out` on a `chat.agent` session, `data` is a UI message chunk (text, reasoning, tool call, or a custom data part). |
| 58 | + |
| 59 | +```text |
| 60 | +id: 42 |
| 61 | +data: {"data":{"type":"text","text":"echo: hello"},"id":42} |
| 62 | +``` |
| 63 | + |
| 64 | +### Control records |
| 65 | + |
| 66 | +Some `.out` events are **control records** rather than data. A control record has an empty body and carries a `trigger-control` header naming its subtype: |
| 67 | + |
| 68 | +| Subtype | Meaning | |
| 69 | +| --- | --- | |
| 70 | +| `turn-complete` | The current turn finished. Carries sibling headers `public-access-token` (a refreshed session token), `session-in-event-id`, and `last-event-id`. | |
| 71 | +| `upgrade-required` | The session needs to hand off to a run on a newer deployed version. | |
| 72 | + |
| 73 | +Route control records by their subtype instead of treating them as message content. The TypeScript SDK does this for you — `session.out.read` filters control records out of the chunk stream and surfaces them through `onControl`. |
| 74 | + |
| 75 | +## Drain records non-streaming |
| 76 | + |
| 77 | +Fetch a batch of records without holding an SSE connection open. Useful for polling or for reading a tail at startup. |
| 78 | + |
| 79 | +```bash Drain .out |
| 80 | +curl "https://api.trigger.dev/realtime/v1/sessions/{session}/out/records?afterEventId=42" \ |
| 81 | + -H "Authorization: Bearer $TRIGGER_TOKEN" |
| 82 | +``` |
| 83 | + |
| 84 | +Pass `afterEventId` to return only records after that sequence number; omit it to read from the start of the retained window. The response is: |
| 85 | + |
| 86 | +```json |
| 87 | +{ |
| 88 | + "records": [ |
| 89 | + { "data": { "type": "text", "text": "echo: hello" }, "id": 43, "seqNum": 43 } |
| 90 | + ] |
| 91 | +} |
| 92 | +``` |
| 93 | + |
| 94 | +Each record carries `data`, `id`, `seqNum`, and an optional `headers` array (present on control records). Page forward by passing the highest `seqNum` you received as the next `afterEventId`. |
| 95 | + |
| 96 | +## Authorization |
| 97 | + |
| 98 | +The action you can take depends on your token and the channel: |
| 99 | + |
| 100 | +| Action | Endpoint | Required authorization | |
| 101 | +| --- | --- | --- | |
| 102 | +| Subscribe (SSE) | `GET .../{io}` | `read:sessions:{id}` — works on both `.in` and `.out` | |
| 103 | +| Drain records | `GET .../{io}/records` | `read:sessions:{id}` — works on both `.in` and `.out` | |
| 104 | +| Append to `.in` | `POST .../in/append` | `write:sessions:{id}` | |
| 105 | +| Append to `.out` | `POST .../out/append` | Secret key only | |
| 106 | + |
| 107 | +Reads work in both directions for a `read:sessions` token. Writes split by direction: a `write:sessions` token can append to `.in`, but `.out` is reserved for the task and requires a secret key. See [session scopes](/management/authentication#session-scopes) for how to mint a token. |
| 108 | + |
| 109 | +## Using the SDK instead |
| 110 | + |
| 111 | +If you're writing TypeScript, the [`sessions` SDK](/ai-chat/sessions) is the ergonomic path. `sessions.open(idOrExternalId)` returns a `SessionHandle` whose `session.in` and `session.out` channels call these endpoints for you, with auto-retry, `Last-Event-ID` resume, and control-record routing built in: |
| 112 | + |
| 113 | +```ts Your backend |
| 114 | +import { sessions } from "@trigger.dev/sdk"; |
| 115 | + |
| 116 | +const session = sessions.open(chatId); |
| 117 | + |
| 118 | +// append to .in |
| 119 | +await session.in.send({ type: "user-message", text: "hello" }); |
| 120 | + |
| 121 | +// read .out over SSE |
| 122 | +const stream = await session.out.read({ signal: AbortSignal.timeout(30_000) }); |
| 123 | +for await (const chunk of stream) { |
| 124 | + console.log(chunk); |
| 125 | +} |
| 126 | +``` |
| 127 | + |
| 128 | +See [`session.in`](/ai-chat/sessions#session-in-—-clients-→-task) and [`session.out`](/ai-chat/sessions#session-out-—-task-→-clients) for the full handle API. |
0 commit comments