> ## Documentation Index
> Fetch the complete documentation index at: https://docs.kiteml.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Digital twins: rebuild a robot episode as a MuJoCo scene

> Point Kite at one episode of a LeRobot dataset and get back an interactable MuJoCo scene of the room, with the objects the robot handles built to size, delivered as one verified download.

A **twin** is one episode of a robotics dataset rebuilt as an interactable simulation. Kite reconstructs the room from the episode's camera, builds the objects the robot handles with their real size and physics, checks every stage by measurement, and delivers the scene as a single archive you open in MuJoCo.

<Note>
  **Private 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.
</Note>

You give Kite one thing: the dataset. A twin takes about 100 minutes, 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 tells you how long a twin typically takes. It creates nothing and costs nothing.

```bash theme={"system"}
curl https://api.kiteml.com/v1/twins/validate \
  -H "Authorization: Bearer $KITE_API_KEY" \
  -H "Kite-Version: 2026-09-27" \
  -H "Content-Type: application/json" \
  -d '{"source": {"type": "huggingface", "repo_id": "lerobot/svla_so101_pickplace"}}'
```

```json theme={"system"}
{
  "object": "twin_validation",
  "ok": true,
  "source": { "type": "huggingface", "repo_id": "lerobot/svla_so101_pickplace" },
  "cameras": ["observation.images.side", "observation.images.up"],
  "scene_camera": "observation.images.side",
  "wrist_camera": null,
  "episodes": 50,
  "fps": 30,
  "format": "v3.0",
  "estimate": { "duration_s": 5910 }
}
```

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:

```json theme={"system"}
{
  "error": {
    "type": "invalid_request_error",
    "code": "camera_not_found",
    "message": "no camera matches 'top'",
    "param": "camera",
    "details": { "cameras": ["observation.images.side", "observation.images.up"] }
  }
}
```

## Create a twin

```bash theme={"system"}
curl https://api.kiteml.com/v1/twins \
  -H "Authorization: Bearer $KITE_API_KEY" \
  -H "Kite-Version: 2026-09-27" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{"source": {"type": "huggingface", "repo_id": "lerobot/svla_so101_pickplace"}}'
```

The response is the twin, `queued`, with `202 Accepted`:

```json theme={"system"}
{
  "id": "twin_01M3GKYV1Z7A0YV4851EGGQYPP",
  "object": "twin",
  "status": "queued",
  "progress": 0.0,
  "source": { "type": "huggingface", "repo_id": "lerobot/svla_so101_pickplace" },
  "camera": { "scene": "observation.images.side", "wrist": null, "available": ["observation.images.side", "observation.images.up"] },
  "download": null,
  "created_at": "2026-09-27T05:03:45.347344Z"
}
```

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

| Field              | Type   | Description                                                                                                                                            |
| ------------------ | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `source`           | object | **Required.** `{"type": "huggingface", "repo_id": "..."}` — the same shape as an augmentation's source. A `huggingface.co` URL works as `repo_id` too. |
| `name`             | string | A short name for the twin.                                                                                                                             |
| `task`             | string | Which objects to build: the ones the robot handles. Defaults to a general manipulation brief.                                                          |
| `prompt`           | string | How to describe the room to the reconstructor.                                                                                                         |
| `camera`           | string | A substring of the camera that should seed the room. Validate lists the choices.                                                                       |
| `wrist_camera`     | string | A substring of the wrist camera to use for close-ups.                                                                                                  |
| `ceiling_m`        | number | Floor-to-ceiling height of the real room — the scale anchor. Default `2.9`.                                                                            |
| `webhook_metadata` | object | Echoed on the twin and on its webhook events.                                                                                                          |

### 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](/platform-api/api-reference#operations-and-webhooks) for `twin.completed`, `twin.failed`, and `twin.canceled` and skip polling.

