Skip to content
annoroid

API

Early access

Plug Annoroid into your data pipeline

A 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.

API keys

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.

How keys are issued

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.

Test keys

  • Runs the full API flow: create, status, results
  • Automated checks only; no human review or other human work
  • Small limits, for trying the integration
  • Jobs and their data are deleted automatically after 7 days

Live keys

  • Creates real work: automated checks plus human review
  • Billed per episode, as agreed for your account
  • Delivery and retention as agreed per order

Quickstart

Three calls with a test key: create a job, check its status, fetch the results.

1. Create a job

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.

2. Check status

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.

3. Fetch results

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"])

Data sources

Your own bucketRecommended for large or sensitive dataNot in the API yet

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.

UploadNot in the API yet

Upload an archive of episodes to the job when your data isn't in cloud storage.

Hugging Face link

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.

Job callbacks

The events below come from the live spec. Set your callback URL in the dashboard.

job.created

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
  }
}

job.processing

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
  }
}

job.in_review

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
  }
}

job.delivered

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
  }
}

job.failed

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
  }
}

Full reference

Generated from the live spec at https://api.annoroid.com/openapi.json (Annoroid API, v1).

health

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

jobs

post/v1/jobs

Create Job

Creates the job and starts finding its episodes; poll GET /v1/jobs/{id}. A dataset that cannot be used (not found, private, not LeRobot) shows as status failed with the reason.

Request body · JobIn

