# Stream public events and live drafts

Server-sent events. Resume with Last-Event-ID set to the SSE id field (the JSON event cursor), not the JSON event id. Deduplicate by JSON event id. With threadId, draft frames carry messageId, streamId and revision and replace earlier snapshots; draft.removed clears them. Draft frames have no event cursor or replay guarantee. Heartbeats arrive every 15 seconds, with authorization revalidated at each heartbeat.

`GET /api/events/stream`

Required permission: `thread:read`.

## Parameters

| Name | In | Type | Required | Description |

| --- | --- | --- | --- | --- |

| `projectId` | query | `string` | No | — |

| `threadId` | query | `string` | No | — |

| `runId` | query | `string` | No | — |

| `types` | query | `string` | No | — |

| `cursor` | query | `string` | No | — |

| `limit` | query | `integer` | No | — |

## Example request

```bash
curl --request GET \
  --url https://api.hoplite.sh/api/events/stream \
  --no-buffer --max-time 60 \
  --header "X-Api-Key: $HOPLITE_API_KEY"
```

## 200 response

Durable event and replaceable draft frames

Content type: `text/event-stream`. Response type: `string`.

```text
id: opaque-cursor
event: run.completed
data: {"id":"event_example","orgId":"org_example","projectId":"project_example","threadId":"thread_example","runId":"run_example","sequence":"42","type":"run.completed","data":{"id":"run_example","status":"completed"},"occurredAt":"2026-09-22T10:00:00.000Z","publishedAt":"2026-09-22T10:00:01.000Z","cursor":"opaque-cursor"}


```

## Response headers

| Header | Type | Statuses | Description |

| --- | --- | --- | --- |

| `x-request-id` | `string` | `200`, `400`, `401`, `402`, `403`, `404`, `409`, `410`, `429`, `500`, `501`, `503` | Request correlation ID. |

| `RateLimit` | `string` | `200`, `400`, `401`, `402`, `403`, `404`, `409`, `410`, `429`, `500`, `501`, `503` | Current ingress policy, remaining requests, and seconds until reset, for example `"ingress";r=599;t=60`. |

| `RateLimit-Policy` | `string` | `200`, `400`, `401`, `402`, `403`, `404`, `409`, `410`, `429`, `500`, `501`, `503` | Ingress quota and window, for example `"ingress";q=600;qu="requests";w=60`. |

| `X-RateLimit-Limit` | `integer` | `200`, `400`, `401`, `402`, `403`, `404`, `409`, `410`, `429`, `500`, `501`, `503` | Maximum requests allowed in the current rolling window. |

| `X-RateLimit-Remaining` | `integer` | `200`, `400`, `401`, `402`, `403`, `404`, `409`, `410`, `429`, `500`, `501`, `503` | Requests remaining in the current rolling window. |

| `X-RateLimit-Reset` | `integer` | `200`, `400`, `401`, `402`, `403`, `404`, `409`, `410`, `429`, `500`, `501`, `503` | Unix timestamp when the current rolling window resets. |

| `Retry-After` | `integer` | `429`, `503` | Seconds until another request should be attempted. |

| `X-Retry-After` | `integer` | `429`, `503` | Legacy retry delay in seconds. |

## Status codes

- `200` — Durable event and replaceable draft frames

- `400` — Invalid request. Validation responses include field paths.

- `401` — The API key is missing, invalid, or lacks the operation's permission.

- `402` — The organization does not have access to the requested product capability.

- `403` — The key's current user no longer has the required organization membership or role.

- `404` — The resource is not available in the authenticated scope.

- `409` — The request conflicts with a retry receipt or current resource state.

- `410` — The event cursor has expired; capture a fresh head and resource snapshot.

- `429` — The API-key rate limit has been exceeded.

- `500` — The API encountered an unexpected failure.

- `501` — The deployment has not configured the requested capability.

- `503` — A required API dependency, including the rate limiter, is unavailable.