> For the complete documentation index, see [llms.txt](https://docs.roboflow.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.roboflow.com/deployment/edge-devices/deployment-manager.md).

# Deployment Manager

## About

Roboflow Deployment Manager lets you easily set up, deploy, and manage computer vision models on edge devices. After training your model and building a workflow, it provides an all-in-one solution for scaling deployments.

You can use Deployment Manager to:

1. Set up new devices with the [Roboflow Inference server](https://inference.roboflow.com/).
2. Configure camera streams for use in a deployment.
3. Deploy [Workflows](https://docs.roboflow.com/workflows) on the edge.
4. Monitor logs, stream status, and telemetry for deployed edge devices.

{% hint style="warning" %}
Deployment Manager is available exclusively for Enterprise customers. [Contact the Roboflow sales team](https://roboflow.com/sales) to learn more about this feature and how it can be used to manage your deployments at scale.
{% endhint %}

<figure><img src="/files/3kiR21SMqAeYJSHoFtbt" alt=""><figcaption></figcaption></figure>

### Key Requirements:

Deployment Manager is built for Roboflow-supported hardware:

* Supported: Hardware purchased via Roboflow
* Unsupported (use at your own risk): NVIDIA Jetson, x86 machines running Debian-based Linux with NVIDIA GPUs
* Not yet supported: Mac or Windows-based systems

Devices must be connected to the internet for setup and ongoing operation, with continuous access to Roboflow for remote management and monitoring.

## Guides

### Setting Up

Follow the [Setting Up](/deployment/edge-devices/deployment-manager/setting-up.md) guides in order to get a device online and running Workflows.

| Guide                                                                                                            | Description                                                              |
| ---------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------ |
| [Hardware Requirements](/deployment/edge-devices/deployment-manager/setting-up/hardware-requirements.md)         | Supported hardware, cameras, network, and outbound traffic requirements. |
| [Add a Device](/deployment/edge-devices/deployment-manager/setting-up/add-a-device.md)                           | Provision a new edge device and register it with your account.           |
| [Add a Stream](/deployment/edge-devices/deployment-manager/setting-up/add-a-stream.md)                           | Configure a camera stream that runs a Workflow.                          |
| [Set up Device Alerts](/deployment/edge-devices/deployment-manager/setting-up/set-up-device-alerts.md)           | Enable email alerts for connectivity, disk, and FPS issues.              |
| [Setup Maintenance Windows](/deployment/edge-devices/deployment-manager/setting-up/setup-maintenance-windows.md) | Schedule when deploy-affecting changes take effect.                      |

### Making Changes

The [Making Changes](/deployment/edge-devices/deployment-manager/making-changes.md) guides cover updating, reconfiguring, and removing devices and streams after setup.

| Guide                                                                                                                        | Description                                                        |
| ---------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------ |
| [Update Device Configuration](/deployment/edge-devices/deployment-manager/making-changes/update-device-configuration.md)     | Manage device settings, service versions, and additional services. |
| [Configure Device Network](/deployment/edge-devices/deployment-manager/making-changes/configure-device-network.md)           | Set IP addressing, gateway, DNS, and hostname from the cloud.      |
| [Configure AI1 Camera Settings](/deployment/edge-devices/deployment-manager/making-changes/configure-ai1-camera-settings.md) | Tune exposure, gain, focus, and other AI1 camera settings live.    |
| [Set a Static IP for a Camera](/deployment/edge-devices/deployment-manager/making-changes/set-camera-static-ip.md)           | Assign a persistent static IP to a GigE Basler or Lucid camera.    |
| [Soft Reset a PoE Port](/deployment/edge-devices/deployment-manager/making-changes/soft-reset-poe-port.md)                   | Power-cycle a PoE port to recover stuck camera links.              |
| [Pause and Resume a Stream](/deployment/edge-devices/deployment-manager/making-changes/stop-a-stream.md)                     | Temporarily pause a running stream and resume it later.            |
| [Trigger a Stream](/deployment/edge-devices/deployment-manager/making-changes/trigger-a-stream.md)                           | Run a Workflow on demand against a Triggered stream.               |
| [Terminate a Stream](/deployment/edge-devices/deployment-manager/making-changes/delete-a-stream.md)                          | Permanently remove a stream and its configuration.                 |
| [Redeploy Deployment Manager](/deployment/edge-devices/deployment-manager/making-changes/redeploy-deployment-manager.md)     | Recover a device's configuration after a failure.                  |
| [Delete a Device](/deployment/edge-devices/deployment-manager/making-changes/delete-a-device.md)                             | Permanently remove a device you no longer use.                     |
| [API Keys for Deployment Manager](/deployment/edge-devices/deployment-manager/making-changes/api-keys-for-device-manager.md) | How device API keys are generated, scoped, and revoked.            |

### Monitoring

The [Monitoring](/deployment/edge-devices/deployment-manager/monitoring.md) guides show how to check stream status, logs, resources, and Event Store health.

| Guide                                                                                                            | Description                                                   |
| ---------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------- |
| [View a Stream](/deployment/edge-devices/deployment-manager/monitoring/view-a-stream.md)                         | View a stream's status, latest frame, and Workflow details.   |
| [View Device Logs](/deployment/edge-devices/deployment-manager/monitoring/view-device-logs.md)                   | Search, filter, and download logs from Roboflow services.     |
| [View the Resource Monitor](/deployment/edge-devices/deployment-manager/monitoring/view-the-resource-monitor.md) | Monitor disk, memory, CPU, GPU, and service container health. |
| [View Event Store Status](/deployment/edge-devices/deployment-manager/monitoring/view-event-store-status.md)     | Monitor Event Store usage, backup progress, and history.      |

## HTTP API

The Deployment Manager API lets you monitor and manage your Roboflow Deployment Manager (RFDM) devices programmatically.

All endpoints are mounted under `/:workspace/devices/v2` on the public API host (`https://api.roboflow.com`). Read endpoints require a [scoped API key](https://docs.roboflow.com/reference/authentication/authentication/scoped-api-keys) with the `device:read` scope; the create endpoint requires `device:update`. Workspace API keys with no explicit scopes list are implicitly granted all scopes (legacy behavior); keys that carry an explicit `scopes` array must include the relevant scope.

With the Deployment Manager API, you can:

* [List and Get Devices](#list-and-get-devices)
* [Create a Device](#create-a-device)
* [Device Config](#device-config)
* [Device Streams](#device-streams)
* [Device Logs and Telemetry](#device-logs-and-telemetry)
* [Device Events](#device-events)

### Authentication

All endpoints accept a workspace API key via either:

* Query string: `?api_key=YOUR_API_KEY`
* Header: `Authorization: Bearer YOUR_API_KEY`

#### Device-scoped API keys

API keys minted for a specific device (for example, by RFDM during install) are narrowed to that device. They may only call routes whose `:deviceId` path parameter equals the key's bound device. They will receive **403** on the workspace-wide list and create endpoints, and on any `:deviceId` path that doesn't match.

### Cross-workspace isolation

A workspace API key can only read or modify devices that belong to its workspace. Requests for a device id owned by a different workspace return **404**, even if the id is otherwise valid.

### Errors

| Status | Meaning                                                                                                                                                                                                                                                                |
| ------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 400    | Invalid query parameter (unknown `time_period`, malformed cursor or date, invalid `direction`, etc.) or invalid request body.                                                                                                                                          |
| 401    | Missing or invalid API key.                                                                                                                                                                                                                                            |
| 403    | Device-scoped API key targeting a different device, the workspace-wide list/create routes, a source device from another workspace during duplication, or a feature-gated request rejected (for example, AI1 creation or `offline_mode` not enabled for the workspace). |
| 404    | Device, stream, config, or source device not found. Read requests for device ids owned by another workspace also return 404.                                                                                                                                           |
| 429    | Rate limit exceeded. Logs are limited to 5 requests per minute per IP and 50 per minute globally; telemetry reads are limited to 60 requests per minute per device with a 10-request burst over 10 seconds.                                                            |

Error responses use one of two shapes:

* Handler-level errors (typically 400, 403, 404, and 429 from the handlers themselves) return `{ "error": "<message>" }`.
* Authentication and workspace-validation failures (typically 401) return a structured `error` object: `{ "error": { "message": "...", "status": 401, "type": "OAuthException", "hint": "..." } }`.

The logs rate limiter additionally returns a plain string body (not JSON) when it triggers a **429**.

### List and Get Devices

#### List Devices

List every device registered in the workspace. Device-scoped API keys cannot call this endpoint and receive **403**.

**Required scope:** `device:read`

{% openapi src="/files/H5x9CDQa7dPNDXC9dDQe" path="/{workspace}/devices/v2" method="get" %}
[deployment-manager.yaml](https://1583372177-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoksNmwps1HmYr8TqHDnX%2Fuploads%2Fgit-blob-d9842382ea00557904abc587230b904ebf240466%2Fdeployment-manager.yaml?alt=media)
{% endopenapi %}

**Example Request**

```bash
curl "https://api.roboflow.com/{workspace}/devices/v2?api_key=$ROBOFLOW_API_KEY"
```

**Example Response**

```json
{
  "data": [
    {
      "id": "abc123",
      "name": "factory-floor-cam-1",
      "status": "online",
      "last_heartbeat": "2026-04-28T12:00:00.000Z",
      "platform": "Linux",
      "platform_release": "5.10.104-tegra",
      "platform_version": "#1 SMP PREEMPT ...",
      "architecture": "aarch64",
      "hostname": "jetson-01",
      "rfdm_version": "1.2.3",
      "type": "jetson",
      "hardware": {
        "processor": "aarch64",
        "gpu": null,
        "total_memory_mb": null,
        "total_disk_space_mb": 124426534912
      },
      "tags": ["production", "line-3"],
      "created_at": "2026-01-15T08:30:00.000Z"
    }
  ]
}
```

The `status` field is `online` if a heartbeat was received within the last 5 minutes, `offline` if older, or `unknown` if no heartbeat has ever been recorded. Newly-provisioned devices appear in the list before their first heartbeat; until then `status` is `unknown` and most monitoring-derived fields are `null`.

{% hint style="warning" %}
`hardware.total_disk_space_mb` is a raw value reported by the device. Despite the `_mb` suffix, RFDM-reported devices currently return this value in bytes (for example `124426534912` for a \~124 GB disk). Treat the unit as device-defined.
{% endhint %}

#### Get Device

Retrieve a single device by id. Returns **404** if the device does not exist or belongs to a different workspace.

**Required scope:** `device:read`

{% openapi src="/files/H5x9CDQa7dPNDXC9dDQe" path="/{workspace}/devices/v2/{deviceId}" method="get" %}
[deployment-manager.yaml](https://1583372177-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoksNmwps1HmYr8TqHDnX%2Fuploads%2Fgit-blob-d9842382ea00557904abc587230b904ebf240466%2Fdeployment-manager.yaml?alt=media)
{% endopenapi %}

**Example Request**

```bash
curl "https://api.roboflow.com/{workspace}/devices/v2/{deviceId}?api_key=$ROBOFLOW_API_KEY"
```

The response body matches a single item from the list endpoint (not wrapped in a `data` array).

### Create a Device

Create a new device in your workspace and return the identifiers needed to install it. Device-scoped API keys cannot call this endpoint and receive **403**.

**Required scope:** `device:update`

{% openapi src="/files/H5x9CDQa7dPNDXC9dDQe" path="/{workspace}/devices/v2" method="post" %}
[deployment-manager.yaml](https://1583372177-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoksNmwps1HmYr8TqHDnX%2Fuploads%2Fgit-blob-d9842382ea00557904abc587230b904ebf240466%2Fdeployment-manager.yaml?alt=media)
{% endopenapi %}

**Example Request**

```bash
curl -X POST "https://api.roboflow.com/{workspace}/devices/v2?api_key=$ROBOFLOW_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"device_name": "factory-floor-cam-2", "device_type": "edge"}'
```

**Example Response**

```json
{
  "deviceId": "abc456",
  "installId": "inst_xyz789"
}
```

Use the returned `installId` to bootstrap the device with the Roboflow Deployment Manager installer.

#### Notes on AI1 devices

* Setting `device_type` to `"ai1"` requires the workspace to have the `deviceAio` feature enabled, otherwise the request returns **403**.
* `offline_mode` is only valid for AI1 devices on workspaces with `roboflowLiteMode`. Other combinations return **400** or **403**.
* For AI1 devices that are not duplicating an existing device and do not specify a `workflow_id`, the slugified `device_name` must contain at least one alphanumeric character, otherwise the request returns **400**.
* When AI1 + offline mode applies, the response also includes an `offlineProvisioningQrPayload` field encoding the QR payload for offline provisioning.

#### Notes on duplication

When `sourceDeviceId` is supplied, the new device is created from a copy of the source device's config. The source device must belong to the same workspace as the request, otherwise the request returns **403**. A non-existent `sourceDeviceId` returns **404**.

### Device Config

#### Get Workspace Default Config

Returns the workspace-level default device configuration. This is the base default config with the workspace's config patch (if any) merged in. Use it as a template when provisioning a new device before calling `POST /:workspace/devices/v2`.

**Required scope:** `device:read`

{% hint style="info" %}
This is a workspace-wide endpoint. Device-scoped API keys will receive **403** since there is no `:deviceId` in the path.
{% endhint %}

**Example Request**

```bash
curl "https://api.roboflow.com/{workspace}/devices/v2/default-config?api_key=$ROBOFLOW_API_KEY"
```

**Example Response**

```json
{
  "config": {
    "version": "1.0.0",
    "config": {
      "inference": { "confidence": 0.7, "threshold": 0.3 },
      "device_type": "edge"
    },
    "services": {}
  },
  "patch": {
    "config": {
      "inference": { "confidence": 0.7 }
    }
  }
}
```

* `config` -- the merged result (base default + workspace patch).
* `patch` -- the workspace's stored patch. Empty object `{}` if the workspace has no custom patch.

#### Get Config

Returns the device's current runtime configuration. The response shape mirrors the Roboflow Deployment Manager (RFDM) config spec.

**Required scope:** `device:read`

{% openapi src="/files/H5x9CDQa7dPNDXC9dDQe" path="/{workspace}/devices/v2/{deviceId}/config" method="get" %}
[deployment-manager.yaml](https://1583372177-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoksNmwps1HmYr8TqHDnX%2Fuploads%2Fgit-blob-d9842382ea00557904abc587230b904ebf240466%2Fdeployment-manager.yaml?alt=media)
{% endopenapi %}

**Example Request**

```bash
curl "https://api.roboflow.com/{workspace}/devices/v2/{deviceId}/config?api_key=$ROBOFLOW_API_KEY"
```

The response is the full configuration document and includes (but is not limited to) the following top-level fields:

* `device_id`, `device_name`
* `workspace_id`
* `version`, `last_updated`, `last_updated_at`, `created_at`
* `config` (the device's runtime config tree, including `device_type`, `stream`, and `offline_mode`)
* `services` (per-service container definitions including `image`, `volumes`, and `environment_variables`)
* `environment_variables` (top-level environment variables, in addition to the per-service ones inside `services`)
* `production_mode`, `last_automatic_update`, `updated`, `updated_by`, `$schema`, `id`

Additional fields written by RFDM may also appear. Treat the response shape as open-ended and only depend on the fields you need.

Returns **404** if the device does not exist or does not belong to your workspace, or if no configuration is stored for it.

{% hint style="warning" %}
The response includes the full configuration as authored. Per-service `environment_variables` and any integration credentials embedded in the config are returned unredacted. Treat the response body as sensitive and do not log it in plaintext.
{% endhint %}

#### Config History

List prior configuration revisions for a device, newest first.

**Required scope:** `device:read`

{% openapi src="/files/H5x9CDQa7dPNDXC9dDQe" path="/{workspace}/devices/v2/{deviceId}/config/history" method="get" %}
[deployment-manager.yaml](https://1583372177-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoksNmwps1HmYr8TqHDnX%2Fuploads%2Fgit-blob-d9842382ea00557904abc587230b904ebf240466%2Fdeployment-manager.yaml?alt=media)
{% endopenapi %}

**Example Request**

```bash
curl "https://api.roboflow.com/{workspace}/devices/v2/{deviceId}/config/history?api_key=$ROBOFLOW_API_KEY&limit=10"
```

**Example Response**

```json
{
  "data": [
    {
      "revision_id": "rev_abc",
      "created_at": "2026-04-20T14:30:00.000Z",
      "created_by": "user_123"
    }
  ],
  "pagination": {
    "next_cursor": "...",
    "has_more": true,
    "limit": 10
  }
}
```

A malformed `cursor` returns **400** with `{"error": "Invalid cursor format"}`.

### Device Streams

#### List Streams

List every stream configured on a device.

**Required scope:** `device:read`

{% openapi src="/files/H5x9CDQa7dPNDXC9dDQe" path="/{workspace}/devices/v2/{deviceId}/streams" method="get" %}
[deployment-manager.yaml](https://1583372177-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoksNmwps1HmYr8TqHDnX%2Fuploads%2Fgit-blob-d9842382ea00557904abc587230b904ebf240466%2Fdeployment-manager.yaml?alt=media)
{% endopenapi %}

**Example Request**

```bash
curl "https://api.roboflow.com/{workspace}/devices/v2/{deviceId}/streams?api_key=$ROBOFLOW_API_KEY"
```

**Example Response**

```json
{
  "data": [
    {
      "id": "stream_abc",
      "name": "entrance-cam",
      "status": "running",
      "pipeline_id": "pipe_123",
      "workflow_id": "wf_456",
      "source": "rtsp://192.168.1.100:554/live",
      "started_at": "2026-04-28T10:00:00.000Z",
      "last_event_at": "2026-04-28T12:30:00.000Z",
      "camera_fps": 29.97,
      "inference_fps": 12.4,
      "sharpness": 0.82,
      "error": null
    }
  ]
}
```

**Source sanitization**

The `source` field is always passed through a sanitizer before it is returned:

* If the source is a URL, any `userinfo` (`scheme://user:pass@host/...`) is stripped. For example, `rtsp://admin:password@192.168.1.100:554/live` becomes `rtsp://192.168.1.100:554/live`.
* If the source is an object, keys whose lowercase name is `password`, `passwd`, `secret`, `api_key`, `apikey`, `auth`, `authorization`, `token`, or `access_token` are dropped from the response. All other keys are preserved.
* Arrays and nested objects are sanitized recursively.

#### Get Stream

Retrieve a single stream. Returns **404** if the stream does not exist on the device, or if the device does not exist or does not belong to your workspace.

**Required scope:** `device:read`

{% openapi src="/files/H5x9CDQa7dPNDXC9dDQe" path="/{workspace}/devices/v2/{deviceId}/streams/{streamId}" method="get" %}
[deployment-manager.yaml](https://1583372177-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoksNmwps1HmYr8TqHDnX%2Fuploads%2Fgit-blob-d9842382ea00557904abc587230b904ebf240466%2Fdeployment-manager.yaml?alt=media)
{% endopenapi %}

**Example Request**

```bash
curl "https://api.roboflow.com/{workspace}/devices/v2/{deviceId}/streams/{streamId}?api_key=$ROBOFLOW_API_KEY"
```

The response body matches a single item from the list endpoint (not wrapped in a `data` array).

### Device Logs and Telemetry

#### Logs

Returns paginated device logs.

**Required scope:** `device:read`

{% openapi src="/files/H5x9CDQa7dPNDXC9dDQe" path="/{workspace}/devices/v2/{deviceId}/logs" method="get" %}
[deployment-manager.yaml](https://1583372177-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoksNmwps1HmYr8TqHDnX%2Fuploads%2Fgit-blob-d9842382ea00557904abc587230b904ebf240466%2Fdeployment-manager.yaml?alt=media)
{% endopenapi %}

**Example Request**

**Rate limit:** 120 requests per minute per workspace. Log reads and log ingest use separate rate-limit buckets, so polling for logs will not count against your ingest quota (and vice versa).

```bash
curl "https://api.roboflow.com/{workspace}/devices/v2/{deviceId}/logs?api_key=$ROBOFLOW_API_KEY&limit=50"
```

**Example Response**

```json
{
  "data": [
    {
      "timestamp": "2026-04-28T12:01:00.000Z",
      "service": "inference",
      "severity": "INFO",
      "message": "Model loaded successfully"
    }
  ],
  "pagination": {
    "next_cursor": "2026-04-28T12:01:00.000Z",
    "has_more": true,
    "limit": 50
  }
}
```

**Rate limits**

This endpoint is rate-limited to **5 requests per minute per IP address** and **50 requests per minute globally**. Excess requests return **429**.

#### Telemetry

Returns aggregated hardware metrics (CPU, memory, disk, GPU) for a device, bucketed over a fixed time window.

**Required scope:** `device:read`

{% openapi src="/files/H5x9CDQa7dPNDXC9dDQe" path="/{workspace}/devices/v2/{deviceId}/telemetry" method="get" %}
[deployment-manager.yaml](https://1583372177-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoksNmwps1HmYr8TqHDnX%2Fuploads%2Fgit-blob-d9842382ea00557904abc587230b904ebf240466%2Fdeployment-manager.yaml?alt=media)
{% endopenapi %}

**Example Request**

```bash
curl "https://api.roboflow.com/{workspace}/devices/v2/{deviceId}/telemetry?api_key=$ROBOFLOW_API_KEY&time_period=24h"
```

The bucket size is fixed per time period:

| `time_period` | `bucket_interval` | `fill_interval_seconds` |
| ------------- | ----------------- | ----------------------- |
| `1h`          | `2 MINUTE`        | 120                     |
| `24h`         | `30 MINUTE`       | 1800                    |
| `7d`          | `4 HOUR`          | 14400                   |
| `14d`         | `8 HOUR`          | 28800                   |

**Example Response**

```json
{
  "time_period": "24h",
  "bucket_interval": "30 MINUTE",
  "fill_interval_seconds": 1800,
  "buckets": [
    {
      "bucket_start": "2026-04-28T00:00:00.000Z",
      "cpu_pct": 42.5,
      "used_memory_mb": 3200,
      "total_memory_mb": 7860,
      "used_disk_space_mb": 15000,
      "total_disk_space_mb": 29000,
      "gpu_pct": 78.2
    }
  ]
}
```

Buckets in the requested window that received no telemetry are still returned, with each metric field set to `null`.

**Rate limits**

This endpoint is rate-limited per device to **60 requests per minute** with an additional **10 requests per 10 seconds** burst limiter. Excess requests return **429**. The same per-device telemetry quota is shared with the device's telemetry ingest path, so abusive reads can affect ingest for the same device.

### Device Events

Returns device and stream lifecycle events (for example device boots, stream starts and stops, errors, config changes).

**Required scope:** `device:read`

{% openapi src="/files/H5x9CDQa7dPNDXC9dDQe" path="/{workspace}/devices/v2/{deviceId}/events" method="get" %}
[deployment-manager.yaml](https://1583372177-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoksNmwps1HmYr8TqHDnX%2Fuploads%2Fgit-blob-d9842382ea00557904abc587230b904ebf240466%2Fdeployment-manager.yaml?alt=media)
{% endopenapi %}

**Example Request**

```bash
curl "https://api.roboflow.com/{workspace}/devices/v2/{deviceId}/events?api_key=$ROBOFLOW_API_KEY&limit=50"
```

**Example Response**

```json
{
  "data": [
    {
      "id": "evt_abc123",
      "event": "device.boot",
      "entity_type": "device",
      "entity_id": "abc123",
      "event_description": "Device started up",
      "error_code": null,
      "metadata": { "source": "boot-loop" },
      "device_timestamp": "2026-04-28 10:00:00",
      "server_timestamp": "2026-04-28 10:00:01",
      "event_end_timestamp": null
    }
  ],
  "pagination": {
    "next_cursor": "...",
    "prev_cursor": "...",
    "has_more": true,
    "limit": 50
  }
}
```

The `device_timestamp`, `server_timestamp`, and `event_end_timestamp` fields are formatted as `YYYY-MM-DD HH:MM:SS[.SSS]` in UTC. They are not normalized to ISO-8601 like other timestamps in this API.

`event_end_timestamp` may also be returned as the epoch-zero string `"1970-01-01 00:00:00.000"` (rather than `null`) when the event has no recorded end timestamp.
