# Send an SMS

> Send a text message from your app to any phone number in about 5 minutes.

Source: https://docs.vlayers.ai/developers/send/sms

Kind: How-to guide

## What you'll build

A request that texts a phone number from your workspace's number, safe to retry, and a way to know when it was delivered.

## Prerequisites

- A secret API key with the `messages:send` scope. Start with a test key. See [Authentication and API keys](https://docs.vlayers.ai/get-started/authentication).
- A phone number in your workspace, assigned to an agent (dashboard → **Phone numbers**). It is your [sender](https://docs.vlayers.ai/get-started/concepts#sender).
- To text real US numbers with a live key, a registered texting program for your number (dashboard → **Channels**).
- curl, Node.js 18 or later with `@voicelayer/api`, or Python 3.9 or later with `voicelayer`.

## Step 1: Send the message

`POST /v1/messages` takes the channel, the recipient in E.164 format (`+` and the country code), and the content. With a test key, use a reserved number such as `+15550123`.

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

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"
      }
    ]
  }
}
```

The answer is `202 Accepted`: the message is queued, not delivered yet. Keep `message.id` to match later events.

## Step 2: Make it safe to retry

Add an `Idempotency-Key` header with a fresh UUID. If your request times out and you retry with the **same** key, VoiceLayer returns the first result instead of sending twice. Generate one key per message you mean to send, and reuse it only for retries. See [Errors, retries and idempotency](https://docs.vlayers.ai/developers/errors-and-retries).

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 3: Choose the sender (when you have several numbers)

Leave `from` out and the API uses your workspace's one number. With several numbers, name one:

```json
{
  "channel": "sms",
  "from": { "phoneNumberId": "<your number's id>" },
  "to": "+15550123",
  "content": { "type": "text", "text": "Your table for two is booked for 7pm." }
}
```

Without `from`, several numbers give `409 sender_ambiguous`; no number at all gives `409 no_sender`.

## Step 4: Know when it is delivered

Delivery is reported as events: `message.sent` when it leaves through the carrier, `message.delivered` when the carrier confirms it, and `message.failed` with `data.error` when it cannot be delivered. Receive them with [webhooks](https://docs.vlayers.ai/developers/receive/webhooks) or the [event stream](https://docs.vlayers.ai/developers/receive/event-stream).

## Long texts and pictures

- A text longer than one SMS goes out as several parts (`split: "auto"`, the default). Each part is billed, and all are listed in `parts`. Send `split: "refuse"` to get `422 content_too_long` instead.
- To send a picture, use the `mms` channel with media content. The image must be at a public `https://` URL:

cURL:

```bash
curl -X POST https://api.vlayers.ai/v1/messages \
  -H "Authorization: Bearer $VOICELAYER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "channel": "mms",
    "to": "+15550123",
    "content": {
      "type": "media",
      "media": [
        {
          "url": "https://vlayers.ai/og.png"
        }
      ],
      "caption": "Here is the menu."
    }
  }'
```

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: 'mms',
    to: '+15550123',
    content: {
      type: 'media',
      media: [{
        url: 'https://vlayers.ai/og.png'
      }],
      caption: 'Here is the menu.'
    }
  }
});
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": "mms",
            "to": "+15550123",
            "content": {
                "type": "media",
                "media": [{
                    "url": "https://vlayers.ai/og.png",
                }],
                "caption": "Here is the menu.",
            },
        }),
    )
    print(result.message.id)
```

## Test it

With a test key, send to any number from `+15550100` to `+15550199`. The message goes to a simulated carrier and costs nothing. A test key that names a real number is refused with `422 test_address_required`. See [Test mode](https://docs.vlayers.ai/developers/test-mode).

## Common errors

| Error | Status | How to fix |
|---|---|---|
| [`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`. |
| [`sender_ambiguous`](https://docs.vlayers.ai/errors/sender_ambiguous) | 409 | Name one of `details.senders` in `from`. |
| [`sms_refused`](https://docs.vlayers.ai/errors/sms_refused) | 403, 429 | For a 403, change what is sent (another number, a destination the workspace may text, consent on file, the recipient's allowed hours). For a 429, retry after the delay, or ask for bulk sending to be enabled. |
| [`recipient_opted_out`](https://docs.vlayers.ai/errors/recipient_opted_out) | 409 | Do not message them on this channel. They opt back in by writing to you again (for SMS, START). |
| [`content_too_long`](https://docs.vlayers.ai/errors/content_too_long) | 422 | Shorten the text, or send it with `split: "auto"` where it fits the part limit. |
| [`wallet_refused`](https://docs.vlayers.ai/errors/wallet_refused) | 402 | Top up the wallet (or turn on auto top-up) in the dashboard, then send again. |
| [`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. |
| [`test_address_required`](https://docs.vlayers.ai/errors/test_address_required) | 422 | Use a reserved test address, or a live key to reach a real person. |

## Next steps

- [Receive with webhooks](https://docs.vlayers.ai/developers/receive/webhooks): hear replies and delivery updates.
- [Conversations and handoff](https://docs.vlayers.ai/developers/handoff): reply in the thread a text starts.
- [Send on WhatsApp](https://docs.vlayers.ai/developers/send/whatsapp).
