# Python SDK

> Reference for vmyard-sdk, the Python 3.9+ client built on the standard library alone.

`vmyard-sdk` is the Python client for the VMYard API. It needs Python 3.9 or newer and uses only the standard library (`urllib`), so it adds no dependencies to your project.

> [!NOTE]
> The SDK is in private preview and is not published to PyPI. Install it from a checkout of the [erphq/vmyard-sdk](https://github.com/erphq/vmyard-sdk) repository (ask your VMYard contact for access):

```sh
git clone https://github.com/erphq/vmyard-sdk.git
pip install ./vmyard-sdk/python
```

## Create a client

```python
from vmyard import VMYard

vy = VMYard(base_url="https://api.example.com")  # key from VMYARD_API_KEY
```

| Argument | Default | Meaning |
| --- | --- | --- |
| `api_key` | `VMYARD_API_KEY` | A `vmy_…` key. Required. |
| `base_url` | `VMYARD_API_URL` | Your API origin. Required: there is no default host. |
| `organization_id` | `VMYARD_ORGANIZATION_ID` | Sent as `X-Organization-ID`. Optional with an API key; must match the key. |
| `max_retries` | 2 | Retries for retryable failures. |
| `timeout` | 60.0 | Seconds per HTTP attempt. |
| `user_agent` | — | Extra `User-Agent` text. |

The client never follows redirects, so the key is never sent to another origin. The client is synchronous.

## Machines

```python
machine = vy.machines.create(name="dev", plan="small", auto_stop_after_seconds=7200)
same = vy.machines.find("dev")          # id, tenant alias, or unique display name
page = vy.machines.list(limit=20)       # one page
everything = vy.machines.list_all()

machine.update(name="api")
machine.stop()
machine.start()
machine.restart()
machine.refresh()                       # reload machine.data
machine.delete()
```

| Call | Route | Notes |
| --- | --- | --- |
| `vy.machines.create(**params)` | `POST /v1/machines` | Resolves plan, image, and region names or IDs to exact versions; waits; returns a `Machine`. |
| `vy.machines.begin_create(**params)` | `POST /v1/machines` | Submits without waiting. |
| `vy.machines.build_create_request(**params)` | — | The exact request `create` would send. |
| `vy.machines.list(limit=, cursor=)` | `GET /v1/machines` | One page; `list_all()` and `iterate()` follow every page. |
| `vy.machines.get(id)` / `find(ref)` | `GET /v1/machines/{id}` | `find` accepts an ID, tenant alias, or unique display name. |
| `machine.update(name=, auto_stop_after_seconds=, never_auto_stop=)` | `PATCH /v1/machines/{id}` | `auto_stop_after_seconds=None` means never. |
| `machine.start()` / `stop()` / `restart()` | `POST …/{action}` | `wait=False` returns the accepted operation instead. |
| `machine.delete()` | `POST …/delete` | |
| `machine.usage(since=, until=)` | `GET …/usage` | Seconds and micros; defaults to the machine's lifetime. |

`create` takes `name`, `alias`, `plan`, `image`, `region`, `resources`, `backend`, `auto_stop_after_seconds`, `never_auto_stop`, and `setup_script`. `machine.id`, `machine.name`, `machine.alias`, `machine.state` (observed), and `machine.desired_state` are shortcuts into `machine.data`, the wire `MachineDto`.

## Commands

```python
result = machine.exec("uname -a", timeout_ms=30_000)
print(result.exit_code, result.stdout, result.stderr, result.timed_out, result.truncated)

with machine.exec(["npm", "test"], stream=True) as stream:
    for frame in stream:
        ...  # started, stdout, stderr (frame.data is bytes), exit (frame.exit_code), error
```

A string runs under `/bin/sh -c`; a list runs as argv. A non-zero exit, timeout, or cancellation raises `OperationError` (see [Running commands](/docs/guides/commands#the-result)).

## Background commands

```python
command = machine.commands.start("npm run dev")
status = machine.commands.status(command["command_id"], log_offset=0, log_limit=65536)
finished = machine.commands.wait(command["command_id"])
machine.commands.kill(command["command_id"])
```

## Files

```python
machine.files.write("app/config.json", '{"debug": true}')   # str or bytes
text = machine.files.read("app/config.json")
data = machine.files.read_bytes("out/image.png")
```

## Operations

```python
operation = vy.operations.get(operation_id)
done = vy.operations.wait(operation_id)        # raises OperationError unless it succeeds
for event in vy.operations.events(operation_id):
    print(event)
```

## Catalog, identity, billing, webhooks

| Call | Route |
| --- | --- |
| `vy.me()` | `GET /v1/me` |
| `vy.catalog.plans()` / `images()` / `regions()` | `GET /v1/plans`, `/v1/images`, `/v1/regions` |
| `vy.billing.get()` / `limits()` / `invoice_preview()` | `GET /v1/billing`, `/v1/limits`, `/v1/billing/invoice-preview` |
| `vy.billing.usage(since=, until=)` | `GET /v1/usage` |
| `vy.webhooks.list()` / `get(id)` | `GET /v1/webhooks[/{id}]` |
| `vy.webhooks.create(name=, url=, events=, enabled=)` | `POST /v1/webhooks`; returns `{webhook, secret}` |
| `vy.webhooks.update(id, …)` / `delete(id)` / `rotate_secret(id)` | `PATCH`/`DELETE /v1/webhooks/{id}`, `POST …/rotate-secret` |
| `vy.webhooks.deliveries(id, limit=)` | `GET /v1/webhooks/{id}/deliveries` |

Responses are plain dictionaries with the API's JSON field names.

## Device sign-in

```python
from vmyard import DeviceAuthorization

device = DeviceAuthorization()  # base URL from VMYARD_API_URL
code = device.request_code(client_name="my-tool on build-host")
print(f"Open {code['verification_uri']} and enter {code['user_code']}")
token = device.wait_for_token(code)  # token["access_token"] is the new API key
```

## Idempotency, retries, and errors

Idempotency, retries, and error classes match the [TypeScript SDK](/docs/sdk/typescript#idempotency-and-retries), with snake_case names (`idempotency_key=`, `retry_after_seconds`, `field_errors`, `correlation_id`). The connection error class is `APIConnectionError`.

## More of the SDK

Like the TypeScript SDK, it also covers ports and port tokens, SSH keys (`set_ssh_key`), internet access (`set_network`), snapshots, forks and restores, templates, environments, and billing changes; this page shows the common calls. For anything else, call the [HTTP API](/docs/api) directly.
