WBX Sandbox

REST API

Everything is plain HTTPS with JSON, so you can use WBX Sandbox from any language. The Python SDK and the MCP server are thin wrappers around these endpoints.

Basics

Base URL
https://sandbox.wbx.cz
Authentication
Send your API key in a header: Authorization: Bearer YOUR_API_KEY (or X-API-Key: YOUR_API_KEY).
Format
Request and response bodies are JSON (Content-Type: application/json), except file uploads and downloads, which are raw bytes. Streaming endpoints return NDJSON – one JSON object per line.
Errors
A non-2xx status with {"error": "message"}.
StatusMeaning
400Invalid request – a missing or wrong parameter; the message says which.
401Missing or invalid API key.
402New sandboxes are paused – your prepaid credit ran out. Top up in Billing; running sandboxes continue.
404The sandbox doesn't exist (any more) or belongs to someone else.
409The sandbox is in the wrong state – e.g. running a command in a paused sandbox.
429A limit was reached – too many sandboxes, vCPU or memory for your plan, a template that isn't in your plan, or no free capacity right now. The message says which; kill sandboxes you no longer need or retry later.
502The agent inside the sandbox didn't respond – usually the sandbox ran out of memory. Create a new one.
503The service is restarting for maintenance – retry in a few seconds.

Sandboxes

Create – POST /v1/sandboxes

curl -s -X POST https://sandbox.wbx.cz/v1/sandboxes \
  -H "Authorization: Bearer YOUR_API_KEY" -H "Content-Type: application/json" \
  -d '{
    "template": "base",
    "timeout_s": 600,
    "env": {"OPENAI_API_KEY": "sk-…"},
    "metadata": {"user": "alice", "chat": "42"},
    "network": {"mode": "allowlist", "allow": ["pypi.org", "files.pythonhosted.org"]}
  }'
FieldDescription
templateTemplate name (default base). See templates.
timeout_sLifetime in seconds. Capped by your plan's maximum.
envEnvironment variables for every command and process in the sandbox.
metadataYour own key–value labels (strings), returned with the sandbox.
network{"mode": "full"} (default), {"mode": "none"}, or {"mode": "allowlist", "allow": [...]}; optional "block": [...]. Can only make your team's network policy stricter. With allowlist, domains apply to web traffic only (HTTP on port 80, HTTPS on port 443, checked by hostname); other ports, UDP/QUIC and IPv6 are blocked, while listed IPs and ranges are reachable directly.

Response 201 – the sandbox object:

{
  "id": "sb_sand02_6f83783733841dee",
  "template": "base",
  "state": "running",
  "vcpu": 2,
  "mem_mb": 512,
  "created_at": "2026-11-07T17:25:03Z",
  "expires_at": "2026-11-07T17:35:03Z",
  "metadata": {"user": "alice", "chat": "42"},
  "network": "allowlist",
  "info": {"start_ms": 4, "warm": true}
}
EndpointWhat it does
GET /v1/sandboxesList your running and paused sandboxes.
GET /v1/sandboxes/{id}One sandbox.
DELETE /v1/sandboxes/{id}Destroy it – disk and memory are discarded.
POST /v1/sandboxes/{id}/timeoutSet the remaining lifetime: {"timeout_s": 600}.
POST /v1/sandboxes/{id}/pauseFreeze it with its memory; not billed while paused.
POST /v1/sandboxes/{id}/resumeContinue a paused sandbox.
GET /v1/sandboxes/{id}/url?port=8080Public HTTPS URL of a port in the sandbox – see Public URLs.
GET /v1/templatesTemplates you can use.

Commands

Run and wait – POST /v1/sandboxes/{id}/exec

curl -s -X POST https://sandbox.wbx.cz/v1/sandboxes/SB_ID/exec \
  -H "Authorization: Bearer YOUR_API_KEY" -H "Content-Type: application/json" \
  -d '{"cmd": "pip install requests && python3 main.py", "cwd": "/home/user", "timeout_ms": 120000}'

