Stream live results
While your tests run, reporters can stream results to TestLookup, and the web app shows them live. This page lists each live route, the credential it needs and the IDs it returns. Most teams stream through a reporter library (SDK) instead of calling these routes directly. See SDK, CLI, and MCP.
Session versus run IDs
Section titled “Session versus run IDs”A live ID can be a readable name, called a slug, instead of a UUID. Use the test_run_id that TestLookup returns for run APIs and run links. Do not build a run address from the live slug. Each route returns the run’s UUID in a named field:
| Route | Run UUID | Other IDs |
|---|---|---|
POST /api/v1/stream/sessions | session_id. The route creates a new UUID, so session_id, run_id and the stored run’s ID are the same value. | session_token, which you send as X-Session-Token on POST /api/v1/stream/events/batch; project_id; expires_in |
POST /api/v1/stream/ingest | session_id | run_id repeats the ID you sent, which may be a slug; accepted; created_session |
GET /api/v1/stream/sessions/{session_id}, GET /api/v1/stream/active | test_run_id | run_id is the display ID, which may be a slug |
Signing in on the WebSocket
Section titled “Signing in on the WebSocket”The live WebSocket, /ws/live/{project_id}, expects the client’s first frame to contain an auth type and a sign-in token (JWT). If that sign-in is malformed, TestLookup can close the connection with code 4400. If it times out or fails, TestLookup can close it with code 4401. Project membership still applies.
The first frame can add "protocol": 2 to receive batched live_batch frames. TestLookup’s connected reply then states the protocol and the frame_interval_ms (200). Every client receives at most one data frame per interval, which is five a second.
For administrators: each user’s live session on a project writes one ws_session row to the audit log, once it has been connected for 5 seconds. A reconnect within 60 seconds of the last disconnect joins the same row. Connecting and disconnecting write no other audit rows. The testlookup_ws_connections_total metric counts connections by outcome: audited, merged, short or audit_failed.
The two SSE streams
Section titled “The two SSE streams”SSE (server-sent events) is a one-way live connection from the server. The live-session SSE route, /api/v1/stream/sse/{project_id}, takes the sign-in token in a token query parameter. It can also accept a Last-Event-ID header. It is separate from the SSE stream of an agent invocation, which uses tickets.
Tickets for agent-run streams
Section titled “Tickets for agent-run streams”An agent invocation is an AI agent run, started with POST /api/v1/agents/{agent_id}/invoke. It streams its progress at GET /api/v1/agents/invocations/{invocation_id}/events.
To read that stream, first create a ticket with POST /api/v1/agents/invocations/{invocation_id}/events/ticket, using a credential that can access the invocation. A ticket is short-lived and works only once, so every reconnect needs a new ticket.
Reconnect
Section titled “Reconnect”When you reconnect, use only the replay ID format of the stream you are reading. For the live WebSocket, the optional last_event_id uses the Redis stream ID format. Do not reuse one route’s token or position in another route.
A reconcile_required frame with a dropped count means the client fell behind and some frames were dropped. Read the current state again instead of assuming that every event arrived.
Why live totals can arrive first
Section titled “Why live totals can arrive first”Live totals can appear before every test row is stored. Each of these has its own state: the run’s completion, the closing of sessions left open, follow-up work, analysis and review.
Closing a session starts storing its details straight away. If that fails, the outbox relay does it later. A scheduled job runs every two minutes and closes sessions that have been idle for five minutes.
Routes and credentials
Section titled “Routes and credentials”Check the exact route in the streaming routes reference and the API reference before you write a client.
Despite its path, /ws/events/{run_id} is an ordinary POST route for sending events, and it needs a credential. With the default LIVE_EVENTS_REQUIRE_PROJECT_KEY=true, it accepts only a project API key and refuses the shared webhook secret. If the key is limited to certain actions, those must include stream:write.
This route takes one event per call. Each call counts against INGEST_EVENT_RATE_LIMIT_PER_MINUTE, which allows 20,000 events per project per minute by default. The batch routes share a separate limit of 200 batches a minute, so use the batch routes when you send many results.
| Route | What it does | Credential |
|---|---|---|
POST /api/v1/stream/sessions | Sends: opens a session | Sign-in token or any API key; QA engineer or higher |
GET /api/v1/stream/sessions/{session_id} | Reads the session’s state | Sign-in token or any API key |
DELETE /api/v1/stream/sessions/{session_id} | Sends: closes and finishes the session | Sign-in token or any API key; QA engineer or higher |
POST /api/v1/stream/events/batch | Sends: a batch of events | The X-Session-Token from opening the session |
POST /api/v1/stream/ingest | Sends: a batch with a key only; the first call opens the session | Project API key with stream:write, or with no limits |
POST /api/v1/stream/http-exchanges | Sends: the HTTP requests and responses one live test captured | Sign-in token or any API key; QA engineer or higher |
GET /api/v1/stream/active | Reads active and recent sessions | Sign-in token or any API key |
GET /api/v1/stream/sse/{project_id} | Receives: live-session SSE | Sign-in token in the token query parameter |
/ws/live/{project_id} (WebSocket) | Receives: live frames | Sign-in token in the first frame |
POST /ws/events/{run_id} | Sends: one event | Project API key, by default |
The WebSocket and SSE routes are not in the OpenAPI document.
How live results are processed
Section titled “How live results are processed”How live results travel
Section titled “How live results travel”- A reporter library opens a session with the API, or sends events with its credential.
- The API holds the events in a Redis stream.
- One event processor reads the events from Redis and publishes them again, in order, for every API process.
- Each API process sends the updates to its own connected users, over WebSocket or SSE, for the projects they can access.
- The processor saves the held test details in the database.
- When the reporter says the run is complete, the API schedules the work that finishes it.
- The processor finishes the run in the database and records its follow-up jobs.
TestLookup processes each event once and then sends it out to every API process. That way, each API process can deliver the same events, in the same order, to its own connected users. Every user must sign in and sees only the projects they can access. As described above, build run links from the returned test_run_id, not from a live slug.
Live totals can appear before every test row is stored. The remaining details and follow-up work are filled in when a session closes, or when TestLookup closes a session that was left open. If a reporter never says its run is complete, scheduled jobs close the session, so the web app does not show it as live forever. How often those jobs run is a setting, not a delivery promise. The code is in the stream service, the recovery service and the fan-out code.
Follow-up work once a run is stored
Section titled “Follow-up work once a run is stored”Once a run is stored, TestLookup writes its follow-up jobs to a list saved with the run, called the downstream outbox. The jobs include storing live details, starting the AI pipeline, comparing suites, notifications and run-completed webhooks. Each entry names the run, the project, the job, the version of its input, its queue and the data it needs. A recovery job retries unfinished work, and each job is safe to run twice. Starting, running and finishing a job each have their own retry limit.
A deleted run stays deleted, because late writes cannot recreate it. Cancelling an AI pipeline does not cancel your CI job or delete its results. A finished test run can still have a pending or failed AI pipeline, and the other way round. So the web app and the API show the two separately.
Logs and outputs
Section titled “Logs and outputs”When you report a problem, give each ID separately: the project ID, run ID, task ID, pipeline ID and stage or invocation ID. Each of these answers a different question: the logs, the project activity, the import counters, the live delay and queue metrics, the task status, and the outbox and pipeline records. A run produces stored test results, history and totals first, then analysis and report records. The reports guide explains the rules for exported reports.
The generated reference for background tasks, queues and schedules lists every background task and when the scheduler runs it.
Live and health routes
Section titled “Live and health routes”The first WebSocket frame looks like {"type":"auth","token":"<JWT>"}. The optional last_event_id uses the Redis stream ID format, two numbers joined by a dash (digits-digits). A client that does not add "protocol": 2 keeps the older frame types. The other rules for the WebSocket and SSE routes are in the sections above.
TestLookup also has health routes:
/health/livechecks that the process is running./health/readychecks the services it cannot work without./health/versionreports which build is running./health/detailsreports on each service it depends on. It can answer 200 with the statusdegraded, which means some of those services have problems.
The /metrics route exists only when metrics are configured, and it is left out of the exported OpenAPI document. A healthy answer alone does not prove that the workers process jobs, that the AI model is installed correctly or that the database updates match the code.
See the feature page on collecting results for the live reporters TestLookup provides.