Skip to main content

instabox Python SDK

A small, typed Python client for instabox sandboxes: create a sandbox, run commands in it, move files, pause and resume it, snapshot it and delete it. Python 3.9-3.13, one dependency (httpx). It installs as instabox-sdk and imports as instabox (the instabox project on PyPI is someone else's):

pip install instabox-sdk

A version bump in pyproject.toml merged to main publishes it to PyPI (.github/workflows/sdk-python-release.yml; see Publishing for the one-time PyPI setup it needs). Until the first release, install it from this repository (that needs read access to it, since the repository is private for now):

pip install "git+https://github.com/InsForge/instabox#subdirectory=sdk/python"

Quickstart

Create an API key on API keys in the dashboard (Full access, or a restricted key with machines:write and machines:delete), then:

export INSTABOX_API_KEY=ibk_...
from instabox import Client

client = Client()  # reads INSTABOX_API_KEY (and INSTABOX_API_URL, default https://api.instabox.sh)

box = client.machines.create(name="my-box")  # template agent-browser; waits until ready
try:
    r = client.machines.exec(box.id, "uname -a")
    print(r.stdout, end="")

    r = client.machines.exec(box.id, ["sh", "-c", "echo oops >&2; exit 3"])
    print(r.exit_code, r.stderr, end="")  # 3 oops
finally:
    client.machines.delete(box.id)

A sandbox pauses after 30 idle minutes by default. To pick one up later, whatever state it is in, and keep working:

box = client.machines.ensure_running("my-box")  # starts or resumes it if needed, waits until ready
client.machines.exec(box.id, "ls /data/home")
client.machines.update(box.id, metadata={"stage": "review"}, auto_suspend_after_seconds=600)
client.machines.delete(box.id, confirm="my-box")  # refuses to delete anything but my-box

Calls

client = Client(api_key=None, base_url=None, timeout=30.0, http_client=None)

Call Returns
client.machines.create(name=None, *, template=None, snapshot_id=None, vcpus=None, mem_mib=None, persistent_disk_gib=None, metadata=None, org_id=None, auto_suspend_after_seconds=None, archive_seconds=None, delete_after_seconds=None, desired_state=None, fail_fast=False, wait=True, timeout=600) Machine, running and ready when wait (created, when desired_state="stopped")
client.machines.list(*, metadata=None) every Machine of your orgs (all pages)
client.machines.list_page(*, cursor=None, limit=None, metadata=None) MachinePage(machines, next_cursor)
client.machines.get(machine) Machine (by ID or name)
client.machines.update(machine, *, name=None, metadata=None, auto_suspend_after_seconds=None, max_run_minutes=None, vcpus=None, mem_mib=None, persistent_disk_gib=None, idempotency_key=None) Machine, updated (PATCH, synchronous)
client.machines.exec(machine, command, *, cwd=None, env=None, stdin=None, timeout=60) ExecResult(stdout, stderr, exit_code, timed_out, truncated, signal)
client.machines.start(machine, *, wait=True, timeout=600) Operation; with wait, the sandbox is running and ready
client.machines.stop(machine, *, shutdown_timeout=None, wait=True, timeout=600) Operation
client.machines.restore(machine, snapshot=None, *, stopped=False, wait=True, timeout=600) Operation; the sandbox's disks are back to the snapshot's (default: the one it was created from), same ID and URLs; with wait, running and ready unless stopped
client.machines.delete(machine, *, confirm=None, wait=True, timeout=600) Operation
client.machines.pause(machine, *, wait=True, timeout=600) Operation; the sandbox is suspended, memory and programs kept
client.machines.resume(machine, *, wait=True, timeout=600) Operation; with wait, the sandbox is running and ready
client.machines.wake(machine) Machine; an idle-paused sandbox is resumed first (up to 45 s)
client.machines.ensure_running(machine, *, timeout=600) Machine, running and ready; a stopped or paused one is started first (one still stopping or pausing is let finish, then started; a start someone else began is watched, not raised)
client.machines.wait_ready(machine, *, timeout=300) Machine once running and ready
client.operations.get(operation_id) / client.operations.wait(operation_id, *, timeout=600) Operation
client.templates.list() [Template]
client.snapshots.list() / get(snapshot_id) [Snapshot] / Snapshot
client.snapshots.create(machine, name, *, description=None, root_only=False) Snapshot (pending while a running sandbox's copy uploads)
client.snapshots.delete(snapshot_id, *, confirm=None) None
client.files.write(machine, path, data, *, mode=None, parents=True, timeout=300) FileWritten(path, size, sha256, mode, created)
client.files.read(machine, path) / read_bytes(machine, path) str (UTF-8) / bytes
client.files.list(machine, path) [FileEntry(name, type, size, mode, mod_time, target)]
client.files.exists(machine, path) / remove(machine, path) bool / None (rm -rf)
  • machine is a sandbox ID (m_...) or its name. create without a name picks py-<random>. Without template or snapshot_id it uses agent-browser, the default template; template may pin a version as id@version (agent-browser@2026-10-15.4).
  • create options: auto_suspend_after_seconds is the idle pause (default 30 minutes; a paid sandbox takes 300-1800 in whole minutes or 0 for never, where the plan allows; the free sandbox is fixed at 1800). archive_seconds stops and delete_after_seconds deletes the sandbox that many seconds (60-86400) after creation, so it cannot outlive a client that died. desired_state="stopped" creates it stopped (nothing to wait for). fail_fast=True has a create no host has room for refused at once (503 no_capacity, ServerError, nothing created) instead of returning an operation that then fails (OperationFailedError).
  • update is PATCH /v1/machines/{id}: name, metadata (replaces it; {} clears) and auto_suspend_after_seconds apply in any state; vcpus/mem_mib (another of the workspace's sizes) need the sandbox stopped and apply at its next start; persistent_disk_gib grows a running sandbox's disk online (a stopped one's at its next start) and shrinks only while stopped: resize wraps it. Absent fields are unchanged.
  • persistent_disk_gib is sent as the API's persistentDiskGiB. On api.instabox.sh the disk (/data) comes with the size, so leave it out: another size is 400 spec_not_allowed (BadRequestError). Resize a paid sandbox's disk later with client.machines.resize(m, disk_gib, restart=False), which sends PATCH /v1/machines/{id} {"persistentDiskGiB": N} (1-1024 GiB; update(m, persistent_disk_gib=N) sends that field as is): a running sandbox grows online; shrinking needs it stopped (restart=True stops and restarts it) and the data plus 10% (at least 1 GiB) to fit, else ConflictError with code disk_too_small. The system disk is the template's: rootDiskGiB from anyone but an admin is 400 spec_not_allowed.
  • vcpus and mem_mib (the API's vcpus and memMiB) name one of the org's sizes at create. A paid sandbox changes size later, while stopped, with client.machines.update(m, vcpus=N, mem_mib=M), which sends PATCH /v1/machines/{id} {"vcpus": N, "memMiB": M} (the CLI's instabox resize <sandbox> --vcpus N --mem-mib M; on a running or paused sandbox add --restart, from outside it), applied at its next start (which fails with no_capacity if the host has no room for it; the sandbox stays stopped at the new size). client.machines.resize takes only the disk. Running or paused is 409 operation_conflict (ConflictError; stop a running sandbox first, start then stop a paused one); another size, or the free sandbox, 400 spec_not_allowed.
  • exec takes a shell string, run with bash -lc as the sandbox's user (dev, HOME /data/home), or an argv list, run with no shell (the API's argv: each element reaches the program exactly as sent, no $ expansion, globbing or word splitting; a program that is not found exits 127). A non-zero exit code is a result, not an error. When timeout (seconds, at most 600) passes, the command is killed and the result has timed_out=True and exit_code=124. exec is not idempotent, so it is never retried. The sandbox must be running (409 machine_not_running, ConflictError): ensure_running first when it may have been paused or stopped.
  • wake is for a sandbox that pauses itself when idle: it records activity on a running one or waits for the automatic pause and its resume to finish, so the next exec finds it running. A sandbox paused or stopped on purpose is not woken; start (or ensure_running) does that.
  • delete(confirm=...) sends X-Confirm-Delete with the sandbox's ID or exact name; any other value is 409 delete_confirmation_required (ConflictError) and nothing is deleted. Without confirm, delete sends the machine it was given (ID or name). snapshots.delete takes the same confirm.
  • create, start, stop, delete, update and the snapshot calls send an Idempotency-Key (random per call). Pass idempotency_key= to retry a call safely: the same key and request return the original response, so a retried create returns the same sandbox.
  • files paths are absolute. write and read stream any size (up to 5 GiB) as the sandbox's user, write replaces the file atomically and parents creates missing directories; a missing file is NotFoundError, a directory ConflictError (not_a_file).
  • Machine has id, name, state (creating, starting, running, stopping, stopped, suspending, suspended, resuming, deleting, deleted, error), ready and running (running and ready), template_id, template_version, snapshot_id, spec, metadata, auto_suspend_after_seconds (None: never; sent as 0, reported as None), max_run_minutes, archive_at, delete_at, guest_user, processes_preserved (after a start or resume: False when the paused programs could not be brought back), free, org_id, operation_id, last_error, created_at and updated_at.
  • Every model keeps the full JSON response in .raw.

Errors

Every error is an instabox.InstaboxError. API errors are instabox.APIError subclasses with the fields of the API's error body: code, message, request_id, retryable, operation_id, plus status (HTTP) and retry_after (seconds to wait before retrying: the Retry-After header, else the body's retryAfterSeconds, which is all a failed operation has; set on rate_limited, retryable no_capacity and machine_cooldown).

Status Class Example code
400 BadRequestError bad_request, spec_not_allowed
401 AuthenticationError unauthenticated, api_key_revoked; missing_api_key when no key is set (raised by Client())
403 PermissionDeniedError insufficient_scope, template_not_allowed
404 NotFoundError not_found
409 ConflictError machine_not_running, operation_conflict, idempotency_conflict
429 RateLimitError quota_exceeded, rate_limited
5xx ServerError no_capacity, host_unavailable

An operation that ends failed raises OperationFailedError (an APIError carrying the operation's error). A wait that runs out raises WaitTimeoutError. A request without a response (network error, or the client timeout) raises APIConnectionError.

import instabox

try:
    instabox.Client(api_key="ibk_wrong").machines.list()
except instabox.AuthenticationError as e:
    print(
        repr(e)
    )  # AuthenticationError(code='unauthenticated', message=..., status=401, request_id=..., ...)

Development

cd sdk/python
python -m venv .venv && . .venv/bin/activate
pip install -e '.[dev]'
ruff check . && ruff format --check . && mypy --strict src/instabox && pytest

tests/test_live.py mirrors the TypeScript SDK's live test against the real API (create, exec, files, update, pause, resume, list, delete); it is skipped unless INSTABOX_LIVE=1:

INSTABOX_LIVE=1 INSTABOX_API_KEY=ibk_... pytest -v tests/test_live.py  # INSTABOX_LIVE_TEMPLATE=t3 on a paid plan

Publishing

.github/workflows/sdk-python-release.yml publishes sdk/python to PyPI as instabox-sdk when a merge to main changes the version in pyproject.toml. It authenticates with PyPI trusted publishing (OIDC), which needs a one-time setup on PyPI by a maintainer with a PyPI account (the first publish of 0.2.0 failed for want of it):

  1. On https://pypi.org/manage/account/publishing/, add a pending publisher for the project name instabox-sdk (or, once the project exists, a publisher on its Publishing page) with owner InsForge, repository instabox, workflow sdk-python-release.yml and environment pypi.
  2. In the GitHub repository, make sure the pypi environment exists (Settings → Environments).
  3. Re-run the failed sdk-python release run from Actions (workflow_dispatch), or merge the next version bump.

Without trusted publishing, a PyPI API token in the repository secret PYPI_API_TOKEN (scoped to the instabox-sdk project) is used instead; leave it unset to use OIDC.

Metadata

Release files for instabox-sdk 0.3.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for instabox-sdk 0.3.1
File Size Uploaded Provenance Actions
instabox_sdk-0.3.1.tar.gz 29.0 kB With provenance Details for instabox_sdk-0.3.1.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for instabox-sdk 0.3.1
File Interpreter ABI Platform Actions
instabox_sdk-0.3.1-py3-none-any.whl Python 3 none any Details for instabox_sdk-0.3.1-py3-none-any.whl

Total release size: 48.5 kB

Release files / instabox_sdk-0.3.1.tar.gz

Download URL instabox_sdk-0.3.1.tar.gz
Size 29.0 kB
Tags Source
SHA-256 checksum
How to use checksums
58321c13e2969d07c9013dd624ea3c71501d7e9a5acafcb06e6003a1b109ff1c
BLAKE2b-256 checksum
How to use checksums
88db623610e2357ae11861566ae953ed944c8afd7cedd9f480ff4bff40b57c18
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 4, 2026.

Transparency log

Release files / instabox_sdk-0.3.1-py3-none-any.whl

Download URL instabox_sdk-0.3.1-py3-none-any.whl
Size 19.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
0e2e3f1e7aaf022925f7a44acf50fc1b3a750449cca587c6fc21b243e535f635
BLAKE2b-256 checksum
How to use checksums
e3fa0e34c99293738a3263b79566523dc5115bf29f7ecc1fd8d0d1f796b9fd32
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 4, 2026.

Transparency log

Security for instabox-sdk 0.3.1

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for instabox_sdk-0.3.1.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 4, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page