How to send a text message with an API in Node.js and Python (2026)
Editorial team · updated · facts checked
To send a text message with an API, open an account with an SMS gateway provider such as Twilio or Telnyx, get a sender number that is registered for US A2P 10DLC or toll-free verified, keep the API key in server-side environment variables, call the provider's SDK from Node.js or Python, and track delivery with a signed status webhook.
This tutorial follows each vendor's official quickstart, SDK README and API reference, all checked on 23 September 2026. The editorial team did not create accounts, run the code or send messages: every snippet is illustrative and needs your own credentials and numbers.
Before you send: account, number and US registration
- An account and a sender number. Twilio's trial lasts 30 days, includes 100 SMS, sends only to up to 5 verified numbers in your sign-up country and allows only Twilio's pre-defined message templates, so custom bodies like the ones below need an upgraded account (the quickstart shows a separate flow for legacy Console trials). Telnyx's trial gives $5 of credit and limits SMS to long codes, one verified destination and 100 messages a day.
- Registration for US traffic. Twilio's overview says anyone texting US numbers from its 10-digit numbers must register for A2P 10DLC, "individuals and hobbyists" included. Twilio's error 30034 page (updated 21 April 2026) says unregistered messages are blocked, while its A2P 10DLC overview still describes extra carrier fees instead; plan for the block. Toll-free numbers need toll-free verification: unverified ones have been blocked since 31 January 2024 (error 30032). Our A2P 10DLC registration guide covers brands, campaigns, review times and fees at six providers.
- Numbers in E.164 format:
+15558675310, no spaces or dashes.
Both platforms handle opt-outs. A recipient who replies STOP is blocked until they text START: Twilio rejects later sends with error 21610, and Telnyx creates a block rule for STOP, UNSUBSCRIBE and similar keywords across the whole messaging profile.
Keep credentials on the server
An SMS API key can send messages billed to your account, so it never goes into browser code, a mobile app bundle or a public repository. Put it in server-side environment variables or your host's secret store:
Illustrative code, written from the official documentation and not run by the editorial team.
# .env (server only; add it to .gitignore)
TWILIO_ACCOUNT_SID=AC...
TWILIO_API_KEY=SK... # API key SID, used to send
TWILIO_API_SECRET=...
TWILIO_AUTH_TOKEN=... # used only to validate webhook signatures
TWILIO_FROM_NUMBER=+15017122661
TELNYX_API_KEY=KEY...
TELNYX_PUBLIC_KEY=... # Ed25519 public key, used to verify webhooks
TELNYX_FROM_NUMBER=+15551234567
Twilio's quickstart sends with an API key and secret, and restricted API keys can be limited to Messaging permissions. Webhook signatures, though, are computed with the account Auth Token (or a shared key, in Public Beta), so the webhook server needs that token too. Telnyx sends the API key as a Bearer token and documents no permission settings for API keys; scoped machine credentials come from OAuth clients.
Send an SMS with Twilio in Node.js and Python
Source: Twilio SMS developer quickstart, page modified 15 September 2026, checked 23 September 2026. SDKs at check time: twilio 6.1.1 on npm (Node.js 20 or later) and twilio 9.11.1 on PyPI. The quickstart lists Python 3.8–3.13, while the twilio-python README lists 3.10–3.13; use 3.10 or later.