Body: cmd (run by /bin/sh -c), optional cwd, env, timeout_ms (0 = no limit), stdin. Response:

{"exit_code": 0, "stdout": "…", "stderr": "", "duration_ms": 2140, "timed_out": false, "truncated": false}

Output is capped at 1 MB per stream (truncated: true) – redirect large output to a file and download it.

Run and stream – POST /v1/sandboxes/{id}/exec/stream

Same body; the response is NDJSON, one event per line as the command runs:

{"type":"start","pid":123}
{"type":"stdout","data":"step 1\n"}
{"type":"stderr","data":"warning: …\n"}
{"type":"exit","exit_code":0,"duration_ms":930,"timed_out":false}

Background processes and terminals

For servers, Jupyter kernels or anything long-running. Output is kept (the last 1 MB per stream), so you can attach later.

EndpointWhat it does
POST /v1/sandboxes/{id}/processStart: {"cmd": "python3 -m http.server 8000", "tag": "web"}. Add "pty": {"cols": 120, "rows": 30} for an interactive terminal, "stdin": true to send input. Returns {"pid": …}.
GET /v1/sandboxes/{id}/processList processes (running and recently finished).
GET /v1/sandboxes/{id}/process/{pid}Process detail and exit status.
GET /v1/sandboxes/{id}/process/{pid}/streamNDJSON: output so far, then live, then {"type":"exit"}.
POST /v1/sandboxes/{id}/process/{pid}/stdinSend input: {"data": "text\n"}; {"eof": true} closes stdin.
POST /v1/sandboxes/{id}/process/{pid}/signal{"signal": "SIGTERM"} (or SIGINT, SIGHUP, SIGKILL).
POST /v1/sandboxes/{id}/process/{pid}/resizeResize a terminal: {"cols": 160, "rows": 40}.
DELETE /v1/sandboxes/{id}/process/{pid}Kill the process and its children.
POST /v1/sandboxes/{id}/envAdd environment variables for later commands: {"env": {"KEY": "value"}}.

Files

# upload (raw bytes; parent directories are created)
curl -s -X PUT "https://sandbox.wbx.cz/v1/sandboxes/SB_ID/files?path=/home/user/data.csv" \
  -H "Authorization: Bearer YOUR_API_KEY" --data-binary @data.csv

# download
curl -s "https://sandbox.wbx.cz/v1/sandboxes/SB_ID/files?path=/home/user/chart.png" \
  -H "Authorization: Bearer YOUR_API_KEY" -o chart.png
EndpointWhat it does
GET / PUT / DELETE /v1/sandboxes/{id}/files?path=Download, upload or delete a file.
GET /v1/sandboxes/{id}/fs/list?path=&depth=1List a directory (name, type, size, mode, modification time); depth up to 5.
GET /v1/sandboxes/{id}/fs/stat?path=Information about one file or directory.
POST /v1/sandboxes/{id}/fs/mkdir{"path": "/home/user/out", "parents": true}
POST /v1/sandboxes/{id}/fs/move{"from": "…", "to": "…", "overwrite": false}
POST /v1/sandboxes/{id}/fs/remove{"path": "…"} – directories are removed with their contents; "recursive": false removes only an empty one.
GET /v1/sandboxes/{id}/fs/watch?path=&recursive=falseNDJSON stream of changes (create, write, remove, rename).
GET /v1/sandboxes/{id}/metricsCPU, memory and disk usage inside the sandbox.

Public URLs

Start a web app, API or dev server in the sandbox and open it from a browser or another service. The app must listen on 0.0.0.0 (not only 127.0.0.1) and keep running – start it in the background:

curl -s -X POST https://sandbox.wbx.cz/v1/sandboxes/SB_ID/exec -H "X-API-Key: $WBX_API_KEY" \
  -d '{"cmd": "nohup python3 -m http.server 8000 >/tmp/srv.log 2>&1 &"}'

curl -s "https://sandbox.wbx.cz/v1/sandboxes/SB_ID/url?port=8000" -H "X-API-Key: $WBX_API_KEY"
{
  "url": "https://8000-…/",
  "url_with_token": "https://8000-…/?wbx_token=…",
  "token": "…"
}

