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) |
machineis a sandbox ID (m_...) or its name.createwithout a name pickspy-<random>. Withouttemplateorsnapshot_idit usesagent-browser, the default template;templatemay pin a version asid@version(agent-browser@2026-10-15.4).createoptions:auto_suspend_after_secondsis 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_secondsstops anddelete_after_secondsdeletes 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=Truehas 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).updateisPATCH /v1/machines/{id}:name,metadata(replaces it;{}clears) andauto_suspend_after_secondsapply in any state;vcpus/mem_mib(another of the workspace's sizes) need the sandbox stopped and apply at its next start;persistent_disk_gibgrows a running sandbox's disk online (a stopped one's at its next start) and shrinks only while stopped:resizewraps it. Absent fields are unchanged.persistent_disk_gibis sent as the API'spersistentDiskGiB. On api.instabox.sh the disk (/data) comes with the size, so leave it out: another size is400 spec_not_allowed(BadRequestError). Resize a paid sandbox's disk later withclient.machines.resize(m, disk_gib, restart=False), which sendsPATCH /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=Truestops and restarts it) and the data plus 10% (at least 1 GiB) to fit, elseConflictErrorwith codedisk_too_small. The system disk is the template's:rootDiskGiBfrom anyone but an admin is400 spec_not_allowed.vcpusandmem_mib(the API'svcpusandmemMiB) name one of the org's sizes at create. A paid sandbox changes size later, while stopped, withclient.machines.update(m, vcpus=N, mem_mib=M), which sendsPATCH /v1/machines/{id}{"vcpus": N, "memMiB": M}(the CLI'sinstabox 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 withno_capacityif the host has no room for it; the sandbox stays stopped at the new size).client.machines.resizetakes only the disk. Running or paused is409 operation_conflict(ConflictError; stop a running sandbox first, start then stop a paused one); another size, or the free sandbox,400 spec_not_allowed.exectakes a shell string, run withbash -lcas the sandbox's user (dev, HOME/data/home), or an argv list, run with no shell (the API'sargv: 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. Whentimeout(seconds, at most 600) passes, the command is killed and the result hastimed_out=Trueandexit_code=124.execis not idempotent, so it is never retried. The sandbox must be running (409 machine_not_running,ConflictError):ensure_runningfirst when it may have been paused or stopped.wakeis 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 nextexecfinds it running. A sandbox paused or stopped on purpose is not woken;start(orensure_running) does that.delete(confirm=...)sendsX-Confirm-Deletewith the sandbox's ID or exact name; any other value is409 delete_confirmation_required(ConflictError) and nothing is deleted. Withoutconfirm,deletesends themachineit was given (ID or name).snapshots.deletetakes the sameconfirm.create,start,stop,delete,updateand the snapshot calls send anIdempotency-Key(random per call). Passidempotency_key=to retry a call safely: the same key and request return the original response, so a retried create returns the same sandbox.filespaths are absolute.writeandreadstream any size (up to 5 GiB) as the sandbox's user,writereplaces the file atomically andparentscreates missing directories; a missing file isNotFoundError, a directoryConflictError(not_a_file).Machinehasid,name,state(creating,starting,running,stopping,stopped,suspending,suspended,resuming,deleting,deleted,error),readyandrunning(running and ready),template_id,template_version,snapshot_id,spec,metadata,auto_suspend_after_seconds(None: never; sent as0, reported asNone),max_run_minutes,archive_at,delete_at,guest_user,processes_preserved(after a start or resume:Falsewhen the paused programs could not be brought back),free,org_id,operation_id,last_error,created_atandupdated_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):
- 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 ownerInsForge, repositoryinstabox, workflowsdk-python-release.ymland environmentpypi. - In the GitHub repository, make sure the
pypienvironment exists (Settings → Environments). - Re-run the failed
sdk-python releaserun 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)
| 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)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| 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 logRelease 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 logSecurity 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.
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 logA 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.