> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.getdial.ai/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.getdial.ai/_mcp/server.

# Python SDK

> Use the async Python Dial client to send messages, place calls, and stream events.

The `dial-sdk` package is an async client for Python 3.11+.

#### [Full API reference](https://sdk.getdial.ai/python)

Every class and method for `dial-sdk`, generated from the package source with examples.

## Install

```bash
pip install dial-sdk
```

## Construct a client

```python
from dial_sdk import DialClient, DialConfig

dial = DialClient(DialConfig(api_key="sk_live_..."))
# Optional: DialClient(DialConfig(api_key="sk_live_...", base_url="https://api.getdial.ai"))
# Optional: DialClient(DialConfig(api_key="sk_live_...", user_agent="my-app/1.1.1"))
```

The client is async; call its methods with `await` inside an event loop.

### Identifying your application

If you're embedding the SDK in your own product or agent runtime, pass `user_agent` so your traffic is identifiable. It's **prepended** to the SDK's own `User-Agent`, which is always still sent:

```
User-Agent: my-app/1.1.1 dial-sdk/0.20.0
```

Use the conventional `name/version` form. The SDK does **not** read this from the environment — you pass it explicitly, exactly as you pass `api_key`. Omit it and the header goes out unchanged.

## Core operations

```python
# List your numbers
numbers = await dial.list_numbers()

# Send an SMS
message = await dial.send_message(
    to="+14155550123",
    from_number_id=numbers[0].id,
    body="Hello from Dial",
)

# Place an AI voice call
call = await dial.make_call(
    to="+14155550123",
    from_number_id=numbers[0].id,
    outbound_instruction="You are confirming a reservation.",
    language="en-US",
)

# Reply or react to a message (from list_messages or a message.received event)
reply = await dial.reply_to_message(message.id, body="On my way!")
reaction = await dial.reply_to_message(message.id, reaction="🔥")

# History
recent_messages = await dial.list_messages()
recent_calls = await dial.list_calls()

# Update a number's properties (any subset)
await dial.set_number_properties(
    numbers[0].id,
    nickname="Support line",
    inbound_instruction="You are ACME's receptionist.",
)

await dial.close()
```

`send_message` and `make_call` choose the from-number with exactly one of
`from_number` (a flexible reference — phone number ID, one of your numbers in
E.164, or a nickname) or `from_number_id` (ID only).

## Typing indicators

Show a typing indicator while composing. iMessage numbers display it; standard
(SMS) numbers have no typing concept and silently ignore it, so the calls are
safe unconditionally.

```python
# Primitives
await dial.start_typing(to_number="+14155550123", from_number="Support line")
await dial.stop_typing(to_number="+14155550123", from_number="Support line")

# Or scope it to a block: starts on enter, stops on exit (even on error)
async with dial.typing(to_number="+14155550123", from_number="Support line") as session:
    await session.send_message(body="Working on it…")  # to/from prefilled
```

Delivering a message or reaction clears the indicator natively on the
recipient's device. The session compensates: after each `session.send_message`
it automatically re-starts the indicator, so inside the block the bubble
persists — the only real stop is `__aexit__` (which runs even when the block
raises). With the bare primitives, remember that a send clears it: call
`start_typing` again to keep composing, and `stop_typing` when you stop
without sending. There is no keep-alive, so a stale indicator may also clear
on its own during a very long pause.

The client makes a **single attempt** per call — it never auto-retries. `MakeCallParams` accepts an optional `idempotency_key`: pass the same key on each retry and a request that actually succeeded returns the original call instead of dialing again. `send_message` has no idempotency key — wrapping it in a retry can send a duplicate. See [Retries and idempotency](/documentation/reference/errors#retries-and-idempotency).

## Stream events

`new_events_connection()` is an async context manager. Use `async with` so the connection closes cleanly.

```python
async with dial.new_events_connection() as conn:
    async for event in conn:
        if event["type"] == "message.received":
            print("inbound:", event["data"]["from"], event["data"]["body"])
```

If you can't use `async with`, call `await conn.close()` yourself.

The stream is **presence-based**, not at-least-once — missed events replay only if you reconnect within 2 minutes. For durable, off-machine delivery, register a [webhook](/documentation/platform/webhooks) — signed and retried at-least-once.

## Types

Methods return typed `PhoneNumber`, `Message`, and `Call` dataclasses. See [Core concepts](/documentation/get-started/core-concepts) for field meanings.