Every sandbox has its own token. Open url_with_token in a browser – the token moves to a cookie for that address and disappears from the URL. From code, call url with the header X-WBX-Token: <token>. Anyone who has the token can use the app until the sandbox ends, so share it only with people who should. HTTP and WebSockets work; your app gets the client address in X-Forwarded-For and its own Authorization and cookies (cookies are limited to that one address). Addresses are available while the sandbox runs; a paused or ended sandbox returns an error page.

Background jobs

Run a command in a fresh sandbox with one call and come back later. The API returns a job_id immediately; the job keeps running without an open connection. When it ends the sandbox is removed, but the log, the result files and an execution receipt are kept for 7 days.

# submit
curl -s -X POST https://sandbox.wbx.cz/v1/jobs -H "Authorization: Bearer YOUR_API_KEY" -H "Idempotency-Key: report-2026-11" \
  -d '{"command": "python analyze.py", "timeout_s": 3600, "artifacts": ["output/"], "max_cost": "0.10"}'

# later: short status, then a result file
curl -s https://sandbox.wbx.cz/v1/jobs/JOB_ID/summary -H "Authorization: Bearer YOUR_API_KEY"
curl -s https://sandbox.wbx.cz/v1/jobs/JOB_ID/artifacts/output/report.csv -H "Authorization: Bearer YOUR_API_KEY" -o report.csv
EndpointWhat it does
POST /v1/jobscommand (required), template, timeout_s (default 3600), artifacts (patterns relative to the working directory, e.g. output/, *.csv), required_artifacts (files that must be saved, otherwise the job fails), env, network, retry {"max_attempts": 2, "on": "system"}, max_cost (in your billing currency; max_cost_eur for EUR accounts), idempotency_key; optional compute_class, optimize_for, placement, max_duration_seconds (alias of timeout_s), dependency_cache, inputs, runtime and wasm (see below). Returns 202; the same idempotency key returns the same job.
GET /v1/jobs/stats?days=30Your cost per completed job, success rate and durations by template and performance class.
GET /v1/jobsYour jobs, newest first (status, limit, cursor).
GET /v1/jobs/{id}Status, inputs (without env values), timing, error, result files and cost.
GET /v1/jobs/{id}/summaryShort status for agents: state, exit code, duration, cost, file names, last log lines, next step.
GET /v1/jobs/{id}/logs?offset=&limit=Part of the output log; a negative offset reads from the end. Updated every few seconds while the job runs.
GET /v1/jobs/{id}/receiptThe execution receipt (see below).
GET /v1/jobs/{id}/artifactsSaved result files (name, size, SHA-256) and files that were skipped.
GET /v1/jobs/{id}/artifacts/{name}Download a result file.
POST /v1/jobs/{id}/cancelStop the job and remove its sandbox.
POST /v1/jobs/{id}/retryNew job with the same inputs. Env values are not stored – send {"env": {…}} again.

States: queued, starting, running, uploading_artifacts, then completed, failed (non-zero exit code, a required file missing, or a platform error after all attempts), cancelled or expired (time limit reached – files written so far are kept). cleanup_pending and recovery_pending mean the platform is still confirming that the previous sandbox is gone; a job never runs in two sandboxes at once.

Limits. Up to 100 result files, 100 MB each and 512 MB per job; 5 GB of stored results per team. Results are kept for 7 days.

Currency. Job costs, price limits, budgets and compute class prices are all in your account's billing currency (CZK, EUR or USD – Billing → Billing details). max_cost and max_total_cost use that currency; the older max_cost_eur and max_total_cost_eur still work for accounts billed in EUR.

Refunds. Attempts that fail because of a problem on our side (an error with "kind": "system", e.g. sandbox_lost or capacity) are credited back automatically within a few minutes after the job ends. The job shows the credited amount in refund; the credit history in Billing lists it as “Refund – job … failed on our side”. Attempts that fail because of the job itself (non-zero exit, timeout, missing result files) are charged as usual.

