# TypeScript SDK

> Send, receive and handle errors from Node.js with the typed @voicelayer/api client.

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

Kind: How-to guide

## 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](https://docs.vlayers.ai/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](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
npm install @voicelayer/api
```

## Step 2: Authenticate

Create one client and pass it to every call. With `throwOnError: true`, a refused request throws instead of returning an `error` field.

```ts
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:

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

`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:

```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. `verifyWebhook` checks the signature and the timestamp, then returns the parsed event:

```ts
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](https://docs.vlayers.ai/developers/receive/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.

```ts
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](https://docs.vlayers.ai/developers/errors-and-retries).

To make a send safe to retry, add 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)
```

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:

```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)
```

`nextCursor` 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 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`](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) | `listMessages({ client, query?: { conversationId?, channel?, direction?, after?, before?, limit? } })` | `GET /v1/messages` |
| Messages | [Send a message](https://docs.vlayers.ai/reference/messages/send-message) | `sendMessage({ client, headers?: { 'idempotency-key' }, body })` | `POST /v1/messages` |
| Conversations | [List conversations](https://docs.vlayers.ai/reference/conversations/list-conversations) | `listConversations({ client, query?: { channel?, bindingId?, controller?, updatedAfter?, cursor?, limit? } })` | `GET /v1/conversations` |
| Conversations | [Retrieve a conversation](https://docs.vlayers.ai/reference/conversations/get-conversation) | `getConversation({ client, path: { id } })` | `GET /v1/conversations/{id}` |
| Conversations | [Hand off a conversation](https://docs.vlayers.ai/reference/conversations/update-conversation) | `updateConversation({ client, path: { id }, headers?: { 'idempotency-key' }, body })` | `PATCH /v1/conversations/{id}` |
| Conversations | [List a conversation's messages](https://docs.vlayers.ai/reference/conversations/list-conversation-messages) | `listConversationMessages({ client, path: { id }, query?: { after?, before?, limit? } })` | `GET /v1/conversations/{id}/messages` |
| Conversations | [Reply in a conversation](https://docs.vlayers.ai/reference/conversations/reply-to-conversation) | `replyToConversation({ client, path: { id }, headers?: { 'idempotency-key' }, body })` | `POST /v1/conversations/{id}/messages` |
| Events | [List events](https://docs.vlayers.ai/reference/events/list-events) | `listEvents({ client, query?: { after?, types?, conversationId?, channel?, limit?, from?, waitSec? } })` | `GET /v1/events` |
| Events | [Stream events](https://docs.vlayers.ai/reference/events/stream-events) | `streamEvents(client, { after?, types?, conversationId?, channel? })` | `GET /v1/events/stream` |
| Webhooks | [List webhook subscriptions](https://docs.vlayers.ai/reference/webhooks/list-webhook-subscriptions) | `listWebhookSubscriptions({ client })` | `GET /v1/webhooks/subscriptions` |
| Webhooks | [Create a webhook subscription](https://docs.vlayers.ai/reference/webhooks/create-webhook-subscription) | `createWebhookSubscription({ client, body })` | `POST /v1/webhooks/subscriptions` |
| Webhooks | [Retrieve a webhook subscription](https://docs.vlayers.ai/reference/webhooks/get-webhook-subscription) | `getWebhookSubscription({ client, path: { id } })` | `GET /v1/webhooks/subscriptions/{id}` |
| Webhooks | [Update a webhook subscription](https://docs.vlayers.ai/reference/webhooks/update-webhook-subscription) | `updateWebhookSubscription({ client, path: { id }, body })` | `PATCH /v1/webhooks/subscriptions/{id}` |
| Webhooks | [Delete a webhook subscription](https://docs.vlayers.ai/reference/webhooks/delete-webhook-subscription) | `deleteWebhookSubscription({ client, path: { id } })` | `DELETE /v1/webhooks/subscriptions/{id}` |
| Webhooks | [List webhook deliveries](https://docs.vlayers.ai/reference/webhooks/list-webhook-deliveries) | `listWebhookDeliveries({ client, query?: { subscriptionId?, type?, status?, since?, until?, limit? } })` | `GET /v1/webhooks/deliveries` |
| Webhooks | [Retry a webhook delivery](https://docs.vlayers.ai/reference/webhooks/redeliver-webhook) | `redeliverWebhook({ client, path: { id } })` | `POST /v1/webhooks/deliveries/{id}/redeliver` |
| Channels | [List channels](https://docs.vlayers.ai/reference/channels/list-channels) | `listChannels({ client })` | `GET /v1/channels` |
| Channels | [List WhatsApp templates](https://docs.vlayers.ai/reference/channels/list-whats-app-templates) | `listWhatsAppTemplates({ client, query: { bindingId } })` | `GET /v1/channels/whatsapp/templates` |
| Test mode | [Simulate an incoming message](https://docs.vlayers.ai/reference/test/create-test-inbound) | `createTestInbound({ client, body })` | `POST /v1/test/inbound` |
| Request logs | [List API request logs](https://docs.vlayers.ai/reference/logs/list-request-logs) | `listRequestLogs({ client, query?: { after?, since?, status?, apiKeyId?, limit? } })` | `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.
- [Direct mode](https://docs.vlayers.ai/developers/sdks/direct): answer messages with a handler instead of a loop.