FieldTypeNotes
type"pipeline_job"default "pipeline_job"
namestringmax length 200 · default ""
sourcerequiredHuggingFaceSource
robotRobot | null
deadlinestring (date-time) | null
outputs"lerobot_v2" | "manifest" | "quality_report"[]default ["lerobot_v2","manifest","quality_report"]
include_borderlinebooleanput borderline episodes in the dataset toodefault false
trim_staticbooleancut 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_urlstring | nullhttps URL told of every status change of this job (overrides the organization's default webhook URL); see the webhooks sectionmax length 2000

Responses

get/v1/jobs

List Jobs

Parameters

limitquery · integermin 1 · max 200 · default 50

Responses

get/v1/jobs/{job_id}

Get Job

Parameters

job_idrequiredpath · string

Responses

get/v1/jobs/{job_id}/episodes

List Episodes

Parameters

job_idrequiredpath · string
statusquery · string | null
decisionquery · string | null
limitquery · integermin 1 · max 1000 · default 100
offsetquery · integermin 0 · default 0

Responses

get/v1/jobs/{job_id}/results

Get Results

Parameters

job_idrequiredpath · string

Responses

annotation jobs

post/v1/annotation-jobs

Create Annotation Job

A labelling job on your own files. Upload them next with POST …/files.

Request body · AnnotationJobIn

FieldTypeNotes
namerequiredstringmax length 200
workspace"annotate" | "lidar"default "annotate"
labelsrequiredLabelIn[]the classes to labelmax 200 items
guidelinesstringinstructions for the annotatorsmax length 20000 · default ""
deadlinestring (date-time) | null
video_to_lidarVideoToLidar | nulllidar only: accept video and make LiDAR from it in the studio
callback_urlstring | nullhttps URL told of every status change of this jobmax length 2000

Responses

post/v1/annotation-jobs/{job_id}/files

Upload Files

Files to label. 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_idrequiredpath · string

Responses

get/v1/annotation-jobs/{job_id}

Get Annotation Job

Parameters

job_idrequiredpath · string

Responses

get/v1/annotation-jobs/{job_id}/tasks

List Tasks

Parameters

job_idrequiredpath · string
statusquery · string | null
limitquery · integermin 1 · max 5000 · default 500
offsetquery · integermin 0 · default 0

Responses

get/v1/annotation-jobs/{job_id}/results

Get Results

The labels of approved tasks (available as each one is approved; ready when all are).

Parameters

job_idrequiredpath · string
limitquery · integermin 1 · max 1000 · default 200
offsetquery · integermin 0 · default 0

Responses

webhooks

get/v1/webhooks

Get Config

Responses

put/v1/webhooks

Set Config

Request body · WebhookConfigIn

FieldTypeNotes
urlstring | nullhttps URL, or null to stop sendingmax length 2000

Responses

post/v1/webhooks/secret

New Secret

Responses

get/v1/webhooks/deliveries

Deliveries

Parameters

job_idquery · string | null
statusquery · string | null
limitquery · integermin 1 · max 500 · default 100

Responses

post/v1/webhooks/deliveries/{delivery_id}/resend

Resend

Parameters

delivery_idrequiredpath · string

Responses

Schemas

AnnotationJobIn

FieldTypeNotes
namerequiredstringmax length 200
workspace"annotate" | "lidar"default "annotate"
labelsrequiredLabelIn[]the classes to labelmax 200 items
guidelinesstringinstructions for the annotatorsmax length 20000 · default ""
deadlinestring (date-time) | null
video_to_lidarVideoToLidar | nulllidar only: accept video and make LiDAR from it in the studio
callback_urlstring | nullhttps URL told of every status change of this jobmax length 2000

AnnotationJobOut

FieldTypeNotes
idrequiredstring
moderequiredstringtest | live
testrequiredboolean
namerequiredstring
statusrequiredstringreceiving (no files yet) | processing | delivered (every task approved)
workspacerequiredstring
labelsrequiredobject[]
tasksrequiredobjecttasks per status, and total
progressrequirednumbershare of tasks approved, 0..1
deadlinerequiredstring (date-time) | null
created_atrequiredstring (date-time)
delivered_atrequiredstring (date-time) | null

Body_upload_files_v1_annotation_jobs__job_id__files_post

FieldTypeNotes
filesrequiredstring[]
task_per"file" | "upload"default "file"
namestringdefault ""

CheckOut

FieldTypeNotes
namerequiredstring
scorerequirednumber | null0..1; null when the check could not run (reason says why)
reasonrequiredstring
flagsobject[]time ranges the check points at: t_start, t_end (s), label

Counts

FieldTypeNotes
totalrequiredinteger
queuedrequiredinteger
in_progressrequiredinteger
processedrequiredinteger
failedrequiredinteger
awaiting_reviewintegerdefault 0
acceptedintegerdefault 0
rejectedintegerdefault 0
borderlineintegerdefault 0

DeliveryOut

FieldTypeNotes
idrequiredstring
eventrequiredstring
job_idrequiredstring | null
moderequiredstring
urlrequiredstring
statusrequiredstringpending | sending | delivered | failed
attemptsrequiredinteger
last_status_coderequiredinteger | null
last_errorrequiredstring | null
next_attempt_atrequiredstring (date-time) | null
created_atrequiredstring (date-time)
delivered_atrequiredstring (date-time) | null
resent_fromrequiredstring | null

EpisodeOut

FieldTypeNotes
idrequiredstring
indexrequiredintegerepisode_index in the source dataset
statusrequiredstring
taskrequiredstring
lengthrequiredinteger | null
errorrequiredstring | null
scorenumber | null0..1 overall quality from the automated checks
checksCheckOut[]
decisionstring | nullaccept | reject | borderline, once a person decided
failure_categorystring | null

EpisodePage

FieldTypeNotes
totalrequiredinteger
limitrequiredinteger
offsetrequiredinteger
episodesrequiredEpisodeOut[]

HuggingFaceSource

FieldTypeNotes
type"huggingface"default "huggingface"
repo_idrequiredstringowner/dataset, or a huggingface.co/datasets/... linke.g. "lerobot/pusht"
revisionstringbranch, tag or commitmax length 100 · default "main"
episodesinteger[] | nullonly these episode indexes (default: all)max 10000 items
max_episodesinteger | nullat most this manymin 1 · max 100000

JobEvent

The body of every callback.

FieldTypeNotes
idrequiredstringevt_...: one per event; repeated on retries and resends
typerequired"job.created" | "job.processing" | "job.in_review" | "job.delivered" | "job.failed"
created_atrequiredstring (date-time)
moderequired"test" | "live"
testrequiredbooleantrue for jobs made with a test key
datarequiredEventData

JobIn

FieldTypeNotes
type"pipeline_job"default "pipeline_job"
namestringmax length 200 · default ""
sourcerequiredHuggingFaceSource
robotRobot | null
deadlinestring (date-time) | null
outputs"lerobot_v2" | "manifest" | "quality_report"[]default ["lerobot_v2","manifest","quality_report"]
include_borderlinebooleanput borderline episodes in the dataset toodefault false
trim_staticbooleancut 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_urlstring | nullhttps URL told of every status change of this job (overrides the organization's default webhook URL); see the webhooks sectionmax length 2000

JobInEvent

FieldTypeNotes
idrequiredstring
typerequired"pipeline_job" | "annotation_job"
namerequiredstring
statusrequired"receiving" | "processing" | "in_review" | "delivered" | "failed"
stagerequiredstring
failure_reasonrequiredstring | null
moderequired"test" | "live"
testrequiredboolean
created_atrequiredstring (date-time)
delivered_atrequiredstring (date-time) | null
linksrequiredJobLinks

JobOut

FieldTypeNotes
idrequiredstring
typerequiredstring
modestringtest (made with a test key: no human work, deleted after a week) | livedefault "live"
testbooleandefault false
namerequiredstring
statusrequiredstringcreated | receiving | processing | in_review | delivered | failed
stagerequiredstring
failure_reasonrequiredstring | null
sourcerequiredobject
outputsrequiredstring[]
countsrequiredCounts
progressrequirednumbershare of episodes past automated processing, 0..1
eta_secondsrequiredinteger | null
deadlinerequiredstring (date-time) | null
created_atrequiredstring (date-time)
updated_atrequiredstring (date-time)
tasksobject | nullannotation jobs: tasks per status, and total

JointLimits

The robot's real joint limits, in the same units and order as observation.state.

FieldTypeNotes
minrequirednumber[]max 256 items
maxrequirednumber[]max 256 items

LabelIn

FieldTypeNotes
namerequiredstringmax length 80
colorstring | null#rrggbbmax length 9
sizenumber[] | nulllidar: usual length, width, height in metresmax 3 items

ResultFile

FieldTypeNotes
pathrequiredstringpath inside the delivery, e.g. dataset/meta/info.json
sizerequiredinteger
sha256requiredstring
urlrequiredstringdownload link; valid until expires_at - ask again for fresh links
expires_atrequiredstring (date-time)

Results

FieldTypeNotes
job_idrequiredstring
statusrequiredstring
readyrequiredboolean
messagerequiredstring
dataset_formatstring | null
episodesinteger | null
filesResultFile[]

ResultsOut

FieldTypeNotes
job_idrequiredstring
readyrequiredbooleanevery task approved
approvedrequiredinteger
totalrequiredinteger
tasksrequiredTaskResult[]

Robot

FieldTypeNotes
joint_limitsJointLimits | nullenables the near-joint-limit check; a dataset's own min/max are not limits

SecretOut

FieldTypeNotes
secretrequiredstringwhsec_...: shown only in this response; the previous one stops working

TaskBrief

FieldTypeNotes
idrequiredstring
namerequiredstring
statusrequiredstring
filesrequiredstring[]
approved_atrequiredstring (date-time) | null
updated_atrequiredstring (date-time)

TaskResult

FieldTypeNotes
task_idrequiredstring
namerequiredstring
filesrequiredobject[]name, kind, sha256 of each file the labels were made on
approved_atrequiredstring (date-time) | null
resultrequiredobjectmanifest: objects with their shapes per frame; point_labels and trajectory for LiDAR work

ValidationError

FieldTypeNotes
locrequiredstring | integer[]
msgrequiredstring
typerequiredstring
inputany
ctxobject

VideoToLidar

FieldTypeNotes
scene"auto" | "indoor" | "outdoor"default "auto"
hfov_degnumber | nullthe camera's horizontal field of viewmin 20 · max 160
beamsintegermin 8 · max 256 · default 64

WebhookConfig

FieldTypeNotes
urlrequiredstring | nulldefault https URL for job events (a job's callback_url wins)
secret_setrequiredboolean
secret_hintrequiredstring | nulllast 4 characters of the current secret
eventsstring[]default ["job.created","job.processing","job.in_review","job.delivered","job.failed"]

WebhookConfigIn

FieldTypeNotes
urlstring | nullhttps URL, or null to stop sendingmax length 2000

Start with a free pilot.

Send 100 episodes through the API or the form. Results come back in 48 hours.