```json theme={"system"}
{
  "id": "twin_01M3GKYV1Z7A0YV4851EGGQYPP",
  "object": "twin",
  "status": "processing",
  "stage": "decompose",
  "progress": 0.281,
  "eta_seconds": 4250,
  "status_message": "decompose: 7 min in, typically 17 min",
  "stages": [
    { "stage": "ingest", "ok": true, "attempts": 1, "seconds": 12.2, "measurement": "10 probe frames, keyframe t002.png, 0 wrist frames" },
    { "stage": "environment", "ok": true, "attempts": 1, "seconds": 423.7, "measurement": "completed panorama 4608x2304, aspect 2.00" }
  ]
}
```

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`, `environment`, `decompose`, `label`, `author`, `refine`, `assemble`, `dynamics`, and `publish`.

## Download the scene

Once the twin has `succeeded`, `download` describes the whole scene as one `.tar.gz`:

```json theme={"system"}
"download": {
  "url": "https://storage.googleapis.com/…/twin_01M3GKYV1Z7A0YV4851EGGQYPP.tar.gz?X-Goog-Algorithm=GOOG4-RSA-SHA256&…",
  "requires_auth": false,
  "expires_at": "2026-09-27T18:17:29Z",
  "filename": "twin_01M3GKYV1Z7A0YV4851EGGQYPP.tar.gz",
  "bytes": 126249063,
  "sha256": "6a6f2debbbf17cbd…",
  "scene": "twin_01M3GKYV1Z7A0YV4851EGGQYPP/scene.xml"
}
```

`url` is a signed link straight to the archive. Fetch it **without** your API key, check the checksum, and extract:

```bash theme={"system"}
curl -fL "$DOWNLOAD_URL" -o twin.tar.gz
shasum -a 256 twin.tar.gz          # compare with download.sha256
tar xzf twin.tar.gz                # twin_01M3GKYV…/scene.xml and assets/
```

The folder holds `scene.xml` — load it with `mujoco.MjModel.from_xml_path(...)` — and `assets/`: meshes, textures, and the room as a Gaussian splat. The link expires after an hour; retrieve the twin again for a fresh one. `GET /v1/twins/:id/archive` does the same in one call: it redirects to a fresh link (`curl -L`).

To see what's in the archive without downloading it, list its files:

```bash theme={"system"}
curl "https://api.kiteml.com/v1/twins/twin_01M3GKYV1Z7A0YV4851EGGQYPP/files?limit=2" \
  -H "Authorization: Bearer $KITE_API_KEY"
```

```json theme={"system"}
{
  "object": "list",
  "data": [
    { "object": "twin_file", "path": "assets/background.obj", "bytes": 39594932, "sha256": "03129cb58e32…" },
    { "object": "twin_file", "path": "assets/background_h.obj", "bytes": 72696, "sha256": "ba50d1736800…" }
  ],
  "has_more": true,
  "next_cursor": "assets/background_h.obj"
}
```

## Without writing HTTP

The Kite CLI and Python SDK wrap the whole flow — create, wait, download, verify, and extract:

<CodeGroup>
  ```bash CLI theme={"system"}
  kite twin create lerobot/svla_so101_pickplace --wait --out ./twins
  ```

  ```python Python theme={"system"}
  from kite_sdk import Kite

  twin = Kite().twins.create("lerobot/svla_so101_pickplace")
  path = twin.wait(on_update=print).download()     # absolute path to <twin id>/scene.xml
  ```
</CodeGroup>

Both put twins in the folder you give, else in `$KITE_TWINS_DIR`, else in `./twins`, and report absolute paths. Agents get the same flow as the MCP tools `kite_twin_validate`, `kite_twin_create`, 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

| Status | Code                                 | Meaning                                                                          |
| ------ | ------------------------------------ | -------------------------------------------------------------------------------- |
| `400`  | `dataset_not_found`                  | The dataset doesn't exist, or isn't public                                       |
| `400`  | `dataset_gated`                      | The dataset is gated — accept its terms on Hugging Face, or use a public dataset |
| `400`  | `dataset_has_no_video`               | The dataset has no camera video to rebuild the room from                         |
| `400`  | `camera_not_found`                   | No camera matches `camera`; `details.cameras` lists the real ones                |
| `400`  | `no_suitable_camera`                 | Every camera is a wrist camera; the room needs one that sees the scene           |
| `403`  | `twins_not_enabled`                  | Twins aren't enabled for this account yet                                        |
| `409`  | `twin_not_ready`                     | Files or the archive requested before the twin succeeded                         |
| `409`  | `not_cancelable` / `already_running` | Cancel on a finished twin, or resume on one still building                       |
| `429`  | `concurrency_limit_exceeded`         | A twin is already building on this account — honor `Retry-After`                 |
| `429`  | `capacity_exceeded`                  | Kite's builders are full — honor `Retry-After`                                   |

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

| Field              | Type            | Description                                                                                    |
| ------------------ | --------------- | ---------------------------------------------------------------------------------------------- |
| `id`               | string          | `twin_` followed by a time-ordered id                                                          |
| `status`           | string          | `queued`, `processing`, `canceling`, `succeeded`, `failed`, or `canceled`                      |
| `progress`         | number          | `0` to `1`, estimated within the running stage                                                 |
| `stage`            | string or null  | The stage running now                                                                          |
| `eta_seconds`      | integer or null | While building: time left, from the stages' typical durations                                  |
| `error`            | object or null  | When `failed`: `code`, `message`, `stage`, `resumable`                                         |
| `source`           | object          | The dataset the episode came from                                                              |
| `camera`           | object or null  | The camera that seeds the room, and every camera the dataset has                               |
| `config`           | object          | The build options you gave at create                                                           |
| `stages`           | array           | Every finished stage: `stage`, `ok`, `attempts`, `seconds`, `measurement`                      |
| `download`         | object or null  | Once `succeeded`: `url`, `requires_auth`, `expires_at`, `filename`, `bytes`, `sha256`, `scene` |
| `webhook_metadata` | object or null  | Whatever you passed at create                                                                  |
| `created_at`       | string          | ISO 8601, UTC                                                                                  |
