---
title: "Sandbox API streams · intentic"
description: "Some routes answer a stream rather than a value: watching a turn, the sandbox-wide event feed, and the operations that take minutes. How to read one, and what each frame carries."
url: "https://intentic.dev/api/streams/"
updated: "2026-08-24"
---

Start here

# Streams

10 routes answer a stream rather than a value: a turn as it happens, the sandbox's own live feed, and the operations that take minutes. They are server-sent events, and reading one takes no library.

**On this page (4 sections)**

- [How to read one](#how-to-read-one)
- [Which routes stream](#which-routes)
- [The sandbox-wide feed](#the-sandbox-feed)
- [The one that is a WebSocket](#the-websocket)

## How to read one

A stream is a response that stays open. Frames are separated by a blank line and each carries one `data:` line holding JSON. Anything that can read a line can read one; in a browser it is `EventSource` for the GET routes and a plain`fetch` reader for the POST ones.

The live feed, on the command line

```bash
curl -N "$SANDBOX/events" -H "authorization: Bearer $SESSION"

data: {"kind":"hello","workspaceId":"…","routes":["agent.run","git.status", …],"build":"…"}

data: {"kind":"heartbeat"}

data: {"kind":"workspaceChanged","paths":["src/app.ts","README.md"]}

data: {"kind":"refsChanged","repos":["root"]}
```

The `-N` matters. Without it the output is buffered, so a stream you are watching arrives in silence and then all at once, which looks exactly like a route that does not work.

## Which routes stream

Found from the contract rather than listed here, so this table cannot fall behind: a route streams when it answers`text/event-stream`, and every one that does is below.

| Route | Carries | Group |
| --- | --- | --- |
| `POST /agent/attach` | Watch a turn happen | [One agent](https://intentic.dev/api/agent/) |
| `POST /capabilities` | Connect something, or change a connection | [Capabilities](https://intentic.dev/api/capabilities/) |
| `POST /vpn/{id}/connect` | Dial a VPN | [VPN](https://intentic.dev/api/vpn/) |
| `POST /exit/{id}/start` | Bring an exit up | [Exit locations](https://intentic.dev/api/exit/) |
| `POST /exit/{id}/use` | Move to another country | [Exit locations](https://intentic.dev/api/exit/) |
| `POST /exit/{id}/rotate` | Take a different address, same country | [Exit locations](https://intentic.dev/api/exit/) |
| `POST /intentic` | Run an infrastructure command | [Platform CLI](https://intentic.dev/api/intentic/) |
| `GET /intentic/apply/events` | Follow the reconcile | [Platform CLI](https://intentic.dev/api/intentic/) |
| `GET /events` | The live event stream | [System](https://intentic.dev/api/system/) |
| `POST /system/computers/{id}/sandboxes/{slug}` | Drive a sandbox on one of your own computers | [System](https://intentic.dev/api/system/) |

## The sandbox-wide feed

`GET /events` is the one to hold open for as long as your program runs. It carries the sandbox's liveness and everything that changed out of band: an opening frame naming this daemon and the routes it implements, heartbeats, boot progress, batches of changed file paths, which repositories' branches moved, the roster of who else is looking, and the fleet.

Those change pushes are not a nicety. An intentic workspace is file-first: the agent edits files and moves branches with its own tools, entirely out of band from every route in this reference. Without the feed, a client is only ever as fresh as the last thing somebody clicked.

## The one that is a WebSocket

`GET /system/terminal` carries a terminal session's raw bytes, and it is a WebSocket rather than an event stream because the traffic goes both ways: you type into it. A WebSocket cannot set headers, so it authorises with a short-lived ticket in the query string instead of the credential every other route takes. It is not in this reference for the same reason the byte routes are not: it is not a JSON contract and cannot be described as one.

More in Start here

[Previous ← When a call fails](https://intentic.dev/api/errors/)
