Sandboxes
Pricing
Both kinds are billed per second of running time from your credit balance; there is no monthly fee. Fleet sandboxes cost less and add pause, resume and workspace snapshots.
| Fleet | Standard | |
|---|---|---|
| Compute | $0.03 per vCPU-hour | $0.0706 per vCPU-hour |
| Memory | $0.01 per GiB-hour | $0.0227 per GiB-hour |
| A typical sandbox | 2 vCPU, about 2 GiB: about $0.08/hour | 2 vCPU, 4 GiB: about $0.23/hour |
| Disk | 20 GiB included | First 2 GiB free, then $0.00015 per GiB-hour, also while stopped |
| Workspace snapshots | $0.021 per GB-month, plus small per-operation fees | Not available |
| Grant credit | Accepted | Paid credit only |
Prices are in US dollars. Details:fleet and snapshot pricingandstandard billing.
Everything below is a plain authenticated HTTP call to https://gateway.codegraff.com. No SDK is required. Send your cg_sk_ key as the bearer token on every request.
Create a sandbox
POST /v1/sandboxes boots a VM and returns its id plus the allocated spec. autoStopMinutes (default 30) is the inactivity window after which it auto-stops to stop billing.
const GATEWAY = "https://gateway.codegraff.com";
const headers = {
Authorization: `Bearer ${process.env.CODEGRAFF_API_KEY}`, // cg_sk_...
"Content-Type": "application/json",
};
const sb = await fetch(`${GATEWAY}/v1/sandboxes`, {
method: "POST",
headers,
body: JSON.stringify({ language: "javascript", autoStopMinutes: 30 }),
}).then((r) => r.json());
console.log(sb.id, sb.cpu, sb.memory, sb.disk); // id + allocated specimport os, requests
GATEWAY = "https://gateway.codegraff.com"
headers = {"Authorization": f"Bearer {os.environ['CODEGRAFF_API_KEY']}"} # cg_sk_...
sb = requests.post(
f"{GATEWAY}/v1/sandboxes",
headers=headers,
json={"language": "javascript", "autoStopMinutes": 30},
).json()
print(sb["id"], sb["cpu"], sb["memory"], sb["disk"])Run a command
POST /v1/sandboxes/:id/exec runs a shell command and, by default, blocks until it finishes, returning exitCode and combined stdout/stderr in result. cwd is optional (defaults to the home directory).
const { exitCode, result } = await fetch(
`${GATEWAY}/v1/sandboxes/${sb.id}/exec`,
{ method: "POST", headers, body: JSON.stringify({ command: "node -v" }) },
).then((r) => r.json());
console.log(exitCode, result); // 0 "v22.x\n"r = requests.post(
f"{GATEWAY}/v1/sandboxes/{sb['id']}/exec",
headers=headers,
json={"command": "node -v"},
).json()
print(r["exitCode"], r["result"]) # 0 "v22.x\n"~95s synchronous limit
exec that runs longer is aborted at ~95s with an exec_timeout error (HTTP 504). Use an async command for anything that might run that long, such as installs, test suites, builds, or clones.Long-running commands (async)
Pass "async": true and the command launches detached inside the sandbox; the call returns immediately with an execId. You then poll GET /v1/sandboxes/:id/exec/:execId with short calls that never approach the cap. The job keeps running across polls and client disconnects until it finishes or the sandbox stops.
// Launch: returns { execId, state: "running" } right away
const { execId } = await fetch(`${GATEWAY}/v1/sandboxes/${sb.id}/exec`, {
method: "POST",
headers,
body: JSON.stringify({ command: "npm ci && npm test", async: true }),
}).then((r) => r.json());
// Poll until done; each call is well under a second
let job;
do {
await new Promise((r) => setTimeout(r, 1000));
job = await fetch(
`${GATEWAY}/v1/sandboxes/${sb.id}/exec/${execId}?full=1`,
{ headers },
).then((r) => r.json());
} while (job.state !== "completed");
console.log(job.output, "→ exit", job.exitCode);import time
launch = requests.post(
f"{GATEWAY}/v1/sandboxes/{sb['id']}/exec",
headers=headers,
json={"command": "npm ci && npm test", "async": True},
).json()
exec_id = launch["execId"]
while True:
job = requests.get(
f"{GATEWAY}/v1/sandboxes/{sb['id']}/exec/{exec_id}",
headers=headers,
params={"full": 1},
).json()
if job["state"] == "completed":
break
time.sleep(1)
print(job["output"], "-> exit", job["exitCode"])A poll returns state (running or completed), exitCode (once done), and output, which contains the last 64 KB of stdout/stderr by default, or pass ?tail=N for N bytes / ?full=1 for everything. To stop a job early, DELETE /v1/sandboxes/:id/exec/:execId kills it and cleans up.
Files
Move files in and out with base64-encoded bodies:
| Call | Body → Response |
|---|---|
POST .../upload | { path, contentBase64 } → { ok: true } |
POST .../download | { path } → { contentBase64 } |
curl -X POST https://gateway.codegraff.com/v1/sandboxes/$ID/upload \
-H "Authorization: Bearer cg_sk_your_key" -H "Content-Type: application/json" \
-d "{\"path\":\"/home/user/app.js\",\"contentBase64\":\"$(base64 < app.js)\"}"Standard and fleet sandboxes
Both kinds use the same endpoints and the same cg_sk_ key. Pick one with provider on create. Leave it out for a standard sandbox. Ask for "provider": "fleet" to run on Condensation's own infrastructure: short leases, a lower price, and workspace snapshots.
| Standard (default) | Fleet | |
|---|---|---|
| Lifetime | Until you stop or delete it; idle auto-stop | A lease of 1–30 minutes (autoStopMinutes) |
| Size | 2 vCPU, 4 GiB RAM | 2 vCPU, about 2 GiB RAM, 20 GiB writable disk |
| Price | About $0.23/hour | About $0.08/hour |
| Commands | Sync and async | Sync only, up to 60 s (timeoutSeconds) |
| Files, stop, delete | Yes | Files and delete yes; stop and start on the persistent tier only |
| Public link | Any port: GET /v1/sandboxes/:id/ports/:port/preview (send the returned token) | Any port: the same call returns a public HTTPS address, no token, live until the lease ends (https://s-<id>-<port>.preview.codegraff.com; port 80 has no suffix) |
| Workspace snapshots | Not available | Yes, stored in private object storage |
const sb = await fetch(`${GATEWAY}/v1/sandboxes`, {
method: "POST",
headers,
body: JSON.stringify({ provider: "fleet", autoStopMinutes: 10 }),
}).then((r) => r.json());
console.log(sb.id, sb.provider, sb.tier, sb.expiresAt);
// "cnd_...", "fleet", "ephemeral", when the lease endsA request that names fleet never falls back to a standard sandbox, so you see the real error: 400 if autoStopMinutes is outside 1–30, 402 if your credit balance cannot cover the lease. Fleet features that do not exist yet (async commands, ports, the live meter) answer 501 or 400 with a clear message rather than failing quietly.
Ephemeral or persistent
A fleet sandbox has one of two tiers, chosen when you create it.
| Ephemeral (default) | Persistent | |
|---|---|---|
| Create with | { "provider": "fleet" } | { "provider": "fleet", "persistent": true } |
| At the end of the lease | Gone: files, processes and memory | Paused: nothing is lost |
POST .../stop | 409, it cannot be paused | Pauses the VM; disk, memory and running processes are kept |
POST .../start | 409 | Resumes into a new lease: leaseSeconds 60–1800, default 600 |
| Compute is billed | For the lease | For each running window, not while paused |
Resume a paused sandbox
In the sandbox dashboard, find your paused fleet sandbox and press Resume. Through the API, list your sandboxes with GET /v1/sandboxes, then send POST /v1/sandboxes/:id/start using your own account key with the sandbox scope. Use the existing sandbox ID, not a new create request.
Resume preserves the paused guest's files, memory and processes. It starts a new paid running window, unless an active fleet reservation covers that window; it does not renew an expired reservation. Wait for the sandbox to report started before sending commands or files. Insufficient funds or unavailable capacity is not a successful resume.
// A persistent sandbox: pause it, come back later, pick up exactly where you left off.
const sb = await fetch(`${GATEWAY}/v1/sandboxes`, {
method: "POST",
headers,
body: JSON.stringify({ provider: "fleet", persistent: true, autoStopMinutes: 10 }),
}).then((r) => r.json());
await fetch(`${GATEWAY}/v1/sandboxes/${sb.id}/stop`, { method: "POST", headers }); // paused
// ...later. leaseSeconds is the length of the new window.
await fetch(`${GATEWAY}/v1/sandboxes/${sb.id}/start`, {
method: "POST",
headers,
body: JSON.stringify({ leaseSeconds: 600 }),
});If your sandbox is ephemeral
POST .../download; or create the sandbox as persistent next time so it pauses instead of ending. Check expiresAt on the sandbox and snapshot with a minute to spare.Bigger fleet sandboxes, with Docker
A fleet sandbox can be bigger than 2 vCPU: add cpu (up to 8), memory (GiB, up to 32) and disk (GiB, up to 100) to a "provider": "fleet" create. Any one of them is enough; the rest default to 2 vCPU, 4 GiB and 20 GiB. The size is rounded up to the nearest one we build (2, 4, 6 or 8 vCPU; 4, 8, 12, 16, 24 or 32 GiB; 20, 40, 60 or 100 GiB of disk), and the response says what you got.
A sized sandbox runs as root with Docker already running, so docker run and docker build work straight away. It is ephemeral and its lease can be up to 24 hours (autoStopMinutes up to 1440), which suits long evaluations and builds. It is priced at the same fleet rates: 4 vCPU and 16 GiB cost about $0.28/hour.
const res = await fetch(`${GATEWAY}/v1/sandboxes`, {
method: "POST",
headers,
body: JSON.stringify({ provider: "fleet", cpu: 4, memory: 16, disk: 60, autoStopMinutes: 360 }),
});
if (res.status === 503) {
// First time anyone asks for this size: it is being prepared. Retry after Retry-After seconds.
}
const sb = await res.json(); // { id, cpu: 4, memory: 16, disk: 60, docker: true, ... }The first request for a size takes a few minutes
503 with "type": "size_preparing" and a Retry-After header; nothing is charged. After that, creating that size takes a second or two. If a size does not fit right now, the create answers 400 with the largest size that does.Large reserved machines
Separate offering; not launched
The intended machine lifecycle uses POST /v1/machines/:id/start to resume and POST /v1/machines/:id/stop to pause, with an account key and a fresh UUID Idempotency-Key for each new action. These machine routes are not currently available as a customer offering or through the sandbox dashboard. Do not send a create request to resume an existing machine.
Activation requires the recipient's explicit acceptance of the quote and storage tariff, confirmed payment and verified protection. Stopping preserves the data but does not cancel the accepted reservation fee. Retained backups are billed separately for actual usage.
Model access inside a fleet sandbox
To run an agent or a model client inside a fleet sandbox without ever putting your cg_sk_ key in the guest, ask for model access when you create it: "codegraff": true. The gateway mints a separate credential for that one sandbox and installs it in the guest itself. It never appears in your request, your response, a command string or a log.
const sb = await fetch(`${GATEWAY}/v1/sandboxes`, {
method: "POST",
headers,
body: JSON.stringify({ provider: "fleet", autoStopMinutes: 15, codegraff: true }),
}).then((r) => r.json());
console.log(sb.codegraff);
// { attached: true, scope: "inference", expiresAt: 1790000000,
// modelBaseUrl: "https://gateway.codegraff.com", command: "graff" }
// Inside the sandbox, run the agent with the installed wrapper. It reads the credential
// itself; do not look for a key in the environment.
await fetch(`${GATEWAY}/v1/sandboxes/${sb.id}/exec`, {
method: "POST",
headers,
body: JSON.stringify({ command: "graff --help" }),
});- Model calls only. The credential can call the model endpoints and nothing else: it cannot create or list sandboxes, read your account, or use any other API.
- Tied to the sandbox and its lease. It stops working when the lease ends, when you delete or pause the sandbox, and when you revoke the key it was minted from.
- Your limits still apply. Calls are billed to the key that created the sandbox and count against that key's monthly budget and your credit balance, exactly like your own requests.
- It fails closed. If access cannot be installed, the create returns
502witherror.typefleet_attach_failedand the sandbox's id inerror.sandboxId. The gateway asks for that sandbox to be deleted, but if the delete also fails it lasts until its lease ends. Do not run anything in it, andDELETEthe id to be sure. - Needs the fleet and the api scope.
codegraff: truerequires"provider": "fleet"and a key with theapiscope; otherwise it answers400or403before anything is created. Leaving it out, or sendingfalse, changes nothing.
A persistent sandbox loses its credential when it pauses. After you resume it, call POST /v1/sandboxes/:id/codegraff with an empty body to install a fresh one; it answers 200 with the same codegraff object, 409 if the sandbox is not running, and 404 if it is not yours.
From an app signed in with Codegraff
An app a user signed in to with Codegraff can call these routes on their behalf, without an API key, once Codegraff has enabled the sandbox scope for the app and the user has approved it. The app sends its access token (cg_at_..., valid for an hour) as the bearer. It can create sandboxes, run commands and move files in the sandboxes it created, and delete them; it cannot see your other sandboxes. Model access with codegraff: true also needs the inference scope. The reservation, snapshot and Harness-device routes are not available to apps. Each app appears on your API keys page; revoke it there to end its access. The app side is described in Sign in with Codegraff.
Workspace snapshots
A snapshot is a compressed copy of a fleet sandbox's /workspace folder, kept in private object storage until you delete it. It outlives the sandbox it came from: restore it into any running fleet sandbox on your account, including one you create after the original is gone. It works on both tiers, so it is how an ephemeral sandbox's work survives.
A snapshot saves files under /workspace. It does not save running processes, memory, or anything installed outside that folder; keep what you need under /workspace. For a full pause of processes and memory, use a persistent sandbox instead.
// 1. Snapshot a running fleet sandbox. It answers 202 while the copy uploads.
const snap = await fetch(`${GATEWAY}/v1/sandboxes/${sb.id}/snapshots`, {
method: "POST",
headers,
body: JSON.stringify({ name: "before-refactor" }), // optional
}).then((r) => r.json());
// { id: "snp_...", state: "creating", ... }
// 2. Wait until it is ready.
let s = snap;
while (s.state === "creating") {
await new Promise((r) => setTimeout(r, 1000));
s = await fetch(`${GATEWAY}/v1/snapshots/${snap.id}`, { headers }).then((r) => r.json());
}
console.log(s.state, s.sizeBytes, s.chargedMicroUsd); // "ready"
// 3. Later, restore it into any running fleet sandbox of yours.
const restore = await fetch(`${GATEWAY}/v1/sandboxes/${other.id}/restore`, {
method: "POST",
headers,
body: JSON.stringify({ snapshotId: snap.id }),
}).then((r) => r.json());
// { restoreId: "rst_...", state: "restoring" }
// 4. Poll until it says "done" (or "failed").
const done = await fetch(
`${GATEWAY}/v1/sandboxes/${other.id}/restore/${restore.restoreId}`,
{ headers },
).then((r) => r.json());| Call | Effect |
|---|---|
POST /v1/sandboxes/:id/snapshots | Start a snapshot of a running fleet sandbox. Optional name. |
GET /v1/snapshots | List your snapshots, newest first. |
GET /v1/snapshots/:id | State, size and what it has cost so far. States: creating, ready, failed, unpaid, deleting. |
POST /v1/sandboxes/:id/restore | Unpack a ready snapshot into a running fleet sandbox: { snapshotId }. |
GET /v1/sandboxes/:id/restore/:restoreId | restoring, then done or failed. |
DELETE /v1/snapshots/:id | Delete it and stop the storage charge. Always allowed. |
Snapshots are private to your account: another account asking for one of your snapshot ids gets 404. Each account can keep 10 snapshots of up to 2 GB each. Snapshot and restore on a standard sandbox answer 501. Closing your account deletes its snapshots. A snapshot whose storage you can no longer pay for turns unpaid, cannot be restored, and is deleted after 30 days; add credits and it is restorable again.
Snapshot once, re-test often
For work you set up once and then test again and again, use fleet sandboxes and snapshots. Build the environment in one fleet sandbox (clone, install, seed data) under /workspace, snapshot it, and from then on start each test run from that snapshot in a fresh fleet sandbox. Every run begins from the same known state, and you pay for the minutes each run uses plus the snapshot's storage.
// Once: build the environment and save it.
const base = await createFleet(); // POST /v1/sandboxes { provider: "fleet" }
await exec(base.id, "cd /workspace && git clone https://github.com/you/app && cd app && npm ci");
const snap = await snapshot(base.id, "app-ready"); // POST /v1/sandboxes/:id/snapshots, wait for "ready"
await del(base.id);
// Every test run: a fresh sandbox from the same starting point.
const run = await createFleet();
await restore(run.id, snap.id); // POST /v1/sandboxes/:id/restore, wait for "done"
await exec(run.id, "cd /workspace/app && npm test");
await del(run.id);You can do the same from the dashboard: Dashboard → Sandboxes lists your fleet sandboxes and snapshots, starts a new fleet sandbox, takes a snapshot, restores one into a running sandbox, and deletes either. Snapshots are kept until you delete them.
Long jobs in a fleet sandbox
nohup npm test > /workspace/test.log 2>&1 &). The command returns at once only if the background job does not write to the command's own output, so redirect both stdout and stderr. Create the sandbox as persistent so the end of a lease pauses it, background job included, instead of ending it; resume to carry on. For a job that must run for hours without a break, use a standard sandbox with async commands; it has no snapshots.A fleet command that runs past timeoutSeconds is stopped inside the sandbox and answers 200 with what it printed so far, exitCode 124 and timedOut: true. If something it started in the background still holds its output open, exitCode is null, again with timedOut: true.
Idle stop and garbage collection (standard sandboxes)
This section is about standard sandboxes. A fleet sandbox has no idle timer: it lasts for its lease, then ends (ephemeral) or pauses (persistent).
Idle sandboxes stop automatically; they do not delete themselves
autoStopMinutes inactivity window (30 minutes by default). Auto-stop ends compute billing and preserves the filesystem, but the stopped sandbox remains in your account until you explicitly call DELETE /v1/sandboxes/:id.The inactivity timer tracks external sandbox activity. Gateway interactions such as exec and file/toolbox calls refresh activity; a process merely running in the background does not. For an unattended build, server, or async job, choose an autoStopMinutes value longer than the expected runtime. Setting it to 0 disables idle auto-stop, but compute metering continues and the gateway still force-stops the sandbox if the account runs out of credit.
| Condition | What Codegraff does |
|---|---|
| Running but idle | Auto-stops after autoStopMinutes. Memory and processes are cleared; disk files are preserved. |
| Running without credit | The billing janitor force-stops it on its next successful pass (scheduled once per minute) so more compute debt cannot accrue. |
| Stopped or archived | The janitor settles residual compute and releases the reservation. Compute no longer accrues; retained storage is billed only above the free 2 GiB allowance. The sandbox is not automatically deleted by Codegraff. |
DELETE | Settles any final usage, removes the billing meter, and permanently destroys the sandbox and its files. |
For disposable workloads, call DELETE /v1/sandboxes/:id in a cleanup or finally block so a failed task does not leave a stopped sandbox behind.
Lifecycle
| Call | Effect |
|---|---|
GET /v1/sandboxes | List your sandboxes. |
GET /v1/sandboxes/:id | State + spec for one sandbox. |
POST .../stop | Stop the VM and settle compute billing. Disk state is preserved. |
POST .../start | Resume a stopped sandbox. |
DELETE /v1/sandboxes/:id | Settle final billing and destroy it for good. |
Each sandbox is private to the key's owner. Calls against a sandbox you do not own return 404.
Billing
Compute is billed per second of running time and debited from your credit balance. The reservation for a fresh sandbox covers its full autoStopMinutes window; a per-minute meter then charges actual uptime (so a long-running or restarted box is billed for exactly the seconds it ran, never the wall-clock gap while stopped).
| Resource | Rate |
|---|---|
| vCPU | 19.6 µUSD/s each |
| Memory | 6.3 µUSD/s per GiB |
| Storage | 0.042 µUSD/s per GiB (first 2 GiB free) |
What it costs
65 µUSD/s, about $0.0039/min or $0.23/hour while running. A stopped sandbox bills only storage (free under 2 GiB, charged once an hour) until you delete it. Hit GET /v1/sandboxes/:id/meter for live cost, or watch the dashboard. Costs are in micro-USD (1,000,000 µUSD = $1).Fleet and snapshot pricing
Fleet compute is $0.03 per vCPU-hour plus $0.01 per GiB-hour, measured in seconds and debited from the same credit balance. A fleet sandbox (2 vCPU, about 2 GiB) costs about $0.08/hour, roughly $0.0013/minute. The 20 GiB of writable disk is included. Your credit is reserved for the lease when you create the sandbox and settled to the seconds actually used when it ends.
Workspace snapshots are billed for the storage they use and a few operations:
| Item | Rate |
|---|---|
| Storage | $0.021 per GB-month (1 GB = 109 bytes), prorated per second and charged daily |
| Creating a snapshot | $6.30 per million write operations; one per 16 MiB plus two |
| Restoring | $0.504 per million reads; one per restore |
| Data transfer out | Free |
What a snapshot costs
GET /v1/snapshots/:id shows chargedMicroUsd for each one. Delete a snapshot you no longer need and its storage charge stops.Using the SDK
If you embed the agent with the TypeScript or Python SDK, the same sandboxes are wrapped in a higher-level client: graff.createSandbox({...}) / graff.create_sandbox(...), then box.exec(...), box.upload(...) / download, and box.stop() / start() / destroy(). They authenticate with the same cg_sk_ key and bill identically. Reach for the raw HTTP API above when you're not in Node or Python, or when you want background exec + polling.