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

# Event streams

> Follow your organisation's events and a session's run over server-sent events, with replay from the last event id: the streams behind MCP resources/subscribe.

Two streams let an agent follow the work instead of polling for it. Both are [server-sent events](https://html.spec.whatwg.org/multipage/server-sent-events.html) on the same `/v1` API as everything else, and take an API key (`atn_live_…`) or an OAuth connection, with the `read` scope.

| Stream | What it carries |
| - | - |
| `GET /v1/events` | Every event in your organisation: the same envelopes, under the same ids, that a [webhook](https://docs.attensira.com/api/webhooks.md) receiver is sent |
| `GET /v1/sessions/{id}/events` | One session's run, step by step, including when it is waiting on you |

## Frames

An event frame has an `id:` line, an `event:` line and one `data:` line of JSON. The `awaiting_person` and `stream_error` frames carry no `id:` line, so they never move your cursor:

```text theme={null}
id: evt_6720a1f3c4e5b6a7d8e9f001
event: change.shipped
data: {"id":"evt_6720a1f3c4e5b6a7d8e9f001","type":"change.shipped","created_at":"2026-10-19T09:12:03Z","project_id":"ws_51ab","data":{...}}
```

A line starting with `:` is a keep-alive, sent every 15 seconds. Ignore it.

## Reconnecting without losing anything

Send the last `id:` you received as `?after=` or as the `Last-Event-ID` header (an `EventSource` does this for you). The stream replays everything after it, in order, then keeps tailing. Nothing between the two connections is lost.

A stream also ends on its own:

| Frame | Why | What to do |
| - | - | - |
| `event: reconnect` (after a `retry: 1000` line) | The stream has been open about nine minutes. The frame carries an `id:`: the cursor to resume from, even if no event was sent to you | Reconnect with that id |
| `event: stream_error`, `code: credential_revoked` | The key was revoked or expired, or the connection's token expired. Checked every 30 seconds | Reconnect with a valid key or token |
| `event: stream_error`, `code: stream_failed` | We could not read the log just now | Reconnect with your last id |

The frame is `stream_error`, not `error`, because an `EventSource` fires its own `error` event on every disconnect.

## How many streams can be open

An organisation may hold 100 streams open at once, both kinds together. Opening one more is a `429` [`rate_limited`](https://docs.attensira.com/api/errors#rate_limited) with `Retry-After: 30`, before any frame is sent. One `GET /v1/events` stream carries every event in the organisation, so most agents need only one; if you meet the limit, look for a reconnect that does not close the stream it replaces.

## GET /v1/events

> [!NOTE]
>
> Read-only. No credits. Works on a spent balance.

Your organisation's events, from every workspace in it.

| Parameter | In | Type | Notes |
| - | - | - | - |
| `after` | query | string | An event id this stream (or a webhook body) carried. Replays from just after it. `0` replays the whole log. Leave it out to follow from now |
| `types` | query | string | Comma-separated [event types](https://docs.attensira.com/api/webhooks.md#events). Leave it out for every type. An unknown type is a `400` that lists the valid ones |

```bash Shell theme={null}
curl -N "https://api.attensira.com/v1/events?types=change.shipped,needs_you" \
  -H "Authorization: Bearer atn_live_<your key>"
```

The log keeps 30 days. An `after` id older than that, or one that is not your organisation's, is a `404`: reconnect without `after` and read anything older from the objects themselves.

An event id here is the `id` in the body a webhook receiver gets for the same event, so a client that takes both can dedupe on it.

## GET /v1/sessions/\{id}/events

> [!NOTE]
>
> Read-only. No credits. Works on a spent balance.

One session's run. Each frame's `id` is the step's sequence number and `event` is its kind (`text.delta`, `tool.start`, `ask.request`, `session.end` and so on). The stream ends at the end of the turn it is in: the first `session.end` after `after` that finishes a run (`completed`, `failed` or `cancelled`). It sends nothing past it, even when a later turn is already in the log; read that turn from its own `after_seq`, or reconnect with the last `id` you received. A run that stops to wait on you also writes a `session.end`, with status `awaiting_person`; the stream stays open through it.

| Parameter | In | Type | Notes |
| - | - | - | - |
| `after` | query | integer | A sequence number this stream sent. `0` (the default) replays from the session's first turn, up to that turn's end |

A session in another organisation is a `404`.

### Following one turn of a thread

A thread's log holds every turn of it. To follow only the turn you just started, open the stream at the `after_seq` the ask answered with:

| Ask | Answer |
| - | - |
| `POST /v1/ask` (with or without `session`) | `{session_id, status, answer, poll_after_ms, after_seq}` |
| `POST /v1/threads` | `{thread, state, after_seq}`, where `after_seq` is always `0` |
| `POST /v1/threads/{id}/messages` | `{thread, state, after_seq}` |

`after_seq` is the last sequence number written before your message, so your message is the first frame of `GET /v1/sessions/{id}/events?after=<after_seq>`. You get every step of this turn, none of an earlier turn's, and the stream ends at this turn's end. A retry with the same `idempotency_key` answers the same `after_seq` as the first call.

```bash Shell theme={null}
curl -s -X POST "https://api.attensira.com/v1/threads/ses_9d02/messages" \
  -H "Authorization: Bearer atn_live_<your key>" \
  -H "Content-Type: application/json" \
  -d '{"message":"And the pricing page?"}'
# {"thread":"attensira://ws_51ab/thread/ses_9d02","state":"queued","after_seq":41}

curl -N "https://api.attensira.com/v1/sessions/ses_9d02/events?after=41" \
  -H "Authorization: Bearer atn_live_<your key>"
```

If you send a message while the thread's previous run is still going, your message is written first, that run writes its last steps (its answer, or its failure) after it, and then your turn is queued. Your message carries `behindRun: true`. A stream opened at your `after_seq`, or reconnected right after your message, leaves that run's last steps out: you get your message, then your turn from the moment it is queued. A stream opened anywhere else sends the log as it is. If that run fails, your turn still runs: the failure is in the log as the run's last word, and the session is not ended. If that run is cancelled after your message, the thread stops with it and your message is not answered: the stream at your `after_seq` ends after your message, with no end frame.

Over MCP, `ask` does this for you. With a `progressToken`, the call follows the turn from `after_seq` and reports each step as a progress notification. Its result carries the same point as `cursor` (on the `ask` verb, in `_meta` as `attensira/cursor`), for a new thread and a continued one alike. MCP `get` takes no cursor: to follow the turn yourself, stream `GET /v1/sessions/{id}/events?after=<cursor>`.

### When the run is waiting on you

When the run stops for an approval, the stream sends an `awaiting_person` frame. It has no `id`, so it never moves your cursor, and it is sent again whenever you reconnect while the run is still waiting:

```json theme={null}
{
  "state": "awaiting_person",
  "session_id": "ses_9d02",
  "thread": "attensira://ws_51ab/thread/ses_9d02",
  "after_seq": 14,
  "ask": {
    "id": "ask_ses_9d02_1",
    "ref": "attensira://ws_51ab/ask/ask_ses_9d02_1",
    "request": { "type": "approval", "toolName": "open_pull_request", "effect": "reversible" },
    "expires_at": "2026-10-21T09:12:03Z",
    "answer": {
      "approve": { "method": "POST", "path": "/v1/inbox/asks/ask_ses_9d02_1/approve?project=ws_51ab", "body": { "session_id": "ses_9d02" } },
      "decline": { "method": "POST", "path": "/v1/inbox/asks/ask_ses_9d02_1/decline?project=ws_51ab", "body": { "session_id": "ses_9d02" } }
    }
  }
}
```

Approving needs a key with an `approve` scope for that change; declining needs `steer`. See [Inbox](https://docs.attensira.com/api/endpoints.md) and [API keys](https://docs.attensira.com/account/api-keys.md). The run carries on once the ask is answered, and the stream carries on with it.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.