Skip to main content
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.

Format

Each event is delivered as a standard SSE frame with three fields.
FieldDescription
idMonotonically increasing integer; serves as the cursor for replay.
eventEvent type string in the format {resource}.{action}.
dataJSON 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.
: 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:
FieldTypeDescription
objectstringAlways "event".
idintegerSame as the SSE id field.
typestringEvent type (matches the SSE event field).
occurred_atstring<datetime>Timestamp of when the event occurred.
dataobjectEvent-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:
  1. 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
    
  2. HTTP headerLast-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

StatusConditionDescription
410 GoneExpired cursorThe cursor points to an event that has been compacted.
Last modified on April 7, 2026