Price limit. With max_cost the worst case (all attempts for the full time limit plus a short start and upload reserve, at your plan's rates) is checked before the job is accepted. If it could exceed the limit, the job is refused and the error tells you the longest time limit that fits.

Execution receipt. Inputs (the command and its SHA-256, template, time limit, SHA-256 of env values), runtime and isolation, network policy, timing, exit code, CPU time and peak memory measured inside the VM (null when they cannot be measured; for wasi jobs the runtime is wasi with wasm-process isolation, and run time, exit code and peak module memory are measured by the WASI runtime on the node, not in a VM), result files with SHA-256, a checksum of the log, and the cost of the sandbox run time at your plan's rates. It is an operational record of the platform, not a cryptographic proof that the computation was not altered.

Secrets. Env values are stored only encrypted with a per-job key, replaced with [REDACTED] in the log, and made unreadable when the job ends (the key is deleted).

Log digest and test results for AI

A long log does not have to go into your agent's context. GET /v1/jobs/{id}/logs?mode=summary&max_chars=6000 returns a compact digest: recognised test results, the error lines with context (repeated lines collapsed, e.g. [×200]) and the end of the log. mode=errors returns only the error blocks, mode=tail only the end; without mode you get raw bytes as before.

curl -s -H "X-API-Key: $WBXSANDBOX_API_KEY" "https://sandbox.wbx.cz/v1/jobs/job_…/logs?mode=summary&max_chars=2000"
{
  "mode": "summary", "status": "failed", "exit_code": 1, "lines": 5214, "size": 412880,
  "text": "[wbx] tests (pytest): 211 passed, 1 failed, 212 total\n  FAILED tests/test_api.py::test_refund – AssertionError: 400 != 200\n[wbx] errors:\n…\n[wbx] end of log:\n…",
  "test_results": [{"framework": "pytest", "passed": 211, "failed": 1, "total": 212,
                    "failures": [{"name": "tests/test_api.py::test_refund", "file": "tests/test_api.py", "message": "AssertionError: 400 != 200"}]}]
}

Recognised output: pytest, go test, Jest, Vitest, cargo test, PHPUnit, JUnit (Maven/Gradle) and mocha. Test results are also part of the job summary and the execution receipt (test_results). Over MCP, get_job_logs returns this digest by default.

Waiting for results

Instead of asking for the status again and again, wait on the server: POST /v1/jobs/wait returns as soon as one ("until": "any", default) or all ("all") of the given jobs or batches have finished, or after timeout_s (max 60) with "done": false – then call again.

curl -s -X POST -H "X-API-Key: $WBXSANDBOX_API_KEY" -H "Content-Type: application/json" \
  -d '{"job_ids": ["job_…", "job_…"], "batch_ids": ["bat_…"], "until": "any", "timeout_s": 60}' https://sandbox.wbx.cz/v1/jobs/wait
{"done": true, "finished": 1, "total": 3, "items": [{"id": "job_…", "kind": "job", "status": "completed", "done": true}, …]}

Webhooks

Add "webhook_url": "https://…" to a job or a batch and WBX sends a POST when it finishes: job.finished (with status, exit code, duration, cost, file names and test results) or batch.finished (counts and cost of the whole batch). Only public https:// addresses are accepted; redirects are not followed. Events are stored in a persistent queue before they are sent, so a restart of our servers does not lose them. A delivery counts only when your endpoint answers 2xx within 10 s; otherwise it is retried after 10 s, 1 min, 5 min, 30 min, 2 h, 6 h and 12 h. The same event can arrive more than once (e.g. when your answer was lost) – use the id to deduplicate, and the API remains the source of truth.

{"event": "job.finished", "id": "job_…", "created_at": "2026-11-09T06:31:12Z", "data": {"id": "job_…", "status": "completed", "exit_code": 0, "cost": "0.000312 EUR", "test_results": […], …}}

Verifying. Every request carries X-WBX-Signature: t=<unix time>,v1=<hex> – HMAC-SHA256 of "<t>.<raw body>" with your account's signing secret (API keys page in the portal, or GET /v1/webhooks/secret; rotate with POST /v1/webhooks/secret/rotate). Reject old timestamps (e.g. more than 5 minutes) to prevent replays. Each delivery also has X-WBX-Event and X-WBX-Delivery.

import hashlib, hmac, time

def verify(secret: str, body: bytes, header: str) -> bool:
    parts = dict(p.split("=", 1) for p in header.split(","))
    if abs(time.time() - int(parts["t"])) > 300:
        return False
    mac = hmac.new(secret.encode(), parts["t"].encode() + b"." + body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(mac, parts["v1"])

Batches (fan-out)

Run many jobs with one call. You set how many may run at the same time and, optionally, a price limit for the whole batch. Each job has its own result; a failed job does not stop the others.

curl -s -X POST https://sandbox.wbx.cz/v1/batches -H "Authorization: Bearer YOUR_API_KEY" -H "Idempotency-Key: nightly-2026-11-08" \
  -d '{"defaults": {"template": "base", "timeout_s": 600, "artifacts": ["output/"]},
       "jobs": [{"command": "python worker.py 1"}, {"command": "python worker.py 2"},
                {"command": "python merge.py", "depends_on": [0, 1],
                 "inputs": [{"from_index": 0, "name": "output/part.csv", "as": "p1.csv"},
                            {"from_index": 1, "name": "output/part.csv", "as": "p2.csv"}]}],
       "max_concurrency": 10, "max_total_cost": "2.00"}'

curl -s https://sandbox.wbx.cz/v1/batches/BATCH_ID -H "Authorization: Bearer YOUR_API_KEY"
curl -s "https://sandbox.wbx.cz/v1/batches/BATCH_ID/report?format=csv" -H "Authorization: Bearer YOUR_API_KEY" -o report.csv
EndpointWhat it does
POST /v1/batchesjobs (1–1000, each with the fields of POST /v1/jobs plus depends_on), defaults, max_concurrency (1–100, default 10), max_total_cost (billing currency; max_total_cost_eur for EUR accounts), name, idempotency_key. The batch is accepted as a whole or not at all.
GET /v1/batchesYour batches, newest first.
GET /v1/batches/{id}Summary: counts by state, cost, cost per completed job, durations (p50/p95), errors by code, first failed jobs, next step.
GET /v1/batches/{id}/reportThe summary plus one row per job; ?format=csv for a spreadsheet.
POST /v1/batches/{id}/cancelCancel the jobs that have not finished yet.

A job with depends_on starts only after those jobs completed successfully; if one of them fails, the job is cancelled with dependency_failed. The batch status is completed, partial (some jobs failed), failed or cancelled.

Budget

Set a monthly and a daily spending limit for background jobs and batches (sandboxes you create directly are not limited by it). The worst-case cost of every job is reserved when it is accepted, so running jobs can never push spending over the limit; when a job ends the reservation is replaced by its actual cost. A job that does not fit is refused with 402 budget_exceeded. Periods are the calendar month and day in UTC. Limits set by WBX for your team apply as well – the lower one wins.

curl -s https://sandbox.wbx.cz/v1/budget -H "Authorization: Bearer YOUR_API_KEY"
curl -s -X PUT https://sandbox.wbx.cz/v1/budget -H "Authorization: Bearer YOUR_API_KEY" -d '{"monthly_limit": "50.00", "daily_limit": "5.00", "alert_pct": 80}'

An empty string removes a limit. The answer shows the limits, what was spent this month and today, what running jobs have reserved and what is left. You can also manage it in Billing .

Compute classes

A compute class is the service level for your sandboxes and jobs: which servers the work runs on, how they are chosen and the price. It is a declared level of service, not a guaranteed speed.

ClassRuns onGood for
economythe lowest-cost servers (default)Python, small jobs, light AI tasks
balancedbalance of price, load and performanceeveryday development, automation, browsers
performanceonly servers with the highest measured performance (formerly cpu-fast)builds, tests, heavy computation
memoryservers with the most free memory; at least 2 vCPU and 8 GBdata processing that needs a lot of RAM
dedicatedonly servers reserved for your organizationenterprise workloads (on request)

Which class is used. compute_class in the request wins; without it your account's default class is used ( Settings → Compute Preferences ), and without a default economy. The server decides – SDKs and MCP send nothing when you don't set it. An unknown class is a 400 error, a class your plan does not include is 403, a class that is temporarily unavailable is 409. Your request is never moved to another class.

Price. Each class has its own price relative to your plan's rates (shown in Compute Preferences). The price of a sandbox or job is fixed when it is created or accepted – later changes of the price list or your default do not change running sandboxes or accepted jobs. The worst-case cost (max_cost, budget) uses the class price.

optimize_for (price, balanced or performance) picks the server inside the class; it never changes the class or its price. When no server of the class is free right now, creating a sandbox fails with 503 and a job waits in the queue. Responses include the resulting compute_class (jobs also class_price).

curl -s https://sandbox.wbx.cz/v1/compute/preferences -H "Authorization: Bearer YOUR_API_KEY"
curl -s -X PUT https://sandbox.wbx.cz/v1/compute/preferences -H "Authorization: Bearer YOUR_API_KEY" -d '{"default_compute_class": "balanced"}'
curl -s -X POST https://sandbox.wbx.cz/v1/jobs -H "Authorization: Bearer YOUR_API_KEY" \
  -d '{"command": "make -j4", "compute_class": "performance", "optimize_for": "price", "max_cost": "0.10"}'

placement (auto, fast, spread) from earlier versions still works; optimize_for takes precedence.

WebAssembly jobs

Short, deterministic tasks compiled to WebAssembly (WASI preview 1) can run without starting a virtual machine: "runtime": "wasi". They start fast and cost less, but have no network; they see only /input (read-only), /output and /tmp. Files written to /output become result files named output/….

curl -s -X POST https://sandbox.wbx.cz/v1/jobs -H "Authorization: Bearer YOUR_API_KEY" \
  -d '{"runtime": "wasi", "timeout_s": 60, "artifacts": ["output/"],
       "wasm": {"module_b64": "'"$(base64 -w0 tool.wasm)"'", "args": ["--fast"], "memory_mb": 128},
       "inputs": [{"job_id": "JOB_ID", "name": "output/data.csv"}]}'

