# Python

pip install sai-sdk. Give Sai tasks from Python.

The Python SDK wraps the API on these pages:

- **Sessions**: give Sai a task on a computer, and get the answer back. Sai decides every click.

> **Note**
>
> Alpha (`0.1`). The surface may still change before `1.0`.

## Install

```bash
pip install sai-sdk
export SAI_API_KEY="sapi_..."
```

The import name is `sai`. Python 3.10 or later; one dependency, `httpx`. Create the key in the [portal](https://platform.simular.ai) (see [Quickstart](/documentation/quickstart)).

## Give Sai a task

```python
from sai import Sai

sai = Sai()
pc = sai.machines.list()[0]

session = sai.sessions.create(machine=pc)
result = session.run("Open Notepad, type hello, save it to the desktop as hello.txt")

print(result.status)  # idle, needs_approval or error
print(result.text)    # Sai's answer
print(result.usage)   # tokens and cost of this task
```

- `machine` can be left out when the account has one computer.
- More `session.run(...)` calls continue the same conversation. `sai.sessions.create()` starts a new one with no memory of the old. See [Sessions](/documentation/concepts/sessions).
- `run()` sends the task and waits. On a free account the first task of the day waits about two minutes while the computer starts.

## Watch it work

`send()` returns at once with a turn. Iterate its events, then read the result:

```python
turn = session.send("Find last month's invoices in Downloads and total them")
for ev in turn.events():
    if ev.kind == "step":
        print("  ", ev.text)
    elif ev.kind == "approval":
        turn.respond(ev.approval, "yes")
result = turn.wait()
```

| `ev.kind`    | What it is                                                         |
| ------------ | ------------------------------------------------------------------ |
| `step`       | A progress line: what Sai is doing                                 |
| `message`    | Sai's text                                                         |
| `approval`   | Sai is waiting for permission. `ev.approval` holds it              |
| `tool_error` | A step failed. Sai usually recovers                                |
| `error`      | The turn failed. `ev.code` is set when the account ran out (below) |
| `finished`   | The turn is complete                                               |

`turn.events(raw=True)` also yields reasoning, tool-call and framing frames as `other`, with the wire event in `ev.raw`. See [Events](/documentation/api-reference/events).

## Approvals

Sai asks before risky actions (see [Approvals](/documentation/concepts/approvals)). Without a handler, `run()` and `wait()` return with `status == "needs_approval"` and the request in `result.approval`. To answer as it comes, pass `on_approval`:

```python
def decide(approval):
    if approval.is_link_only:
        print("Approve in a browser:", approval.url)
        return None  # a person answers it; keep waiting
    if approval.type == "choice":
        return ("yes", [[q["options"][0]["value"]] for q in approval.questions])
    return "yes"

result = session.run("Clean up the temp folder", on_approval=decide)
```

The handler returns `"yes"`, `"no"`, `"task"` (allow the rest of this task), a `(response, selections)` pair for a choice, or `None` to leave the approval to someone else. A link-only approval cannot be answered over the API: `respond()` raises `LinkOnlyApprovalError` with `approval_url`.

## Files, models, stopping

```python
report = sai.files.upload("report.pdf")
session.run("Summarise the attached report", attachments=[report])

allowed = [m.id for m in sai.models.list() if m.allowed]
session.run("...", model="anthropic/claude-opus-5-5")  # sticks to the session

turn = session.send("...")
turn.abort()
```

A message sent while a turn is running steers that turn; `turn.queued` is then `True`.


## Other calls

```python
sai.account.get()      # plan and spendable credit
sai.models.list()      # models, with `allowed` for this plan
sai.machines.live(pc)  # a view-only stream of the screen; see Live view
```

## Errors

Every non-2xx response raises a `SaiError` subclass carrying `status`, `code`, `message` and `retry_after`:

| Exception                 | When                                                                |
| ------------------------- | ------------------------------------------------------------------- |
| `AuthenticationError`     | `401`: the key is missing, unknown or revoked                       |
| `BadRequestError`         | `400`: for example several computers and no `machine=`              |
| `PermissionDeniedError`   | `403`: plan or ownership, such as `model_not_allowed_for_plan`      |
| `ConflictError`           | `409`: for example `machine_busy`, or an approval already answered  |
| `LinkOnlyApprovalError`   | The approval must be answered in a browser: `e.approval_url`        |
| `RateLimitError`          | `429`: a rate limit, or the free plan's daily computer time         |
| `ServiceUnavailableError` | `503`: the computer did not come online. Honor `retry_after`        |

A task that ran out of credit is not an exception. It ends with `result.status == "error"` and `result.error_code` set to `insufficient_credits` or `free_computer_time_exhausted`. Stop retrying for today. See [Billing and limits](/documentation/concepts/billing-and-limits).

## Configuration

| Argument   | Environment   | Default                  |
| ---------- | ------------- | ------------------------ |
| `api_key`  | `SAI_API_KEY` | required                 |
| `base_url` | `SAI_API_URL` | `https://api.simular.ai` |
| `timeout`  |               | 60 seconds per request   |
