Skip to main content
A twin is one episode of a robotics dataset rebuilt as an interactable simulation. Kite builds the objects the robot handles with their real size and physics, places the robot in a calibrated copy of the cell, and replays the episode’s recorded motion in it. It then judges the twin against the episode’s video and repairs it for a few rounds, and delivers the best round as a single archive you open in MuJoCo.
Beta. Twins are enabled per account. A call from an account that isn’t enabled returns 403 with code twins_not_enabled. Ask your Kite contact to turn them on.
You give Kite two things: the dataset, and the robot in it. A twin takes a few hours, so the API is asynchronous — create it, then poll it or get a webhook when it’s done.

Check a dataset first

POST /v1/twins/validate runs every check a create runs — the dataset exists and has camera video, which camera will seed the room, and the robot can drive it — and tells you how long a twin typically takes. It creates nothing and costs nothing.
If something is wrong, the error names it and the field to fix. For example, a camera that doesn’t exist lists the ones that do:
The robot is checked the same way. Each value the dataset records per frame drives one of the robot’s joints, so a robot that takes a different number is refused, with the ones that fit:

Create a twin

The response is the twin, queued, with 202 Accepted:
The dataset is checked before anything starts, so a twin that can’t work is refused at create, for free, with the same errors as validate.

Request body

Idempotency

Send an Idempotency-Key header with every create. A retry with the same key and body returns the same twin — even while the first request is still being processed — so a dropped connection never builds two twins. Reusing a key with a different body returns 409 idempotency_key_reused. One twin builds at a time per account; a second create returns 429 concurrency_limit_exceeded with a Retry-After header.

Track progress

Retrieve the twin with GET /v1/twins/:id. Poll every 30 seconds or so, or register a webhook for twin.completed, twin.failed, and twin.canceled and skip polling.
A twin moves through queued → processing → succeeded. failed and canceled are the other final states, and canceling sits between a cancel and the build actually stopping. Each finished stage reports what it measured about its own output — ingest, author, refine, assemble, dynamics, twin, and publish. twin is the robot calibrated and replayed in the cell, judged against the episode’s video, and repaired, round by round; its measurement is the best round’s score. environment, decompose, and label are reported as skipped: the twin stage builds the room.

Download the scene

Once the twin has succeeded, download describes the whole scene as one .tar.gz:
url is the API’s archive path. Fetch it with your API key: it redirects to a signed link to the archive, valid for an hour, and curl doesn’t send your key on to it. Check the checksum, and extract:
The folder holds scene.xml — the robot, the room, and the objects; load it with mujoco.MjModel.from_xml_path(...) — and assets/, its meshes and textures. Beside them, real_twin.mp4 shows the episode next to its twin, and verdict.json holds the judge’s score and the differences it found. If no round could be exported as a whole twin, the archive holds the objects alone, and the twin stage says so. The path doesn’t expire: each fetch redirects to a fresh link. To see what’s in the archive without downloading it, list its files:

Without writing HTTP

The Kite CLI wraps the whole flow — create, wait, download, verify, and extract:
It puts twins in the folder you give, else in $KITE_TWINS_DIR, else in ./twins, and reports absolute paths. --robot needs kiteml-cli 0.6.0 or later. Agents get the same flow as the MCP tools kite_twin_validate, kite_twin_create (with robot), and kite_twin_status, whose next field is the exact download command.

Cancel and resume

POST /v1/twins/:id/cancel stops a twin that’s building. It’s canceling for a few seconds, until the build has stopped, then canceled. A failed twin whose error.resumable is true, or a canceled twin, can continue with POST /v1/twins/:id/resume. Every stage it finished is kept, so only the rest runs again.

Errors

A twin that stops part-way reports status: "failed" with error.code of interrupted, or stage_failed when a stage’s own check didn’t pass. Both are resumable.

The twin object