APIZ guide
Python SDK
Use Python on the trusted host to grant temporary APIZ access to a native sandbox.
The sandbox runs its usual agent, APIZ CLI and official service tools. The package
name is apiz-sdk; the import name is apiz.
Install
Use Python 3.11 or later. Create a project and install the SDK from PyPI:
mkdir apiz-demo
cd apiz-demo
python -m venv .venv
source .venv/bin/activate
python -m pip install apiz-sdk e2b==2.49.1 python-dotenv
On Windows, activate with .venv\Scripts\activate instead. The distribution is
apiz-sdk; Python imports use apiz. E2B is optional; install only the provider
you use. python-dotenv loads this tutorial's .env file. To install just the
core SDK, run python -m pip install apiz-sdk.
Tutorial: Read An S3 Object Inside E2B
1. Prepare APIZ and the host environment
In the Web Console, configure an S3 API connection, attach a read Policy, and add an S3 Binding to an existing Client. Have an existing allowed object ready. Create a User API Key that can issue/revoke credentials for that Client's Team.
Create a .env file in your apiz-demo project:
APIZ_USER_API_KEY=replace-with-your-user-api-key
APIZ_SERVER_URL=https://current-host
APIZ_TEAM_ID=replace-with-your-team-id
APIZ_CLIENT_ID=replace-with-your-client-id
E2B_API_KEY=replace-with-your-e2b-api-key
APIZ_EXAMPLE_BINDING=replace-with-your-s3-binding-alias
APIZ_EXAMPLE_OBJECT_KEY=demo/hello.txt
Replace the placeholders:
| Variable | Value |
|---|---|
APIZ_USER_API_KEY | Host User API Key, not an APIZ_TOKEN |
APIZ_SERVER_URL | Reachable HTTPS Control Plane URL |
APIZ_TEAM_ID | Team containing the Client |
APIZ_CLIENT_ID | Existing Client ID |
E2B_API_KEY | E2B account key, kept on the host |
APIZ_EXAMPLE_BINDING | S3 Binding alias on the Client |
APIZ_EXAMPLE_OBJECT_KEY | Allowed object key, e.g. demo/hello.txt |
The bucket comes from the Binding. You do not pass upstream AWS credentials to the sandbox. The cloud sandbox must reach APIZ and CLI/package download endpoints.
2. Run the synchronous host
Save the following code as main.py. In the next step, create guest_s3.py
beside it. load_dotenv() reads your .env; exported shell variables take
precedence. Run python main.py after creating both files. This creates and
later deletes an E2B sandbox.
"""Caller-owned E2B + APIZ CLI. Host keys are read explicitly, never printed."""
import os
from pathlib import Path
from dotenv import load_dotenv
from apiz.access import issue_access
from apiz.cli import install_command
from apiz.management import Management
from e2b import Sandbox
def main() -> None:
load_dotenv()
with Management(
api_key=os.environ["APIZ_USER_API_KEY"],
server_url=os.environ["APIZ_SERVER_URL"],
team_id=os.environ["APIZ_TEAM_ID"],
) as api:
sandbox = Sandbox.create(api_key=os.environ["E2B_API_KEY"], timeout=600)
try:
sandbox.files.write(
"/home/user/guest_s3.py",
Path(__file__).with_name("guest_s3.py").read_text(),
)
installed = sandbox.commands.run("python -m pip install boto3", timeout=120)
if installed.exit_code:
raise RuntimeError("Guest dependency installation failed")
sandbox.commands.run(
install_command(server_url=api.server_url), timeout=120
)
with issue_access(
api,
client_id=os.environ["APIZ_CLIENT_ID"],
ttl_seconds=600,
env={
"APIZ_CLI_PATH": "/tmp/apiz/bin/apiz",
**{
name: os.environ[name]
for name in ("APIZ_EXAMPLE_BINDING", "APIZ_EXAMPLE_OBJECT_KEY")
},
},
) as access:
result = sandbox.commands.run(
"python /home/user/guest_s3.py",
envs=access.export_env(),
timeout=30,
)
assert result.exit_code == 0
assert sandbox.commands.run("true").exit_code == 0
finally:
sandbox.kill()
print("PASS: access revoked; native sandbox remained usable until caller cleanup")
if __name__ == "__main__":
main()
The native Sandbox remains yours. install_command() generates a command
that you explicitly execute before issuance. export_env() supplies the Client
token and selected guest settings. The guest uses the explicit APIZ_CLI_PATH;
APIZ does not modify global PATH or existing processes.
3. Understand the guest
Save this as guest_s3.py beside main.py. The host uploads it; you do not run
it on your own machine. It uses CLI setup followed by the official boto3 SDK:
"""Run inside the sandbox: APIZ CLI supplies credentials to the official SDK."""
import json
import os
import subprocess
import boto3
from botocore.config import Config
def main() -> None:
setup = subprocess.run(
["apiz", "--output", "json", "client", "handoff", "setup", "--format", "json"],
capture_output=True,
timeout=30,
check=False,
)
if setup.returncode:
raise RuntimeError("APIZ CLI setup failed")
manifest = json.loads(setup.stdout)
item = next(
item
for item in manifest["items"]
if item["meta"]["binding_alias"] == os.environ["APIZ_EXAMPLE_BINDING"]
and item["adapter"] == "s3"
)
auth = item["auth"]
with boto3.client(
"s3",
endpoint_url=item["endpoint"],
region_name=auth["region"],
aws_access_key_id=auth["access_key_id"],
aws_secret_access_key=auth["secret_access_key"],
aws_session_token=auth.get("session_token"),
config=Config(
signature_version="s3v4",
s3={"addressing_style": "path"},
connect_timeout=5,
read_timeout=10,
),
) as client:
response = client.get_object(
Bucket=item["meta"]["bucket"], Key=os.environ["APIZ_EXAMPLE_OBJECT_KEY"]
)
response["Body"].close()
print("PASS: official boto3 GetObject through APIZ")
if __name__ == "__main__":
try:
main()
except Exception:
raise SystemExit("Guest request failed; inspect APIZ access logs") from None
The setup JSON is sensitive and stays in memory. Its endpoint and credentials are
APIZ-issued values. The guest selects one S3 setup item, but that selection does
not narrow the default token's Client authority; use Bindings and Policies for
that boundary. The sample verifies GetObject and closes the response stream;
it deliberately does not print object contents or setup values.
4. Verify the outcome
python main.py
The host ends with PASS: access revoked; native sandbox remained usable until caller cleanup. Guest output is captured. In Access Logs, find the Client's
S3 request and inspect its outcome and Policy evidence. On leaving the context,
the credential group is revoked; the sandbox is deleted by your own finally.
A successful CLI inspect alone does not prove S3 access.
Case: Optional CLI Installation
install_command is synchronous and performs no I/O, including in async programs.
Execute its result through your native provider SDK only when installation is needed:
from apiz.cli import install_command
command = install_command(
server_url=api.server_url,
install_dir="/tmp/apiz/bin",
runtime="python",
)
sandbox.commands.run(command, timeout=120)
Choose runtime="sh" for curl/wget (the default), "python" for python3, or
"node" for Node.js 22+. Every entrypoint executes the same install.sh and
requires sh, tar and standard Unix utilities. See CLI installation.
No credentials are created or embedded. Use /tmp/apiz/bin/apiz afterward, or
configure PATH yourself. If the CLI is already installed, omit this entire step.
Case: Customize Or Reuse The Native Sandbox
Before granting access, use sandbox.files.write, sandbox.commands.run, and
other native operations to upload your agent and install dependencies. Replace
python /home/user/guest_s3.py with your installed agent's entrypoint. Keep the
envs=access.export_env() handoff for that process, with an explicit CLI path.
Only pass selected application variables in env; never forward os.environ.
You can grant access to a sandbox you created earlier. Remove sandbox.kill()
only when another owner is responsible for its lifetime. Revocation does not
remove files, undo commands or erase plaintext the guest already received.
Do not persist live credentials in snapshots or images.
Case: Async Host
Use AsyncManagement, native AsyncSandbox, and issue_access_async together:
from apiz.access import issue_access_async
from apiz.cli import install_command
from apiz.management import AsyncManagement
from e2b import AsyncSandbox
async def run_existing(
api: AsyncManagement, sandbox: AsyncSandbox, client_id: str
) -> None:
await sandbox.commands.run(install_command(server_url=api.server_url), timeout=120)
async with await issue_access_async(
api, client_id=client_id, ttl_seconds=600
) as access:
result = await sandbox.commands.run(
"/tmp/apiz/bin/apiz --output json client handoff inspect",
envs=access.export_env(),
timeout=30,
)
if result.exit_code != 0:
raise RuntimeError("Guest command failed")
That fragment checks readiness on caller-owned objects. To run the complete
async S3 tutorial, save this as main_async.py beside the same .env and
guest_s3.py:
"""Caller-owned E2B + APIZ CLI. Host keys are read explicitly, never printed."""
import asyncio
import os
from pathlib import Path
from dotenv import load_dotenv
from apiz.access import issue_access_async
from apiz.cli import install_command
from apiz.management import AsyncManagement
from e2b import AsyncSandbox
async def main() -> None:
load_dotenv()
async with AsyncManagement(
api_key=os.environ["APIZ_USER_API_KEY"],
server_url=os.environ["APIZ_SERVER_URL"],
team_id=os.environ["APIZ_TEAM_ID"],
) as api:
sandbox = await AsyncSandbox.create(
api_key=os.environ["E2B_API_KEY"], timeout=600
)
try:
await sandbox.files.write(
"/home/user/guest_s3.py",
Path(__file__).with_name("guest_s3.py").read_text(),
)
installed = await sandbox.commands.run(
"python -m pip install boto3", timeout=120
)
if installed.exit_code:
raise RuntimeError("Guest dependency installation failed")
await sandbox.commands.run(
install_command(server_url=api.server_url), timeout=120
)
async with await issue_access_async(
api,
client_id=os.environ["APIZ_CLIENT_ID"],
ttl_seconds=600,
env={
"APIZ_CLI_PATH": "/tmp/apiz/bin/apiz",
**{
name: os.environ[name]
for name in ("APIZ_EXAMPLE_BINDING", "APIZ_EXAMPLE_OBJECT_KEY")
},
},
) as access:
result = await sandbox.commands.run(
"python /home/user/guest_s3.py",
envs=access.export_env(),
timeout=30,
)
assert result.exit_code == 0
assert (await sandbox.commands.run("true")).exit_code == 0
finally:
await sandbox.kill()
print("PASS: access revoked; native sandbox remained usable until caller cleanup")
if __name__ == "__main__":
asyncio.run(main())
python main_async.py
install_command() and export_env() are synchronous local operations even on async
access. Async cancellation does not eliminate the independent cleanup attempt.
Keep AsyncManagement open until cleanup finishes.
Case: Explicit Lifetime Without with
In an orchestrator, you may keep an access object across several operations. The following function illustrates manual ownership on an existing sandbox:
from apiz.access import issue_access
from apiz.cli import install_command
def inspect_with_explicit_cleanup(api, sandbox, client_id):
sandbox.commands.run(install_command(server_url=api.server_url), timeout=120)
access = issue_access(api, client_id=client_id, ttl_seconds=600)
try:
return sandbox.commands.run(
"/tmp/apiz/bin/apiz --output json client handoff inspect",
envs=access.export_env(),
timeout=30,
)
finally:
report = access.revoke()
if not report.complete:
# Persist these safe references for reconciliation; never print tokens.
print("Revocation requires reconciliation", access.reference)
Manual revoke() returns a report; it does not guarantee success just because
it returned. revocation is succeeded, unknown, or not_owned.
Repeated calls return the cached result, including unknown; use management
lookup/revocation for reconciliation rather than expecting a retry.
Management.close() only releases HTTP resources; it does not revoke groups.
Case: Manage Credentials Without A Provider
Save the following as management.py and run python management.py. It needs
only the four management variables in .env and creates no sandbox:
"""Run against a preconfigured Client on a trusted host; no sandbox is created."""
import os
from dotenv import load_dotenv
from apiz.errors import CreationOutcomeUnknown, CreationOutputUnavailable
from apiz.management import Management
def main():
load_dotenv()
with Management(
api_key=os.environ["APIZ_USER_API_KEY"],
server_url=os.environ["APIZ_SERVER_URL"],
team_id=os.environ["APIZ_TEAM_ID"],
) as management:
prepared = management.credentials.prepare(
os.environ["APIZ_CLIENT_ID"],
ttl_seconds=900,
all_bindings=True,
label="SDK example",
)
# Persist this safe ID before submission in a real orchestrator.
print("Creation operation:", prepared.operation_id)
try:
issued = management.credentials.create(prepared)
except CreationOutcomeUnknown:
status = management.creation_operations.get(
prepared.client_id, prepared.operation_id
)
print("Creation state:", status["state"])
if status["state"] == "committed":
management.credentials.revoke(status["credential_group_id"])
# not_observed is still unknown. Preserve the operation ID for later lookup.
raise
except CreationOutputUnavailable as error:
if error.credential_group_id:
management.credentials.revoke(error.credential_group_id)
raise
try:
print("Created group:", issued.credential_group_id)
print("Expires at:", issued.expires_at)
# Use issue_access for guest credential lifecycle.
finally:
management.credentials.revoke(issued.credential_group_id)
if __name__ == "__main__":
main()
credentials.prepare allocates an operation identity locally; credentials.create
submits the request. Preserve the operation ID before submission. If the outcome
is unknown, query creation_operations.get(client_id, operation_id) before
retrying. not_observed is still unknown, not proof that no credential exists.
For a custom executor, issue_access(api, client_id=..., ttl_seconds=...) returns
an access object without provider CLI installation. Your executor must install
and invoke the CLI through native commands. The provider guides show those commands.
Management API Reference
All network methods have async counterparts on AsyncManagement and accept an
optional timeout in seconds.
| Method | Purpose |
|---|---|
clients.list() / clients.get(id) | Discover/read Clients visible to the key |
clients.bindings(id) / clients.setup_options(id) | Inspect available configuration |
credentials.prepare(id, ttl_seconds=..., all_bindings=True) | Prepare issuance and stable operation ID |
credentials.create(prepared) | Submit issuance; result contains sensitive credentials |
credentials.get(group_id) / credentials.revoke(group_id) | Read group metadata / revoke |
creation_operations.get(client_id, operation_id) | Reconcile an uncertain creation |
access_logs.list(client_id=..., decision="deny", limit=20) | Find denied requests |
access_logs.request(request_id) | Retrieve evidence for a known request |
A Team-scoped management key is required; the key's permissions and your current role still constrain these operations. This is not yet a complete SDK for every APIZ management endpoint.
Timeouts, Failures And Recovery
ttl_seconds controls credential expiry; native sandbox timeout controls sandbox
lifetime. Grant timeout bounds preparation operations, cleanup_timeout bounds
cleanup, and each native guest command has its own timeout. They are different
budgets. A timeout bounds waiting; it does not prove the server or guest stopped.
| Failure | Next action |
|---|---|
ConfigurationError | Correct URL, IDs, options or forbidden environment values before retrying |
APIZError | Inspect safe status/code/reason; check key scope or requested resource |
CreationOutcomeUnknown | Look up the saved creation operation; do not blindly reissue |
CreationOutputUnavailable | Reconcile/revoke the known group; plaintext recovery may be unavailable |
AccessError | Inspect preparation stage and cleanup metadata; check guest tools/network |
CleanupError | The body completed but automatic revocation is not confirmed; reconcile |
Use cleanup_report(error) and access_reference(error) from apiz.access for
safe recovery metadata. When the body fails, automatic cleanup preserves the
original exception and attaches these references. Never dump setup manifests,
export_env() results or full third-party exceptions to shared logs.
Provider Guides And Next Steps
- E2B: recommended first tutorial and native commands/files.
- Daytona: process API, network tier restrictions and simulation scope.
- Modal: app/image/client configuration and
.aiobehavior. - Vercel: scoped Python session, credentials and command results.
- TypeScript for the equivalent host integration.
- Policy SDK for authoring request decisions rather than host code.
Type Checking
The package ships inline Python annotations and py.typed; no separate stub
package is needed. Editors and type checkers read the same files that execute.
Synchronous calls return their documented result directly; asynchronous calls
return it when awaited. Manifest and extensible metadata exports retain dynamic
JSON fields. Static typing does not replace the SDK's runtime wire validation.