Send a message

POST
/v1/messages

Sends a text or media message on any channel your workspace has connected: SMS, WhatsApp, Telegram, Discord or email. Use it to start a conversation or to message an address directly; to answer someone, POST /v1/conversations/{id}/messages is simpler.

The answer is 202 Accepted: the message is queued, not delivered yet. Delivery arrives as message.sent, message.delivered or message.failed events. Send an Idempotency-Key so a retry never sends twice.

Requires an API key with the messages:send scope.

Authorization

apiKey
headerAuthorizationBearer <token>

A secret API key from the dashboard (Developers → API keys): vl_live_…, or vl_test_… for test mode. Each operation says which scope the key needs.

Header Parameters

idempotency-key?string

A unique value (a UUID works) that makes a retry safe: sending the same key again returns the first result instead of acting twice. 1–255 printable characters.

Length1 <= length <= 255

Request Body

application/json
  1. body
channel*string

The channel to send on: sms, mms, whatsapp, telegram, discord or email. GET /v1/channels lists what your workspace can use.

Length1 <= length <= 64
from?||

Which of your senders to use: { "phoneNumberId" } for SMS and MMS, { "bindingId" } for WhatsApp, Telegram and Discord, { "mailboxId" } for email. Leave it out when you have one sender on the channel.

to*|

Who receives it: a phone number in E.164 format (+14155550123), the person's address on an app channel, an email address, or { "conversationId" } to answer an existing conversation.

content*||

What to send: { "type": "text", "text" }, { "type": "media", "media": [{ "url" }], "caption" } (MMS). { "type": "template", "template" } is reserved for message templates, which are not available yet: it is refused with 422 operation_not_supported.

subject?string

Email only: the subject of a new email. A reply keeps its thread's subject.

Length1 <= length <= 200
split?string

auto (the default) sends a text longer than the channel allows as several messages, each billed. refuse answers 422 content_too_long instead.

Default"auto"
Value in"auto""refuse"
metadata?

Your own key/value pairs, up to 4 KB of JSON. Stored with the message and repeated in its events, so you can match an event to your records.

Response Body

Accepted: the first part is the message; parts lists every provider message

application/json
  1. response
message*
Example Request
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."    }  }'
Example Response
{  "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"      }    ]  }}