get/health
Service health
{"status": "ok"} when the database and Redis both answer, otherwise 503.Responses
- 200Successful Response
- 503the database or Redis is not answering
API
Early accessA REST API: create a job from your dataset, check its status, fetch the results. Start with a test key, then switch to live.
The API is in early access. Endpoints may change before general availability. The full reference below is generated from the live spec, so it always matches what the API accepts today. To get started, request a free pilot.
Send your key in the Authorization: Bearer header and keep it in an environment variable such as ANNOROID_API_KEY, never in code. Keys start with ak_; the dashboard shows whether each one is test or live.
You create your own keys: sign in to the client dashboard at app.annoroid.com, open API keys and create a test or live key. The full key is shown once, when you create it. Annoroid never emails API keys and will never ask you for one; if you receive a key by email, don't use it and tell us at contact@annoroid.com.
Three calls with a test key: create a job, check its status, fetch the results.
Point the job at a LeRobot dataset on Hugging Face. With a test key, keep it small: a few episodes are enough to see the whole flow.
import os, requests
API = "https://api.annoroid.com"
H = {"Authorization": f"Bearer {os.environ['ANNOROID_API_KEY']}"} # ak_…
job = requests.post(f"{API}/v1/jobs", headers=H, json={
"name": "quickstart",
"source": {
"type": "huggingface",
"repo_id": "lerobot/aloha_static_coffee",
"max_episodes": 5,
},
}).json()
print(job["id"], job["status"])A dataset that can't be used (not found, private, not LeRobot) still creates the job; it shows status failed with the reason.
Poll the job. progress is the share of episodes past automated processing, and counts breaks them down.
import time
while True:
job = requests.get(f"{API}/v1/jobs/{job['id']}", headers=H).json()
print(job["status"], job["stage"], f"{job['progress']:.0%}")
if job["status"] in ("delivered", "failed"):
break
time.sleep(10)Statuses: created → receiving → processing → in_review → delivered, or failed. Per-episode scores and checks: GET /v1/jobs/{id}/episodes.
Once ready is true, results lists every file in the delivery with its size, SHA-256 and a download link. Links expire; ask again for fresh ones.
res = requests.get(f"{API}/v1/jobs/{job['id']}/results", headers=H).json()
if res["ready"]:
for f in res["files"]:
path = os.path.join("delivery", f["path"])
os.makedirs(os.path.dirname(path), exist_ok=True)
with open(path, "wb") as out:
out.write(requests.get(f["url"]).content)
else:
print(res["message"])We read from and write back to your bucket (AWS S3, Google Cloud Storage, Cloudflare R2 or any S3-compatible storage) with scoped, time-limited credentials you control and can revoke.
Upload an archive of episodes to the job when your data isn't in cloud storage.
A public LeRobot dataset on the Hugging Face Hub: "source": {"type": "huggingface", "repo_id": "owner/dataset"}, optionally pinned to a revision and limited to some episodes.
Today the API reads from Hugging Face. For your own bucket or an upload, we set it up with you during the pilot until the API supports it.
The events below come from the live spec. Set your callback URL in the dashboard.
The job was accepted. Pipeline jobs start finding their episodes; labelling jobs wait for files. Verify the signature first (see the API description).
{
"id": "…",
"type": "job.created",
"created_at": "2026-01-01T00:00:00Z",
"mode": "test",
"test": false,
"data": {
"job": null
}
}Work started: episodes are being processed, or files are being labelled. Verify the signature first (see the API description).
{
"id": "…",
"type": "job.created",
"created_at": "2026-01-01T00:00:00Z",
"mode": "test",
"test": false,
"data": {
"job": null
}
}Automated processing finished; people are reviewing (test jobs: decided automatically). Verify the signature first (see the API description).
{
"id": "…",
"type": "job.created",
"created_at": "2026-01-01T00:00:00Z",
"mode": "test",
"test": false,
"data": {
"job": null
}
}Results are ready: fetch them from data.job.links.results. Verify the signature first (see the API description).
{
"id": "…",
"type": "job.created",
"created_at": "2026-01-01T00:00:00Z",
"mode": "test",
"test": false,
"data": {
"job": null
}
}The job stopped; data.job.failure_reason says why. Verify the signature first (see the API description).
{
"id": "…",
"type": "job.created",
"created_at": "2026-01-01T00:00:00Z",
"mode": "test",
"test": false,
"data": {
"job": null
}
}Generated from the live spec at https://api.annoroid.com/openapi.json (Annoroid API, v1).
/healthService health
{"status": "ok"} when the database and Redis both answer, otherwise 503.Responses
/v1/jobsCreate Job
failed with the reason.Request body · JobIn
| Field | Type | Notes |
|---|---|---|
| type | "pipeline_job" | default "pipeline_job" |
| name | string | max length 200 · default "" |
| sourcerequired | HuggingFaceSource | |
| robot | Robot | null | |
| deadline | string (date-time) | null | |
| outputs | "lerobot_v2" | "manifest" | "quality_report"[] | default ["lerobot_v2","manifest","quality_report"] |
| include_borderline | boolean | put borderline episodes in the dataset toodefault false |
| trim_static | boolean | cut still time at the start and end of each episodedefault true |
| smoothing | "none" | "light" | light: 3-frame moving average on state and actiondefault "none" |
| callback_url | string | null | https URL told of every status change of this job (overrides the organization's default webhook URL); see the webhooks sectionmax length 2000 |
Responses
/v1/jobsList Jobs
Parameters
| limit | query · integer | min 1 · max 200 · default 50 |
Responses
/v1/jobs/{job_id}Get Job
Parameters
| job_idrequired | path · string |
Responses
/v1/jobs/{job_id}/episodesList Episodes
Parameters
| job_idrequired | path · string | |
| status | query · string | null | |
| decision | query · string | null | |
| limit | query · integer | min 1 · max 1000 · default 100 |
| offset | query · integer | min 0 · default 0 |
Responses
/v1/jobs/{job_id}/resultsGet Results
Parameters
| job_idrequired | path · string |
Responses
/v1/annotation-jobsCreate Annotation Job
Request body · AnnotationJobIn
| Field | Type | Notes |
|---|---|---|
| namerequired | string | max length 200 |
| workspace | "annotate" | "lidar" | default "annotate" |
| labelsrequired | LabelIn[] | the classes to labelmax 200 items |
| guidelines | string | instructions for the annotatorsmax length 20000 · default "" |
| deadline | string (date-time) | null | |
| video_to_lidar | VideoToLidar | null | lidar only: accept video and make LiDAR from it in the studio |
| callback_url | string | null | https URL told of every status change of this jobmax length 2000 |
Responses
/v1/annotation-jobs/{job_id}/filesUpload Files
task_per=file (default): one task per file. task_per=upload: all of
this upload is one task - a point-cloud sequence with its calib and poses.txt, say.Parameters
| job_idrequired | path · string |
Responses
/v1/annotation-jobs/{job_id}Get Annotation Job
Parameters
| job_idrequired | path · string |
Responses
/v1/annotation-jobs/{job_id}/tasksList Tasks
Parameters
| job_idrequired | path · string | |
| status | query · string | null | |
| limit | query · integer | min 1 · max 5000 · default 500 |
| offset | query · integer | min 0 · default 0 |
Responses
/v1/annotation-jobs/{job_id}/resultsGet Results
ready when all are).Parameters
| job_idrequired | path · string | |
| limit | query · integer | min 1 · max 1000 · default 200 |
| offset | query · integer | min 0 · default 0 |
Responses
/v1/webhooksGet Config
Responses
/v1/webhooksSet Config
Request body · WebhookConfigIn
| Field | Type | Notes |
|---|---|---|
| url | string | null | https URL, or null to stop sendingmax length 2000 |
Responses
/v1/webhooks/secretNew Secret
Responses
/v1/webhooks/deliveriesDeliveries
Parameters
| job_id | query · string | null | |
| status | query · string | null | |
| limit | query · integer | min 1 · max 500 · default 100 |
Responses
/v1/webhooks/deliveries/{delivery_id}/resendResend
Parameters
| delivery_idrequired | path · string |
Responses
| Field | Type | Notes |
|---|---|---|
| namerequired | string | max length 200 |
| workspace | "annotate" | "lidar" | default "annotate" |
| labelsrequired | LabelIn[] | the classes to labelmax 200 items |
| guidelines | string | instructions for the annotatorsmax length 20000 · default "" |
| deadline | string (date-time) | null | |
| video_to_lidar | VideoToLidar | null | lidar only: accept video and make LiDAR from it in the studio |
| callback_url | string | null | https URL told of every status change of this jobmax length 2000 |
| Field | Type | Notes |
|---|---|---|
| idrequired | string | |
| moderequired | string | test | live |
| testrequired | boolean | |
| namerequired | string | |
| statusrequired | string | receiving (no files yet) | processing | delivered (every task approved) |
| workspacerequired | string | |
| labelsrequired | object[] | |
| tasksrequired | object | tasks per status, and total |
| progressrequired | number | share of tasks approved, 0..1 |
| deadlinerequired | string (date-time) | null | |
| created_atrequired | string (date-time) | |
| delivered_atrequired | string (date-time) | null |
| Field | Type | Notes |
|---|---|---|
| filesrequired | string[] | |
| task_per | "file" | "upload" | default "file" |
| name | string | default "" |
| Field | Type | Notes |
|---|---|---|
| namerequired | string | |
| scorerequired | number | null | 0..1; null when the check could not run (reason says why) |
| reasonrequired | string | |
| flags | object[] | time ranges the check points at: t_start, t_end (s), label |
| Field | Type | Notes |
|---|---|---|
| totalrequired | integer | |
| queuedrequired | integer | |
| in_progressrequired | integer | |
| processedrequired | integer | |
| failedrequired | integer | |
| awaiting_review | integer | default 0 |
| accepted | integer | default 0 |
| rejected | integer | default 0 |
| borderline | integer | default 0 |
| Field | Type | Notes |
|---|---|---|
| idrequired | string | |
| eventrequired | string | |
| job_idrequired | string | null | |
| moderequired | string | |
| urlrequired | string | |
| statusrequired | string | pending | sending | delivered | failed |
| attemptsrequired | integer | |
| last_status_coderequired | integer | null | |
| last_errorrequired | string | null | |
| next_attempt_atrequired | string (date-time) | null | |
| created_atrequired | string (date-time) | |
| delivered_atrequired | string (date-time) | null | |
| resent_fromrequired | string | null |
| Field | Type | Notes |
|---|---|---|
| idrequired | string | |
| indexrequired | integer | episode_index in the source dataset |
| statusrequired | string | |
| taskrequired | string | |
| lengthrequired | integer | null | |
| errorrequired | string | null | |
| score | number | null | 0..1 overall quality from the automated checks |
| checks | CheckOut[] | |
| decision | string | null | accept | reject | borderline, once a person decided |
| failure_category | string | null |
| Field | Type | Notes |
|---|---|---|
| totalrequired | integer | |
| limitrequired | integer | |
| offsetrequired | integer | |
| episodesrequired | EpisodeOut[] |
| Field | Type | Notes |
|---|---|---|
| jobrequired | JobInEvent |
| Field | Type | Notes |
|---|---|---|
| detail | ValidationError[] |
| Field | Type | Notes |
|---|---|---|
| type | "huggingface" | default "huggingface" |
| repo_idrequired | string | owner/dataset, or a huggingface.co/datasets/... linke.g. "lerobot/pusht" |
| revision | string | branch, tag or commitmax length 100 · default "main" |
| episodes | integer[] | null | only these episode indexes (default: all)max 10000 items |
| max_episodes | integer | null | at most this manymin 1 · max 100000 |
The body of every callback.
| Field | Type | Notes |
|---|---|---|
| idrequired | string | evt_...: one per event; repeated on retries and resends |
| typerequired | "job.created" | "job.processing" | "job.in_review" | "job.delivered" | "job.failed" | |
| created_atrequired | string (date-time) | |
| moderequired | "test" | "live" | |
| testrequired | boolean | true for jobs made with a test key |
| datarequired | EventData |
| Field | Type | Notes |
|---|---|---|
| type | "pipeline_job" | default "pipeline_job" |
| name | string | max length 200 · default "" |
| sourcerequired | HuggingFaceSource | |
| robot | Robot | null | |
| deadline | string (date-time) | null | |
| outputs | "lerobot_v2" | "manifest" | "quality_report"[] | default ["lerobot_v2","manifest","quality_report"] |
| include_borderline | boolean | put borderline episodes in the dataset toodefault false |
| trim_static | boolean | cut still time at the start and end of each episodedefault true |
| smoothing | "none" | "light" | light: 3-frame moving average on state and actiondefault "none" |
| callback_url | string | null | https URL told of every status change of this job (overrides the organization's default webhook URL); see the webhooks sectionmax length 2000 |
| Field | Type | Notes |
|---|---|---|
| idrequired | string | |
| typerequired | "pipeline_job" | "annotation_job" | |
| namerequired | string | |
| statusrequired | "receiving" | "processing" | "in_review" | "delivered" | "failed" | |
| stagerequired | string | |
| failure_reasonrequired | string | null | |
| moderequired | "test" | "live" | |
| testrequired | boolean | |
| created_atrequired | string (date-time) | |
| delivered_atrequired | string (date-time) | null | |
| linksrequired | JobLinks |
| Field | Type | Notes |
|---|---|---|
| jobrequired | string | GET this for the job's status |
| resultsrequired | string | GET this for the results once delivered |
| Field | Type | Notes |
|---|---|---|
| idrequired | string | |
| typerequired | string | |
| mode | string | test (made with a test key: no human work, deleted after a week) | livedefault "live" |
| test | boolean | default false |
| namerequired | string | |
| statusrequired | string | created | receiving | processing | in_review | delivered | failed |
| stagerequired | string | |
| failure_reasonrequired | string | null | |
| sourcerequired | object | |
| outputsrequired | string[] | |
| countsrequired | Counts | |
| progressrequired | number | share of episodes past automated processing, 0..1 |
| eta_secondsrequired | integer | null | |
| deadlinerequired | string (date-time) | null | |
| created_atrequired | string (date-time) | |
| updated_atrequired | string (date-time) | |
| tasks | object | null | annotation jobs: tasks per status, and total |
The robot's real joint limits, in the same units and order as observation.state.
| Field | Type | Notes |
|---|---|---|
| minrequired | number[] | max 256 items |
| maxrequired | number[] | max 256 items |
| Field | Type | Notes |
|---|---|---|
| namerequired | string | max length 80 |
| color | string | null | #rrggbbmax length 9 |
| size | number[] | null | lidar: usual length, width, height in metresmax 3 items |
| Field | Type | Notes |
|---|---|---|
| pathrequired | string | path inside the delivery, e.g. dataset/meta/info.json |
| sizerequired | integer | |
| sha256required | string | |
| urlrequired | string | download link; valid until expires_at - ask again for fresh links |
| expires_atrequired | string (date-time) |
| Field | Type | Notes |
|---|---|---|
| job_idrequired | string | |
| statusrequired | string | |
| readyrequired | boolean | |
| messagerequired | string | |
| dataset_format | string | null | |
| episodes | integer | null | |
| files | ResultFile[] |
| Field | Type | Notes |
|---|---|---|
| job_idrequired | string | |
| readyrequired | boolean | every task approved |
| approvedrequired | integer | |
| totalrequired | integer | |
| tasksrequired | TaskResult[] |
| Field | Type | Notes |
|---|---|---|
| joint_limits | JointLimits | null | enables the near-joint-limit check; a dataset's own min/max are not limits |
| Field | Type | Notes |
|---|---|---|
| secretrequired | string | whsec_...: shown only in this response; the previous one stops working |
| Field | Type | Notes |
|---|---|---|
| idrequired | string | |
| namerequired | string | |
| statusrequired | string | |
| filesrequired | string[] | |
| approved_atrequired | string (date-time) | null | |
| updated_atrequired | string (date-time) |
| Field | Type | Notes |
|---|---|---|
| task_idrequired | string | |
| namerequired | string | |
| filesrequired | object[] | name, kind, sha256 of each file the labels were made on |
| approved_atrequired | string (date-time) | null | |
| resultrequired | object | manifest: objects with their shapes per frame; point_labels and trajectory for LiDAR work |
| Field | Type | Notes |
|---|---|---|
| locrequired | string | integer[] | |
| msgrequired | string | |
| typerequired | string | |
| input | any | |
| ctx | object |
| Field | Type | Notes |
|---|---|---|
| scene | "auto" | "indoor" | "outdoor" | default "auto" |
| hfov_deg | number | null | the camera's horizontal field of viewmin 20 · max 160 |
| beams | integer | min 8 · max 256 · default 64 |
| Field | Type | Notes |
|---|---|---|
| urlrequired | string | null | default https URL for job events (a job's callback_url wins) |
| secret_setrequired | boolean | |
| secret_hintrequired | string | null | last 4 characters of the current secret |
| events | string[] | default ["job.created","job.processing","job.in_review","job.delivered","job.failed"] |
| Field | Type | Notes |
|---|---|---|
| url | string | null | https URL, or null to stop sendingmax length 2000 |
Send 100 episodes through the API or the form. Results come back in 48 hours.