Send an SMS
Send a text message from your app to any phone number in about 5 minutes.
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:sendscope. Start with a test key. See Authentication and API keys. - A phone number in your workspace, assigned to an agent (dashboard → Phone numbers). It is your 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 withvoicelayer.
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 -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."
}
}'{
"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.
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 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:
{
"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 or the 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 inparts. Sendsplit: "refuse"to get422 content_too_longinstead. - To send a picture, use the
mmschannel with media content. The image must be at a publichttps://URL:
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."
}
}'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.
Common errors
| Error | Status | How to fix |
|---|---|---|
no_sender | 409 | Connect a sender for the channel in the dashboard (for email, create a mailbox), or name one in from. |
sender_ambiguous | 409 | Name one of details.senders in from. |
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 | 409 | Do not message them on this channel. They opt back in by writing to you again (for SMS, START). |
content_too_long | 422 | Shorten the text, or send it with split: "auto" where it fits the part limit. |
wallet_refused | 402 | Top up the wallet (or turn on auto top-up) in the dashboard, then send again. |
idempotency_conflict | 409 | Use a new key for a new message; repeat the original body exactly to replay the original request. |
test_address_required | 422 | Use a reserved test address, or a live key to reach a real person. |
Next steps
- Receive with webhooks: hear replies and delivery updates.
- Conversations and handoff: reply in the thread a text starts.
- Send on WhatsApp.