Illustrative code, written from the official documentation and not run by the editorial team.
// send-sms.js (npm install twilio)
const twilio = require("twilio");
const client = twilio(process.env.TWILIO_API_KEY, process.env.TWILIO_API_SECRET, {
accountSid: process.env.TWILIO_ACCOUNT_SID,
});
async function sendSms(to, body) {
const message = await client.messages.create({
body,
from: process.env.TWILIO_FROM_NUMBER,
to,
statusCallback: "https://example.com/sms/status",
});
return message.sid; // save it: status callbacks refer to it as MessageSid
}
sendSms("+15558675310", "Your order #1042 has shipped.").then(console.log);
Illustrative code, written from the official documentation and not run by the editorial team.
# send_sms.py (pip install twilio)
import os
from twilio.rest import Client
client = Client(
os.environ["TWILIO_API_KEY"],
os.environ["TWILIO_API_SECRET"],
os.environ["TWILIO_ACCOUNT_SID"],
)
message = client.messages.create(
body="Your order #1042 has shipped.",
from_=os.environ["TWILIO_FROM_NUMBER"],
to="+15558675310",
status_callback="https://example.com/sms/status",
)
print(message.sid, message.status) # a new message starts as "queued"
Testing without charges. With test credentials (a test Account SID and Auth Token), Twilio "doesn't charge your account" and does not reach real phones. Magic numbers return set results: From +15005550006 passes validation, while To +15005550001 returns 21211, +15005550004 returns 21610 and +15005550009 returns 21614. Test sends never trigger status callbacks, so webhook testing needs a trial or paid account.
Send an SMS with Telnyx in Node.js and Python
Source: Telnyx, Send your first message, checked 23 September 2026. SDKs at check time: telnyx 7.23.0 on npm (Node.js 20 or later) and telnyx 4.181.0 on PyPI (Python 3.9 or later). Assign the sending number to a Messaging Profile first, or the API answers 403.
Illustrative code, written from the official documentation and not run by the editorial team.
// send-sms.mjs (npm install telnyx)
import Telnyx from "telnyx";
const client = new Telnyx({ apiKey: process.env.TELNYX_API_KEY });
const { data: message } = await client.messages.send(
{
from: process.env.TELNYX_FROM_NUMBER,
to: "+15559876543",
text: "Your order #1042 has shipped.",
webhook_url: "https://example.com/sms/telnyx-events",
},
{ maxRetries: 0 }, // see "Retries" below: the SDK retries twice by default
);
console.log(message.id, message.to[0].status); // "queued"
Illustrative code, written from the official documentation and not run by the editorial team.
# send_sms.py (pip install telnyx)
import os
from telnyx import Telnyx
client = Telnyx(api_key=os.environ["TELNYX_API_KEY"], max_retries=0)
response = client.messages.send(
from_=os.environ["TELNYX_FROM_NUMBER"],
to="+15559876543",
text="Your order #1042 has shipped.",
webhook_url="https://example.com/sms/telnyx-events",
)
print(response.data.id, response.data.to[0].status)
Testing. Telnyx publishes no test credentials for messaging, and its pricing docs say trial credit "is not a general nonbillable sandbox". The quickstart has you buy two Telnyx numbers and text between them, which it says needs no carrier registration; its sample response shows a per-message cost. The trial page does not say whether a second Telnyx number counts as the one allowed destination.
Delivery status webhooks and signature checks
The API response only means the provider accepted the message; delivery results arrive later as webhooks.
Twilio posts form-encoded status callbacks as a message moves from queued to sent (or failed), then delivered or undelivered, with an ErrorCode on failures. Callbacks can arrive out of order, so keep the latest status per MessageSid. The X-Twilio-Signature header is an HMAC-SHA1 over the full URL and the sorted POST parameters, keyed with your Auth Token. It carries no timestamp, and no replay window is documented. Use the SDK validators; Twilio advises against hand-written HMAC code.
Illustrative code, written from the official documentation and not run by the editorial team.
// status-webhook.js (npm install express twilio)
const express = require("express");
const twilio = require("twilio");
const app = express();
app.use(express.urlencoded({ extended: false }));
// twilio.webhook() checks X-Twilio-Signature with TWILIO_AUTH_TOKEN; invalid requests get 403
app.post("/sms/status", twilio.webhook(), async (req, res) => {
const { MessageSid, MessageStatus, ErrorCode } = req.body;
await saveStatus(MessageSid, MessageStatus, ErrorCode); // your upsert
res.sendStatus(200);
});
app.listen(3000);
Illustrative code, written from the official documentation and not run by the editorial team.
# status_webhook.py (pip install flask twilio)
import os
from flask import Flask, abort, request
from twilio.request_validator import RequestValidator
app = Flask(__name__)
validator = RequestValidator(os.environ["TWILIO_AUTH_TOKEN"])
@app.post("/sms/status")
def sms_status():
signature = request.headers.get("X-Twilio-Signature", "")
if not validator.validate(request.url, request.form, signature):
abort(403)
save_status(request.form["MessageSid"], request.form["MessageStatus"], request.form.get("ErrorCode"))
return "", 200
Validation fails when the URL your app sees differs from the one Twilio called. Twilio's Express guide names two causes: TLS ending at a proxy in front of the app, and ngrok https during local work.
Telnyx sends JSON events: message.sent, then message.finalized with to[].status of delivered, delivery_failed, sending_failed or delivery_unconfirmed. It signs {timestamp}|{raw body} with Ed25519, expects a 2xx within 2 seconds, and makes up to 3 attempts per URL before trying the failover URL. Events can repeat, so deduplicate on data.id. Verify the raw bytes: the telnyx-node README warns that parsing and re-serializing the body "invalidates the signature". Both SDKs reject timestamps more than 300 seconds off.
Illustrative code, written from the official documentation and not run by the editorial team.
// telnyx-webhook.mjs (npm install express telnyx)
import express from "express";
import Telnyx from "telnyx";
const client = new Telnyx(); // reads TELNYX_API_KEY and TELNYX_PUBLIC_KEY
const app = express();
app.post("/sms/telnyx-events", express.raw({ type: "application/json" }), async (req, res) => {
let event;
try {
event = await client.webhooks.unwrap(req.body.toString("utf8"), { headers: req.headers });
} catch {
return res.sendStatus(401);
}
res.sendStatus(200); // acknowledge fast, then process
if (event.data.event_type === "message.finalized") {
await saveStatus(event.data.id, event.data.payload.id, event.data.payload.to[0].status);
}
});
app.listen(3000);
Illustrative code, written from the official documentation and not run by the editorial team.
# telnyx_webhook.py (pip install flask "telnyx[webhooks]")
from flask import Flask, request
from telnyx import Telnyx
client = Telnyx() # reads TELNYX_API_KEY and TELNYX_PUBLIC_KEY
app = Flask(__name__)
@app.post("/sms/telnyx-events")
def telnyx_events():
try:
event = client.webhooks.unwrap(request.get_data(as_text=True), headers=dict(request.headers))
except ValueError:
return "", 401
if event.data.event_type == "message.finalized":
save_status(event.data.id, event.data.payload.id, event.data.payload.to[0].status)
return "", 200
This code follows the current SDK source on GitHub, not every Telnyx sample. The messaging webhook page calls webhooks.constructEvent (Node.js, on a re-serialized body) and telnyx.Webhook.construct_event (Python), names the current SDKs do not define. In Node.js, unwrap() is async, yet the README and the repository's Express example call it without await, so in that example a failed check escapes its try block.
Retries, idempotency and rate limits
Neither send endpoint documents an idempotency key, so a blind retry after a timeout can text a customer twice.
- Twilio: requests that get error 20429 "aren't processed and are safe to retry after backing off".
twilio-nodedoes this withautoRetry: true(off by default, up to 3 retries). - Telnyx: its retries guide says "Do not automatically retry a timed-out POST" that can send a message. Yet both Telnyx SDKs retry connection errors, 408, 409, 429 and 5xx twice by default, and the quickstart's Python sample retries sends on 5xx. Set
maxRetries: 0ormax_retries=0on sends and retry 429s yourself after theretry-afterheader.

