> ## Documentation Index
> Fetch the complete documentation index at: https://boxrouter.ai/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Use the Python SDK

> Install the BoxRouter Python SDK, configure it, choose sync or async, and handle its errors.

The `boxrouter` package gives Python a synchronous and an asynchronous client for the BoxRouter API.
It needs Python 3.11 or later. `python -m pip show boxrouter` prints the version you have.

## Install

```bash theme={"theme":"css-variables"}
python -m pip install boxrouter
```

Your first sandbox is in the [quickstart](https://boxrouter.ai/docs/quickstart.md#run-a-command-and-clean-up).

## Configure the Client

For each setting, the client uses the argument first, then the environment variable, then the default.

| Argument | Environment Variable | Default |
| - | - | - |
| `api_key` | `BOXROUTER_API_KEY` | None. The client raises `ValueError` without one. |
| `base_url` | `BOXROUTER_BASE_URL` | `https://api.boxrouter.ai/api/v1` |
| `timeout` | None | 360 s for reads and writes; 10 s for connect and pool waits |
| `transport` | None | HTTPX's default |
| `max_retries` | None | 2. The client resends a call whose error says `retry: safe`; `0` turns it off. |

`timeout` takes an `httpx.Timeout`. Its limits apply to individual network waits, not the whole request; see [HTTPX timeouts](https://www.python-httpx.org/advanced/timeouts/). Creating a sandbox that sets up an image can wait minutes, so leave the default read timeout or raise it. [Command options](https://boxrouter.ai/docs/commands.md#options) set a command's own limit.

The client resends an API call only when the error says `retry: safe`, up to `max_retries` times, with the same idempotency key. [Recover from Errors](https://boxrouter.ai/docs/errors.md#let-the-client-retry-for-you) has the rules.

## Choose Sync or Async

`BoxRouter` blocks. `AsyncBoxRouter` has the same methods, with three calling patterns:

* Await coroutine methods such as `create`, `exec` and `destroy`.
* Enter the async context managers returned by `terminal` and `launch` with `async with`.
* Use `async for` for an async iterator such as `volumes.list_files`.

```python theme={"theme":"css-variables"}
import asyncio
from boxrouter import AsyncBoxRouter

async def main() -> None:
    async with AsyncBoxRouter() as client:
        async with await client.sandboxes.create() as box:
            print((await box.exec("echo hello")).stdout, end="")

asyncio.run(main())
```

Output

```text theme={"theme":"css-variables"}
hello
```

Leaving `async with box` requests the sandbox's destruction, even when your code raised. Leaving
`async with client` closes HTTP connections and leaves every sandbox as it was.

## Handle Errors

| Failure | Raised As | What It Carries |
| - | - | - |
| BoxRouter refused or could not finish the call | `APIError` | `status`, `code`, `outcome`, `retry`, `retry_after_seconds`, `fix`, `details`, `operation_id`, `sandbox_id`, `request_id`, `idempotency_key`, `response` |
| A volume file transfer fails | An `httpx.HTTPError` | Error details. |
| No API key | `ValueError` | The variable to set |

Branch on `error.retry`, not on `error.status`: [the status never decides a retry](https://boxrouter.ai/docs/errors.md#read-the-retry-field).

A command that exits nonzero or reaches its timeout is a result: read `exit_code` and `timed_out`, because nothing is
raised. Pass `check=True` to raise `CommandFailed` for either. Save this as `errors.py`. It runs a command in a sandbox
that does not exist and prints the error's `code` and `outcome`:

```python theme={"theme":"css-variables"}
from boxrouter import APIError, BoxRouter

with BoxRouter() as client:
    try:
        client.sandboxes.exec("sb_missing", "echo hello")
    except APIError as error:
        print(error.code, error.outcome)
        print(error.fix)
```

```bash theme={"theme":"css-variables"}
python errors.py
```

Output

```text theme={"theme":"css-variables"}
not_found not_started
Check the ID or path: it may be misspelled, removed, or in another workspace.
```

A `retry` of `check_outcome` means BoxRouter cannot tell whether the call ran. Read the operation before resending
([Recover from Errors](https://boxrouter.ai/docs/errors.md#check-whether-your-work-started) has the steps). Quote `request_id` to
[support](https://boxrouter.ai/support).

When the workload and the sandbox's cleanup both fail, the SDK raises a `BaseExceptionGroup` holding both;
catch it with `except*`. If only the cleanup fails, you get its error alone. Either way it carries the
sandbox ID as a note. A failed cleanup does not prove the sandbox stopped: keep its ID and look it up
under **Sandboxes** before you retry.

## Where Each Task Lives

* [Files](https://boxrouter.ai/docs/files.md): `read` and `write` sandbox files.
* [Volumes](https://boxrouter.ai/docs/volumes.md): upload and download files outside a sandbox.
* [Commands](https://boxrouter.ai/docs/commands.md) and [background processes](https://boxrouter.ai/docs/processes.md).
* [Pause and resume](https://boxrouter.ai/docs/pause-resume.md).
* [Find an SDK or CLI Call](https://boxrouter.ai/docs/sdk.md): each call in Python, TypeScript, and the CLI.

## Next Steps

> [card] [Find an SDK or CLI Call](https://boxrouter.ai/docs/sdk.md)
>
> The Python call for each sandbox task.

> [card] [Recover from Errors](https://boxrouter.ai/docs/errors.md)
>
> When resending a failed call is safe.

> [card] [Use the TypeScript SDK](https://boxrouter.ai/docs/typescript.md)
>
> The same tasks in TypeScript.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.