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.
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);
- Ack immediately, process later. A slow webhook endpoint gets your callbacks queued or dropped.
- Be idempotent. Key every update on the message id; duplicate callbacks are normal.
- Alert on
failedrates, not single failures. A spike in failures on one destination usually means a route or number-quality issue. Start with an HLR lookup on the failing numbers. - Time
deliveredminussent. That latency distribution is your real delivery quality. Worth graphing per country.
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.
Related reading
FAQ
How do I send an SMS from Node.js without an SDK?
Should I retry failed SMS sends?
How do delivery receipts work in Node.js?
How do I test SMS sending in CI without paying per message?
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 →