# Python SDK

> Send, receive and handle errors from Python with the typed voicelayer client.

Source: https://docs.vlayers.ai/developers/sdks/python

Kind: How-to guide

## What you'll build

A Python script that sends a message, reads incoming events, verifies webhooks and handles errors, using the `voicelayer` package.

Every operation in the [API reference](https://docs.vlayers.ai/reference) is a method on a resource class, named after it in snake case.

## Prerequisites

- Python 3.9 or later.
- A secret API key in `VOICELAYER_API_KEY`. Use a test key (`vl_test_…`) while you build: see [Authentication and API keys](https://docs.vlayers.ai/get-started/authentication).
- No phone number needed: with a test key, your workspace's free [test sender](https://docs.vlayers.ai/developers/test-mode#your-free-test-sender) stands in for one.

## Step 1: Install

```bash
pip install voicelayer
```

> Publishing soon: voicelayer is not on PyPI yet. Until it is, install it from a checkout of the VoiceLayer repository (`sdks/python/voicelayer`), as shown.

```bash
pip install ./voicelayer/sdks/python/voicelayer
```

## Step 2: Authenticate

A `Configuration` holds your key; an `ApiClient` opens the connection. Each resource has its own class: `MessagesApi`, `ConversationsApi`, `EventsApi`, `WebhooksApi`, `ChannelsApi`, `TestApi` and `LogsApi`.

```python
import os
import voicelayer

config = voicelayer.Configuration(access_token=os.environ["VOICELAYER_API_KEY"])
with voicelayer.ApiClient(config) as client:
    channels = voicelayer.ChannelsApi(client).list_channels()
    print([c.id for c in channels.channels])
```

The client sends `Authorization: Bearer <key>` to `https://api.vlayers.ai`. Pass `host=` to `Configuration` only if VoiceLayer gives you another host.

## Step 3: Send a message

Request bodies are models: build one with `from_dict`.

cURL:

```bash
curl -X POST https://api.vlayers.ai/v1/messages \
  -H "Authorization: Bearer $VOICELAYER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "channel": "sms",
    "to": "+15550123",
    "content": {
      "type": "text",
      "text": "Your table for two is booked for 7pm."
    }
  }'
```

TypeScript:

```ts
import { createVoiceLayerClient, sendMessage } from '@voicelayer/api';

const client = createVoiceLayerClient({ apiKey: process.env.VOICELAYER_API_KEY!, throwOnError: true });

const { data } = await sendMessage({
  client,
  body: {
    channel: 'sms',
    to: '+15550123',
    content: {
      type: 'text',
      text: 'Your table for two is booked for 7pm.'
    }
  }
});
console.log(data.message.id);
```

Python:

```python
import os
import voicelayer

config = voicelayer.Configuration(access_token=os.environ["VOICELAYER_API_KEY"])
with voicelayer.ApiClient(config) as client:
    api = voicelayer.MessagesApi(client)
    result = api.send_message(
        send_message_request=voicelayer.SendMessageRequest.from_dict({
            "channel": "sms",
            "to": "+15550123",
            "content": {
                "type": "text",
                "text": "Your table for two is booked for 7pm.",
            },
        }),
    )
    print(result.message.id)
```

You get `202 Accepted` with the queued message:

Response `202`:

```json
{
  "message": {
    "id": "0192f5b0-8d1f-7b62-8c3e-5f9a2b7d4e32",
    "conversationId": "0192f5b0-7c3e-7a51-9b2d-4e8f1a6c3d21",
    "channel": "sms",
    "direction": "outbound",
    "status": "queued",
    "text": "Your table for two is booked for 7pm.",
    "media": [],
    "from": "+14155550100",
    "to": "+15550123",
    "bindingId": null,
    "metadata": {},
    "error": null,
    "createdAt": "2026-10-20T15:04:05.000Z",
    "parts": [
      {
        "id": "0192f5b0-8d1f-7b62-8c3e-5f9a2b7d4e32",
        "status": "queued"
      }
    ]
  }
}
```

## Step 4: Receive messages

`stream_events` opens the event stream, reconnects after a drop and resumes from the last cursor, so you never miss or repeat an event. Store `item.cursor` after you handle each event.

cURL:

```bash
curl -N "https://api.vlayers.ai/v1/events/stream?types=message.*" \
  -H "Authorization: Bearer $VOICELAYER_API_KEY" \
  -H "Accept: text/event-stream"
```

TypeScript:

```ts
import { createVoiceLayerClient, streamEvents } from '@voicelayer/api';

const client = createVoiceLayerClient({ apiKey: process.env.VOICELAYER_API_KEY! });

// Reconnects after a drop and resumes from the last cursor, so nothing is missed.
for await (const { cursor, event } of streamEvents(client, { types: ['message.*'] })) {
  console.log(event.type, event.data);
  // save `cursor` once the event is handled; pass it back as `after` to resume
}
```

Python:

```python
import os
import voicelayer
from voicelayer.events import stream_events

config = voicelayer.Configuration(access_token=os.environ["VOICELAYER_API_KEY"])

# Reconnects after a drop and resumes from the last cursor, so nothing is missed.
for item in stream_events(config, types=["message.*"]):
    print(item.event["type"], item.event["data"])
    # save item.cursor once the event is handled; pass it back as after= to resume
```

To receive webhooks instead, verify each delivery against the raw request body before you parse it. `verify_webhook` checks the signature and the timestamp, then returns the parsed event:

```python
import os
from flask import Flask, request
from voicelayer.webhooks import verify_webhook, WebhookVerificationError

app = Flask(__name__)

@app.post("/voicelayer/webhooks")
def webhook():
    try:
        event = verify_webhook(
            request.get_data(),  # the raw body, before any JSON parsing
            request.headers.get("x-voicelayer-signature"),
            os.environ["VOICELAYER_WEBHOOK_SECRET"],
        )
    except WebhookVerificationError as err:
        return err.reason, 400
    print(event["type"], event["data"])
    return "", 204
```

See [Receive with webhooks](https://docs.vlayers.ai/developers/receive/webhooks) to create the subscription.

## Step 5: Handle errors

A refused request raises `voicelayer.exceptions.ApiException` (or a subclass such as `BadRequestException`, `UnauthorizedException`, `ConflictException`). `status` is the HTTP status and `body` is the JSON error body, with its `error` code, `requestId`, `retryable` and `docs` link.

```python
import json
import os
import voicelayer
from voicelayer.exceptions import ApiException

config = voicelayer.Configuration(access_token=os.environ["VOICELAYER_API_KEY"])
with voicelayer.ApiClient(config) as client:
    try:
        voicelayer.MessagesApi(client).send_message(
            send_message_request=voicelayer.SendMessageRequest.from_dict(
                {"channel": "sms", "to": "+15550123", "content": {"type": "text", "text": "Hello"}}
            ),
        )
    except ApiException as err:
        body = json.loads(err.body) if err.body else {}
        print(err.status, body.get("error"), body.get("message"))
        print("retryable:", body.get("retryable"), "request:", body.get("requestId"), "docs:", body.get("docs"))
```

Retry when `retryable` is true, with backoff and the same `idempotency_key`. Fix the request for anything else. See [Errors, retries and idempotency](https://docs.vlayers.ai/developers/errors-and-retries).

To make a send safe to retry, pass an `idempotency_key`. A retry with the same key never sends twice:

cURL:

```bash
curl -X POST https://api.vlayers.ai/v1/messages \
  -H "Authorization: Bearer $VOICELAYER_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "channel": "sms",
    "to": "+15550123",
    "content": {
      "type": "text",
      "text": "Your table for two is booked for 7pm."
    }
  }'
```

TypeScript:

```ts
import { randomUUID } from 'node:crypto';
import { createVoiceLayerClient, sendMessage } from '@voicelayer/api';

const client = createVoiceLayerClient({ apiKey: process.env.VOICELAYER_API_KEY!, throwOnError: true });

const { data } = await sendMessage({
  client,
  headers: { 'idempotency-key': randomUUID() },
  body: {
    channel: 'sms',
    to: '+15550123',
    content: {
      type: 'text',
      text: 'Your table for two is booked for 7pm.'
    }
  }
});
console.log(data.message.id);
```

Python:

```python
import os
import uuid
import voicelayer

config = voicelayer.Configuration(access_token=os.environ["VOICELAYER_API_KEY"])
with voicelayer.ApiClient(config) as client:
    api = voicelayer.MessagesApi(client)
    result = api.send_message(
        send_message_request=voicelayer.SendMessageRequest.from_dict({
            "channel": "sms",
            "to": "+15550123",
            "content": {
                "type": "text",
                "text": "Your table for two is booked for 7pm.",
            },
        }),
        idempotency_key=str(uuid.uuid4()),
    )
    print(result.message.id)
```

## Step 6: Page through lists

List calls return a cursor. To read a conversation's history newest first, pass `before="end"`, then each page's `prev_cursor` back as `before`:

cURL:

```bash
curl "https://api.vlayers.ai/v1/conversations/$CONVERSATION_ID/messages?before=end&limit=20" \
  -H "Authorization: Bearer $VOICELAYER_API_KEY"
```

TypeScript:

```ts
import { createVoiceLayerClient, listConversationMessages } from '@voicelayer/api';

const client = createVoiceLayerClient({ apiKey: process.env.VOICELAYER_API_KEY!, throwOnError: true });

const { data } = await listConversationMessages({
  client,
  path: {
    id: process.env.CONVERSATION_ID!
  },
  query: {
    before: 'end',
    limit: 20
  }
});
console.log(data.messages);
```

Python:

```python
import os
import voicelayer

config = voicelayer.Configuration(access_token=os.environ["VOICELAYER_API_KEY"])
with voicelayer.ApiClient(config) as client:
    api = voicelayer.ConversationsApi(client)
    result = api.list_conversation_messages(
        id=os.environ["CONVERSATION_ID"],
        before="end",
        limit=20,
    )
    print(result.messages)
```

`next_cursor` works the same way forward with `after`. See [Pagination](https://docs.vlayers.ai/developers/pagination).

## Test it

With a test key, simulate a person texting you from a reserved number, then reply in the conversation it opens. Nothing reaches a real carrier and it costs nothing.

cURL:

```bash
curl -X POST https://api.vlayers.ai/v1/test/inbound \
  -H "Authorization: Bearer $VOICELAYER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "channel": "sms",
    "from": "+15550123",
    "text": "Do you have a table for two tonight?"
  }'
```

TypeScript:

```ts
import { createVoiceLayerClient, createTestInbound } from '@voicelayer/api';

const client = createVoiceLayerClient({ apiKey: process.env.VOICELAYER_API_KEY!, throwOnError: true });

const { data } = await createTestInbound({
  client,
  body: {
    channel: 'sms',
    from: '+15550123',
    text: 'Do you have a table for two tonight?'
  }
});
console.log(data.conversationId);
```

Python:

```python
import os
import voicelayer

config = voicelayer.Configuration(access_token=os.environ["VOICELAYER_API_KEY"])
with voicelayer.ApiClient(config) as client:
    api = voicelayer.TestApi(client)
    result = api.create_test_inbound(
        create_test_inbound_request=voicelayer.CreateTestInboundRequest.from_dict({
            "channel": "sms",
            "from": "+15550123",
            "text": "Do you have a table for two tonight?",
        }),
    )
    print(result.conversation_id)
```

Save the printed id as `CONVERSATION_ID`, then reply:

cURL:

```bash
curl -X POST "https://api.vlayers.ai/v1/conversations/$CONVERSATION_ID/messages" \
  -H "Authorization: Bearer $VOICELAYER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "content": {
      "type": "text",
      "text": "Yes, we have a table at 7pm. Shall I book it?"
    }
  }'
```

TypeScript:

```ts
import { createVoiceLayerClient, replyToConversation } from '@voicelayer/api';

const client = createVoiceLayerClient({ apiKey: process.env.VOICELAYER_API_KEY!, throwOnError: true });

const { data } = await replyToConversation({
  client,
  path: {
    id: process.env.CONVERSATION_ID!
  },
  body: {
    content: {
      type: 'text',
      text: 'Yes, we have a table at 7pm. Shall I book it?'
    }
  }
});
console.log(data.message.id);
```

Python:

```python
import os
import voicelayer

config = voicelayer.Configuration(access_token=os.environ["VOICELAYER_API_KEY"])
with voicelayer.ApiClient(config) as client:
    api = voicelayer.ConversationsApi(client)
    result = api.reply_to_conversation(
        id=os.environ["CONVERSATION_ID"],
        reply_to_conversation_request=voicelayer.ReplyToConversationRequest.from_dict({
            "content": {
                "type": "text",
                "text": "Yes, we have a table at 7pm. Shall I book it?",
            },
        }),
    )
    print(result.message.id)
```

See [Test mode](https://docs.vlayers.ai/developers/test-mode).

## Advanced: the generated client

The classes above are the whole client today. A shorter one, `VoiceLayer()` with `vl.messages.send(channel=..., to=..., text=...)`, ships in the first release of `voicelayer`, and these classes keep working after it.

## Common errors

| Error | Status | How to fix |
|---|---|---|
| [`unauthorized`](https://docs.vlayers.ai/errors/unauthorized) | 401 | Send `Authorization: Bearer <key>` with a secret key from the dashboard (Developers → API keys). Test keys start with `vl_test_`. |
| [`insufficient_scope`](https://docs.vlayers.ai/errors/insufficient_scope) | 403 | Use a key with the scope named in `details.required`, or one whose restriction covers this channel or Binding. |
| [`invalid_request`](https://docs.vlayers.ai/errors/invalid_request) | 400 | Read `message` and `details.issues` (the validation issues, by path), fix the request and send it again. |
| [`no_sender`](https://docs.vlayers.ai/errors/no_sender) | 409 | Connect a sender for the channel in the dashboard (for email, create a mailbox), or name one in `from`. |
| [`idempotency_conflict`](https://docs.vlayers.ai/errors/idempotency_conflict) | 409 | Use a new key for a new message; repeat the original body exactly to replay the original request. |
| [`rate_limited`](https://docs.vlayers.ai/errors/rate_limited) | 429 | Wait for `retry-after`, then retry (with the same Idempotency-Key for a send). |

## Method reference

Every call in the SDK, by resource. Each links to its full reference page with parameters and examples.

| Resource | Call | Method | REST |
|---|---|---|---|
| Messages | [List messages](https://docs.vlayers.ai/reference/messages/list-messages) | `MessagesApi(client).list_messages(conversation_id=None, channel=None, direction=None, after=None, before=None, limit=None)` | `GET /v1/messages` |
| Messages | [Send a message](https://docs.vlayers.ai/reference/messages/send-message) | `MessagesApi(client).send_message(send_message_request, idempotency_key=None)` | `POST /v1/messages` |
| Conversations | [List conversations](https://docs.vlayers.ai/reference/conversations/list-conversations) | `ConversationsApi(client).list_conversations(channel=None, binding_id=None, controller=None, updated_after=None, cursor=None, limit=None)` | `GET /v1/conversations` |
| Conversations | [Retrieve a conversation](https://docs.vlayers.ai/reference/conversations/get-conversation) | `ConversationsApi(client).get_conversation(id)` | `GET /v1/conversations/{id}` |
| Conversations | [Hand off a conversation](https://docs.vlayers.ai/reference/conversations/update-conversation) | `ConversationsApi(client).update_conversation(id, update_conversation_request, idempotency_key=None)` | `PATCH /v1/conversations/{id}` |
| Conversations | [List a conversation's messages](https://docs.vlayers.ai/reference/conversations/list-conversation-messages) | `ConversationsApi(client).list_conversation_messages(id, after=None, before=None, limit=None)` | `GET /v1/conversations/{id}/messages` |
| Conversations | [Reply in a conversation](https://docs.vlayers.ai/reference/conversations/reply-to-conversation) | `ConversationsApi(client).reply_to_conversation(id, reply_to_conversation_request, idempotency_key=None)` | `POST /v1/conversations/{id}/messages` |
| Events | [List events](https://docs.vlayers.ai/reference/events/list-events) | `EventsApi(client).list_events(after=None, types=None, conversation_id=None, channel=None, limit=None, from=None, wait_sec=None)` | `GET /v1/events` |
| Events | [Stream events](https://docs.vlayers.ai/reference/events/stream-events) | `stream_events(config, after=None, types=None, conversation_id=None, channel=None)` | `GET /v1/events/stream` |
| Webhooks | [List webhook subscriptions](https://docs.vlayers.ai/reference/webhooks/list-webhook-subscriptions) | `WebhooksApi(client).list_webhook_subscriptions()` | `GET /v1/webhooks/subscriptions` |
| Webhooks | [Create a webhook subscription](https://docs.vlayers.ai/reference/webhooks/create-webhook-subscription) | `WebhooksApi(client).create_webhook_subscription(create_webhook_subscription_request)` | `POST /v1/webhooks/subscriptions` |
| Webhooks | [Retrieve a webhook subscription](https://docs.vlayers.ai/reference/webhooks/get-webhook-subscription) | `WebhooksApi(client).get_webhook_subscription(id)` | `GET /v1/webhooks/subscriptions/{id}` |
| Webhooks | [Update a webhook subscription](https://docs.vlayers.ai/reference/webhooks/update-webhook-subscription) | `WebhooksApi(client).update_webhook_subscription(id, update_webhook_subscription_request)` | `PATCH /v1/webhooks/subscriptions/{id}` |
| Webhooks | [Delete a webhook subscription](https://docs.vlayers.ai/reference/webhooks/delete-webhook-subscription) | `WebhooksApi(client).delete_webhook_subscription(id)` | `DELETE /v1/webhooks/subscriptions/{id}` |
| Webhooks | [List webhook deliveries](https://docs.vlayers.ai/reference/webhooks/list-webhook-deliveries) | `WebhooksApi(client).list_webhook_deliveries(subscription_id=None, type=None, status=None, since=None, until=None, limit=None)` | `GET /v1/webhooks/deliveries` |
| Webhooks | [Retry a webhook delivery](https://docs.vlayers.ai/reference/webhooks/redeliver-webhook) | `WebhooksApi(client).redeliver_webhook(id)` | `POST /v1/webhooks/deliveries/{id}/redeliver` |
| Channels | [List channels](https://docs.vlayers.ai/reference/channels/list-channels) | `ChannelsApi(client).list_channels()` | `GET /v1/channels` |
| Channels | [List WhatsApp templates](https://docs.vlayers.ai/reference/channels/list-whats-app-templates) | `ChannelsApi(client).list_whats_app_templates(binding_id)` | `GET /v1/channels/whatsapp/templates` |
| Test mode | [Simulate an incoming message](https://docs.vlayers.ai/reference/test/create-test-inbound) | `TestApi(client).create_test_inbound(create_test_inbound_request)` | `POST /v1/test/inbound` |
| Request logs | [List API request logs](https://docs.vlayers.ai/reference/logs/list-request-logs) | `LogsApi(client).list_request_logs(after=None, since=None, status=None, api_key_id=None, limit=None)` | `GET /v1/request-logs` |

## Next steps

- [Receive with webhooks](https://docs.vlayers.ai/developers/receive/webhooks) or [the event stream](https://docs.vlayers.ai/developers/receive/event-stream).
- [Hand off a conversation](https://docs.vlayers.ai/developers/handoff) between your code, your agent and people.
- [Python framework packages](https://docs.vlayers.ai/developers/frameworks/python) for Claude, OpenAI Agents and LangGraph.
