Skip to content

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 - fetch has 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 (+420 or +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.

javascript
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:

EncodingChars in 1st segmentChars in later parts
GSM-7 (plain Latin text)160153
UCS-2 (accented or non-Latin characters)7067

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:

javascript
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.

bash
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.

javascript
// .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:

StatusCodeWhat it means
400invalid_numberNot a valid Czech or Slovak phone number.
400invalid_bodyThe message text must be 1 to 765 characters.
400unsupported_countryNumber outside CZ/SK - not supported in the MVP.
400recipient_cannot_receive_smsA landline number - the SMS could not arrive. Not billed.
402insufficient_creditYour credit does not cover the price of all segments.
429rate_limitedMore than 1 SMS per 2 seconds, or over 100 SMS per account per day.
502send_failedThe carrier rejected the request. Credit was not charged, try again.
502send_unknownWe 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:

javascript
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.

Try it for free