Skip to content
Docs

Python SDK Reference

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

For JavaScript and TypeScript, see the JS SDK Reference.

Install the Vercel Python package:

Terminal
uv add vercel

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

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 when you need a custom HTTP client, credentials, or service configuration.

Create and start a sandbox:

main.py
import asyncio
 
from vercel import sandbox
 
 
async def main() -> None:
    async with sandbox.create_sandbox() as box:
        result = await box.run_process(
            "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.
  • 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.

Don't await or enter the same creation operation more than once.

The function accepts these keyword arguments:

ParameterTypeDescription
project_idstr | NoneProject that owns the sandbox. The SDK uses your resolved credentials when omitted.
namestr | NoneStable sandbox name. Vercel generates one when omitted.
imagestr | NoneVercel Container Registry image reference.
sourceGitSource | TarballSource | SnapshotSource | NoneInitial filesystem source.
portslist[int] | NonePorts to expose through the sandbox routes.
execution_time_limitint | float | timedelta | NoneMaximum runtime for the current session. Numbers represent seconds.
resourcesSandboxResources | NoneRequested virtual CPUs and memory.
persistentbool | NoneWhether Vercel snapshots the filesystem when the session stops.
network_policyNetworkPolicy | NoneOutbound network policy.
envMapping[str, str] | NoneEnvironment variables available to processes in the sandbox.
tagsMapping[str, str] | NoneMetadata used to organize and query sandboxes.
snapshot_expirationint | float | timedelta | SnapshotExpiration | NoneDefault snapshot lifetime. Numbers represent seconds. Zero disables expiration.
snapshot_retentionSnapshotRetention | NoneAutomatic snapshot retention policy.
destroyboolWhether 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.

Create a persistent sandbox without automatic cleanup:

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_process("uv", ["sync"], check=True)
    finally:
        await box.stop()
 
 
asyncio.run(main())

Create a sandbox from an existing named sandbox:

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_process(
        "python",
        ["script.py"],
        capture_output=True,
        check=True,
    )
    print(result.stdout)

The fork inherits the source sandbox's current snapshot and configuration. Values passed to fork_sandbox() replace the corresponding inherited values. 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.

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

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

get_sandbox() accepts name, optional project_id, and optional include_system_routes. The lookup is passive and doesn't start a new runtime session.

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:

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_process(), create_process(), process 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.

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

main.py
box, created = await sandbox.get_or_create_sandbox(
    name="my-development-environment",
    persistent=True,
)
 
if created:
    await box.run_process("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.

Start a new runtime session from a stopped persistent sandbox:

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() returns an async iterator and follows pagination cursors automatically:

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:

TypeFields
SandboxQueryByCreatedAtsort_order, optional tag
SandboxQueryByNamesort_order, optional name_prefix, optional tag
SandboxQueryByStatusUpdatedAtsort_order
SandboxQueryByCurrentSnapshotIdsort_order
TagFilterExact key and value match

All query functions accept page_size and cursor. query_sandboxes() also accepts project_id.

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.

PropertyTypeDescription
namestrStable sandbox identity.
current_session_idstrIdentifier for the current runtime session.
current_sessionSandboxRuntimeSession | NoneCurrent session handle when the API response includes it.
imagestr | NoneResolved image reference.
statusSandboxStatus | NoneCurrent lifecycle state.
persistentbool | NoneWhether stop creates an automatic snapshot.
current_snapshot_idstr | NoneSnapshot used for the next resume.
project_idstr | NoneOwning project.
cwdstr | NoneDefault working directory.
regionstr | NoneRuntime region.
memoryint | NoneMemory in megabytes.
vcpusint | NoneNumber of virtual CPUs.
execution_time_limittimedelta | NoneCurrent session execution limit.
network_policyNetworkPolicy | NoneCurrent outbound network policy.
snapshot_expirationtimedelta | NoneDefault snapshot expiration.
snapshot_retentionSnapshotRetentionState | NoneActive retention policy.
status_updated_atint | NoneUnix timestamp for the latest status update.
created_atint | NoneUnix creation timestamp.
updated_atint | NoneUnix update timestamp.
tagsdict[str, str] | NoneCopy of the sandbox tags.
routestuple[SandboxRouteState, ...]Exposed routes. Each route has url, port, subdomain, and system.
rawdict | NoneCopy of the raw API response data.
fsSandboxFilesystemFilesystem for the current session.

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

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

main.py
box = await sandbox.get_sandbox(name="my-development-environment")
 
async with box.session() as runtime_session:
    result = await runtime_session.run_process(
        "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.

Find the URL for an exposed port:

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 mutable sandbox configuration:

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, env, tags, snapshot_expiration, snapshot_retention, and current_snapshot_id. Only non-None values are sent. Passing snapshot_retention=None explicitly removes the retention policy. ports replaces the complete exposed port list. Set current_snapshot_id to choose the snapshot restored on the next resume. The method refreshes and returns the same Sandbox handle.

Increase the current session's execution limit:

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 the current runtime session without deleting a persistent sandbox:

main.py
await box.stop()

Delete the sandbox, its sessions, and its snapshots permanently:

main.py
await box.destroy()

Fetch one page of resources that belong to a sandbox:

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.

Run a process and wait for it to exit:

main.py
result = await box.run_process(
    "python",
    ["-m", "pytest"],
    cwd="/vercel/sandbox",
    env={"PYTHONUNBUFFERED": "1"},
    kill_after=120,
    capture_output=True,
    check=True,
)
 
print(result.returncode)
print(result.stdout)
print(result.stderr)

By default, run_process() 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.

ParameterTypeDefaultDescription
commandstrRequiredExecutable or command name.
argsSequence[str] | NoneNoneArguments excluding the executable.
cwdstr | NoneNoneWorking directory.
envMapping[str, str] | NoneNoneEnvironment variables added to the process.
sudoboolFalseRun with elevated privileges.
kill_afterfloat | timedelta | NoneNoneServer-side time before SIGKILL. Numbers represent seconds.
checkboolFalseRaise subprocess.CalledProcessError for a nonzero exit code.
stdoutTextIO | int | NoneNoneLocal destination or subprocess output sentinel.
stderrTextIO | int | NoneNoneLocal destination or subprocess output sentinel.
capture_outputboolFalseCapture stdout and stderr on the result.

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.

Start a process and return before it exits:

main.py
import sys
 
process = await box.create_process(
    "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_process() 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.

MemberDescription
idProcess identifier.
nameExecutable name.
argsCommand arguments.
cwdWorking directory.
session_idOwning runtime session.
started_atUnix start timestamp.
returncodeExit code, or None while running.
statusProcessStatus.RUNNING or ProcessStatus.EXITED.
stdout, stderrSingle-use TextReader streams, or None when dropped or merged.
stdinAlways None. Process standard input isn't supported.
refresh()Refresh process state.
wait()Wait for exit and return the exit code.
communicate()Read both streams and wait. Returns (stdout, stderr). Process stdin isn't supported.
send_signal()Send an integer, signal.Signals value, or name such as "SIGTERM".
terminate()Send SIGTERM.
kill()Send SIGKILL.

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

Get an existing process or list processes in the current session:

main.py
process = await box.get_process("cmd_123", wait=True)
processes = await box.query_processes()

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.

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

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")

Upload related files in one operation:

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.

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.

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

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.

main.py
import asyncio
from datetime import timedelta
 
from vercel import sandbox
from vercel.sandbox import SnapshotSource
 
 
async def main() -> None:
    snapshot = None
    restored = None
 
    async with sandbox.create_sandbox() as box:
        await box.fs.mkdir("state")
        await box.fs.write_text("state/message.txt", "saved\n")
        snapshot = await box.snapshot(expiration=timedelta(days=7))
 
    try:
        restored = await sandbox.create_sandbox(
            source=SnapshotSource(snapshot_id=snapshot.id),
        )
        print(await restored.fs.read_text("state/message.txt"))
    finally:
        if restored is not None:
            await restored.destroy()
        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.

PropertyTypeDescription
idstrSnapshot identifier.
source_session_idstrRuntime session that produced the snapshot.
regionstrStorage region.
status"created" | "deleted" | "failed"Snapshot state.
size_bytesintStored size.
expires_atint | NoneUnix expiration timestamp.
created_at, updated_atintUnix timestamps.
last_used_atint | NoneLatest restore timestamp.
creation_methodstr | NoneHow the snapshot was created.
parent_idstr | NoneParent snapshot when present.

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

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 project_id, name, page_size, cursor, and sort_order.

Inspect session history across sandboxes:

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

Clone a Git repository when the sandbox starts:

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.

Initialize from a remotely accessible tarball:

main.py
from vercel.sandbox import TarballSource
 
box = await sandbox.create_sandbox(
    source=TarballSource(url="https://example.com/source.tar.gz")
)

Restore a filesystem snapshot:

main.py
from vercel.sandbox import SnapshotSource
 
box = await sandbox.create_sandbox(
    source=SnapshotSource(snapshot_id="snap_123")
)

SandboxResources accepts optional vcpus and memory fields:

main.py
from vercel.sandbox import SandboxResources
 
resources = SandboxResources(vcpus=2, memory=4096)

SnapshotRetention controls automatic snapshot retention:

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 100. delete_evicted controls whether Vercel deletes snapshots removed from the retention window.

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()
)
main.py
from vercel.sandbox import NetworkPolicy
 
policy = NetworkPolicy.custom(
    allow={
        "api.github.com": (),
        "pypi.org": (),
    }
)
 
box = await sandbox.create_sandbox(network_policy=policy)

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

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']}"
                        }
                    )
                ]
            )
        ]
    }
)

