TypeScript SDK
Send, receive and handle errors from Node.js with the typed @voicelayer/api client.
What you'll build
A Node.js script that sends a message, reads incoming events, verifies webhooks and handles errors, using @voicelayer/api.
Every operation in the API reference is a typed function named after it.
Prerequisites
- Node.js 18 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
npm install @voicelayer/apiStep 2: Authenticate
Create one client and pass it to every call. With throwOnError: true, a refused request throws instead of returning an error field.
import { createVoiceLayerClient } from '@voicelayer/api';
const client = createVoiceLayerClient({ apiKey: process.env.VOICELAYER_API_KEY!, throwOnError: true });The client sends Authorization: Bearer <key> to https://api.vlayers.ai. The package is an ES module and the examples use top-level await: in a new project, run npm pkg set type=module once, then run a file with npx tsx example.ts.
Step 3: Send a message
Each function takes { client, path, query, headers, body }, as the operation needs.
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
streamEvents opens the event stream, reconnects after a drop and resumes from the last cursor, so you never miss or repeat an event. Store 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. verifyWebhook checks the signature and the timestamp, then returns the parsed event:
import { verifyWebhook, WebhookVerificationError } from '@voicelayer/api';
export async function POST(request: Request) {
const payload = await request.text(); // the raw body, before any JSON parsing
try {
const event = verifyWebhook({
payload,
headers: request.headers,
secret: process.env.VOICELAYER_WEBHOOK_SECRET!
});
console.log(event.type, event.data);
return new Response(null, { status: 204 });
} catch (err) {
if (err instanceof WebhookVerificationError) return new Response(err.reason, { status: 400 });
throw err;
}
}See Receive with webhooks to create the subscription.
Step 5: Handle errors
A refusal throws VoiceLayerApiError. Read code to decide what to do, and quote requestId if you contact support.
import { createVoiceLayerClient, sendMessage, VoiceLayerApiError } from '@voicelayer/api';
const client = createVoiceLayerClient({ apiKey: process.env.VOICELAYER_API_KEY!, throwOnError: true });
try {
await sendMessage({
client,
body: { channel: 'sms', to: '+15550123', content: { type: 'text', text: 'Hello' } }
});
} catch (err) {
if (err instanceof VoiceLayerApiError) {
console.error(err.status, err.code, err.message);
console.error('retryable:', err.retryable, 'request:', err.requestId, 'docs:', err.docs);
} else {
throw err; // a network failure: retry with the same idempotency key
}
}retryable is true for a rate limit or a temporary failure. Retry those 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, add 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."
}
}'Use another host only if VoiceLayer gives you one: createVoiceLayerClient({ apiKey, baseUrl: 'https://api.example.com' }).
Step 6: Page through lists
List calls return a cursor. To read a conversation's history newest first, ask for before: 'end', then pass each page's prevCursor back as before:
curl "https://api.vlayers.ai/v1/conversations/$CONVERSATION_ID/messages?before=end&limit=20" \
-H "Authorization: Bearer $VOICELAYER_API_KEY"nextCursor 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 functions above are the whole client today. A shorter one, new VoiceLayer() with voicelayer.messages.send({ channel, to, text }), arrives in the next @voicelayer/api release, and these functions 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 | listMessages({ client, query?: { conversationId?, channel?, direction?, after?, before?, limit? } }) |
| Messages | Send a messagePOST /v1/messages | sendMessage({ client, headers?: { 'idempotency-key' }, body }) |
| Conversations | List conversationsGET /v1/conversations | listConversations({ client, query?: { channel?, bindingId?, controller?, updatedAfter?, cursor?, limit? } }) |
| Conversations | Retrieve a conversationGET /v1/conversations/{id} | getConversation({ client, path: { id } }) |
| Conversations | Hand off a conversationPATCH /v1/conversations/{id} | updateConversation({ client, path: { id }, headers?: { 'idempotency-key' }, body }) |
| Conversations | List a conversation's messagesGET /v1/conversations/{id}/messages | listConversationMessages({ client, path: { id }, query?: { after?, before?, limit? } }) |
| Conversations | Reply in a conversationPOST /v1/conversations/{id}/messages | replyToConversation({ client, path: { id }, headers?: { 'idempotency-key' }, body }) |
| Events | List eventsGET /v1/events | listEvents({ client, query?: { after?, types?, conversationId?, channel?, limit?, from?, waitSec? } }) |
| Events | Stream eventsGET /v1/events/stream | streamEvents(client, { after?, types?, conversationId?, channel? }) |
| Webhooks | List webhook subscriptionsGET /v1/webhooks/subscriptions | listWebhookSubscriptions({ client }) |
| Webhooks | Create a webhook subscriptionPOST /v1/webhooks/subscriptions | createWebhookSubscription({ client, body }) |
| Webhooks | Retrieve a webhook subscriptionGET /v1/webhooks/subscriptions/{id} | getWebhookSubscription({ client, path: { id } }) |
| Webhooks | Update a webhook subscriptionPATCH /v1/webhooks/subscriptions/{id} | updateWebhookSubscription({ client, path: { id }, body }) |
| Webhooks | Delete a webhook subscriptionDELETE /v1/webhooks/subscriptions/{id} | deleteWebhookSubscription({ client, path: { id } }) |
| Webhooks | List webhook deliveriesGET /v1/webhooks/deliveries | listWebhookDeliveries({ client, query?: { subscriptionId?, type?, status?, since?, until?, limit? } }) |
| Webhooks | Retry a webhook deliveryPOST /v1/webhooks/deliveries/{id}/redeliver | redeliverWebhook({ client, path: { id } }) |
| Channels | List channelsGET /v1/channels | listChannels({ client }) |
| Channels | List WhatsApp templatesGET /v1/channels/whatsapp/templates | listWhatsAppTemplates({ client, query: { bindingId } }) |
| Test mode | Simulate an incoming messagePOST /v1/test/inbound | createTestInbound({ client, body }) |
| Request logs | List API request logsGET /v1/request-logs | listRequestLogs({ client, query?: { after?, since?, status?, apiKeyId?, limit? } }) |
Next steps
- Receive with webhooks or the event stream.
- Hand off a conversation between your code, your agent and people.
- Direct mode: answer messages with a handler instead of a loop.