A pattern that works with both: write an outbox row keyed by the business event (for example order-1042-shipped) before sending, store the returned message ID, and after a timeout check that row and the provider's message log before sending again. Log the message ID, status, error code and attempt number. Telnyx's guide adds a rule worth copying: do not log credentials or sensitive payloads, which for SMS we read as message bodies and phone numbers.
Other SMS APIs compared
AI search engines researching this topic also looked up AWS End User Messaging and Vonage; Plivo and Sinch complete the SMS shortlist in our catalog. Facts come from our provider data files, checked on 23 September 2026.
| Provider | Send call | Auth | Webhook signing | Test path |
|---|---|---|---|---|
| AWS End User Messaging | SendTextMessage | IAM keys, SigV4 | Via Amazon SNS: RSA, no freshness window | Sandbox (10 verified numbers), simulator numbers, free DryRun |
| Plivo | POST /v1/Account/{auth_id}/Message/ | Basic (Auth ID, token) | HMAC-SHA256 with nonce, no timestamp | None: self-serve plans exclude SMS |
| Sinch | Conversation API (SMS API: existing customers only) | OAuth 2.0 or Basic | HMAC-SHA256 with timestamp and nonce, if a secret is set | Trial credit, one test number; sandbox only vendor-claimed |
| Telnyx | POST /v2/messages | Bearer API key | Ed25519 with timestamp, 5-minute window | $5 trial credit |
| Twilio | POST /2010-04-01/Accounts/{AccountSid}/Messages.json | API key or Auth Token (Basic) | HMAC-SHA1, no timestamp | Test credentials, magic numbers; 30-day trial |
| Vonage | Messages API POST /v1/messages | Key and secret, or JWT | HS256 JWT with body hash | Trial credit, 4 test numbers, demo text appended |
What 1,000 US texts a month cost
Scenario from our dataset: 1,000 single-segment outbound SMS a month on a registered 10DLC number, with number rental and the lowest monthly campaign fee listed.
| Provider | Per segment | Monthly total | One-time fees |
|---|---|---|---|
| Telnyx | $0.004 | $6.60 | $4.50 brand, $15 campaign review, $1 number |
| Sinch | $0.0078 | $8.80 plus 10DLC fees (listed in a Sinch community article that returned a Cloudflare challenge) | $1 number setup |
| AWS End User Messaging | $0.00774 | $10.74 | $4.50 registration |
| Twilio | $0.0083 | $10.95 | $4.50 brand, $15 vetting |
| Vonage | €0.0102 | €12.63 (billed in EUR) | 4.50 brand, 15 vetting (USD or EUR) |
| Plivo | $0.0077 | $1,000 (SMS only on the Enterprise plan, from $1,000 a month) | $4.50 brand, $15 vetting |
Carrier pass-through fees come on top, excluded because carriers set them. For AT&T, T-Mobile, Verizon and US Cellular, Twilio and Telnyx list $0.0035 to $0.005 per outbound segment, so 1,000 texts add about $3.50 to $5.00 by our arithmetic. The scored comparison is in our SMS API rankings, the US SMS rankings and our list of Twilio alternatives.
Troubleshooting documented error codes
| Code | Provider | Meaning (vendor docs) | Fix |
|---|---|---|---|
| 21211 / 40001 | Twilio / Telnyx | To is not valid E.164 | Add + and the country code |
| 21608 | Twilio | Unverified recipient on a trial (or unapproved compliance profile) | Verify the number, or upgrade |
| 21610 | Twilio | Recipient replied STOP | Stop sending until they text START |
| 21614 | Twilio | Not a mobile number, often a landline | Check the number type |
| 30034; 40301 or 47000 | Twilio; Telnyx | Unregistered 10DLC sender | Finish brand and campaign registration |
| 30032 | Twilio | Unverified toll-free number | Complete toll-free verification |
| 30007 / 40008 | Twilio / Telnyx | Filtered by the carrier or provider | Review content and opt-in |
| 40300 | Telnyx | Used on three pages for: number not on a messaging profile, STOP block, unreachable destination | Read the error title and detail |
| 20429 / HTTP 429 | Twilio / Telnyx | Rate limited | Back off, then retry |
Related guides
- Connect an AI agent to WhatsApp and SMS: webhooks, Meta's AI rules and human handoff
- One-time password (OTP): meaning and how it works
- A2P 10DLC registration guide: brands, campaigns and fees
- Toll-free texting verification: the form, review times and common rejections
- SMS sender ID rules by country, for sending outside the US
- SMS OTP vs WhatsApp, email and voice codes, if you are sending sign-in codes
- Vibe coding security checklist: API keys, OTP abuse, SMS pumping and webhook signatures in AI-built apps
- Best SMS APIs, scored with published weights
- SMS API rankings for the United States
- Twilio SMS API card and Telnyx SMS API card, with every fact sourced
- Query this catalog from an AI agent over our API and MCP server
About this guide
The PingMyUsers editorial team wrote this tutorial for developers adding SMS to a Node.js or Python backend; Sensaria AG in Switzerland operates the directory. Providers do not pay for placement, and none reviewed this page. To report an error, email contact@sensaria.ch with the page and the source that shows the correct fact.
Methodology
On 23 September 2026 we read the Twilio and Telnyx quickstarts, webhook, trial and test-credential pages, SDK READMEs and SDK source, and took SDK versions from npm and PyPI. Both get full examples because each has one quickstart with Node.js and Python samples and signs webhooks sent straight to your endpoint. AWS End User Messaging has fuller free test tools but needs IAM, SigV4 and Amazon SNS. Costs come from our pricing engine; scoring rules are on the methodology page.
Last updated
23 September 2026: first version; all facts and SDK versions checked that day. Next scheduled re-check: March 2027, or sooner if an SDK major version changes.
Frequently asked questions
What is the quickest way to send a text message from Node.js or Python?
Install the provider's SDK and call one method: client.messages.create() on Twilio or client.messages.send() on Telnyx. Twilio's test credentials run that call without charges.
Can I send SMS through an API for free?
Only on trials: Twilio includes 100 SMS for 30 days (pre-defined templates only), Telnyx $5 of credit, both to verified numbers only. None of the six providers above lists a permanent free SMS allowance.
Do I need 10DLC registration to send SMS from my app?
In the US, yes, for local 10-digit numbers; Twilio rejects unregistered 10DLC traffic with error 30034. Toll-free numbers need toll-free verification instead.
How do I know if a text message was delivered?
Set a status webhook URL (Twilio statusCallback, Telnyx webhook_url), verify each request's signature and store the latest status per message ID, such as delivered or undelivered on Twilio.
Is it safe to retry a failed SMS API request?
Retry 429 responses after backing off. After a timeout, check the provider's message log first: neither Twilio nor Telnyx documents an idempotency key for sends.
Can I call an SMS API directly from the browser or a mobile app?
No. Anyone could copy the key and send texts billed to you. Call your own backend, which holds the key in environment variables.
Sources
- Twilio — SMS developer quickstart — checked 23 September 2026
- Twilio — Track the message status of outbound messages — checked 23 September 2026
- Twilio — Outbound message status in status callbacks — checked 23 September 2026
- Twilio — Secure webhooks — checked 23 September 2026
- Twilio — Webhook shared keys (Public Beta) — checked 23 September 2026
- Twilio — Secure your Express app by validating incoming Twilio requests — checked 23 September 2026
- Twilio — Secure your Flask app by validating incoming Twilio requests — checked 23 September 2026
- Twilio — Test credentials — checked 23 September 2026
- Twilio — Free trials — checked 23 September 2026
- Twilio — Restricted API keys — checked 23 September 2026
- Twilio — SMS pricing, United States — checked 23 September 2026
- Twilio — A2P 10DLC overview (registration includes individuals and hobbyists) — checked 23 September 2026
- Twilio — A2P 10DLC registration and campaign fees — checked 23 September 2026
- Twilio — Error 20429: Too many requests — checked 23 September 2026
- Twilio — Error 21608 — checked 23 September 2026
- Twilio — Error 21610: Attempt to send to unsubscribed recipient — checked 23 September 2026
- Twilio — Error 30032: Toll-Free Number Has Not Been Verified — checked 23 September 2026
- Twilio — Error 30034: US A2P 10DLC, message from an unregistered number — checked 23 September 2026
- twilio-node README (auto-retry) — checked 23 September 2026
- twilio-python README (supported Python versions) — checked 23 September 2026
- npm — twilio 6.1.1 — checked 23 September 2026
- PyPI — twilio 9.11.1 — checked 23 September 2026
- Telnyx — Send your first message — checked 23 September 2026
- Telnyx — Receiving webhooks for messaging — checked 23 September 2026
- Telnyx — API retries and reliability — checked 23 September 2026
- Telnyx — Trial account privileges and limitations — checked 23 September 2026
- Telnyx — SMS API pricing — checked 23 September 2026
- Telnyx — Pricing (developer docs: trial credit is not a sandbox) — checked 23 September 2026
- Telnyx — Opt-in/out management — checked 23 September 2026
- Telnyx — Getting started with 10DLC — checked 23 September 2026
- telnyx-node README (webhooks, retries) — checked 23 September 2026
- telnyx-node — webhook verification example — checked 23 September 2026
- telnyx-python README (retries, requirements) — checked 23 September 2026
- telnyx-python — webhooks resource source (unwrap) — checked 23 September 2026
- npm — telnyx 7.23.0 — checked 23 September 2026
- PyPI — telnyx 4.181.0 — checked 23 September 2026
- AWS — SendTextMessage (SMS and Voice v2 API reference) — checked 23 September 2026
- AWS End User Messaging — SMS sandbox — checked 23 September 2026
- AWS End User Messaging — Simulator phone numbers — checked 23 September 2026
- AWS End User Messaging — pricing — checked 23 September 2026
- Plivo — Plans and pricing (Professional vs Enterprise) — checked 23 September 2026
- Plivo — Messages API — checked 23 September 2026
- Plivo — Signature validation (messaging, V2) — checked 23 September 2026
- Vonage — SMS API overview — checked 23 September 2026
- Vonage — Limitations of a trial account — checked 23 September 2026
- Vonage — Signing messages — checked 23 September 2026
- Sinch — Getting started with an SMS channel (Conversation API) — checked 23 September 2026
- Sinch — Conversation API callbacks — checked 23 September 2026
- Sinch — SMS pricing — checked 23 September 2026