How-to guide

Python SDK

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

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 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.
  • No phone number needed: with a test key, your workspace's free test sender stands in for one.

Step 1: Install

Once published
pip install voicelayer

Publishing soon

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.
Until then: from a checkout of the repository
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.

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 -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."
    }
  }'

You get 202 Accepted with the queued message:

Response · 202
{
  "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 -N "https://api.vlayers.ai/v1/events/stream?types=message.*" \
  -H "Authorization: Bearer $VOICELAYER_API_KEY" \
  -H "Accept: text/event-stream"

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:

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 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.

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.

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

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."
    }
  }'

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 "https://api.vlayers.ai/v1/conversations/$CONVERSATION_ID/messages?before=end&limit=20" \
  -H "Authorization: Bearer $VOICELAYER_API_KEY"

next_cursor works the same way forward with after. See 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 -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?"
  }'

Save the printed id as CONVERSATION_ID, then reply:

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?"
    }
  }'

See 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

ErrorStatusHow to fix
unauthorized401Send Authorization: Bearer <key> with a secret key from the dashboard (Developers → API keys). Test keys start with vl_test_.
insufficient_scope403Use a key with the scope named in details.required, or one whose restriction covers this channel or Binding.
invalid_request400Read message and details.issues (the validation issues, by path), fix the request and send it again.
no_sender409Connect a sender for the channel in the dashboard (for email, create a mailbox), or name one in from.
idempotency_conflict409Use a new key for a new message; repeat the original body exactly to replay the original request.
rate_limited429Wait 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.

ResourceCallMethod
MessagesList messages
GET /v1/messages
MessagesApi(client).list_messages(conversation_id=None, channel=None, direction=None, after=None, before=None, limit=None)
MessagesSend a message
POST /v1/messages
MessagesApi(client).send_message(send_message_request, idempotency_key=None)
ConversationsList conversations
GET /v1/conversations
ConversationsApi(client).list_conversations(channel=None, binding_id=None, controller=None, updated_after=None, cursor=None, limit=None)
ConversationsRetrieve a conversation
GET /v1/conversations/{id}
ConversationsApi(client).get_conversation(id)
ConversationsHand off a conversation
PATCH /v1/conversations/{id}
ConversationsApi(client).update_conversation(id, update_conversation_request, idempotency_key=None)
ConversationsList a conversation's messages
GET /v1/conversations/{id}/messages
ConversationsApi(client).list_conversation_messages(id, after=None, before=None, limit=None)
ConversationsReply in a conversation
POST /v1/conversations/{id}/messages
ConversationsApi(client).reply_to_conversation(id, reply_to_conversation_request, idempotency_key=None)
EventsList events
GET /v1/events
EventsApi(client).list_events(after=None, types=None, conversation_id=None, channel=None, limit=None, from=None, wait_sec=None)
EventsStream events
GET /v1/events/stream
stream_events(config, after=None, types=None, conversation_id=None, channel=None)
WebhooksList webhook subscriptions
GET /v1/webhooks/subscriptions
WebhooksApi(client).list_webhook_subscriptions()
WebhooksCreate a webhook subscription
POST /v1/webhooks/subscriptions
WebhooksApi(client).create_webhook_subscription(create_webhook_subscription_request)
WebhooksRetrieve a webhook subscription
GET /v1/webhooks/subscriptions/{id}
WebhooksApi(client).get_webhook_subscription(id)
WebhooksUpdate a webhook subscription
PATCH /v1/webhooks/subscriptions/{id}
WebhooksApi(client).update_webhook_subscription(id, update_webhook_subscription_request)
WebhooksDelete a webhook subscription
DELETE /v1/webhooks/subscriptions/{id}
WebhooksApi(client).delete_webhook_subscription(id)
WebhooksList webhook deliveries
GET /v1/webhooks/deliveries
WebhooksApi(client).list_webhook_deliveries(subscription_id=None, type=None, status=None, since=None, until=None, limit=None)
WebhooksRetry a webhook delivery
POST /v1/webhooks/deliveries/{id}/redeliver
WebhooksApi(client).redeliver_webhook(id)
ChannelsList channels
GET /v1/channels
ChannelsApi(client).list_channels()
ChannelsList WhatsApp templates
GET /v1/channels/whatsapp/templates
ChannelsApi(client).list_whats_app_templates(binding_id)
Test modeSimulate an incoming message
POST /v1/test/inbound
TestApi(client).create_test_inbound(create_test_inbound_request)
Request logsList API request logs
GET /v1/request-logs
LogsApi(client).list_request_logs(after=None, since=None, status=None, api_key_id=None, limit=None)

Next steps

On this page