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

# Realtime (WebSocket)

> Stream your workspace events live over a WebSocket.

For low-latency agents, open a WebSocket and SentFromAI pushes your events the moment they happen —
no webhook endpoint required.

## Connect

```
wss://api.sentfrom.ai/v1/realtime?token=sf_live_…
```

Authentication is the `token` query parameter (a WebSocket handshake can't set headers), using
the same API key as the REST API.

<CodeGroup>
  ```ts TypeScript theme={null}
  const ws = new WebSocket(`wss://api.sentfrom.ai/v1/realtime?token=${process.env.SENTFROMAI_API_KEY}`);

  ws.onmessage = (e) => {
    const frame = JSON.parse(e.data);
    if (frame.event === "connected") return;        // handshake ack
    if (frame.event === "message.received") {
      console.log("New email:", frame.data.message.subject);
    }
  };
  ```

  ```python Python theme={null}
  import json, os, websockets, asyncio

  async def main():
      url = f"wss://api.sentfrom.ai/v1/realtime?token={os.environ['SENTFROMAI_API_KEY']}"
      async with websockets.connect(url) as ws:
          async for raw in ws:
              frame = json.loads(raw)
              if frame["event"] == "message.received":
                  print("New email:", frame["data"]["message"]["subject"])

  asyncio.run(main())
  ```
</CodeGroup>

## Frames

The first frame acknowledges the connection:

```json theme={null}
{ "event": "connected", "data": { "tenant_id": "…" } }
```

After that, every event is pushed as `{ event, data }`, where `data` carries the same fields as the
corresponding [webhook payload](/concepts/receiving):

```json theme={null}
{
  "event": "message.received",
  "data": {
    "inbox_id": "…",
    "thread_id": "…",
    "message": { "id": "…", "from": "…", "subject": "…", "text": "…", "attachments": [] }
  }
}
```

The event names match webhooks: `message.received`, `message.delivered`, `message.bounced`,
`message.complained`, `message.rejected`.

<Note>
  Realtime frames nest the payload under `data`; webhooks deliver the same fields flat alongside
  `event`. Read `frame.data.*` over the socket.
</Note>

## Reconnecting

If the socket drops, reconnect and resume. The stream is **live-only** (it doesn't replay missed
events), so for guaranteed delivery pair it with a [webhook](/concepts/receiving) or reconcile with
`GET /messages` on reconnect.

## Realtime vs webhooks

|                               | Realtime (WebSocket)         | Webhooks                        |
| ----------------------------- | ---------------------------- | ------------------------------- |
| Latency                       | Instant push                 | Instant push                    |
| Needs a public URL            | No                           | Yes                             |
| Guaranteed delivery / retries | No (live-only)               | Yes (retried)                   |
| Best for                      | Interactive agents, live UIs | Reliable server-side processing |

Many apps use **both** — a webhook for durable processing and the WebSocket for a live view.
