The SDK is a thin wrapper over the sandbox REST API. It saves you three things: building requests by hand, retrying when the platform is busy, and forgetting to delete a sandbox when your code throws. Everything here matches version 0.2.0 of both packages.
If you have never created a sandbox, the 5-minute connection guide gets you a token first. What a sandbox is and what's inside it is covered in Sandboxes.
Install and configure
pip install eqvps # Python 3.8+
npm i @eqvps/sdk # Node.js 18+, Deno, Bun
export EQVPS_API_KEY=... # your EQVPS account token
The same program in both languages: create a sandbox, run code, delete it.
from eqvps import Sandbox
with Sandbox.create(tariff="small") as sb:
r = sb.run("print(2 + 2)")
print(r.exit_code, r.stdout) # 0 4
import { Sandbox } from "@eqvps/sdk";
await Sandbox.with({ tariff: "small" }, async (sb) => {
const r = await sb.run("print(2 + 2)");
console.log(r.exit_code, r.stdout); // 0 4
});
Creating and finding sandboxes
Sandbox.create(...) starts a sandbox, usually in about a second. Python takes keyword arguments, TypeScript one options object.
| Python | TypeScript | Default | Meaning |
|---|---|---|---|
mode | mode | "ephemeral" | "ephemeral" (per second) or "persistent" (per started hour, keeps its disk) |
tariff | tariff | "small" | micro, small, standard, plus, pro, max |
idle_timeout | idleTimeout | 300 | seconds without activity before an ephemeral sandbox is deleted, up to 3600 |
ttl | ttl | — | maximum lifetime in seconds: up to 86400 ephemeral, 2592000 persistent |
env | env | — | environment variables for every command, stored encrypted |
api_key, base_url | apiKey, baseUrl | env vars | override EQVPS_API_KEY / EQVPS_API_URL |
timeout | timeoutMs | 70 s | HTTP timeout per request |
max_retries | maxRetries | 3 | retries on 429/503 |
Sandbox.connect(id) attaches to an existing sandbox, typically a persistent one you created yesterday. Sandbox.list() returns every sandbox on the account, running and paused.
Properties: id (sb_ + 24 hex characters), mode, tariff, state (running, starting, paused, pausing, resuming, deleting, deleted), env_keys / envKeys (names only, values never come back) and info (the raw object). refresh() reloads them.
kill() deletes the sandbox and stops billing. Calling it on a sandbox that is already gone is not an error.
Running code
run(code, language="python", timeout=30) sends code on stdin to Python 3.12, Node.js 22 or bash ("python", "node", "bash"). exec(command, cwd=None, stdin=None, timeout=30) runs a shell command: a string goes through bash -lc, a list is executed as argv without a shell.
Both return an ExecResult:
| Field | Meaning |
|---|---|
exit_code | process exit code |
stdout, stderr | output, up to 1 MiB per stream |
timed_out | the command hit its timeout |
truncated | output was cut |
duration_ms | how long it ran |
ok | exit_code == 0 and not timed out |
A synchronous call can run for 55 seconds at most. Both methods also take env for that one call; it overrides the values set at create.
Long jobs: background tasks
Pass background=True (TypeScript: { background: true }) and you get a Task straight away. It has no 55-second limit and can run until the sandbox's lifetime ends. Up to 8 tasks per sandbox.
task = sb.exec("cd /root/app && python3 -m pytest -q", background=True)
result = task.wait(on_output=lambda out, err: print(out, end=""))
print(result.state, result.exit_code) # done 0
const task = await sb.exec("cd /root/app && npm test", { background: true });
const result = await task.wait({ onOutput: (out) => process.stdout.write(out) });
| Method | What it does |
|---|---|
task.logs() | new stdout and stderr since the previous call |
task.status() | state without consuming output: running, done, failed, killed, timeout |
task.wait(timeout, poll_interval=2, on_output) | polls until the task ends, returns TaskResult |
task.kill() | stops the task and its processes |
sb.task(id) / sb.tasks() | reattach to a task / list tasks |
If wait hits its own timeout it raises SandboxTimeoutError, but the task keeps running. A running task also keeps an ephemeral sandbox from being deleted for inactivity.
Files and usage
upload(path, content, mode=None) writes a file at an absolute path and returns its size. download(path) returns bytes, download_text(path) / downloadText(path) a string. One transfer is limited to 5 MB.
usage() returns running seconds, CPU seconds used, outbound bytes, the hourly price, billed_usd and estimated_total_usd. tariffs() is a plain function that needs no key and returns current prices and limits. Billing rules are on Sandbox limits and billing.
Errors
Every API error is a subclass of EqvpsError with status (HTTP), code (stable string) and body. retry_after / retryAfter is set when the server sends it.
| Class | HTTP | Typical code | What to do |
|---|---|---|---|
AuthenticationError | 401 | unauthenticated | check the token |
InsufficientBalanceError | 402 | insufficient_balance | top up the balance |
NotFoundError | 404 | not_found | wrong id, or a file that doesn't exist |
SandboxPausedError | 409 | sandbox_paused | paused after the balance ran out; resumes after a top-up |
SandboxDeletedError | 410 | sandbox_deleted | gone for good |
FileTooLargeError | 413 | file_too_large | keep transfers under 5 MB |
ValidationError | 422 | invalid_request | fix the parameters |
RateLimitError | 429 | too_many_concurrent, too_many_tasks, rate_limited | wait and retry |
BudgetExceededError | 429 | budget_exceeded | your daily or monthly sandbox limit was hit |
CapacityError | 503 | capacity | try again shortly or pick a smaller tariff |
SandboxTimeoutError | — | request_timeout, wait_timeout | the HTTP request (or wait) timed out, not the command |
Two limits cause most 429s: an account runs 2 commands at the same time and holds up to 20 sandboxes. The SDK retries these when the server says how long to wait; after three attempts you get the exception.
When to skip the SDK
Any language with an HTTP client can call the API directly; the endpoints are in the OpenAPI file at https://eqvps.com/openapi.json. Agents in Claude, Cursor or other MCP clients don't need code at all: the MCP server has the same sandbox tools. And if you just want to see it work, the sandbox page lists the tariffs and the $1 trial for new accounts.
Comments
No comments yet. Be the first.