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

# Server-sent events

> Subscribe to real-time events from the Miru Agent via Server-Sent Events (SSE).

The Miru Agent exposes a Server-Sent Events (SSE) stream at the `/events` endpoint that pushes notifications to your application in real time. Instead of polling the deployments endpoint, your application can open a single, long-lived connection and react to state changes as they happen.

## Events vs polling

Use events when your application needs to react immediately to deployment transitions—for example, reloading configuration files the moment a new deployment lands on disk.

Use polling (via `GET /v0.2/deployments/current`) when your application only needs to infrequently check the current state and does not require instant notification.

## Connecting

The Device API is served over a Unix socket.

To connect via curl, use the `--unix-socket` flag with the `--no-buffer` flag to ensure events are printed as they arrive:

```bash theme={null}
curl --no-buffer \
  --unix-socket /run/miru/miru.sock \
  --request GET \
  --url http://localhost/v0.2/events
```

<Warning>
  Ensure you have proper permissions to access the Unix socket. See [Authentication](/developers/device-api/authn) for more details.
</Warning>

## SDK support

The Device API SDKs do NOT support Server-Sent Events natively.

While the SDKs do generate the event types from the OpenAPI specification, a dedicated SDK method for streaming events is not yet available.

To consume the SSE stream, use a standard SSE client library for your language. Then, parse the `data` field of the event data object by discriminating on the `type` field in the event envelope (see [Event Data](/developers/device-api/events#event-data)).

<Info>
  Support for SSE will be added to the Device API SDKs in a future release.
</Info>

## Versioning

Server-sent events are versioned alongside the Device API. The first Device API version that supports SSE events is [v0.2.1](/changelog/device-api#v0-2-1).

So, the event envelope and event types streamed from endpoint `/v0.2/events` in `v0.2.1` may be different from the event envelope and event types streamed from endpoint `/v0.3/events` in `v0.3.0`. As an example, `deployment.deployed` in `v0.2.1` may contain different fields than `deployment.deployed` in `v0.3.0`.

To be clear, Miru does not version individual event types using `{resource}.{action}.{version}` (e.g. `deployment.deployed.v1`, `deployment.removed.v1`) strings like some other APIs do. Instead, each Device API version exposes a particular set of event types in the form `{resource}.{action}` (e.g. `deployment.deployed`, `deployment.removed`).

Miru's stability guarantees for event versioning adhere to the policy specified in the [Device API versioning](/developers/device-api/versions#stability-guarantees) section.

During **beta** (`v0.x.y`)

* Minor version bumps (e.g. `v0.1.0` to `v0.2.0`) may include breaking changes
* Patch version bumps (e.g. `v0.2.0` to `v0.2.1`) are always backward-compatible

After **stable** (`v1.0`+)

* Major version bumps (e.g. `v1.x` to `v2.0`) may include breaking changes
* Minor version bumps (e.g. `v1.0` to `v1.1`) are additive only—no breaking changes

## Event frames

An *event frame* is the wire-level structure each event takes as it arrives on the SSE stream. Every event is delivered as a single frame composed of a small set of fields—an identifier, a type, and a JSON payload—following the standard SSE format.

### Format

Each event is delivered as a standard SSE frame with three fields.

| Field   | Description                                                        |
| ------- | ------------------------------------------------------------------ |
| `id`    | Monotonically increasing integer; serves as the cursor for replay. |
| `event` | Event type string in the format `{resource}.{action}`.             |
| `data`  | JSON envelope containing the event payload.                        |

Below is an example of an event frame:

```
id: 42
event: deployment.deployed
data: {"object":"event","id":42,"type":"deployment.deployed","occurred_at":"2026-03-10T12:00:00Z","data":{"deployment_id":"dpl_123","release_id":"rls_123","status":"deployed","activity_status":"deployed","error_status":"none","target_status":"deployed","deployed_at":"2026-03-10T12:00:00Z"}}
```

### Delivery semantics

Events are delivered **at-least-once**. Under normal operation each event is delivered exactly once, but after a reconnection the replay window may include events the client has already seen.

If needed, clients can deduplicate by the event `id` field—it is a monotonically increasing integer that uniquely identifies each event.

### Heartbeats

The agent sends SSE comment lines approximately every 30 seconds to keep the connection alive and prevent idle connections from being closed.

```text theme={null}
: heartbeat
```

Comment lines should be ignored by SSE client libraries.

## Event data

### Envelope

The envelope is the JSON object carried in the `data` field of every event frame. It wraps the event-specific payload in a small set of metadata fields that are common to every event regardless of type. This allows clients to identify the event, deduplicate by id, and dispatch on type before they need to know anything about a particular event type.

The envelope has the following top-level fields:

| Field         | Type               | Description                                         |
| ------------- | ------------------ | --------------------------------------------------- |
| `object`      | `string`           | Always `"event"`.                                   |
| `id`          | `integer`          | Same as the SSE `id` field.                         |
| `type`        | `string`           | Event type (matches the SSE `event` field).         |
| `occurred_at` | `string<datetime>` | Timestamp of when the event occurred.               |
| `data`        | `object`           | Event-specific payload. Shape varies by event type. |

Below is an example of an envelope:

```json theme={null}
{
  "object": "event",
  "id": 42,
  "type": "deployment.deployed",
  "occurred_at": "2026-03-10T12:00:00Z",
  "data": { "...": "..." }
}
```

### Event types

Every event has a *type*—a string in the form `{resource}.{action}` (e.g. `deployment.deployed`)—that identifies what happened on the device. The type is carried in both the SSE `event` field and the envelope's `type` field, so clients can dispatch on it without parsing the rest of the payload.

For example, when a deployment's config instances are written to the device's file system, the agent emits a `deployment.deployed` event:

```
event: deployment.deployed
data: {"object":"event","id":42,"type":"deployment.deployed","occurred_at":"2026-03-10T12:00:00Z","data":{...}}
```

For the full list of event types supported by a Device API version, visit the [Event Types](/references/device-api/latest/events) section in the Device API reference.

## Subscription options

### Cursor-based replay

A *cursor* is a bookmark into the event stream—specifically, the `id` of the last event a client has successfully processed. Because event ids are monotonically increasing integers, the cursor uniquely identifies a position in the stream and lets the agent know exactly which events the client has already seen.

Clients can use a cursor to resume from a known position after reconnection, picking up where they left off without missing or re-processing events (subject to retention; see [Retention and compaction](#retention-and-compaction)).

When providing a cursor, the stream begins by replaying all retained events after the given cursor (non-inclusive), then continues to deliver live events as they occur.

Pass the cursor in one of two ways:

1. **Query parameter** — `?after=<id>` (takes precedence)

   ```bash theme={null}
   # Resume from event 42 using the query parameter
   curl --no-buffer \
     --unix-socket /run/miru/miru.sock \
     --request GET \
     --url http://localhost/v0.2/events?after=42
   ```

2. **HTTP header** — `Last-Event-ID: <id>` (standard SSE reconnection header)

   ```bash theme={null}
   # Resume from event 42 using the Last-Event-ID header
   curl --no-buffer \
     --unix-socket /run/miru/miru.sock \
     --request GET \
     --url http://localhost/v0.2/events \
     --header "Last-Event-ID: 42"
   ```

### Type filtering

It's often the case that clients only need to subscribe to a subset of all possible event types. For example, an application may only need to know when a deployment is deployed, but not when it is removed.

To subscribe to specific event types, pass a comma-separated list via the `types` query parameter:

```bash theme={null}
curl --no-buffer \
  --unix-socket /run/miru/miru.sock \
  --request GET \
  --url http://localhost/v0.2/events?types=deployment.deployed,deployment.removed
```

If `types` is omitted, all event types are sent.

## Retention and compaction

Events are persisted locally on the device in JSONL format. The agent automatically compacts the event log to bound disk usage. When compaction removes old events, cursors pointing to removed events become invalid.

If the cursor provided via `after` or `Last-Event-ID` is older than the earliest retained event, the server responds with `410 Gone`.

## Errors

| Status     | Condition      | Description                                            |
| ---------- | -------------- | ------------------------------------------------------ |
| `410 Gone` | Expired cursor | The cursor points to an event that has been compacted. |
