149 countries · crypto-native · no KYC

Send SMS with Node.js in 2026: API Tutorial With Retries & DLRs

No SDK required: native fetch, a retry wrapper that respects idempotency, and a delivery-receipt webhook. Production-shaped Node SMS in about sixty lines.

$0.035/msg from sub-100ms median 98.6% delivered
Send SMS with Node.js in 2026: API Tutorial With Retries & DLRs — smsroute
$0.004
per SMS from
149
countries
60s
to first message
6
crypto rails
To send SMS with Node.js you need exactly one HTTPS POST, and Node 18+ ships fetch natively. So this tutorial uses zero dependencies. No SDK means no version drift, no transitive vulnerabilities, and code that transfers to any provider by changing one URL. We use SMSRoute endpoints here (SMSRoute is a no-KYC SMS API with crypto billing: BTC, ETH, USDT, XMR, LTC, and SOL). The signup-to-first-send path takes minutes; the shape is identical anywhere.

Setup: no SDK, on purpose

How do I set up an SMS API without an SDK?

With SMSRoute, you skip SDKs entirely. Just sign up with email (no KYC), fund with crypto, and get your API key. Then send SMS via a simple HTTP POST request. No dependencies, no version conflicts, just your Node.js code and our REST API. Minutes to first message.

To send SMS with Node.js you need exactly one HTTPS POST, and Node 18+ ships fetch natively. So this tutorial uses zero dependencies. No SDK means no version drift, no transitive vulnerabilities, and code that transfers to any provider by changing one URL. We use SMSRoute endpoints here (SMSRoute is a no-KYC SMS API with crypto billing: BTC, ETH, USDT, XMR, LTC, and SOL). The signup-to-first-send path takes minutes; the shape is identical anywhere.

// send.js — Node 18+, no dependencies
const API = "https://api.smsroute.cc/sms/send";

export async function sendSms(to, from, message) {
  const res = await fetch(API, {
    method: "POST",
    headers: {
      "Authorization": `Bearer ${process.env.SMSROUTE_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ to, from, message }),
    signal: AbortSignal.timeout(10_000),
  });
  if (!res.ok) throw new Error(`send failed: ${res.status}`);
  return res.json();
}

Two habits in those few lines matter more than any library. The key comes from the environment, never from source. And every network call carries a timeout — a hung OTP send blocks a user's login, which is worse than a failed one.

Retries without double-sending

How can I retry SMS delivery without sending duplicates?

SMSRoute's API includes idempotency keys. Generate a unique key per message attempt; if a retry is needed, send the same key again. Our system detects duplicates and only delivers once. Combined with automatic failover across routes, you get reliable delivery without double charges.

Retries without double-sending — comparison diagram

Naive retry loops re-send messages users already received. The rule: retry only on failures where the message definitely did not go out.

Response Retry? Why
Network error / timeout Yes, with backoff The request may never have arrived
5xx server error Yes, with backoff Provider-side failure before acceptance
429 rate limited Yes, after the Retry-After delay Explicit invitation to retry later
4xx (bad number, auth, balance) No — fix the request Retrying a rejected request re-fails identically
2xx accepted Never The message went out; a retry double-sends
export async function sendWithRetry(to, text, tries = 3) {
  for (let i = 0; i < tries; i++) {
    try {
      return await sendSms(to, text);
    } catch (err) {
      const retriable = err.name === "TimeoutError" ||
                        /send failed: 5\d\d/.test(err.message);
      if (!retriable || i === tries - 1) throw err;
      await new Promise(r => setTimeout(r, 500 * 2 ** i)); // 0.5s, 1s, 2s
    }
  }
}

For OTP, cap end-to-end time. Three retries with backoff already spends ~4 seconds; a login flow should fail over to a resend button rather than keep grinding. The UX ladder in our OTP best-practices guide.

Delivery receipts: the webhook that closes the loop

How do I get SMS delivery receipts via webhook?

SMSRoute sends real-time DLR webhooks to your endpoint. Each webhook includes message status (delivered, failed, etc.), timestamp, and message ID. Just register your callback URL in the dashboard (no polling needed). You'll know exactly when each message reaches its destination.

A queued response means the provider accepted your message, not that a phone received it. Delivery receipts (DLRs) arrive later by webhook. Store the message id at send time, then reconcile.

// webhook.js — receives DLR callbacks
import { createServer } from "node:http";

createServer((req, res) => {
  let body = "";
  req.on("data", c => body += c);
  req.on("end", () => {
    const dlr = JSON.parse(body);      // { id, status, ts }
    // idempotent update keyed on dlr.id — DLRs can arrive twice
    markDelivery(dlr.id, dlr.status);   // "delivered" | "failed" | "expired"
    res.writeHead(200).end();          // ack fast; process async
  });
}).listen(3000);

Testing without spending

Can I test SMS sending without paying?

Yes. SMSRoute gives free test credits on signup. Use them to verify routes, test your Node.js code, and confirm DLR webhooks work (all before funding your account). No credit card required. Only when you're satisfied do you add balance and go live.

Wrap the sender in an interface and inject a fake in CI — the full pattern, including magic failure numbers and one nightly real-credential smoke test, is in our test-numbers and sandbox guide. Your Node test needs three cases: the happy path asserts on response *shape* (id present, status string), the retry path asserts a 500-then-200 sequence sends exactly once successfully, and the webhook path asserts duplicate DLRs update state once. Sixty lines of app code, three tests, and you have an SMS layer you can refactor without fear.

Costs at production time are per message and per destination — budget with the international cost guide, and check the live rate on each country page before a launch rather than assuming US pricing travels. SMSRoute's published route pages list delivery from $0.004/message (premium direct-carrier corridors up to $0.035) with sub-100ms median submission and ~98.6% delivered success (smsroute.cc route pages, 2026). For reference, the GSMA provides global mobile ecosystem standards, and the IETF RFC 3428 defines SIP extensions for instant messaging, which underpin many SMS-over-IP protocols.

FAQ

How do I send an SMS from Node.js without an SDK?
One fetch call: POST the recipient (E.164 format) and message text to your provider's send endpoint with a Bearer key from an environment variable. Node 18+ has fetch built in, so there are zero dependencies. Add a timeout via AbortSignal and you have the core in a dozen lines.
Should I retry failed SMS sends?
Only failures where the message definitely did not send: network errors, timeouts, and 5xx responses — with exponential backoff and a small cap. Never retry success or 4xx responses. This prevents both dropped messages and the double-send problem that makes users distrust your codes.
How do delivery receipts work in Node.js?
Your provider POSTs a callback to your webhook when a message's status changes. Store the message id at send time, update state idempotently when the DLR arrives (duplicates happen), acknowledge with a 200 immediately, and process asynchronously. Alert on failure-rate spikes per destination, not individual failures.
How do I test SMS sending in CI without paying per message?
Inject a fake sender behind the same interface as the real client and assert on response shape, retry behavior, and webhook idempotency. Reserve one real send — to your own phone via test credit — as a pre-release smoke test. No per-commit spend, full coverage of the failure paths.

Send your first SMS in 5 minutes

No KYC. Pay with BTC, ETH, USDT, XMR, LTC, and SOL. Live routes to 149 countries.

Get an API key →