# Live streams

Source: https://solidhooks.com/docs/streams

> Tail events and deliveries as they happen over Server-Sent Events, and resume cleanly after a reconnect.

Two endpoints let you react to activity without polling: `GET /v1/events/stream` and `GET /v1/deliveries/stream`. Both return `text/event-stream`.

Both are forward-only. A stream starts at the moment you connect and emits nothing historical. For history, use `GET /v1/events` and `GET /v1/deliveries`. Both close after 30 minutes, so treat the close as a reconnect signal, not an error.

## How it works

Each message carries an SSE `id`, and its `data` is the same JSON shape the matching list endpoint returns. Reconnect with a `Last-Event-ID` header and the server resumes from exactly that point, including rows that share the same timestamp. You do not keep a cursor of your own. A `Last-Event-ID` takes precedence over `after`.

The two streams differ in what counts as activity, and that changes how you consume them.

|                          | `/v1/events/stream`                               | `/v1/deliveries/stream`                            |
| ------------------------ | ------------------------------------------------- | -------------------------------------------------- |
| Anchored on              | `createdAt`                                       | Most recent attempt, falling back to `createdAt`   |
| Same id emitted again    | No. Events are immutable, so each is emitted once | Yes. Once on creation, then again on every attempt |
| What to do with a repeat | Nothing to do                                     | Replace by id, do not append                       |
| Scope parameters         | `sourceId`                                        | `eventId`, `status`                                |
| Permission               | `event: read`                                     | `delivery: read`                                   |

For a delivery, a manual retry re-enters the stream when its next attempt starts, not when you call [retry](https://solidhooks.com/docs/replay).

## Parameters

| Parameter  | Streams    | Meaning                                                                                                                |
| ---------- | ---------- | ---------------------------------------------------------------------------------------------------------------------- |
| `after`    | both       | Start from this time instead of now. Clamped to the last 24 hours. For deliveries it is matched against activity time. |
| `sourceId` | events     | Only events for this source.                                                                                           |
| `eventId`  | deliveries | Only deliveries for this event.                                                                                        |
| `status`   | deliveries | Only deliveries in this status.                                                                                        |

`after` is a start-from filter for a fresh stream, not a resume mechanism. A value older than 24 hours is ignored, and the stream starts at the moment of connection. These streams are not a history export.

## Authenticate

The stream endpoints take the same API key as the rest of `/v1`, sent as `Authorization: Bearer $SDHK_KEY`. They are project-scoped, so `X-Project-Id` applies as it does elsewhere.

> **Browser EventSource cannot connect directly**
>
> The browser `EventSource` API cannot set an `Authorization` header, and the stream does not accept the key any other way. Do not put an API key in a query string. Connect from a server, or from a script that can set headers, and relay what you need to a browser yourself.

## Tail from the command line

```bash
curl -N "$API/v1/events/stream?sourceId=src_..." \
  -H "authorization: Bearer $SDHK_KEY" \
  -H 'accept: text/event-stream'
```

`-N` turns off curl's buffering so messages print as they arrive. To resume after a drop, add the last `id` you saw:

```bash
curl -N "$API/v1/events/stream" \
  -H "authorization: Bearer $SDHK_KEY" \
  -H 'accept: text/event-stream' \
  -H 'last-event-id: <id from the last message>'
```

Treat the `id` as opaque. Pass it back unchanged.

## Consume from Node

This reads the stream with `fetch`, remembers the last `id`, and reconnects when the 30-minute close arrives. It keeps the latest copy of each delivery by id.

```ts
const API = process.env.API ?? 'https://api.solidhooks.com';
const headers = {
  authorization: `Bearer ${process.env.SDHK_KEY}`,
  accept: 'text/event-stream',
};

const deliveries = new Map<string, unknown>();
let lastEventId: string | undefined;

async function tail(): Promise<void> {
  const res = await fetch(`${API}/v1/deliveries/stream?status=failed`, {
    headers: lastEventId ? { ...headers, 'last-event-id': lastEventId } : headers,
  });
  if (!res.ok || !res.body) throw new Error(`stream refused: ${res.status}`);

  const decoder = new TextDecoder();
  let buffer = '';

  for await (const chunk of res.body) {
    buffer += decoder.decode(chunk as Uint8Array, { stream: true });
    const messages = buffer.split('\n\n');
    buffer = messages.pop() ?? '';

    for (const message of messages) {
      let id: string | undefined;
      const data: string[] = [];
      for (const line of message.split('\n')) {
        if (line.startsWith('id:')) id = line.slice(3).trim();
        if (line.startsWith('data:')) data.push(line.slice(5).trimStart());
      }
      if (data.length === 0) continue;

      const delivery = JSON.parse(data.join('\n')) as { id: string };
      deliveries.set(delivery.id, delivery);
      if (id) lastEventId = id;
    }
  }
}

for (;;) {
  try {
    await tail();
  } catch (error) {
    console.error(error);
    await new Promise((resolve) => setTimeout(resolve, 2000));
  }
}
```

The loop reconnects whenever the stream ends, which covers the 30-minute close. The `last-event-id` header makes each reconnect continue where the last one stopped.

## Next steps

* [Events](https://solidhooks.com/docs/events) — what an event row contains
* [Deliveries](https://solidhooks.com/docs/deliveries) — statuses and attempts
* [Replay and retry](https://solidhooks.com/docs/replay) — act on what you see in the stream
* [API keys](https://solidhooks.com/docs/api-keys) — scope a key to read events and deliveries only