---
title: Python SDK
path: sdks/python
status: published
---

# Python SDK — `scailabs-scaigrid`

Official Python client for ScaiGrid. Sync **and** async clients, first-class
streaming for chat completions, typed errors, and dedicated sub-clients for
ScaiBot, ScaiSpeak, and ScaiDial.

## Install

```bash
pip install scailabs-scaigrid
```

The top-level import name is `scaigrid` (the distribution name carries the
ScaiLabs brand prefix, but the package itself stays short).

Python ≥ 3.10 supported. The only required dependencies are `httpx` and
`pydantic`.

## Quickstart

```python
from scaigrid import ScaiGrid

with ScaiGrid(api_key="sgk_...") as client:
    # List models
    for m in client.models.list(modality="chat"):
        print(m["slug"])

    # Streaming chat
    for chunk in client.inference.chat_stream(
        model="anthropic/claude-haiku-4-5",
        messages=[{"role": "user", "content": "Hello!"}],
        metadata={"agent_id": "..."},   # flows into accounting
    ):
        print(chunk["delta"].get("content", ""), end="", flush=True)
```

Async variant — same API, different prefix:

```python
from scaigrid import AsyncScaiGrid

async with AsyncScaiGrid(api_key="sgk_...") as client:
    resp = await client.inference.chat(
        model="anthropic/claude-haiku-4-5",
        messages=[{"role": "user", "content": "Hi"}],
    )
```

## Authentication

Pick exactly one of three modes:

```python
# 1. Static API key (most common)
client = ScaiGrid(api_key="sgk_...")

# 2. Pre-minted JWT
client = ScaiGrid(access_token=jwt)

# 3. OAuth client_credentials against ScaiKey — cached + auto-refresh
client = ScaiGrid.from_client_credentials(
    client_id="...",
    client_secret="...",
    token_url="https://scaikey.scailabs.ai/api/v1/platform/oauth/token",
)
```

For full control (custom auth, refresh, token caching) implement the
`Auth` protocol and pass it as `auth=`:

```python
from scaigrid import Auth, ScaiGrid

class MyAuth:
    def header(self) -> str: ...
    async def header_async(self) -> str: ...

client = ScaiGrid(auth=MyAuth())
```

## Resources

```python
# Models — list, get
client.models.list(modality="chat")
client.models.get("anthropic/claude-haiku-4-5")

# Inference — chat (sync), chat_stream (Iterator), embed
client.inference.chat(model=..., messages=...)
client.inference.chat_stream(model=..., messages=...)
client.inference.embed(model="text-embedding-3-small", input=["..."])

# Modules — list/get/enable/disable, generic proxy
client.modules.list()
client.modules.proxy("scaimatrix", "POST", "/collections", json_body={...})
```

## Module sub-clients

First-class wrappers for the most-used module routes:

### ScaiBot

```python
bot = client.scaibot.bots.create(
    name="support",
    model="anthropic/claude-haiku-4-5",
    display_name="Support Bot",
)

calls = client.scaibot.voice.list_calls(limit=20)
transcript = client.scaibot.voice.get_transcript(call_id)
cost = client.scaibot.voice.get_cost(call_id)
```

### ScaiSpeak

```python
audio = client.scaispeak.synthesize(
    voice_id="voice_...",
    text="Hello, world!",
)
# audio["audio"] is decoded bytes; audio["audio_base64"] is the raw payload.

voices = client.scaispeak.voices.list(language="nl")
```

### ScaiDial

```python
extensions = client.scaidial.extensions.list()
client.scaidial.extensions.create(number="1042", type="bot", target_ref=bot.id)

matches = client.scaidial.directory.lookup("billing")
client.scaidial.dialplans.add_rule(
    dialplan_id, action_type="goto_extension",
    action_params={"extension_number": "1042"},
)
```

For modules without a dedicated wrapper (ScaiMatrix, ScaiMind, ScaiPersona,
etc.), reach for `client.modules.proxy(module_id, method, path, ...)`.

## Streaming

`chat_stream` returns an iterator of typed chunk dicts. Each chunk is the
native ScaiGrid shape — `delta`, `finish_reason`, `usage`, `model` — not
the OpenAI-compat envelope.

```python
buffer = []
for chunk in client.inference.chat_stream(model=..., messages=...):
    delta = chunk.get("delta", {}).get("content")
    if delta:
        buffer.append(delta)
    if chunk.get("finish_reason"):
        print(f"finished: {chunk['finish_reason']}")
print("".join(buffer))
```

The async client yields the same shape via `async for`.

## Errors

Every exception is a subclass of `ScaiGridError` with the same `code`
string as the server. Catch the specific subclass you care about:

```python
from scaigrid import (
    ScaiGridError, NotFoundError, RateLimitedError,
    BudgetExceededError, ValidationError,
)

try:
    client.inference.chat(...)
except RateLimitedError as exc:
    print(f"retry after {exc.retry_after}s")
except BudgetExceededError:
    # Tenant or user budget cap hit
    ...
except ScaiGridError as exc:
    # Catch-all — exc.code, exc.http_status, exc.request_id all populated.
    ...
```

Full taxonomy: `AuthError`, `AuthTokenInvalidError`,
`AuthTokenExpiredError`, `PermissionDeniedError`, `NotFoundError`,
`ConflictError`, `ValidationError`, `RateLimitedError`,
`BudgetExceededError`, `BackendError`, `BackendTimeoutError`,
`BackendRateLimitedError`, `InternalServerError`, `TimeoutError`,
`TransportError`.

## Pagination

Paginated endpoints expose a cursor in the response. The SDK lets you
iterate manually:

```python
cursor = None
while True:
    page = client.scaibot.voice.list_calls(limit=100, cursor=cursor)
    for call in page["items"]:
        process(call)
    cursor = page.get("pagination", {}).get("next_cursor")
    if not cursor:
        break
```

## Request IDs + observability

Every method accepts an optional `request_id=` kwarg. The SDK forwards it
as `X-Request-ID` and ScaiGrid propagates it through logs, audit records,
and downstream module calls.

```python
client.inference.chat(
    model=..., messages=...,
    request_id="req-batch-job-7",
)
```

Server-side exceptions surface the `request_id` on the raised exception
so you can join client- and server-side traces.

## Retries

The HTTP layer retries `429` and idempotent `5xx` up to three times
(exponential backoff with jitter, honours `Retry-After`). Tune via
the constructor:

```python
client = ScaiGrid(api_key="sgk_...", max_retries=5, timeout=120)
```

POST requests (the inference path) are **not** retried by default; the
user-side retry policy lives in the caller.

## Download

The current release is available on
[`/downloads`](https://www.scailabs.ai/downloads) under **ScaiGrid →
Python**, with SHA-256 verification.

## See also

- [Authentication](/docs/scaigrid/getting-started/authentication)
- [Chat completions](/docs/scaigrid/api-guides/chat-completions)
- [Inference reference](/docs/scaigrid/reference/inference)
- [SDKs overview](/docs/scaigrid/sdks)
