APIZDocs

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:

VariableValue
APIZ_USER_API_KEYHost User API Key, not an APIZ_TOKEN
APIZ_SERVER_URLReachable HTTPS Control Plane URL
APIZ_TEAM_IDTeam containing the Client
APIZ_CLIENT_IDExisting Client ID
E2B_API_KEYE2B account key, kept on the host
APIZ_EXAMPLE_BINDINGS3 Binding alias on the Client
APIZ_EXAMPLE_OBJECT_KEYAllowed 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.

MethodPurpose
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.

FailureNext action
ConfigurationErrorCorrect URL, IDs, options or forbidden environment values before retrying
APIZErrorInspect safe status/code/reason; check key scope or requested resource
CreationOutcomeUnknownLook up the saved creation operation; do not blindly reissue
CreationOutputUnavailableReconcile/revoke the known group; plaintext recovery may be unavailable
AccessErrorInspect preparation stage and cleanup metadata; check guest tools/network
CleanupErrorThe 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 .aio behavior.
  • 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.