Fleet
Telemetry ingest, rollups, and retention
Robots report metric readings in batches rather than one request per sample. Batches are idempotent on a client-supplied batch_id, accept a backlog up to seven days old, and are aggregated into minute and hour rollups that outlive the raw rows behind them.
On this page
Start here
Batch telemetry keeps reporting efficient for charts and rollups. A stable batch_id makes retries safe, per-reading validation isolates bad samples, and timestamps control acceptance and aggregation.
Before you begin
- A claimed robot credential authorized for the robot self-service endpoint
- Timestamped readings no more than seven days old and durable storage for batch_id
Put it into practice
-
Build a bounded batch
Collect readings with metric names, numeric values, and timestamps that meet the request limits. Assign the batch a stable identifier before the first send.
-
Submit and inspect the acknowledgement
POST to /api/v1/robots/me/telemetry with the robot's credential. Record accepted, duplicate, and the indexed errors array. There is no rejected response field; count errors if the client needs a rejected total.
-
Retry or repair selectively
Reuse the same batch_id only when the original result is unknown. If specific readings are malformed, correct those samples and submit them as a new batch.
A batch is acknowledged, replaying batch_id writes nothing twice, valid readings reach charts, and bad indexes are visible for correction.
01 Request
referencePOST /api/v1/robots/me/telemetry
Authenticated with the robot's own credential and requiring the fleet:write scope. The robot is resolved from the authenticated identity; there is no robot id in the request. The body may be flat or wrapped in a telemetry object.
| Field | Required | Rules |
|---|---|---|
| batch_id | No | Client-supplied, at most 200 characters. Omitted or blank means the server generates one and no dedupe is possible. |
| readings | Yes | 1 to 500 readings. An empty or larger list is rejected outright. |
| readings[].metric_name | Yes | Free-form, at most 120 characters |
| readings[].metric_value | Yes | Number |
| readings[].unit | No | At most 40 characters |
| readings[].status | No | normal, warning, or critical; defaults to normal |
| readings[].recorded_at | No | ISO 8601. Defaults to receipt time. Accepted from 7 days in the past to 5 minutes in the future. |
| readings[].metadata | No | Object |
02 Batch example
examplePOST /api/v1/robots/me/telemetry
Authorization: Bearer {robot_credential}
{
"batch_id": "b-2026-08-03T11:00:00Z-001",
"readings": [
{"metric_name": "battery_level", "metric_value": 82.5, "unit": "percent"},
{"metric_name": "motor_temperature", "metric_value": 61.2, "unit": "celsius",
"status": "warning", "recorded_at": "2026-08-03T10:59:30Z"}
]
}
Response 200:
{
"robot_id": "0f9d1c2e-3b4a-4c5d-8e6f-7a8b9c0d1e2f",
"batch_id": "b-2026-08-03T11:00:00Z-001",
"accepted": 2,
"duplicate": false,
"errors": []
}
03 Idempotency and partial batches
detailsReplaying a batch_id is acknowledged, not re-ingested
A batch_id already accepted for that robot returns 200 with accepted 0, duplicate true, and an empty errors list. Nothing is written a second time. This holds even if the replayed payload differs, so a store-and-forward client can retry a batch it is unsure about.
Malformed readings are reported per index
Valid readings in a mixed batch are accepted; invalid ones come back in errors as {"index": 0, "errors": ["metric_name can't be blank"]}. The batch is still recorded as consumed, so a retry cannot re-insert the half that succeeded.
04 Error responses
reference| Status | Response field and value | When |
|---|---|---|
| 400 | detail: "batch_too_large" | More than 500 readings |
| 400 | detail: "empty_batch" | readings is empty |
| 400 | detail: "invalid_readings" | readings is not a list |
| 400 | detail: "invalid_batch_id" | batch_id is not a string, or exceeds 200 characters |
| 403 | detail: "The token does not have sufficient scope" | Missing fleet:write; extra.required_scopes names it |
| 403 | error: "workspace_frozen" / "workspace_paused" / "workspace_archived" | The workspace is not accepting machine traffic. detail is the sentence form of the same state. |
| 404 | detail: "Resource not found" | The credential has no linked robot |
| 410 | title: "Robot decommissioned", type: problems/robot-decommissioned | The robot is retired. detail explains that the record is kept for history but can no longer report. |
| 429 | title: "Plan quota exceeded", type: problems/quota-exceeded | telemetry_points_per_month is exhausted. detail names your plan and limit; extra carries resource, plan, limit, usage, and upgrade_url. The batch is not ingested. |
| 429 | detail: "Rate limit exceeded. Please retry later." | Per-credential rate limiting; extra.retry_after_seconds and a retry-after header |
05 Rollups and chart granularity
detailsTwo aggregate resolutions
A rollup worker maintains 1-minute and 1-hour aggregates per robot per metric, each carrying min, max, avg, count, and last_value. Charts read rollups, never raw rows.
Which resolution a range uses
The 1h range reads minute buckets. The 24h, 7d, and 30d ranges read hour buckets. A series is capped at 800 points, and at most 100 distinct metric names are offered per selector.
Backfilled readings are rolled up too
A batch whose oldest accepted reading is more than 10 minutes old schedules a backfill rollup, so a robot draining an offline backlog still lands in the right buckets.
06 Retention
reference| Data | Window | Notes |
|---|---|---|
| Raw readings | 30 days (default) | Configurable, but held at 90 days until the rollup backfill migration has run so charts are never left without a source |
| Minute rollups | 90 days | Fixed |
| Hour rollups | 400 days | Fixed -- year-over-year trends survive raw pruning |
| Batch id ledger | 90 days | The idempotency window; older batch ids can be reused |
07 Sibling device and operator surfaces
detailsSame robot, more than batch ingest
The same identity also exposes GET /api/v1/robots/me, POST /api/v1/robots/me/heartbeat (battery_level 0-100, location, firmware_version, metadata; status is forced to online and last_seen_at is server-set), and POST /api/v1/robots/me/diagnostics. Operators read and write diagnostics at GET/POST /api/v1/operator/robots/:id/diagnostics, plus GET .../health, GET .../maintenance-check, and GET /api/v1/operator/fleet/maintenance-predictions. The console is /app/fleet/diagnostics. Successful batch ingest publishes telemetry.batch_ingested.
Single-reading diagnostics are not telemetry quota
POST /api/v1/robots/me/diagnostics writes one reading and is not counted against telemetry_points_per_month. It is bounded only by per-credential rate limiting. The batch ingest path is the metered one.
Troubleshooting
- The replay says duplicate with zero accepted
- That is the idempotency success path. The first accepted batch owns the ID even if the replay body differs.
- Only some readings are rejected
- Use the reported indexes to repair malformed values or timestamps. Do not resend already accepted samples under the old batch_id.
- Charts change granularity
- The selected time range chooses minute or hour rollups. Raw retention and aggregate retention differ, so long-range views need not match a raw-sample export point for point.