# Stream Playground run events

```http
GET /api/public/v1/competitions/mathematics-distillation-challenge-equational-theories-stage2/playground/runs/{runId}/events
Accept: text/event-stream
```

**Scope**: `playground.read`.

Streams the persisted lifecycle and per-problem events for a Run owned by the calling account. Use this endpoint for low-latency progress notifications and [List Playground Run Results](./list-playground-results.md) for durable result pagination and optional answer retrieval.

The stream is replayable. A first connection receives the Run's stored public events from the beginning and then follows new events. A resumed connection receives only events after the supplied `Last-Event-ID`.

Read access remains available if the Playground becomes read-only or the competition closes.

## Authentication and clients

Authentication uses the normal `Authorization: Bearer` header. Native browser `EventSource` cannot attach that header, so it cannot call this endpoint directly. Use a Fetch-based SSE client or another SSE library that supports request headers.

Do not put an API key in the URL or an `access_token` query parameter; URLs may be retained in browser history, proxy logs, and monitoring systems.

## Path parameters

| Parameter | Type | Required | Description |
| :--- | :--- | :---: | :--- |
| `runId` | string | Yes | Run ID returned by create, list, or detail. |

## Request headers

| Header | Required | Description |
| :--- | :---: | :--- |
| `Authorization` | Yes | `Bearer $SAIR_API_KEY`. |
| `Accept` | Yes | Must include `text/event-stream`. |
| `Last-Event-ID` | No | Opaque `id` value from the last fully processed event. Pass it unchanged when reconnecting. |

The request has no body and no query parameters.

## Response stream

Successful responses return `200 OK`, use `Content-Type: text/event-stream; charset=utf-8`, and do not use the JSON response envelope. Process each complete event frame as it arrives.

| Response header | Value |
| :--- | :--- |
| `Content-Type` | `text/event-stream; charset=utf-8` |
| `Cache-Control` | `no-cache, no-transform` |
| `X-Accel-Buffering` | `no` where the serving proxy supports it |

Every public event frame contains `id`, `event`, and one JSON `data` object:

```text
id: s2evt_01JSTAGE200000000000000003
event: result.created
data: {"runId":"run_01JSTAGE200000000000000001","result":{"resultId":"s2res_01JSTAGE200000000000000001","sequence":0,"problem":{"position":0,"problemId":"prob_01JSTAGE200000000000000001"},"outcome":"accepted","verdict":"true","judge":{"status":"accepted"},"diagnostic":null,"elapsedMs":1234,"completedAt":"2026-08-30T08:21:33Z"},"progress":{"completedProblems":1,"totalProblems":1},"summary":{"accepted":1,"rejected":0,"errors":0,"notAttempted":0},"occurredAt":"2026-08-30T08:21:33Z"}
```

The `id` is an opaque stable deduplication and resume token. It is distinct from the result's numeric `sequence`; do not decode or construct either value.

## Delivery and reconnection

- Events are persisted with the Run and delivered at least once. Store the event `id` only after processing its complete `data` payload.
- Reconnect with `Last-Event-ID`. The service replays stored events strictly after that ID, then continues following the Run.
- A reconnect can repeat the last event when a connection is lost before the client persists its ID. Make event handling idempotent and deduplicate by `id`.
- Without `Last-Event-ID`, the service replays the complete public event history for the Run.
- Event IDs remain valid while the Run record exists. An unknown, malformed, or cross-Run ID is rejected before the stream starts.
- During idle periods, the service sends an SSE comment heartbeat at least every 15 seconds. Heartbeats have no event ID, are not replayed, and must be ignored by application logic.
- The service may send an SSE `retry` field with a reconnect delay. Clients should honor it or use capped exponential backoff.

Opening or reopening a stream consumes the normal read-request quota. Individual data frames and heartbeats do not count as additional HTTP requests.

## Event types

All `data` objects contain `runId` and an ISO 8601 UTC `occurredAt` timestamp.

| Event | When | Event-specific data |
| :--- | :--- | :--- |
| `run.queued` | The Run has been accepted and its initial credits reserved. | `status: "pending"`, `track`, `progress`, `creditsHeld`, `canCancel: true`, and `createdAt`. |
| `run.started` | Run execution starts or resumes after an interruption. | `status: "running"`, `progress`, `canCancel: false`, and `startedAt`. |
| `run.requeued` | An interrupted practice evaluator returns unfinished work to the queue. | `status: "pending"`, `progress`, `canCancel: false`, original `startedAt`, and a safe `reason`. |
| `problem.started` | A Solo problem execution starts. The same problem can emit another start after an interrupted evaluator retry. | `problem.position`, `problem.problemId`, and `startedAt`. |
| `result.created` | A completed result becomes available. | Compact `result`, current `progress`, and current outcome `summary`. |
| `run.terminal` | Final status, results, usage, and settled credits are available. | Terminal `status`, `progress`, `summary`, `creditsCharged`, nullable `failure`, and `finishedAt`. |

