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
It is on PyPI as instabox-sdk. A version bump in
pyproject.toml merged to main publishes a new release
(.github/workflows/sdk-python-release.yml; see Publishing).
Quickstart
Create an API key on API keys in the dashboard
(Full access, or a restricted key with machines:write and machines:delete, which is
enough for the quickstart; the snapshots calls also need images:write (create),
images:read (list and get) and images:delete (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 (this block runs on its own: it makes my-box first, as an earlier run
would have, and deletes it at the end):
from instabox import Client
client = Client()
client.machines.create(name="my-box") # stands in for a sandbox made earlier
try:
box = client.machines.ensure_running("my-box") # starts or resumes it, waits until ready
print(client.machines.exec(box.id, "ls /data/home").stdout, end="")
client.machines.update(box.id, metadata={"stage": "review"}) # labels: any plan
# Paid plans only; on the free plan another idle time is 403 spec_not_allowed:
# client.machines.update(box.id, auto_suspend_after_seconds=600)
finally:
client.machines.delete("my-box", 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; Snapshot.status is pending while a running sandbox's copy uploads, then ready (or failed, with error): poll get until it is ready |
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, and another value is403 spec_not_allowed,PermissionDeniedError).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.Snapshothasid,name,status(pending,ready,failed),source_machine_id,template_id,org_id,description,error(why it failed) andcreated_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 (a size or disk the plan does not offer) |
| 401 | AuthenticationError |
missing_key, malformed_key, invalid_key, expired_key, revoked_key; missing_api_key when no key is set (raised by Client()) |
| 403 | PermissionDeniedError |
missing_scope, template_not_allowed, spec_not_allowed (another idle time on the free plan) |
| 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 |
spec_not_allowed is a plan or size limit and takes the status of what was refused: 400 for a
size or disk size the plan does not offer, 403 for another idle time
(auto_suspend_after_seconds) on the free plan or starting a paid sandbox larger than its plan
now allows, 409 for restoring a snapshot that does not fit the sandbox's disk.
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='malformed_key', message=..., status=401, ...)
# A well-formed key the server does not know is code='invalid_key'.
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.2
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.2.tar.gz | 29.4 kB | With provenance | Details for instabox_sdk-0.3.2.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| instabox_sdk-0.3.2-py3-none-any.whl | Python 3 | none | any | Details for instabox_sdk-0.3.2-py3-none-any.whl |
Total release size: 49.3 kB
Release files / instabox_sdk-0.3.2.tar.gz
| Download URL | instabox_sdk-0.3.2.tar.gz |
|---|---|
| Size | 29.4 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
033ca5e1cf13c25e07121ba3cb0f00a6ebe12ae66df663db6dac67afe6d32b33
|
|
BLAKE2b-256 checksum How to use checksums |
d216576a819941c4c0a56836fe023dcdf3873cea326a762ed57bae79f3e1f7fc
|
| 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 5, 2026.
Transparency logRelease files / instabox_sdk-0.3.2-py3-none-any.whl
| Download URL | instabox_sdk-0.3.2-py3-none-any.whl |
|---|---|
| Size | 20.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
e088f08f35b339594f89209ed14125b2da6d13756d49857f050cffc48c16185f
|
|
BLAKE2b-256 checksum How to use checksums |
1c0579dfdd1890086d8e687b151436a6ee644dc4298101b9b27cbecb8c708fa5
|
| 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 5, 2026.
Transparency logSecurity for instabox-sdk 0.3.2
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.2.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 5, 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.