Concepts
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:
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 you built;
- may have part of the lane’s 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:
{
"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
sha256digest; - 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.
{
"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 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:
{
"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 lists the codes each operation can return.