Send SMS from Node.js in 10 Minutes
September 2, 2026 · 6 min read · Updated October 9, 2026
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_.... - At least one credit top-up. SMS require a first top-up; the welcome credit does not cover them - you can try calls with it, not texts.
- A recipient number in Czechia or Slovakia (
+420or+421) - we do not send SMS to other countries. - You do NOT need your own phone number. Messages go out under the shared sender name
SMSinfo- carriers allow only approved sender names on Czech networks, so a custom sender name is not possible with the shared sender, and recipients cannot reply to SMSinfo. Put your identity in the message text itself, as in the example below. If you want replies, send from your own SMS number instead (an add-on, see the note 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 SMSinfo 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, we do not send SMS there. |
| 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, or the account has not topped up yet - SMS require a first top-up, the welcome credit does not cover them. |
| 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 five possible values: pending (stored, still sending), sent (the carrier confirmed it was sent), failed (the carrier rejected it - not billed), unknown (no delivery confirmation arrived, typically a carrier-side timeout - it is still billed in full, since the message may have gone through), and received (only for messages received on an SMS number, that is with direction=in; sent messages never have it).
Every message also carries the fields direction (out for sent messages, in for received ones), smsNumber (the SMS number the message was sent from or received on; null for messages under the shared sender name SMSinfo) and deliveryStatus (delivered or undelivered, a delivery report from the carrier; it exists only for messages sent from an SMS number and is null for all others).
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));
}Only the SMS number add-on receives SMS
volai voice numbers cannot receive SMS, so a reply to a message sent under the shared name SMSinfo never shows up anywhere. To receive SMS and to send from your own number, add an SMS number: a Czech mobile number for SMS only, with no calls, 390 CZK per 30 days excluding VAT, received SMS included. You read received messages in GET /v1/messages?direction=in, in the MCP tool list_messages or in the message.received webhook, and we keep them for 90 days; sending from the number costs 1.90 CZK per segment and works only to Czech numbers. To send from the number, add the fromNumber field to POST /v1/messages (take the number from GET /v1/sms-numbers). The daily cap for sending from the number is 20 SMS per account during the first 30 days after you buy it, 50 afterwards. An account can hold at most two SMS numbers and can buy one only after the first credit top-up. We do not guarantee verification codes from services (banks, Google and similar) - the number is meant for texting with people, and receiving has been verified on the O2 network. The details are in the SMS numbers section of the API reference. For when Twilio is the better fit for receiving SMS, 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 for messages under the shared name, see above; messages sent from your own SMS number get a delivery report) - 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 and a number - SMS work after your first top-up.
What's next
Related articles
Five Questions Before You Get an AI Receptionist
Price, a 5-minute setup, forwarding a number from O2/T-Mobile/Vodafone, Slovak numbers and VAT - five answers straight from volai's rate card and settings.
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.