---
title: Concepts
description: Tenants, lanes and keys; sandboxes and their limits; images; jobs; safe retries; and errors.
---

## Tenants, lanes and keys

A **tenant** is one customer of the platform, usually one app. A **lane** is one of that tenant's environments, for example `staging` or `production`. Lanes are kept apart: each has its own sandboxes, limits and keys.

An **API key** belongs to exactly one lane, and the lane is always taken from the key. A key looks like `ibk_<public id>_<secret>`. It is shown once, when it is made, and the platform keeps only a digest of it. Send it as a bearer token:

```http
Authorization: Bearer ibk_...
```

A call with a missing, unknown or revoked key gets `401 unauthorized`. A key used on another lane's sandbox, job or image gets `404 not_found`, so ids never leak between lanes. Usage shows only the key's own lane.

## Sandboxes

A **sandbox** is an isolated place to run code. Each one:

- has no network and holds no secrets;
- has a working directory, made when it is created;
- lives for a set time, two minutes unless you ask for up to five, then stops on its own;
- gets the lane's CPU and memory, unless you ask for other values within the lane's caps;
- runs on the lane's default image, unless you name an [image](#images) you built;
- may have part of the lane's [store folder](#a-read-only-store-folder) attached, read-only;
- may have a **name** of your own, unique among the lane's running sandboxes, so you can remove it by name after a crash.

A sandbox is `pending` while it is being made, then `running`, then `stopped`. A call on a stopped sandbox gets `409 sandbox_stopped`, except reading its status, which shows `stopped`, and removing it, which succeeds. New statuses may be added, so treat one you do not know as "not running".

### A read-only store folder

A lane may have a store folder attached: files your app keeps in the platform's storage, under the lane's own root. A create can attach part of it, read-only, with `store`:

```json
{
  "workdir": "/work",
  "store": { "mount_path": "/work/in", "key_prefix": "runs/1/" }
}
```

`mount_path` is an absolute path inside `workdir`. `key_prefix` is relative to the lane's root: no leading `/`, it ends in `/`, and it has no `.` or `..` part. The sandbox sees only the files under that prefix, and cannot change them. A prefix the platform does not allow gets `400 store_prefix_refused`, and a lane with no store folder gets `409 store_unavailable`. The sandbox record shows the attached folder as `store`, or `null`.

### Running commands

A command is an argument list, not a shell line: `["python", "-c", "print(1)"]`. To use a shell, run one: `["sh", "-c", "ls | wc -l"]`. You can pass a working directory, up to 8 environment variables and a timeout of up to 120 seconds.

A command ends one of two ways. It **exited**, with its exit code, or it **timed out** and was stopped. A timeout is a normal result, not an error. Standard output comes back cut at the lane's limit (64 KiB unless your lane says otherwise), with a flag that says whether it was cut. In this version a command that timed out returns no output: `stdout` is `null`.

### Files

Files move through the API as bytes, up to 32 MiB per call. One call can write several files. Reading a file that does not exist gets `404 file_not_found`; a file larger than you allow gets `413 file_too_large`.

## Images

An **image** is what a sandbox runs on. Each lane has a default image. To run on your own, write a **recipe** and build it:

- a base image, pinned by its `sha256` digest;
- the Python packages to install, each at one exact version, with the hashes of its files;
- optionally, modules to import once the packages are in, as a check that the build works.

A recipe has no build commands, so the same recipe always builds the same image. Nothing is installed that is not listed with its hash.

```json
{
  "base": {
    "reference": "python:3.13-slim",
    "digest": "sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
  },
  "packages": [
    {
      "name": "numpy",
      "version": "2.3.1",
      "hashes": [
        "sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
      ]
    }
  ],
  "import_check": ["numpy"]
}
```

A build is a [job](#jobs) of kind `image_build`. When it is `done`, its result names the image: `{ "image_id": "img_..." }`. When it is `failed`, its `error.code` says why:

| Code | Meaning |
| --- | --- |
| `build_failed` | The build itself failed, for example a package would not install. |
| `import_check_failed` | The image built, but a module in `import_check` would not import. |
| `build_lost` | The platform lost track of the build. Build the recipe again. |
| `unavailable` | A service behind the API failed. Build the recipe again after a short wait. |

New codes may be added, so treat a code you do not know as a failure. An image is `building`, then `ready` or `failed`. Create a sandbox with `image` set to a ready image's id. A create from an image that is not ready gets `409 image_not_built`. Getting an image can also answer `409 image_not_built`, after a lane changes provider: the image has no build on the lane's current provider, so rebuild it from its recipe.

## Jobs

Work that takes longer than one call returns a **job** at once. You check it until it is `done`, `failed` or `cancelled`, and you can ask to cancel it. A job moves forward only: `accepted`, `running`, then one of the three final states.

## Safe retries

Every call that creates or writes takes an `Idempotency-Key`. A retry with the same key and the same body is the same call:

- a create returns the same sandbox;
- a file write returns its first result;
- a call that is still running gets `409 in_progress`; retry it later with the same key;
- a command that has finished returns its outcome and exit code, but not its output, which the platform does not keep.

The same key with a different body gets `409 idempotency_mismatch`. Keys are kept for 24 hours.

The SDKs and the command line tool send a fresh key on every such call, and retry up to twice on their own after a dropped connection or a `503`, reusing the key. They never retry a command on their own, because a replay returns no output, and they never retry a `409 in_progress`.

## Errors

Every error has the same shape, with a stable code, a message for people and the request's id:

```json
{
  "error": {
    "code": "sandbox_stopped",
    "message": "The sandbox has stopped: its lifetime or size limit was reached, or it was removed.",
    "request_id": "req_..."
  }
}
```

Every response also carries the request id in its `X-Request-Id` header. Quote it when you report a problem. New codes may be added over time, so handle an unknown code by its HTTP status. The [API reference](/api) lists the codes each operation can return.
