---
title: "Create a sandbox"
method: POST
path: "/sandboxes"
tags: ["Sandboxes"]
---

# Create a sandbox

`POST /sandboxes`

## Request

- Base URL: `https://api.hedra.com/v3`
- URL: `https://api.hedra.com/v3/sandboxes`
- Auth: one of:
  - API key in header `Authorization`
  - HTTP bearer

Start one instance per replica, paying the first lease from the API
wallet before any instance is created. The sandbox runs until you stop
it, its command exits, the wallet cannot pay a lease, or its
`ttl_seconds` ends, when you give one. 404 while sandboxes are not enabled
for your workspace.

## Headers

- `Idempotency-Key` string

## Request body

- CreateSandboxRequest: A GPU sandbox to run: one instance per replica, billed to the API wallet.
  - `name` string, required: The sandbox's name: lowercase letters, digits and hyphens, starting and ending with a letter or digit, at most 32 characters. Unique among your creating, active and stopping sandboxes.
  - `image` string, required: The image reference to run.
  - `accelerator` string, required: A GPU model and a GPU count per instance, `<gpu model>:<count>` such as `h100:1`. The count is 1 to 99; `GET /sandboxes/gpus` lists the GPU models.
  - `replicas` integer: How many instances, at most your workspace's sandbox quota.
  - `resources` SandboxResources: CPU, memory, scratch disk and shared memory per instance.
    - `cpu` integer, nullable: CPU cores.
    - `memory_gb` integer, nullable: Memory, in GB.
    - `scratch_gb` integer, nullable: Scratch disk, in GB.
    - `shm_gb` integer, nullable: Shared memory at `/dev/shm`, in GB. Needs `memory_gb` at least as large. Unset: 8 GB per GPU, capped at half of `memory_gb` when `memory_gb` is set, and at least 1 GB.
  - `command` string[], nullable: What runs in place of the image's entrypoint; at most 16384 bytes written as JSON. Absent runs the image's `ENTRYPOINT`/`CMD`. The main process's stdin stays open, as under `docker run -i`, so a shell or an interpreter waits for input. The sandbox ends when the main process exits unless `restart` restarts it; set `idle` for a sandbox with no main process.
  - `idle` boolean: Run no main process: the sandbox starts with nothing running and stays up for sessions until you stop it, or, when you give them, `ttl_seconds` ends or it is idle for `idle_ttl_seconds`. Not with `command`; `restart` has no effect. The image's entrypoint is not run.
  - `restart` 'never' | 'on-failure' | 'always': When hedra-compute restarts the command; hedra-compute's own values.
  - `env` object: Environment variables for every instance; at most 32768 bytes written as JSON, no empty key or key containing `=`, and no key starting `SANDBOX_`, which the sandbox reserves. Answers return the keys only, as `env_keys`.
  - `ttl_seconds` integer, nullable: The sandbox's lifetime in seconds, counted from create, 120 to 31,536,000 (365 days); every instance is stopped when it ends. Leave it out for a sandbox that runs until you stop it, its command exits, or the wallet cannot pay a lease.
  - `idle_ttl_seconds` integer, nullable: For an `idle` sandbox only: end it, with `end_reason` `idle_ttl_ended`, once it has had no activity for this many seconds, 120 to 31,536,000. Activity is an exec or shell session, from its start until its process exits, and a request to its `url`, until its response ends; a port-forward, an attach, and a process left running after its session ends are not. With more than one replica, the instances stop together once every one is idle. The stop can land a minute or so after the limit. Leave it out for no idle limit.
  - `ports` SandboxPort[], nullable: The port to serve at the sandbox's `url`; at most 1. With `ports`, `name` never contains `--`. `POST /sandboxes/{sandbox_id}/endpoint-token` mints the token `url` requires.
    - `port` integer, required: The port the image serves on: 1 to 65535, other than 18000, which the sandbox reserves.
    - `health` string, nullable: An absolute HTTP path on `port` that the readiness probe requests, at most 256 characters.
  - `secrets` string[], nullable: The names of workspace secrets every instance receives, each as an environment variable of its own name; at most 32. `GET /sandboxes/secrets` lists the names your workspace holds.
  - `egress` union: Where every instance may connect: `all`, every public address; `default`, PyPI and Hugging Face on 80 and 443; `none`, nothing beyond what the sandbox needs to run; or the hosts and IP ranges listed. Beyond what the sandbox needs to run, no form reaches a private, loopback or link-local address.
    - 'all' | 'default' | 'none': Where a sandbox may connect, beside the rule form; hedra-compute's own values.
    - SandboxEgressRules: The hosts and IP ranges a sandbox may connect to, on the listed TCP ports, beside what the sandbox itself needs to run.
      - `hosts` string[]: DNS names the sandbox may connect to, each a name such as `api.github.com` or one with a single leading `*.` label such as `*.githubusercontent.com`; at most 32.
      - `cidrs` string[]: IP ranges the sandbox may connect to, each in canonical CIDR form such as `203.0.113.0/24`; at most 8. Private, loopback, link-local and other reserved addresses inside a range are never reached, and the create answers 400 for a range wholly inside them.
      - `ports` integer[]: The TCP ports `hosts` and `cidrs` are reached on, 1 to 65535; 80 and 443 when omitted. At most 8.
      - `default` boolean: Whether the sandbox also reaches PyPI and Hugging Face on 80 and 443.

## Response `201`

Successful Response