Marathon Runs emit results for the whole batch at completion, including `not_attempted` outcomes where no answer was produced. They do not emit a separate `problem.started` event for each problem.

The compact object in `result.created.result` uses the same `resultId`, `sequence`, problem position, outcome, solver `verdict`, judge status, safe diagnostic, nullable `elapsedMs`, and `completedAt` semantics as the results endpoint. Retrieve answer details from the results endpoint when needed.

## Terminal behavior

`run.terminal` is the last event. The service emits it only after all visible result rows and final credit settlement are readable from the detail and results endpoints, then flushes the frame and closes the connection.

If a connection closes without `run.terminal`, treat the close as an interruption and reconnect with `Last-Event-ID`. Do not infer Run completion from an EOF, timeout, proxy disconnect, or network error.

Connecting to an already terminal Run replays the requested event history through `run.terminal` and then closes. Reconnecting after the terminal event ID returns an empty successful stream and closes immediately.

## Example request

`curl --no-buffer` prints each frame as it arrives:

```bash
curl --no-buffer "https://api.sair.foundation/api/public/v1/competitions/mathematics-distillation-challenge-equational-theories-stage2/playground/runs/run_01JSTAGE200000000000000001/events" \
  -H "Authorization: Bearer $SAIR_API_KEY" \
  -H "Accept: text/event-stream"
```

Resume after the last event your client fully processed:

```bash
curl --no-buffer "https://api.sair.foundation/api/public/v1/competitions/mathematics-distillation-challenge-equational-theories-stage2/playground/runs/run_01JSTAGE200000000000000001/events" \
  -H "Authorization: Bearer $SAIR_API_KEY" \
  -H "Accept: text/event-stream" \
  -H "Last-Event-ID: s2evt_01JSTAGE200000000000000003"
```

## Example stream

```text
retry: 3000

id: s2evt_01JSTAGE200000000000000001
event: run.queued
data: {"runId":"run_01JSTAGE200000000000000001","status":"pending","track":"solo","progress":{"completedProblems":0,"totalProblems":1},"creditsHeld":0.1,"canCancel":true,"createdAt":"2026-08-30T08:21:30Z","occurredAt":"2026-08-30T08:21:30Z"}

id: s2evt_01JSTAGE200000000000000002
event: run.started
data: {"runId":"run_01JSTAGE200000000000000001","status":"running","progress":{"completedProblems":0,"totalProblems":1},"canCancel":false,"startedAt":"2026-08-30T08:21:31Z","occurredAt":"2026-08-30T08:21:31Z"}

: keep-alive

id: s2evt_01JSTAGE200000000000000003
event: result.created
data: {"runId":"run_01JSTAGE200000000000000001","result":{"resultId":"s2res_01JSTAGE200000000000000001","sequence":0,"problem":{"position":0,"problemId":"prob_01JSTAGE200000000000000001"},"outcome":"accepted","verdict":"true","judge":{"status":"accepted"},"diagnostic":null,"elapsedMs":1234,"completedAt":"2026-08-30T08:21:33Z"},"progress":{"completedProblems":1,"totalProblems":1},"summary":{"accepted":1,"rejected":0,"errors":0,"notAttempted":0},"occurredAt":"2026-08-30T08:21:33Z"}

id: s2evt_01JSTAGE200000000000000004
event: run.terminal
data: {"runId":"run_01JSTAGE200000000000000001","status":"done","progress":{"completedProblems":1,"totalProblems":1},"summary":{"accepted":1,"rejected":0,"errors":0,"notAttempted":0},"creditsCharged":0,"failure":null,"finishedAt":"2026-08-30T08:21:34Z","occurredAt":"2026-08-30T08:21:34Z"}
```

## Errors

Errors detected before streaming starts use the standard JSON error envelope.

| HTTP | Code | When |
| :---: | :--- | :--- |
| `400` | `MALFORMED_BODY` | `Last-Event-ID` is malformed, unknown, or belongs to another Run. |
| `404` | `NOT_FOUND` | The Run does not exist, belongs to another account, or belongs to another competition. |
| `406` | `NOT_ACCEPTABLE` | `Accept` does not include `text/event-stream`. |

After a `200` response begins, the HTTP status cannot change. An unexpected server or network failure closes the stream; reconnect with the last processed event ID. See [Errors](../../../errors.md) for authentication, scope, and rate-limit errors.
