---
title: Python SDK Reference
product: vercel
url: /docs/sandbox/python-sdk-reference
canonical_url: "https://vercel.com/docs/sandbox/python-sdk-reference"
last_updated: 2026-08-25
type: reference
prerequisites:
  - /docs/sandbox
related:
  - /docs/sandbox/sdk-reference
  - /docs/sandbox/concepts/drives
  - /docs/sandbox/concepts/regions
  - /docs/sandbox/concepts/secure-compute
  - /docs/sandbox/concepts/authentication
summary: Reference for the Vercel Sandbox Python SDK, including sandbox lifecycle, processes, files, drives, snapshots, persistence, and network policies.
install_vercel_plugin: npx plugins add vercel/vercel-plugin
---

# Python SDK Reference

Use the Vercel Sandbox Python SDK to create isolated Linux microVMs, run processes, manage files and drives, and preserve environments between sessions.


<!-- docsgraph:related -->
## Related pages

> **For AI agents:** Follow these links to understand how this page connects to the rest of the Vercel ecosystem. For the full cross-link map (inbound, outbound, prerequisites, and semantic neighbors), see the .graph.md link below.

- [Python on Vercel is now generally available](https://vercel.com/changelog/python-on-vercel-is-now-generally-available)
- [Build an agentic app in FastAPI with OpenAI Agents API and Vercel Sandbox](https://vercel.com/kb/guide/fastapi-openai-agents-api-vercel-sandbox) — Learn how to build a repository Q&A app using FastAPI, OpenAI Agents API, and Vercel Sandbox, with isolated code inspect
- [Build AI agents in Python with the AI SDK](https://vercel.com/kb/guide/python-and-ai-sdk) — Stream model responses, get typed output, and build tool-using agents in Python on Vercel.
- [Call third-party APIs from Python with Vercel Connect](https://vercel.com/kb/guide/python-and-vercel-connect) — Call Slack and Linear APIs from Python with Vercel Connect. Request app and user tokens, handle consent, and verify prov
- [How Vercel Drives work and how to use them](https://vercel.com/kb/guide/vercel-drives) — Learn how Vercel Drives give Vercel Sandbox persistent, shareable storage, and how to create, mount, snapshot, fork, lis
- [Python on Vercel is now generally available](https://vercel.com/blog/python-on-vercel-is-now-generally-available)
- [Persistence](https://vercel.com/docs/sandbox/concepts/persistent-sandboxes) — Sandboxes automatically save their filesystem state when stopped and restore it when resumed. No manual snapshot managem
- [Snapshots](https://vercel.com/docs/sandbox/concepts/snapshots) — Save and restore sandbox state with snapshots for faster startups and environment sharing.
- [Sandboxes](https://vercel.com/docs/agent-resources/vercel-mcp/tools/sandboxes) — Vercel MCP tools for sandboxes.
- [Understanding Sandboxes](https://vercel.com/docs/sandbox/concepts) — Learn how Vercel Sandboxes provide on-demand, isolated compute environments for running untrusted code, testing applicat
- [Get a named sandbox](https://vercel.com/docs/rest-api/sandboxes/get-a-named-sandbox) — GET /v2/sandboxes/{name} — Retrieves a named sandbox by name, including its current sandbox and routes. If the sandbox i

Full cross-link map for this page: [/docs/sandbox/python-sdk-reference.graph.md](/docs/sandbox/python-sdk-reference.graph.md)
<!-- /docsgraph:related -->

For JavaScript and TypeScript, see the [JS SDK Reference](/docs/sandbox/sdk-reference).

Follow [Run untrusted Python code safely with Vercel Sandbox](/kb/guide/python-and-vercel-sandbox) to execute code in an isolated microVM, manage files, restrict network access, and reuse prepared environments with snapshots.

## Install the SDK

Install the Vercel Python package:

```bash filename="Terminal"
uv add vercel
```

The examples on this page use the asynchronous API. You can import it as a module:

```python filename="main.py"
from vercel import sandbox
```

The SDK creates and reuses a default session when you first use it. Most applications don't need to create one. See [SDK sessions](#sdk-sessions) when you need a custom HTTP client, credentials, or service configuration.

## Create a sandbox

### `create_sandbox()`

Create and start a sandbox:

```python filename="main.py"
import asyncio

from vercel import sandbox


async def main() -> None:
    async with sandbox.create_sandbox() as box:
        result = await box.run_command(
            "python",
            ["-c", "print('Hello from Vercel Sandbox!')"],
            capture_output=True,
            check=True,
        )
        print(result.stdout)


asyncio.run(main())
```

`create_sandbox()` returns a single-use operation that you can either await or use as an async context manager:

- `async with sandbox.create_sandbox() as box` stops and destroys the sandbox on exit, including snapshots no other sandbox uses.
- `box = await sandbox.create_sandbox()` leaves lifecycle management to you. Call `stop()` to end the current session or `destroy()` to delete the sandbox.
- Pass `destroy=False` to stop, but not destroy, a context-managed sandbox on exit.

> **💡 Note:** Don't await or enter the same creation operation more than once.

The function accepts these keyword arguments:

| Parameter              | Type                                                                | Description                                                                              |
| ---------------------- | ------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- |
| `name`                 | `str \| None`                                                       | Stable sandbox name. Vercel generates one when omitted.                                  |
| `image`                | `str \| None`                                                       | Vercel Container Registry image reference.                                               |
| `source`               | `GitSource \| TarballSource \| SnapshotSource \| None`              | Initial filesystem source.                                                               |
| `ports`                | `list[int] \| None`                                                 | Ports to expose through the sandbox routes.                                              |
| `execution_time_limit` | `int \| float \| timedelta \| None`                                 | Maximum runtime for the current session. Numbers represent seconds.                      |
| `resources`            | `SandboxResources \| None`                                          | Requested virtual CPUs and memory.                                                       |
| `persistent`           | `bool \| None`                                                      | Whether Vercel snapshots the filesystem when the session stops.                          |
| `network_policy`       | `NetworkPolicy \| None`                                             | Outbound network policy.                                                                 |
| `network_id`           | `str \| None`                                                       | Secure Compute network ID to attach to the sandbox.                                      |
| `env`                  | `Mapping[str, str] \| None`                                         | Environment variables available to processes in the sandbox.                             |
| `tags`                 | `Mapping[str, str] \| None`                                         | Metadata used to organize and query sandboxes.                                           |
| `mounts`               | `Mapping[str \| PurePosixPath, str \| Drive \| DriveMount] \| None` | Drives keyed by absolute mount path. Drives use read-write mode by default.              |
| `snapshot_expiration`  | `int \| float \| timedelta \| SnapshotExpiration \| None`           | Default snapshot lifetime. Numbers represent seconds. Zero disables expiration.          |
| `snapshot_retention`   | `SnapshotRetention \| None`                                         | Automatic snapshot retention policy.                                                     |
| `region`               | `str \| None`                                                       | Preferred region for the sandbox.                                                        |
| `failover_regions`     | `Iterable[str] \| None`                                             | Regions to try if the preferred region lacks capacity.                                   |
| `destroy`              | `bool`                                                              | Whether context-manager exit destroys the sandbox after stopping it. Defaults to `True`. |

When you omit `image`, the Sandbox API uses `vercel/sandbox/universal:latest`. You can pass a bare repository, tag, digest, or fully qualified Vercel Container Registry reference. The `box.image` property contains the resolved image reference.

The SDK selects the owning project from its resolved credentials. To use another project, configure `SandboxServiceOptions` with credentials for that project.

Drive mount paths must be absolute, and the sandbox's main region must match each mounted drive's region. See [Drives](/docs/sandbox/concepts/drives) and [Regions](/docs/sandbox/concepts/regions).

Create a persistent sandbox without automatic cleanup:

```python filename="main.py"
import asyncio
from datetime import timedelta

from vercel import sandbox
from vercel.sandbox import SnapshotRetention


async def main() -> None:
    box = await sandbox.create_sandbox(
        name="my-development-environment",
        persistent=True,
        execution_time_limit=timedelta(minutes=30),
        snapshot_retention=SnapshotRetention(count=3),
        tags={"environment": "development"},
    )

    try:
        await box.run_command("uv", ["sync"], check=True)
    finally:
        await box.stop()


asyncio.run(main())
```

### `fork_sandbox()`

Create a sandbox from an existing named sandbox:

```python filename="main.py"
from vercel import sandbox
from vercel.sandbox import SandboxResources

async with sandbox.fork_sandbox(
    source_sandbox="production-agent",
    name="debug-agent",
    resources=SandboxResources(vcpus=4, memory=8192),
    tags={"purpose": "debug"},
) as forked:
    result = await forked.run_command(
        "python",
        ["script.py"],
        capture_output=True,
        check=True,
    )
    print(result.stdout)
```

The fork uses the named source sandbox as its starting point and inherits its configuration and Secure Compute network (`network_id`), except for drive mounts. Values passed to `fork_sandbox()` replace the corresponding inherited values. Pass `network_id` to override the inherited network. Pass `mounts` to attach drives to the fork. Like `create_sandbox()`, the returned operation is single-use, awaitable, and an async context manager. Context-manager exit stops and destroys the fork by default.

## Retrieve and resume sandboxes

### `get_sandbox()`

Fetch a sandbox by name without starting a new runtime session:

```python filename="main.py"
box = await sandbox.get_sandbox(name="my-development-environment")
print(box.status)
print(box.current_snapshot_id)
```

`get_sandbox()` accepts `name` and optional `include_system_routes`. The lookup is passive and doesn't start a new runtime session. The SDK uses the project from its resolved credentials.

Process and filesystem operations on the returned handle automatically resume a stopped persistent sandbox. The same `box` handle adopts the replacement session before retrying the operation:

```python filename="main.py"
box = await sandbox.get_sandbox(name="my-development-environment")

# Resumes the sandbox if it is stopped, then reads the restored filesystem.
content = await box.fs.read_text("state.json")
print(content)
```

This behavior applies to `run_command()`, `create_command()`, command queries, methods on `box.fs`, `extend_execution_time_limit()`, `update_network_policy()`, and `snapshot()`. Lifecycle and sandbox configuration operations, including `stop()`, `destroy()`, and `update()`, don't auto-resume.

### `get_or_create_sandbox()`

Retrieve a named sandbox or create it when it doesn't exist:

```python filename="main.py"
box, created = await sandbox.get_or_create_sandbox(
    name="my-development-environment",
    persistent=True,
)

if created:
    await box.run_command("uv", ["sync"], check=True)
```

The function resumes an existing sandbox by default. Pass `resume=False` for a passive lookup. If the sandbox's latest snapshot no longer exists, the SDK deletes the stale sandbox and creates a replacement with the same name. The `created` value is `True` when the SDK creates or replaces the sandbox.

### `resume_sandbox()`

Start a new runtime session from a stopped persistent sandbox:

```python filename="main.py"
async with sandbox.resume_sandbox(
    name="my-development-environment"
) as box:
    content = await box.fs.read_text("state.json")
    print(content)
```

`resume_sandbox()` returns a single-use operation. You can await it to manage the lifecycle yourself or use it as an async context manager. Context-manager exit stops the resumed session but doesn't destroy the sandbox.

Use `resume_sandbox()` when the runtime session must start before another operation. For lazy resume, use `get_sandbox()` and call a process or filesystem method on the returned handle.

### Query sandboxes

`query_sandboxes()` returns an async iterator and follows pagination cursors automatically:

```python filename="main.py"
from vercel import sandbox
from vercel.sandbox import SandboxQueryByName, TagFilter

query = SandboxQueryByName(
    name_prefix="user-123-",
    sort_order="desc",
    tag=TagFilter(key="environment", value="development"),
)

async for box in sandbox.query_sandboxes(query=query, page_size=50):
    print(box.name, box.status)
```

Use one of these query models:

| Type                              | Fields                                               |
| --------------------------------- | ---------------------------------------------------- |
| `SandboxQueryByCreatedAt`         | `sort_order`, optional `tag`                         |
| `SandboxQueryByName`              | `sort_order`, optional `name_prefix`, optional `tag` |
| `SandboxQueryByStatusUpdatedAt`   | `sort_order`                                         |
| `SandboxQueryByCurrentSnapshotId` | `sort_order`                                         |
| `TagFilter`                       | Exact `key` and `value` match                        |

All query functions accept `page_size` and `cursor`. Queries use the project from the SDK's resolved credentials.

## Sandbox handles

A `Sandbox` handle describes the persistent sandbox and its current runtime session. A sandbox has at most one active session. When a stopped persistent sandbox resumes, the new session replaces the stopped session as the current one.

### Properties

| Property               | Type                              | Description                                                              |
| ---------------------- | --------------------------------- | ------------------------------------------------------------------------ |
| `name`                 | `str`                             | Stable sandbox identity.                                                 |
| `current_session_id`   | `str`                             | Identifier for the current runtime session.                              |
| `current_session`      | `SandboxRuntimeSession \| None`   | Current session handle when the API response includes it.                |
| `image`                | `str \| None`                     | Resolved image reference.                                                |
| `status`               | `SandboxStatus \| None`           | Current lifecycle state.                                                 |
| `persistent`           | `bool \| None`                    | Whether stop creates an automatic snapshot.                              |
| `current_snapshot_id`  | `str \| None`                     | Snapshot used for the next resume.                                       |
| `project_id`           | `str \| None`                     | Owning project.                                                          |
| `cwd`                  | `str \| None`                     | Default working directory.                                               |
| `region`               | `str \| None`                     | Runtime region.                                                          |
| `failover_regions`      | `tuple[str, ...]`                 | Configured failover regions.                                             |
| `memory`               | `int \| None`                     | Memory in megabytes.                                                     |
| `vcpus`                | `int \| None`                     | Number of virtual CPUs.                                                  |
| `execution_time_limit` | `timedelta \| None`               | Current session execution limit.                                         |
| `network_policy`       | `NetworkPolicyReadback \| None`   | Cached saved-policy metadata. See [policy metadata](#inspect-policy-metadata). |
| `network_id`           | `str \| None`                     | Attached Secure Compute network ID.                                      |
| `snapshot_expiration`  | `timedelta \| None`               | Default snapshot expiration.                                             |
| `snapshot_retention`   | `SnapshotRetentionState \| None`  | Active retention policy.                                                 |
| `status_updated_at`    | `int \| None`                     | Unix timestamp for the latest status update.                             |
| `created_at`           | `int \| None`                     | Unix creation timestamp.                                                 |
| `updated_at`           | `int \| None`                     | Unix update timestamp.                                                   |
| `tags`                 | `dict[str, str] \| None`          | Copy of the sandbox tags.                                                |
| `routes`               | `tuple[SandboxRouteState, ...]`   | Exposed routes. Each route has `url`, `port`, `subdomain`, and `system`. |
| `mounts`               | `dict[str, SandboxMount] \| None` | Drives mounted by path. Each mount has `drive` and `mode`.               |
| `raw`                  | `dict \| None`                    | Copy of the raw API response data.                                       |
| `fs`                   | `SandboxFilesystem`               | Filesystem for the current session.                                      |

`SandboxStatus` can be `PENDING`, `RUNNING`, `STOPPING`, `STOPPED`, `FAILED`, `ABORTED`, or `SNAPSHOTTING`.

### `session()`

Use `session()` when you want to scope work to the sandbox's active runtime session and clean it up when the block exits:

```python filename="main.py"
box = await sandbox.get_sandbox(name="my-development-environment")

async with box.session() as runtime_session:
    result = await runtime_session.run_command(
        "python",
        ["script.py"],
        capture_output=True,
        check=True,
    )
    print(runtime_session.id, result.stdout)
```

`session()` resumes the sandbox if needed and returns its current `SandboxRuntimeSession`. Operations through this handle stay attached to that session and don't trigger automatic resume.

Choose the lifecycle behavior that fits your application:

- `async with box.session() as runtime_session` attempts to stop the acquired session when the block exits. It doesn't destroy the parent sandbox.
- `runtime_session = await box.session()` doesn't perform automatic cleanup. Call `await runtime_session.stop()` when you're done.
- Use methods on `box` instead when you want a stopped sandbox to resume automatically.

A sandbox has only one active session at a time. On context exit, the SDK requests a stop for the session acquired on entry. If that session has already stopped and the sandbox has resumed, the request is a no-op and the new active session keeps running.

Each asynchronous `session()` operation is single-use. Don't await or enter the same operation more than once. Complete concurrent work before leaving the context when you need deterministic cleanup.

### Routes

Find the URL for an exposed port:

```python filename="main.py"
def route_url(box: sandbox.Sandbox, port: int) -> str | None:
    for route in box.routes:
        if route.port == port:
            return route.url
    return None


box = await sandbox.create_sandbox(ports=[3000])
print(route_url(box, 3000))
```

### `update()`

Update mutable sandbox configuration:

```python filename="main.py"
from datetime import timedelta

from vercel.sandbox import NetworkPolicy, SandboxResources, SnapshotRetention

await box.update(
    resources=SandboxResources(vcpus=2),
    execution_time_limit=timedelta(minutes=30),
    persistent=True,
    ports=[3000, 8000],
    network_policy=NetworkPolicy.deny_all(),
    tags={"environment": "production"},
    snapshot_retention=SnapshotRetention(count=2),
)
```

You can update `ports`, `execution_time_limit`, `resources`, `persistent`, `network_policy`, `network_id`, `env`, `tags`, `mounts`, `snapshot_expiration`, `snapshot_retention`, `current_snapshot_id`, `region`, and `failover_regions`. Only non-`None` values are sent, except that `snapshot_retention=None` removes the retention policy and `network_id=None` detaches the [Secure Compute](/docs/sandbox/concepts/secure-compute) network. Changes to `network_id` apply from the sandbox's next session. For saved and live `network_policy` updates, see [updating a running session](#update-a-running-session). `ports` replaces the complete exposed port list. `mounts` replaces all drive mounts for the next session, and an empty mapping removes all mounts. Pass `failover_regions=[]` to clear failover regions. Set `current_snapshot_id` to choose the snapshot restored on the next resume. The method refreshes and returns the same `Sandbox` handle.

### `extend_execution_time_limit()`

Increase the current session's execution limit:

```python filename="main.py"
from datetime import timedelta

session = await box.extend_execution_time_limit(timedelta(minutes=15))
print(session.execution_time_limit)
```

The duration adds to the current limit. The service rejects durations shorter than one second.

### `stop()` and `destroy()`

Stop the current runtime session without deleting a persistent sandbox:

```python filename="main.py"
await box.stop()
```

Delete the sandbox and its sessions. By default, snapshots remain until they expire:

```python filename="main.py"
await box.destroy()
```

To asynchronously delete snapshots that no other sandbox uses, pass `delete_orphan_snapshots=True` to `destroy()`. Snapshots referenced by another sandbox remain available. Context-managed sandbox creation requests this orphan cleanup on exit by default, unlike an explicit `destroy()` call.

### `list_sessions()` and `list_snapshots()`

Fetch one page of resources that belong to a sandbox:

```python filename="main.py"
sessions = await box.list_sessions(page_size=20, sort_order="desc")
snapshots = await box.list_snapshots(page_size=20, sort_order="desc")
```

Use the module-level query functions when you want automatic pagination.

## Manage drives

[Drives](/docs/sandbox/concepts/drives) provide persistent storage that you can mount into sandboxes. A drive belongs to one project and region, and its files remain available after a sandbox stops.

### Create or retrieve a drive

Use `get_or_create_drive()` to retrieve a named drive or create it when it doesn't exist. It returns a tuple containing the drive and `created`, a boolean indicating whether the drive was created by this call:

```python filename="main.py"
from vercel import sandbox

drive, created = await sandbox.get_or_create_drive(
    name="workspace-cache",
    max_size_bytes=200 * 1024 * 1024 * 1024,
    region="sfo1",
)
```

The function accepts these keyword arguments:

| Parameter        | Type          | Description                                                          |
| ---------------- | ------------- | -------------------------------------------------------------------- |
| `name`           | `str`         | Project-local drive name.                                            |
| `max_size_bytes` | `int \| None` | Maximum size for a new drive. The project default applies if omitted. |
| `region`         | `str \| None` | Region for a new drive. Uses the SDK default when omitted.            |

When you omit `region`, the SDK uses `SandboxServiceOptions.region`, which reads `VERCEL_REGION` by default. If neither is set, the SDK omits the region and the Sandbox API creates the drive in `iad1`. Requesting an existing drive with a different region or maximum size fails with a `conflict` error.

### Mount a drive

Pass a mapping of absolute mount paths to drive handles or names. Drives mount in read-write mode by default:

```python filename="main.py"
drive, created = await sandbox.get_or_create_drive(name="workspace-cache")

box = await sandbox.create_sandbox(
    name="my-sandbox",
    mounts={"/data": drive},
)
```

Call `drive.snapshot()` to create a read-only, point-in-time mount that multiple sandboxes can use concurrently:

```python filename="main.py"
reader = await sandbox.create_sandbox(
    name="reader",
    mounts={"/data": drive.snapshot()},
)
```

You can also construct a mount explicitly with `DriveMount(drive, mode="read-write")` or `DriveMount(drive, mode="snapshot")`.

`create_sandbox()`, `fork_sandbox()`, and `get_or_create_sandbox()` accept `mounts`. A fork doesn't inherit its source sandbox's mounts. Pass `mounts` to attach drives to the fork.

Mount paths must be absolute and can't overlap. You can mount each drive once and mount up to four drives per sandbox. All mounted drives must be in the sandbox's main region.

### List drives

`query_drives()` returns an async iterator and follows pagination cursors automatically:

```python filename="main.py"
from vercel.sandbox import DriveQueryByName

query = DriveQueryByName(
    name_prefix="workspace",
    sort_order="asc",
)

async for drive in sandbox.query_drives(query=query, page_size=10):
    print(drive.name, drive.current_sandbox_name or "detached")
```

Use one of these query models:

| Type                    | Fields                               |
| ----------------------- | ------------------------------------ |
| `DriveQueryByCreatedAt` | `sort_order`                         |
| `DriveQueryByUpdatedAt` | `sort_order`                         |
| `DriveQueryByName`      | `sort_order`, optional `name_prefix` |

`query_drives()` also accepts `page_size` and `cursor`. It uses the project from the SDK's resolved credentials.

### Drive properties and deletion

| Property                   | Type          | Description                             |
| -------------------------- | ------------- | --------------------------------------- |
| `id`                       | `str`         | Drive identifier.                       |
| `name`                     | `str`         | Project-local drive name.               |
| `project_id`               | `str`         | Owning project.                         |
| `region`                   | `str`         | Storage region.                         |
| `max_size_bytes`           | `int`         | Maximum drive size in bytes.            |
| `current_session_id`       | `str \| None` | Attached runtime session, when present. |
| `current_sandbox_name`     | `str \| None` | Attached sandbox, when present.         |
| `created_at`, `updated_at` | `int`         | Unix timestamps.                        |

Delete a detached drive and all its data:

```python filename="main.py"
await drive.delete()
```

Use `await sandbox.delete_drive(name="workspace-cache")` when you don't have a drive handle. You can't delete a drive while it is attached to a running sandbox.

## Run processes

### `run_command()`

Run a process and wait for it to exit:

```python filename="main.py"
result = await box.run_command(
    "python",
    ["-m", "pytest"],
    env={"PYTHONUNBUFFERED": "1"},
    kill_after=120,
    capture_output=True,
    check=True,
)

print(result.returncode)
print(result.stdout)
print(result.stderr)
```

`run_command()` replaces `run_process()`, which still works as a deprecated alias and emits a `DeprecationWarning`.

By default, `run_command()` streams remote stdout and stderr to the matching local streams. Set `capture_output=True` to store both streams on the returned `CompletedProcess`. You can stream to custom text writers with `stdout` and `stderr`.

| Parameter        | Type                                              | Default  | Description                                                                    |
| ---------------- | ------------------------------------------------- | -------- | ------------------------------------------------------------------------------ |
| `command`        | `str`                                             | Required | Executable or command name.                                                    |
| `args`           | `Sequence[str] \| None`                           | `None`   | Arguments excluding the executable.                                            |
| `cwd`            | `str \| None`                                     | `None`   | Working directory.                                                             |
| `env`            | `Mapping[str, str] \| None`                       | `None`   | Environment variables added to the process.                                    |
| `sudo`           | `bool`                                            | `False`  | Run with elevated privileges.                                                  |
| `kill_after`     | `float \| timedelta \| None`                      | `None`   | Server-side time before `SIGKILL`. Numbers represent seconds.                  |
| `check`          | `bool`                                            | `False`  | Raise `subprocess.CalledProcessError` for a nonzero exit code.                 |
| `stdout`         | `TextIO \| int \| None`                           | `None`   | Local destination or `subprocess` output sentinel.                             |
| `stderr`         | `TextIO \| int \| None`                           | `None`   | Local destination or `subprocess` output sentinel.                             |
| `capture_output` | `bool`                                            | `False`  | Capture stdout and stderr on the result.                                       |
| `input`          | `str \| bytes \| bytearray \| memoryview \| None` | `None`   | Data written to stdin, which is then closed. Text is encoded as UTF-8.         |
| `on_start`       | `Callable[[Command], None] \| None`               | `None`   | Synchronous callback that receives a `Command` handle when the process starts. |

`CompletedProcess` includes `id`, `name`, `args`, `cwd`, `session_id`, `started_at`, `returncode`, `stdout`, and `stderr`. Its `check_returncode()` method raises `subprocess.CalledProcessError` after the process completes unsuccessfully.

### `create_command()`

Start a process and return before it exits:

```python filename="main.py"
import sys

process = await box.create_command(
    "sh",
    ["-lc", "for i in 1 2 3; do echo $i; sleep 1; done"],
)

assert process.stdout is not None
async for line in process.stdout:
    sys.stdout.write(line)

returncode = await process.wait()
```

`create_command()` replaces `create_process()`, which still works as a deprecated alias and emits a `DeprecationWarning`.

`create_command()` accepts `command`, `args`, `cwd`, `env`, `sudo`, and `kill_after`. It also accepts `stdout` and `stderr` using `subprocess.PIPE`, `subprocess.DEVNULL`, or `subprocess.STDOUT`. Both streams default to `subprocess.PIPE`. Pass `stdin=subprocess.PIPE` to write to the process's standard input. The default, `None`, gives the process an empty stdin, as does `subprocess.DEVNULL`.

This example writes to stdin and reads the output:

```python filename="main.py"
import subprocess

process = await box.create_command("cat", stdin=subprocess.PIPE)

stdout, stderr = await process.communicate("hello from stdin\n")
print(stdout)
```

### Command properties and methods

| Member             | Description                                                                                                                                           |
| ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
| `id`               | Process identifier.                                                                                                                                   |
| `name`             | Executable name.                                                                                                                                      |
| `args`             | Command arguments.                                                                                                                                    |
| `cwd`              | Working directory.                                                                                                                                    |
| `session_id`       | Owning runtime session.                                                                                                                               |
| `started_at`       | Unix start timestamp.                                                                                                                                 |
| `returncode`       | Exit code, or `None` while running.                                                                                                                   |
| `status`           | `CommandStatus.RUNNING` or `CommandStatus.EXITED`.                                                                                                    |
| `stdout`, `stderr` | Single-use `TextReader` streams, or `None` when dropped or merged.                                                                                    |
| `stdin`            | A `CommandStdin` writer when created with `stdin=subprocess.PIPE`, otherwise `None`.                                                                  |
| `refresh()`        | Refresh process state.                                                                                                                                |
| `wait()`           | Wait for exit and return the exit code.                                                                                                               |
| `communicate()`    | Write optional `input` to stdin, read both streams, and wait. Returns `(stdout, stderr)`. Passing `input` without a stdin writer raises `ValueError`. |
| `send_signal()`    | Send an integer, `CommandSignal` member, or name such as `"SIGTERM"`. Passing `signal.Signals` is deprecated.                                         |
| `terminate()`      | Send `SIGTERM`.                                                                                                                                       |
| `kill()`           | Send `SIGKILL`.                                                                                                                                       |

A `TextReader` supports `read()`, `readline()`, async iteration, and `aclose()`. Each stream moves forward and can't rewind.

A `CommandStdin` writer supports `send()` for bytes and `aclose()` to close stdin. Each `send()` returns once the process accepts the bytes. After the process exits or stops reading, `send()` raises `anyio.BrokenResourceError`. If a request fails, it raises `SandboxStdinError`.

Get an existing command or list commands in the current session:

```python filename="main.py"
process = await box.get_command("cmd_123", wait=True)
processes = await box.query_commands()
```

`get_process()` and `query_processes()` are deprecated aliases for `get_command()` and `query_commands()`. A command handle returned by `get_command()` or `query_commands()` always has a `stdin` writer. If the process was started without stdin, the first `send()` raises `SandboxStdinError`.

`communicate()` always closes stdin when you pass `input`, even an empty string. With no `input`, it closes stdin only if the handle owns it: the command was created with `stdin=subprocess.PIPE`, or the handle has written to it. A handle from `get_command()` or `query_commands()` leaves stdin open, so a command that reads until end of file, such as `cat`, keeps running and `communicate()` waits for it. Call `communicate("")` or `await process.stdin.aclose()` to send end of file.

## Manage files

Use `box.fs` to access the current runtime session's filesystem. Relative paths resolve from the sandbox working directory. Filesystem `path` and `cwd` arguments accept `str` or `pathlib.PurePosixPath`.

### Filesystem methods

| Method                                                                 | Description                                                                |
| ---------------------------------------------------------------------- | -------------------------------------------------------------------------- |
| `open(path, mode, ...)`                                                | Open a lazy streaming reader or writer. Supports `r`, `rb`, `w`, and `wb`. |
| `mkdir(path, recursive=True)`                                          | Create a directory.                                                        |
| `read_text(path, encoding="utf-8", errors="strict")`                   | Read a complete text file.                                                 |
| `read_bytes(path)`                                                     | Read a complete binary file.                                               |
| `write_text(path, text, encoding="utf-8", errors="strict", mode=None)` | Write a complete text file.                                                |
| `write_bytes(path, data, mode=None)`                                   | Write a complete binary file.                                              |
| `batch()`                                                              | Stage multiple writes and upload them together on context exit.            |
| `exists(path)`                                                         | Check whether a path exists.                                               |
| `is_file(path)`                                                        | Check whether a path is a regular file.                                    |
| `is_dir(path)`                                                         | Check whether a path is a directory.                                       |
| `listdir(path=".")`                                                    | Return `DirectoryEntry` values.                                            |
| `remove(path, recursive=False, missing_ok=False)`                      | Remove a file or directory.                                                |
| `rename(source, destination)`                                          | Move or rename a path.                                                     |

All methods accept an optional `cwd` keyword argument.

### Read and write complete files

```python filename="main.py"
await box.fs.mkdir("workspace")
await box.fs.write_text("workspace/input.txt", "hello\n")
await box.fs.write_bytes("workspace/data.bin", b"\x00\x01")

text = await box.fs.read_text("workspace/input.txt")
data = await box.fs.read_bytes("workspace/data.bin")
```

### Batch file writes

Upload related files in one operation:

```python filename="main.py"
async with box.fs.batch(cwd="workspace") as batch:
    batch.write_text("main.py", "print('hello')\n")
    batch.write_bytes("data.bin", b"\x00\x01", mode=0o600)
```

`batch.write_text()` and `batch.write_bytes()` stage data locally. The context uploads all staged files when it exits successfully.

### Inspect and change paths

```python filename="main.py"
if await box.fs.exists("workspace/main.py"):
    entries = await box.fs.listdir("workspace")
    for entry in entries:
        print(entry.path, entry.kind)

await box.fs.rename("workspace/main.py", "workspace/app.py")
await box.fs.remove("workspace/data.bin", missing_ok=True)
```

`DirectoryEntry.kind` is `file`, `directory`, `symlink`, or `other`. Its `path` is relative to the listed directory.

### Stream large files

Use `open()` to avoid loading a complete file into memory:

```python filename="main.py"
from pathlib import Path

import anyio

local_path = anyio.Path("archive.tar")

async with (
    await anyio.open_file(local_path, "rb") as source,
    box.fs.open("archive.tar", "wb", permissions=0o600) as target,
):
    while chunk := await source.read(64 * 1024):
        await target.write(chunk)
```

Streaming handles expose `name`, `mode`, and `closed`. They also provide `readable()`, `writable()`, and `seekable()` state methods. Sandbox files are sequential streams, so `seekable()` returns `False`.

Readers support `read()`, `readline()`, async iteration, and `aclose()`. Binary readers also support `readinto()`. Writers support `write()`, `writelines()`, `flush()`, and `aclose()`.

For binary uploads, pass `size` when you know the expected byte count. The SDK raises `SandboxUploadSizeMismatchError` if the stream produces a different number of bytes.

## Work with snapshots

### Create and restore a snapshot

Keep the producer sandbox until restoration completes. A default `create_sandbox()` context stops and destroys its sandbox on exit, and asynchronously deletes snapshots that no other sandbox uses. Snapshot expiration doesn't prevent this explicit deletion.

Await the producer to manage its lifetime yourself. The restored sandbox's context handles its cleanup, and the nested `finally` blocks attempt producer and snapshot cleanup even if setup, restoration, or another cleanup step raises:

```python filename="main.py"
import asyncio
from datetime import timedelta

from vercel import sandbox
from vercel.sandbox import SnapshotSource


async def main() -> None:
    producer = await sandbox.create_sandbox()
    snapshot = None
    try:
        await producer.fs.mkdir("state")
        await producer.fs.write_text("state/message.txt", "saved\n")
        snapshot = await producer.snapshot(expiration=timedelta(days=7))

        async with sandbox.create_sandbox(
            source=SnapshotSource(snapshot_id=snapshot.id),
        ) as restored:
            print(await restored.fs.read_text("state/message.txt"))
    finally:
        try:
            await producer.destroy()
        finally:
            if snapshot is not None:
                await snapshot.delete()


asyncio.run(main())
```

`SnapshotExpiration` accepts seconds or `timedelta`. Use zero for no expiration. Nonzero expiration must be between one day and 10 years.

### Snapshot properties

| Property                   | Type                                 | Description                                 |
| -------------------------- | ------------------------------------ | ------------------------------------------- |
| `id`                       | `str`                                | Snapshot identifier.                        |
| `source_session_id`        | `str`                                | Runtime session that produced the snapshot. |
| `region`                   | `str`                                | Storage region.                             |
| `status`                   | `"created" \| "deleted" \| "failed"` | Snapshot state.                             |
| `size_bytes`               | `int`                                | Stored size.                                |
| `expires_at`               | `int \| None`                        | Unix expiration timestamp.                  |
| `created_at`, `updated_at` | `int`                                | Unix timestamps.                            |
| `last_used_at`             | `int \| None`                        | Latest restore timestamp.                   |
| `creation_method`          | `str \| None`                        | How the snapshot was created.               |
| `parent_id`                | `str \| None`                        | Parent snapshot when present.               |

Call `await snapshot.delete()` to delete a snapshot.

### Get and query snapshots

```python filename="main.py"
snapshot = await sandbox.get_snapshot(snapshot_id="snap_123")

async for item in sandbox.query_snapshots(
    name="my-development-environment",
    page_size=50,
    sort_order="desc",
):
    print(item.id, item.status)
```

`query_snapshots()` accepts optional `name`, `page_size`, `cursor`, and `sort_order`. It uses the project from the SDK's resolved credentials.

### Query runtime sessions

Inspect session history across sandboxes:

```python filename="main.py"
async for runtime_session in sandbox.query_sessions(
    name="my-development-environment",
    sort_order="desc",
):
    print(runtime_session.id, runtime_session.status)
```

A `SandboxRuntimeSession` exposes `id`, `sandbox_name`, `project_id`, `status`, `cwd`, `region`, `memory`, `vcpus`, `execution_time_limit`, `network_policy`, `network_id`, `requested_at`, `started_at`, and `stopped_at`.

A runtime session also provides `fs`, process methods, `refresh()`, `extend_execution_time_limit()`, `update_network_policy()`, `snapshot()`, and `stop()`. These methods operate on that session and don't trigger automatic resume after it stops.

`SnapshotRetentionState` describes the active retention policy through `count`, optional `expiration`, and `delete_evicted` properties.

## Configure sandbox sources

### `GitSource`

Clone a Git repository when the sandbox starts:

```python filename="main.py"
from vercel.sandbox import GitSource

box = await sandbox.create_sandbox(
    source=GitSource(
        url="https://github.com/vercel/sandbox-example-next.git",
        revision="main",
        depth=1,
    )
)
```

`GitSource` accepts `url`, optional `depth`, optional `revision`, and optional `username` and `password` for HTTP basic authentication.

### `TarballSource`

Initialize from a remotely accessible tarball:

```python filename="main.py"
from vercel.sandbox import TarballSource

box = await sandbox.create_sandbox(
    source=TarballSource(url="https://example.com/source.tar.gz")
)
```

### `SnapshotSource`

Restore a filesystem snapshot:

```python filename="main.py"
from vercel.sandbox import SnapshotSource

box = await sandbox.create_sandbox(
    source=SnapshotSource(snapshot_id="snap_123")
)
```

## Configure resources and retention

`SandboxResources` accepts optional `vcpus` and `memory` fields:

```python filename="main.py"
from vercel.sandbox import SandboxResources

resources = SandboxResources(vcpus=2, memory=4096)
```

`SnapshotRetention` controls automatic snapshot retention:

```python filename="main.py"
from datetime import timedelta

from vercel.sandbox import SnapshotRetention

retention = SnapshotRetention(
    count=3,
    expiration=timedelta(days=7),
    delete_evicted=True,
)
```

`count` must be between one and 10. `delete_evicted` controls whether Vercel deletes snapshots removed from the retention window.

## Configure network access

### Allow or deny all traffic

```python filename="main.py"
from vercel.sandbox import NetworkPolicy

allowed = await sandbox.create_sandbox(
    network_policy=NetworkPolicy.allow_all()
)

denied = await sandbox.create_sandbox(
    network_policy=NetworkPolicy.deny_all()
)
```

### Allow selected domains

```python filename="main.py"
from vercel.sandbox import NetworkPolicy

policy = NetworkPolicy.custom(
    allow={
        "api.github.com": (),
        "pypi.org": (),
    }
)

box = await sandbox.create_sandbox(network_policy=policy)
```

### Broker credentials

Add a secret header to matching outbound requests without exposing the secret to the sandbox process:

```python filename="main.py"
import os

from vercel.sandbox import (
    NetworkPolicy,
    NetworkPolicyRule,
    NetworkPolicyTransform,
)

policy = NetworkPolicy.custom(
    allow={
        "api.github.com": [
            NetworkPolicyRule(
                transform=[
                    NetworkPolicyTransform(
                        headers={
                            "Authorization": f"Bearer {os.environ['GITHUB_TOKEN']}"
                        }
                    )
                ]
            )
        ]
    }
)
```

### Match requests

Select requests for a rule by path, method, query string, or headers. A matcher selects requests for an action; the matcher alone does not allow or deny traffic:

```python filename="main.py"
from vercel.sandbox import (
    NetworkPolicyKeyValueMatcher,
    NetworkPolicyMatcher,
    NetworkPolicyRequestMatcher,
    NetworkPolicyRule,
    NetworkPolicyTransform,
)

rule = NetworkPolicyRule(
    match=NetworkPolicyRequestMatcher(
        path=NetworkPolicyMatcher.starts_with("/v1/"),
        method=["POST"],
        query=[
            NetworkPolicyKeyValueMatcher(
                key=NetworkPolicyMatcher.exact("stream"),
                value=NetworkPolicyMatcher.regex("^(true|false)$"),
            )
        ],
    ),
    transform=[
        NetworkPolicyTransform(headers={"x-api-key": "your_api_key_here"})
    ],
)
```

`NetworkPolicyMatcher` provides `exact()`, `starts_with()`, and `regex()` constructors. All specified dimensions must match; any listed method can match. `NetworkPolicyRule` requires exactly one action: a nonempty `transform`, `forward_url`, or `response`. `NetworkPolicyTransform` accepts `headers`, a mapping of header names to values. `NetworkPolicySubnets` accepts optional `allow` and `deny` CIDR lists.

### Return a direct response

Use `NetworkPolicyResponse` to answer from the network proxy without contacting the destination. Rules run in declaration order, and the first matching rule handles the request. A rule without `match` matches all requests to the domain. If no rule matches, the request reaches the allowed domain unchanged.

This policy injects a credential for `POST` requests to `/v1/` paths and returns `403` for all other requests to the domain:

```python filename="policy.py"
from vercel.sandbox import (
    NetworkPolicy,
    NetworkPolicyMatcher,
    NetworkPolicyRequestMatcher,
    NetworkPolicyResponse,
    NetworkPolicyRule,
    NetworkPolicyTransform,
)

policy = NetworkPolicy.custom(
    allow={
        "api.example.com": [
            NetworkPolicyRule(
                match=NetworkPolicyRequestMatcher(
                    path=NetworkPolicyMatcher.starts_with("/v1/"),
                    method=["POST"],
                ),
                transform=[
                    NetworkPolicyTransform(
                        headers={"x-api-key": "your_api_key_here"}
                    )
                ],
            ),
            NetworkPolicyRule(response=NetworkPolicyResponse(status_code=403)),
        ]
    }
)
```

| `NetworkPolicyResponse` argument | Type | Required | Description |
| --- | --- | --- | --- |
| `status_code` | `int` | Yes | Integer from 200 through 599. |
| `headers` | `Mapping[str, str] \| None` | No | Response headers. Names must be unique, ignoring case. |
| `body` | `str \| None` | No | Response body. Requires `content_type`; omit for status codes 204, 205, and 304. |
| `content_type` | `str \| None` | When `body` is set | Nonempty content type of the response body, required even for an empty string body. |

The proxy manages `connection`, `content-length`, `keep-alive`, `proxy-connection`, `trailer`, `transfer-encoding`, and `upgrade`. The SDK or API rejects these response headers, regardless of capitalization. Python validates `connection`, `content-length`, and `transfer-encoding` locally with `ValueError`; the API rejects the other protected headers with HTTP 400.

### Inspect policy metadata

`box.network_policy` returns cached saved-policy metadata, and `runtime_session.network_policy` returns cached metadata for that session. Both properties return `NetworkPolicyReadback | None`. Reading these properties doesn't fetch fresh state.

Readback omits secret header values, matcher values, response headers and bodies, and the original ordering across action types. You cannot submit readback as a `NetworkPolicy`. Retain the authored policy separately for updates.

| Exported type | Fields |
| --- | --- |
| `NetworkPolicyReadback` | `mode`, `allowed_domains`, `subnets`, `injection_rules`, `forward_rules`, and `response_rules`. |
| `NetworkPolicyInjectionRuleSummary` | `domain`, `header_names`, and `match_dimensions`. Header names contain no secret values. |
| `NetworkPolicyForwardRuleSummary` | `domain`, `forward_url`, and `match_dimensions`. |
| `NetworkPolicyResponseRuleSummary` | `domain`, `status_code`, and `match_dimensions`. |

The domain and rule collections are tuples. `subnets` is `NetworkPolicySubnets | None`. `match_dimensions` contains dimension names (`"path"`, `"method"`, `"query"`, or `"headers"`), rather than authored matchers.

### Update a running session

Use `box.update(network_policy=...)` to replace the current session's policy, when eligible, and save the policy for future sessions:

```python filename="main.py"
from vercel.sandbox import NetworkPolicy

await box.update(network_policy=NetworkPolicy.deny_all())
```

The method never resumes a stopped sandbox. It uses the cached current session, skipping the live update if no session is attached or its cached status is `STOPPED`, `STOPPING`, `ABORTED`, or `FAILED`. Otherwise, it applies the live policy before saving configuration and tags. Live-policy errors propagate and prevent the SDK's later sandbox configuration-and-tags request. A later save failure can leave the live policy changed, so the operation is not atomic.

Each policy replaces the previous policy in full. Use `NetworkPolicy.allow_all()` to remove restrictions. Omit `network_policy` or pass `None` to leave it unchanged. The method returns the same `Sandbox` handle; the synchronous equivalent returns the same `SyncSandbox` handle without `await`.

To update and run work on one specific session, acquire it with `runtime_session = await box.session()`, call `await runtime_session.update_network_policy(policy)`, then run the workload through that same handle. Acquiring a session can resume the sandbox; subsequent operations on the acquired handle stay on that session and propagate failures instead of resuming another one. The SDK targets that session directly. When it is still the named sandbox's current session, the API also persists the policy for future sessions; updates to older sessions do not replace the current saved policy.

The sandbox-level `box.update_network_policy()` is deprecated and emits `DeprecationWarning`. It targets the current session endpoint, can auto-resume the sandbox, and returns a `SandboxRuntimeSession`. Its synchronous equivalent returns `SyncSandboxRuntimeSession`. The runtime-session method remains available for pinned updates. Both methods call the session endpoint, which also persists the policy when the target remains the named sandbox's current session. The parent handle's cached saved-policy metadata can remain stale.

## Synchronous API

The synchronous API mirrors the asynchronous API. Import it explicitly from `vercel.sandbox`:

```python filename="main.py"
from vercel.sandbox import sync as sandbox

with sandbox.create_sandbox() as box:
    result = box.run_command(
        "python",
        ["-c", "print('Hello from Vercel Sandbox!')"],
        capture_output=True,
        check=True,
    )
    print(result.stdout)
```

Remove `await` and use regular context managers and iterators. Sync handle names include `SyncSandbox`, `SyncSandboxRuntimeSession`, `SyncProcess`, `SyncDrive`, `SyncSnapshot`, and sync filesystem reader and writer types.

Use `with box.session() as runtime_session:` to clean up the acquired session on exit, or `runtime_session = box.session()` to manage cleanup yourself.

Use the asynchronous API unless your application is synchronous. Don't call the synchronous API from an active async event loop.

## Exported protocol and union types

These exported types support precise annotations:

| Type                                       | Purpose                                                                        |
| ------------------------------------------ | ------------------------------------------------------------------------------ |
| `CreateSandboxOperation`                   | Single-use awaitable and async context manager returned by `create_sandbox()`. |
| `ForkSandboxOperation`                     | Single-use awaitable and async context manager returned by `fork_sandbox()`.   |
| `ResumeSandboxOperation`                   | Single-use awaitable and async context manager returned by `resume_sandbox()`. |
| `SandboxSessionOperation`                  | Single-use awaitable and async context manager returned by `box.session()`.    |
| `SandboxSource`                            | Union of `GitSource`, `TarballSource`, and `SnapshotSource`.                   |
| `SandboxQuery`                             | Union of the four sandbox query models.                                        |
| `DriveQuery`                               | Union of the three drive query models.                                         |
| `DriveReference`                           | Drive name or handle accepted by `DriveMount`.                                 |
| `SandboxFilesystemBatch`                   | Async batch returned by `box.fs.batch()`.                                      |
| `SandboxTextReader`, `SandboxBinaryReader` | Async streaming file readers returned by `box.fs.open()`.                      |
| `SandboxTextWriter`, `SandboxBinaryWriter` | Async streaming file writers returned by `box.fs.open()`.                      |

## Errors

Catch `SandboxError` to handle any Sandbox SDK error. Catch a more specific type when your application can recover differently:

| Error                            | Meaning                                                                       |
| -------------------------------- | ----------------------------------------------------------------------------- |
| `SandboxApiError`                | The Sandbox API returned an error. Inspect `status_code`, `code`, and `data`. |
| `SandboxCredentialsError`        | The SDK couldn't resolve valid credentials.                                   |
| `SandboxTerminalStateError`      | Sandbox creation reached a terminal failure state.                            |
| `SandboxTimeoutError`            | A session stayed in a lifecycle transition beyond the SDK deadline.           |
| `SandboxCleanupError`            | Context-managed cleanup failed.                                               |
| `SandboxResponseError`           | A successful API response was malformed.                                      |
| `SandboxStreamError`             | A process log stream reported an error.                                       |
| `SandboxStdinError`              | Writing to or closing a process's stdin failed.                               |
| `SandboxInvalidHandleError`      | A handle isn't attached to a valid session or execution mode.                 |
| `SandboxFilesystemError`         | Base error for filesystem operations.                                         |
| `SandboxFilesystemCommandError`  | A command-backed filesystem operation failed.                                 |
| `SandboxFilesystemWriteError`    | The API rejected a batch write.                                               |
| `SandboxPathNotFoundError`       | A required remote path doesn't exist.                                         |
| `SandboxFilesystemTransferError` | Base error for streaming transfers.                                           |
| `SandboxUploadSizeMismatchError` | An upload didn't match its declared size.                                     |

`run_command(check=True)` raises Python's `subprocess.CalledProcessError` for nonzero exit codes.

```python filename="main.py"
from subprocess import CalledProcessError

from vercel.sandbox import SandboxApiError

try:
    await box.run_command("python", ["-m", "pytest"], check=True)
except CalledProcessError as error:
    print(error.returncode, error.stderr)
except SandboxApiError as error:
    print(error.status_code, error.code)
```

## Authentication

By default, the SDK resolves credentials in this order:

1. A Vercel OpenID Connect (OIDC) token from the current Vercel request or `VERCEL_OIDC_TOKEN`.
2. `VERCEL_TOKEN`, `VERCEL_PROJECT_ID`, and `VERCEL_TEAM_ID`.

For local development, run `vercel link` and `vercel env pull`, then load `.env.local` into your Python process with a tool such as `python-dotenv`. For other environments, set all three access-token variables. See [Sandbox Authentication](/docs/sandbox/concepts/authentication).

## CLI-only features

The Python SDK doesn't currently expose interactive PTY shells. Use `sandbox connect` for an interactive shell.

Installing `vercel` also installs the `sandbox` and `vercel-sandbox` console commands. These commands delegate to the JavaScript Sandbox CLI through `npx`, so they require Node.js. The Python API itself doesn't require Node.js.

## SDK sessions

An SDK session owns the HTTP connection pool and service clients used by Vercel Python SDK calls. The SDK creates and reuses a default session, so most applications should call the SDK directly.

Creating a session without configuration is redundant because it behaves like the default session. Create an explicit session only when you need to customize its HTTP client or a service's configuration.

### `session()`

Import `session` from `vercel.api` and use it as an async or sync context manager:

```python
def session(
    *,
    service_options: Sequence[ServiceOptions] | None = None,
    httpx_client_factory: HttpxClientFactory | None = ...,
) -> SessionContext: ...
```

| Parameter              | Description                                                                                                                                                     |
| ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `service_options`      | Service-specific configuration. For Sandbox, pass `SandboxServiceOptions` from the async or sync Sandbox module.                                                |
| `httpx_client_factory` | Factory for a custom HTTP client. Return `httpx.AsyncClient` for async calls or `httpx.Client` for sync calls. Omit it to inherit the active session's factory. |

The session closes the HTTP client returned by `httpx_client_factory` when its context exits. Nested sessions inherit their parent's configuration. A nested service option replaces the option for that service. Don't use sync Sandbox calls inside an async session or async Sandbox calls inside a sync session.

### Configure the HTTP client

Pass an HTTPX client factory to configure the client used by Vercel services:

```python filename="main.py"
import asyncio
import os

import httpx

from vercel import sandbox
from vercel.api import session


def create_http_client() -> httpx.AsyncClient:
    # Services such as Sandbox use this underlying HTTPX client.
    return httpx.AsyncClient(proxy=os.environ["CORPORATE_PROXY_URL"])


async def main() -> None:
    async with session(httpx_client_factory=create_http_client):
        async with sandbox.create_sandbox() as box:
            await box.run_command("python", ["--version"], check=True)


asyncio.run(main())
```

The SDK closes the client when the session exits.

### Configure Sandbox

Pass `SandboxServiceOptions` to configure Sandbox calls in the session. This example reads credentials from custom environment variables and increases the timeout for streaming file transfers:

```python filename="main.py"
import asyncio
import os
from datetime import timedelta

from vercel import sandbox
from vercel.api import session
from vercel.sandbox import SandboxCredentials, SandboxServiceOptions


async def resolve_credentials() -> SandboxCredentials:
    return SandboxCredentials(
        token=os.environ["ACME_VERCEL_TOKEN"],
        project_id=os.environ["ACME_VERCEL_PROJECT_ID"],
        team_id=os.environ["ACME_VERCEL_TEAM_ID"],
    )


async def main() -> None:
    sandbox_options = SandboxServiceOptions(
        credentials_factory=resolve_credentials,
        file_transfer_timeout=timedelta(minutes=10),
    )

    async with session(service_options=[sandbox_options]):
        async with sandbox.create_sandbox() as box:
            print(box.name)


asyncio.run(main())
```

`SandboxServiceOptions` accepts these parameters:

| Parameter               | Default                               | Description                                                                                                      |
| ----------------------- | ------------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
| `base_url`              | `https://vercel.com/api`              | Sandbox API base URL. Override it only for testing or Vercel-managed development environments.                   |
| `credentials_factory`   | Default OIDC and environment resolver | Callable that returns `SandboxCredentials`. Use an async callable for async Sandbox and a sync callable for sync. |
| `file_transfer_timeout` | 5 minutes                             | Timeout for streaming file uploads and downloads.                                                               |
| `region`                | `VERCEL_REGION` or `None`             | Default region for Sandbox operations, including Drive creation.                                                |


---

[View full sitemap](/docs/sitemap)


If you encounter incorrect information, can’t find what you need, need a capability Vercel doesn’t currently offer, or have any other feedback to report, please do it by following the instructions at https://vercel.com/feedback.md.