- Sandbox: One GPU sandbox.
  - `id` string, required: The sandbox's id (`sandbox_<uuid>`).
  - `name` string, required: The sandbox's name, as created.
  - `accelerator` string, required: The request's `accelerator`.
  - `gpu_model` string, required: The `accelerator`'s GPU model.
  - `replicas` integer, required: How many instances.
  - `status` 'creating' | 'active' | 'stopping' | 'ended' | 'failed', required: Where a sandbox is in its life; stored in ``sandbox_billings.status``.
  - `end_reason` 'caller_stopped' | 'ttl_ended' | 'exited' | 'instance_failed' | 'unpaid' | 'lease_lapsed' | 'access_removed' | 'idle_ttl_ended', required: Why a sandbox ended, or, on an `active` one, that its last lease was refused.
  - `created_at` string, date-time, required
  - `started_at` string, date-time, nullable, required: The earliest `started_at` of its instances; null on the list and before any instance starts.
  - `ended_at` string, date-time, nullable, required: When the last instance stopped; null until then.
  - `ttl_ends_at` string, date-time, nullable, required: When the sandbox's lifetime ends: `created_at` plus `ttl_seconds`; null for a sandbox created without `ttl_seconds`.
  - `env_keys` string[], required: The keys of `env`, never its values.
  - `image` string, nullable: The image the sandbox runs, as created. Set wherever `instances` is and the sandbox has an instance; null on the list, on the create answer, and while it has none.
  - `command` string[], nullable: The `command` the sandbox runs, as created and returned as given, so a secret belongs in `env` rather than here; null when `image` is, when the image's own `ENTRYPOINT`/`CMD` runs, and for an `idle` sandbox.
  - `instances` SandboxInstance[], nullable, required: One entry per replica; null on the list.
    - `replica` integer, required: The replica's number, from 0.
    - `status` 'starting' | 'running' | 'stopped' | 'failed', required: One instance's state, derived from its hedra-compute charge times.
    - `started_at` string, date-time, nullable, required: When the instance started running; null while it starts.
    - `stopped_at` string, date-time, nullable, required: When the instance stopped; null while it runs.
    - `failure_reason` 'quota_exceeded' | 'no_capacity' | 'image_pull_failed' | 'start_failed' | 'process_exited' | 'oom_killed' | 'gpu_error' | 'platform_error' | 'other', required: The instance failures v3 publishes; `platform_error` means the platform lost the instance or could not stage it, so creating the sandbox again may succeed, and `other` covers reasons hedra-compute adds later.
  - `url` string, nullable: The address of the port in `ports`, the same for every replica; call it with a token from `POST /sandboxes/{sandbox_id}/endpoint-token`. Set wherever `instances` is; null on the list, for a sandbox created without `ports`, and while its pool serves no address.
  - `billing` SandboxBillingSummary, required: What a sandbox costs, has paid and was charged.
    - `usd_per_hour` number, required: The sandbox's rate per hour for all its instances, after your workspace's discount.
    - `paid_usd` number, required: What the sandbox has paid from the API wallet, net of refunds.
    - `charged_usd` number, nullable, required: The charge once settled. Before that, on `GET /sandboxes/{sandbox_id}` only, the charge so far; null on the list.
    - `settled` boolean, required: Whether the charge is final and the difference from `paid_usd` has been refunded or debited.

## Other responses

- `400`: Invalid or failed-validation request.
- `401`: Authentication failed: missing, malformed, or invalid credentials.
- `402`: The API wallet cannot pay the sandbox's first lease, after one auto-recharge attempt. `error.billing.required` is that lease. Nothing was created.
- `403`: Insufficient scope or plan.
- `404`: Resource not found.
- `409`: `ALREADY_EXISTS`: you have a creating, active or stopping sandbox of this name, or this `Idempotency-Key` was used with another body. `ALREADY_IN_PROGRESS`: the create this key names is still running; retry after `Retry-After`.
- `429`: Rate limit exceeded.
- `500`: Internal error.
- `503`: Hedra could not start the sandbox now, or could not tell whether it started. Retry with the same `Idempotency-Key`.

## Changes

- **2026-11-04** `6ef35a11cefe`: 1 warning, 3 info
  - added the new `idle_ttl_ended` enum value to the `end_reason/anyOf[subschema #1: SandboxEndReason]/` response property for the response status `201`
  - added the new optional request property `idle_ttl_seconds`
  - added the optional property `command` to the response with the `201` status
  - added the optional property `image` to the response with the `201` status
- **2026-10-30** `5a58ee38990e`: 1 info
  - added the new optional request property `resources/anyOf[subschema #1: SandboxResources]/shm_gb`
- **2026-10-28** `c482e691c208`: 1 breaking, 4 info
  - response property `ttl_ends_at` list-of-types was widened by adding types `null` to media type `application/json` of response `201`
  - added the new optional request property `egress`
  - added the new optional request property `idle`
  - the request property `ttl_seconds` became optional
  - …1 more
- **2026-10-24** `9e0b37b16bb7`: 1 warning
  - added the new `platform_error` enum value to the `instances/anyOf[subschema #1]/items/failure_reason/anyOf[subschema #1: SandboxFailureReason]/` response property for the response status `201`
- **2026-10-23** `e733493214f4`: 1 info
  - endpoint added

[Operation history](https://skmtc.dev/hedra-labs/apis/hedra-api/changes/sandboxes/post.md)

---

[API](https://skmtc.dev/hedra-labs/apis/hedra-api.md) - [All operations](https://skmtc.dev/hedra-labs/apis/hedra-api/llms.txt) - [OpenAPI document](https://skmtc.dev/hedra-labs/apis/hedra-api/revisions/6ef35a11cefe?raw)
