Send SMS from Node.js in 10 Minutes
September 2, 2026 · 6 min read
To send an SMS from Node.js, you need one POST request to https://volai.cz/v1/messages with an Authorization: Bearer vk_... header and a JSON body of {to, body}. Node 20 ships fetch built in, so there is no package to install. A working example follows right below, including error handling and idempotency so a network hiccup does not send the same message twice.
What you will need
- Node.js 18 or newer -
fetchhas been built in since this version; we recommend the currently supported LTS line (24, as of September 2, 2026). - A volai account and an API key from the portal (Settings -> API & MCP), shaped like
vk_.... - A recipient number in Czechia or Slovakia (
+420or+421) - the MVP does not support sending SMS to other countries. - You do NOT need your own phone number. The sender is always the fixed name
volai- carriers do not allow a custom SMS sender name. Put your identity in the message text itself, as in the example below.
Send your first SMS
The request needs just two fields: to (recipient number) and body (message text). The response returns the message id, its status, the price in cents of a koruna (priceHal), and the segment count (segments) - more on both below.
const response = await fetch("https://volai.cz/v1/messages", {
method: "POST",
headers: {
Authorization: "Bearer vk_YOUR_KEY",
"Content-Type": "application/json",
},
body: JSON.stringify({
to: "+420777123456",
body: "Hi from volai! - My App",
}),
});
if (!response.ok) {
const { error } = await response.json();
throw new Error("volai " + error.code + ": " + error.message);
}
const message = await response.json();
console.log(message);
// { id: "msg_7c1f9a2e", status: "sent", priceHal: 136, segments: 1 }What do SMS cost, and what are segments?
A single-segment SMS costs 1.36 CZK excluding VAT (price as of September 2, 2026). How many segments a message takes depends on whether it contains characters outside the GSM-7 alphabet - carriers segment SMS per the GSM 03.38 standard:
| Encoding | Chars in 1st segment | Chars in later parts |
|---|---|---|
| GSM-7 (plain Latin text) | 160 | 153 |
| UCS-2 (accented or non-Latin characters) | 70 | 67 |
A single character outside the base GSM-7 alphabet (accented letters, emoji, most non-Latin scripts) flips the WHOLE message to UCS-2, even if it is the only such character in the text. Tip: to reliably stay inside one segment, stick to plain GSM-7 characters.
Example: an accented message over 70 characters
This message is 112 characters long and contains accented characters, so it is counted as UCS-2 (67 characters per segment for a multi-part message) - it comes out to 2 segments, so 2.72 CZK, not 1.36 CZK:
await fetch("https://volai.cz/v1/messages", {
method: "POST",
headers: {
Authorization: "Bearer vk_YOUR_KEY",
"Content-Type": "application/json",
},
body: JSON.stringify({
to: "+420777123456",
body: "Příliš žluťoučký kůň úpěl ďábelské ódy - a tahle věta má přes sedmdesát znaků, takže spadne do druhého segmentu.",
}),
});
// -> { id: "msg_9a3f1c7d", status: "sent", priceHal: 272, segments: 2 }Idempotency: retrying safely
Add an Idempotency-Key header with any string you choose, such as an order id. If the request fails on a network error and your workflow retries it with the SAME header, volai returns the stored response from the first attempt instead of sending the message a second time - the key is valid for 24 hours.
curl -X POST https://volai.cz/v1/messages \
-H "Authorization: Bearer vk_YOUR_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-4471" \
-d '{"to": "+420777123456", "body": "Your order #4471 was received."}'Keep your API key safe
An API key (vk_...) carries the same weight as a password - anyone who has it can send messages and spend credit in your name. Never write it into source code you commit to a repository. It belongs in an environment variable (process.env.VOLAI_API_KEY in Node.js) or in whatever secrets manager your hosting provider offers.
// .env.local (never commit this file)
// VOLAI_API_KEY=vk_your_key
const response = await fetch("https://volai.cz/v1/messages", {
method: "POST",
headers: {
Authorization: "Bearer " + process.env.VOLAI_API_KEY,
"Content-Type": "application/json",
},
body: JSON.stringify({ to, body }),
});Call the endpoint from a server, never from the browser - anyone could read the key out of the browser's developer tools and start sending messages on your account. If a key ever leaks (say, an accidental commit), generate a new one in the portal and revoke the old one - the old value stops working immediately.
Test it safely first
Before wiring SMS sending into a real process (say, order confirmations), send your first few messages to your own phone number. You will see exactly what the customer sees - how the volai sender name looks, how long delivery takes, and whether segmentation behaves the way you expect. Only then switch to over to a real customer number.
What do you do when the API returns an error?
An error response always has the shape {error: {code, message}}, with an HTTP status that matches the kind of failure:
| Status | Code | What it means |
|---|---|---|
| 400 | invalid_number | Not a valid Czech or Slovak phone number. |
| 400 | invalid_body | The message text must be 1 to 765 characters. |
| 400 | unsupported_country | Number outside CZ/SK - not supported in the MVP. |
| 400 | recipient_cannot_receive_sms | A landline number - the SMS could not arrive. Not billed. |
| 402 | insufficient_credit | Your credit does not cover the price of all segments. |
| 429 | rate_limited | More than 1 SMS per 2 seconds, or over 100 SMS per account per day. |
| 502 | send_failed | The carrier rejected the request. Credit was not charged, try again. |
| 502 | send_unknown | We handed the message to the carrier but got no delivery confirmation - it is billed, so please do not blindly resend it. |
What states can a message have?
The status field, both in the list (GET /v1/messages) and in a single message, has four possible values: pending (stored, still sending), sent (the carrier confirmed it was sent), failed (the carrier rejected it - not billed), and unknown (no delivery confirmation arrived, typically a carrier-side timeout - it is still billed in full, since the message may have gone through).
How do you send a batch of SMS?
The rate limit allows at most one SMS every two seconds per account, up to 100 SMS a day. When sending several messages at once (say, appointment reminders), a short pause between requests is simpler than catching rate_limited and retrying:
const message = "Reminder: your appointment is tomorrow - My App";
for (const to of recipients) {
const response = await fetch("https://volai.cz/v1/messages", {
method: "POST",
headers: {
Authorization: "Bearer vk_YOUR_KEY",
"Content-Type": "application/json",
},
body: JSON.stringify({ to, body: message }),
});
if (!response.ok) {
const { error } = await response.json();
console.error("volai " + to + ": " + error.code);
}
// The limit is at most 1 SMS per 2 seconds per account - a short
// pause between requests is simpler than handling rate_limited retries.
await new Promise((resolve) => setTimeout(resolve, 2100));
}The API cannot see inbound SMS
volai only SENDS SMS, it cannot receive them. If a customer replies to your message, that reply never shows up - not in GET /v1/messages, not in a webhook. Do not rely on two-way SMS conversations. For when Twilio is the better fit for that specific need, see the volai vs. Twilio comparison.
In short: one POST /v1/messages call with to and body sends the SMS, an Idempotency-Key header protects a retrying workflow from sending it twice, and the price is per segment, based on whether the text contains non-GSM-7 characters. Keep the key out of your repository, and send yourself the first message before you send anyone else's.
What is next
The full REST API reference (numbers, calls, webhooks, agents) lives at /docs/api. Want to know the instant volai hands a message to the carrier, without polling GET /v1/messages? Subscribe to the message.sent event - it only confirms the carrier accepted the message, not that it reached the phone (volai cannot see that, see above) - see the webhooks documentation. And if you need a phone number that answers calls too, not just SMS, take a look at AI Receptionist.
Try volai - first 50 CZK on us
Sign up in a minute and your account already has starter credit for calls, SMS and a number.
What's next
Related articles
Make Outbound Phone Calls from an n8n Workflow
Call a customer straight from n8n: an HTTP Request node against volai's REST API, workflow data in the agent's prompt, and results delivered by webhook.
Give Claude Code a Phone Number with MCP
Install volai into Claude Code with one command, then make your first call and send an SMS with a plain-English prompt - no code, no API docs.
How Much Does an AI Receptionist Cost
A line-by-line breakdown of AI receptionist pricing on volai: number, minutes, agent surcharge, three volume scenarios, and what Czech competitors charge.