Data operations
Control episode recording, then query and annotate the results.
An episode is one recorded attempt at a task. Episodes are created by operators in the headset, by physical buttons, or through this API — all three produce the same data.
The flow end to end: the robot records an episode and syncs it to the platform → you query and annotate it → you curate episodes into a dataset and snapshot it → you export the manifest to your training pipeline. To react to new episodes without polling, use webhooks.
Two kinds of episode data
The API keeps a hard line between what the robot recorded and what you add later:
- Recorded facts are stamped by the robot at collection time: outcome, operator, control mode, timing, and segments. They are immutable after ingest.
- Annotations are everything added after the fact: QA status, curation tags, corrected outcomes. They live in a separate mutable layer and never overwrite recorded facts.
Episode rows expose the two layers as separate metadata and annotations fields, so the split survives into your queries.
Control recording
POST /v1/robots/{id}/episodes/start
POST /v1/robots/{id}/episodes/finish
POST /v1/robots/{id}/episodes/fail
POST /v1/robots/{id}/episodes/discardRequires robots:command. Each verb tells the robot to make one recording transition. The robot stamps the recording with its active task and operator context (the optional task_id/metadata body fields are reserved and not attached yet).
curl -X POST https://api-prod.avearobotics.com/v1/robots/unit-04/episodes/start \
-H "Authorization: Bearer ak_..."{
"status": "ok",
"result": {
"cmd_id": "cmd-a1b2c3d4e5f6",
"command": "episodes.start",
"robot_id": "5f3a9c2e-...",
"ok": true
}
}Recording states
A robot is idle, recording, or briefly finalizing while it closes a recording. Each verb is valid in exactly one state:
| Robot state | Valid call | Effect |
|---|---|---|
| idle | start | Recording begins |
| recording | finish | Recording ends with outcome success |
| recording | fail | Recording ends with outcome failure |
| recording | discard | Recording ends with outcome discarded; the recorded files are deleted, the row is kept |
| finalizing | — | Wait a few seconds for the robot to return to idle |
A verb sent in any other state fails with 500 internal — the robot refuses the transition and nothing changes. Other errors any verb can return:
| Response | Meaning |
|---|---|
422 validation_failed | The robot's runtime predates native episode commands (fail works on every version) |
503 robot_offline | Robot is not connected |
504 command_timeout | No acknowledgment in time — the transition may still happen; retry with the same Idempotency-Key, never blind |
After finish or fail, the episode row appears in GET /v1/episodes once the robot syncs the recording.
Query episodes
GET /v1/episodes
GET /v1/episodes/{id}
GET /v1/episodes/{id}/segmentsRequires data:read. The list endpoint paginates with cursors and filters on:
?robot_id= ?task_id= ?subtask_id= ?operator_id=
?outcome= ?from= ?to= ?control_mode=
?policy_version_id= ?eval_id=It also takes dotted equality filters into the two JSON layers: ?metadata.building=hq matches recorded metadata, ?annotation.qa=approved matches annotations.
Episode rows carry both recorded fields (recorded_outcome, control_mode, operator_id, metadata, …) and the mutable annotations and outcome set through this API.
Segments are the episode's ordered subtask timeline — one row per subtask the operator stepped through, with segment_index, the subtask, and start/end offsets into the recording.
Sync status
Episode rows include a replicas array — the episode's cloud-sync state per destination (granted → syncing → synced → verified, with grant_id, error, and updated_at). Filter the list by it:
?sync_state=unverified|synced|verified # &to={destination_id} scopes it to one destinationunverified matches episodes reported but not yet verified at any (or the given) destination.
Annotate episodes
PATCH /v1/episodes/{id}Requires data:write. The body shallow-merges into the episode's annotations; a null value deletes that key. Annotations only — recorded facts cannot be modified through any endpoint.
curl -X PATCH https://api-prod.avearobotics.com/v1/episodes/3e1b... \
-H "Authorization: Bearer ak_..." \
-H "Content-Type: application/json" \
-d '{ "annotations": { "qa": "approved", "curation": ["grasp-failures-study"] } }'To correct an outcome after review:
PATCH /v1/episodes/{id}/outcomeBody: { "outcome": "failure", "reason": "operator misfiled" }. Outcome changes append to an audit log; the robot-recorded outcome is kept forever alongside the override.
Export
GET /v1/episodes/exportRequires data:read. Returns an NDJSON manifest — one row per matching episode, with data locations — for feeding training pipelines. Accepts the same filters as the list endpoint, up to limit rows per request (default 1000, max 5000); continue from the X-Next-Cursor response header until it is absent.
Datasets
A dataset is a saved episode filter that you freeze when it's time to train. It has two states:
- Live — membership is whatever currently matches the filter, and grows as new episodes sync.
- Snapshotted — membership is pinned to an exact episode list, permanently, so a training run is reproducible.
POST /v1/datasets
GET /v1/datasets
GET /v1/datasets/{id}
POST /v1/datasets/{id}/snapshot
GET /v1/datasets/{id}/manifestRequires data:read for reads, data:write for writes. Create one with a name and a filter (the same fields the episode list accepts, including dotted metadata.*/annotation.* keys):
curl -X POST https://api-prod.avearobotics.com/v1/datasets \
-H "Authorization: Bearer ak_..." \
-H "Content-Type: application/json" \
-d '{
"name": "sort-bin-a-training-v1",
"filter": { "task_id": "d81f42a7-...", "outcome": "success" }
}'What each call does depends on the dataset's state:
| Call | Live | Snapshotted |
|---|---|---|
GET /v1/datasets/{id} | Returns the dataset; episode_count is null | Returns the dataset with its pinned episode_count |
POST .../snapshot | Pins membership now; returns snapshot_at and episode_count | 409 conflict — snapshotting is one-shot |
GET .../manifest | 409 conflict — snapshot first | NDJSON: a header line with the filter, then one line per member episode; page with X-Next-Cursor |
Two safe defaults apply at snapshot time: eval episodes stay out unless the filter names an eval_id (no train-on-test by accident), and discarded episodes stay out unless the filter names an outcome.
Webhooks
Coming soon — subscribe to events such as episode.finalized instead of polling the list endpoint. Until then, poll GET /v1/episodes with a ?from= filter.