Limits: module up to 8 MB inline (up to 32 MB as an input named in wasm.module), memory 16–1024 MB, time up to 10 minutes, stdin up to 4 MB, 64 output files (64 MB together). Clocks and random numbers inside the module are deterministic. Price: measured run time × (1 vCPU + the memory limit) at your plan's rates. The isolation is a sandboxed process, which is weaker than a virtual machine – use the default runtime for anything that needs a full Linux system. GET /v1/runtimes lists runtimes with their isolation level and capabilities.

Passing files between jobs

inputs gives a job result files of your earlier finished jobs without downloading them: {"job_id": "…", "name": "output/data.csv", "as": "data.csv"}. A job in a virtual machine finds them in ./input/ of its working directory, a WebAssembly job in /input/. Up to 32 files and 64 MB; the SHA-256 is checked when the job is accepted and again when it starts. Only your own jobs can be used. Inside a batch, from_index refers to another job of the same batch and the job waits for it.

Package cache

Sandboxes with full internet access use a package cache on the server for pip / uv, npm and Go modules automatically (the variables PIP_INDEX_URL, npm_config_registry and GOPROXY are set for you). Packages are downloaded from the official registries and checked against the registry's checksums before they are stored; nobody can write into the cache, so it cannot be poisoned by another customer. Go modules are checked against sum.golang.org before they are stored (private modules are passed through without caching). Turn it off for a job or sandbox with "dependency_cache": false. It is not used with an allowlist or without network.

Limits

How many sandboxes you can run at once, their maximum lifetime and the templates you can use depend on your plan – see pricing. When a limit is reached the API answers 429 with an explanation.