Restrict a rule by path, method, query string, or headers:

main.py
from vercel.sandbox import (
    NetworkPolicyKeyValueMatcher,
    NetworkPolicyMatcher,
    NetworkPolicyRequestMatcher,
    NetworkPolicyRule,
)
 
rule = NetworkPolicyRule(
    match=NetworkPolicyRequestMatcher(
        path=NetworkPolicyMatcher.starts_with("/v1/"),
        method=["POST"],
        query=[
            NetworkPolicyKeyValueMatcher(
                key=NetworkPolicyMatcher.exact("stream"),
                value=NetworkPolicyMatcher.regex("^(true|false)$"),
            )
        ],
    )
)

NetworkPolicyMatcher provides exact(), starts_with(), and regex() constructors. NetworkPolicyRule also accepts forward_url. NetworkPolicyTransform can set headers and declare header_names. NetworkPolicySubnets accepts optional allow and deny CIDR lists.

Update a running session's policy:

main.py
await box.update_network_policy(NetworkPolicy.deny_all())

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

main.py
from vercel.sandbox import sync as sandbox
 
with sandbox.create_sandbox() as box:
    result = box.run_process(
        "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, 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.

These exported types support precise annotations:

TypePurpose
CreateSandboxOperationSingle-use awaitable and async context manager returned by create_sandbox().
ForkSandboxOperationSingle-use awaitable and async context manager returned by fork_sandbox().
ResumeSandboxOperationSingle-use awaitable and async context manager returned by resume_sandbox().
SandboxSessionOperationSingle-use awaitable and async context manager returned by box.session().
SandboxSourceUnion of GitSource, TarballSource, and SnapshotSource.
SandboxQueryUnion of the four sandbox query models.
SandboxFilesystemBatchAsync batch returned by box.fs.batch().
SandboxTextReader, SandboxBinaryReaderAsync streaming file readers returned by box.fs.open().
SandboxTextWriter, SandboxBinaryWriterAsync streaming file writers returned by box.fs.open().

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

ErrorMeaning
SandboxApiErrorThe Sandbox API returned an error. Inspect status_code, code, and data.
SandboxCredentialsErrorThe SDK couldn't resolve valid credentials.
SandboxTerminalStateErrorSandbox creation reached a terminal failure state.
SandboxTimeoutErrorA session stayed in a lifecycle transition beyond the SDK deadline.
SandboxCleanupErrorContext-managed cleanup failed.
SandboxResponseErrorA successful API response was malformed.
SandboxStreamErrorA process log stream reported an error.
SandboxInvalidHandleErrorA handle isn't attached to a valid session or execution mode.
SandboxFilesystemErrorBase error for filesystem operations.
SandboxFilesystemCommandErrorA command-backed filesystem operation failed.
SandboxFilesystemWriteErrorThe API rejected a batch write.
SandboxPathNotFoundErrorA required remote path doesn't exist.
SandboxFilesystemTransferErrorBase error for streaming transfers.
SandboxUploadSizeMismatchErrorAn upload didn't match its declared size.

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

main.py
from subprocess import CalledProcessError
 
from vercel.sandbox import SandboxApiError
 
try:
    await box.run_process("python", ["-m", "pytest"], check=True)
except CalledProcessError as error:
    print(error.returncode, error.stderr)
except SandboxApiError as error:
    print(error.status_code, error.code)

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.

The Python SDK doesn't currently expose interactive PTY shells or the Drives API. Use sandbox connect for an interactive shell. Use the Sandbox CLI or JavaScript SDK for Drive operations.

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.

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.

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

def session(
    *,
    service_options: Sequence[ServiceOptions] | None = None,
    httpx_client_factory: HttpxClientFactory | None = ...,
) -> SessionContext: ...
ParameterDescription
service_optionsService-specific configuration. For Sandbox, pass SandboxServiceOptions from the async or sync Sandbox module.
httpx_client_factoryFactory 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.

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

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_process("python", ["--version"], check=True)
 
 
asyncio.run(main())

The SDK closes the client when the session exits.

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:

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:

ParameterDefaultDescription
base_urlhttps://vercel.com/apiSandbox API base URL. Override it only for testing or Vercel-managed development environments.
credentials_factoryDefault OIDC and environment resolverCallable that returns SandboxCredentials. Use an async callable for async Sandbox and a sync callable for sync.
file_transfer_timeout5 minutesTimeout for streaming file uploads and downloads.
Last updated August 25, 2026

Was this helpful?

supported.