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:
curl --no-buffer \
--unix-socket /run/miru/miru.sock \
--request GET \
--url http://localhost/v0.2/events
Ensure you have proper permissions to access the Unix socket. See Authentication for more details.
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).
Support for SSE will be added to the Device API SDKs in a future release.
Versioning
Server-sent events are versioned alongside the Device API. The first Device API version that supports SSE events is 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 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.
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.
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:
{
"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 filesystem, 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 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).
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:
-
Query parameter —
?after=<id> (takes precedence)
# 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
-
HTTP header —
Last-Event-ID: <id> (standard SSE reconnection header)
# 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:
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. |