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
pip install voicelayerPublishing soon
sdks/python/voicelayer), as shown.pip install ./voicelayer/sdks/python/voicelayerStep 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:
{
"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 "", 204See 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
| Error | Status | How to fix |
|---|---|---|
unauthorized | 401 | Send Authorization: Bearer <key> with a secret key from the dashboard (Developers → API keys). Test keys start with vl_test_. |
insufficient_scope | 403 | Use a key with the scope named in details.required, or one whose restriction covers this channel or Binding. |
invalid_request | 400 | Read message and details.issues (the validation issues, by path), fix the request and send it again. |
no_sender | 409 | Connect a sender for the channel in the dashboard (for email, create a mailbox), or name one in from. |
idempotency_conflict | 409 | Use a new key for a new message; repeat the original body exactly to replay the original request. |
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 |
|---|---|---|
| Messages | List messagesGET /v1/messages | MessagesApi(client).list_messages(conversation_id=None, channel=None, direction=None, after=None, before=None, limit=None) |
| Messages | Send a messagePOST /v1/messages | MessagesApi(client).send_message(send_message_request, idempotency_key=None) |
| Conversations | List conversationsGET /v1/conversations | ConversationsApi(client).list_conversations(channel=None, binding_id=None, controller=None, updated_after=None, cursor=None, limit=None) |
| Conversations | Retrieve a conversationGET /v1/conversations/{id} | ConversationsApi(client).get_conversation(id) |
| Conversations | Hand off a conversationPATCH /v1/conversations/{id} | ConversationsApi(client).update_conversation(id, update_conversation_request, idempotency_key=None) |
| Conversations | List a conversation's messagesGET /v1/conversations/{id}/messages | ConversationsApi(client).list_conversation_messages(id, after=None, before=None, limit=None) |
| Conversations | Reply in a conversationPOST /v1/conversations/{id}/messages | ConversationsApi(client).reply_to_conversation(id, reply_to_conversation_request, idempotency_key=None) |
| Events | List eventsGET /v1/events | EventsApi(client).list_events(after=None, types=None, conversation_id=None, channel=None, limit=None, from=None, wait_sec=None) |
| Events | Stream eventsGET /v1/events/stream | stream_events(config, after=None, types=None, conversation_id=None, channel=None) |
| Webhooks | List webhook subscriptionsGET /v1/webhooks/subscriptions | WebhooksApi(client).list_webhook_subscriptions() |
| Webhooks | Create a webhook subscriptionPOST /v1/webhooks/subscriptions | WebhooksApi(client).create_webhook_subscription(create_webhook_subscription_request) |
| Webhooks | Retrieve a webhook subscriptionGET /v1/webhooks/subscriptions/{id} | WebhooksApi(client).get_webhook_subscription(id) |
| Webhooks | Update a webhook subscriptionPATCH /v1/webhooks/subscriptions/{id} | WebhooksApi(client).update_webhook_subscription(id, update_webhook_subscription_request) |
| Webhooks | Delete a webhook subscriptionDELETE /v1/webhooks/subscriptions/{id} | WebhooksApi(client).delete_webhook_subscription(id) |
| Webhooks | List webhook deliveriesGET /v1/webhooks/deliveries | WebhooksApi(client).list_webhook_deliveries(subscription_id=None, type=None, status=None, since=None, until=None, limit=None) |
| Webhooks | Retry a webhook deliveryPOST /v1/webhooks/deliveries/{id}/redeliver | WebhooksApi(client).redeliver_webhook(id) |
| Channels | List channelsGET /v1/channels | ChannelsApi(client).list_channels() |
| Channels | List WhatsApp templatesGET /v1/channels/whatsapp/templates | ChannelsApi(client).list_whats_app_templates(binding_id) |
| Test mode | Simulate an incoming messagePOST /v1/test/inbound | TestApi(client).create_test_inbound(create_test_inbound_request) |
| Request logs | List API request logsGET /v1/request-logs | LogsApi(client).list_request_logs(after=None, since=None, status=None, api_key_id=None, limit=None) |
Next steps
- Receive with webhooks or the event stream.
- Hand off a conversation between your code, your agent and people.
- Python framework packages for Claude, OpenAI Agents and LangGraph.