PingMyUsers

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

  1. 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.
  2. 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.
  3. 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.

Twilio SMS developer quickstart, Python prerequisites listing Python 3.8–3.13
Twilio's SMS developer quickstart, prerequisites for Python. The page lists Python 3.8–3.13; the SDK README lists 3.10–3.13. Captured by the editorial team on 23 September 2026.

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-node does this with autoRetry: 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: 0 or max_retries=0 on sends and retry 429s yourself after the retry-after header.
Telnyx API retries guide: do not automatically retry a timed-out POST that sends a message
Telnyx's API retries and reliability guide. Captured by the editorial team on 23 September 2026.

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.

ProviderSend callAuthWebhook signingTest path
AWS End User MessagingSendTextMessageIAM keys, SigV4Via Amazon SNS: RSA, no freshness windowSandbox (10 verified numbers), simulator numbers, free DryRun
PlivoPOST /v1/Account/{auth_id}/Message/Basic (Auth ID, token)HMAC-SHA256 with nonce, no timestampNone: self-serve plans exclude SMS
SinchConversation API (SMS API: existing customers only)OAuth 2.0 or BasicHMAC-SHA256 with timestamp and nonce, if a secret is setTrial credit, one test number; sandbox only vendor-claimed
TelnyxPOST /v2/messagesBearer API keyEd25519 with timestamp, 5-minute window$5 trial credit
TwilioPOST /2010-04-01/Accounts/{AccountSid}/Messages.jsonAPI key or Auth Token (Basic)HMAC-SHA1, no timestampTest credentials, magic numbers; 30-day trial
VonageMessages API POST /v1/messagesKey and secret, or JWTHS256 JWT with body hashTrial 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.

ProviderPer segmentMonthly totalOne-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

CodeProviderMeaning (vendor docs)Fix
21211 / 40001Twilio / TelnyxTo is not valid E.164Add + and the country code
21608TwilioUnverified recipient on a trial (or unapproved compliance profile)Verify the number, or upgrade
21610TwilioRecipient replied STOPStop sending until they text START
21614TwilioNot a mobile number, often a landlineCheck the number type
30034; 40301 or 47000Twilio; TelnyxUnregistered 10DLC senderFinish brand and campaign registration
30032TwilioUnverified toll-free numberComplete toll-free verification
30007 / 40008Twilio / TelnyxFiltered by the carrier or providerReview content and opt-in
40300TelnyxUsed on three pages for: number not on a messaging profile, STOP block, unreachable destinationRead the error title and detail
20429 / HTTP 429Twilio / TelnyxRate limitedBack off, then retry

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

  1. Twilio — SMS developer quickstart — checked 23 September 2026
  2. Twilio — Track the message status of outbound messages — checked 23 September 2026
  3. Twilio — Outbound message status in status callbacks — checked 23 September 2026
  4. Twilio — Secure webhooks — checked 23 September 2026
  5. Twilio — Webhook shared keys (Public Beta) — checked 23 September 2026
  6. Twilio — Secure your Express app by validating incoming Twilio requests — checked 23 September 2026
  7. Twilio — Secure your Flask app by validating incoming Twilio requests — checked 23 September 2026
  8. Twilio — Test credentials — checked 23 September 2026
  9. Twilio — Free trials — checked 23 September 2026
  10. Twilio — Restricted API keys — checked 23 September 2026
  11. Twilio — SMS pricing, United States — checked 23 September 2026
  12. Twilio — A2P 10DLC overview (registration includes individuals and hobbyists) — checked 23 September 2026
  13. Twilio — A2P 10DLC registration and campaign fees — checked 23 September 2026
  14. Twilio — Error 20429: Too many requests — checked 23 September 2026
  15. Twilio — Error 21608 — checked 23 September 2026
  16. Twilio — Error 21610: Attempt to send to unsubscribed recipient — checked 23 September 2026
  17. Twilio — Error 30032: Toll-Free Number Has Not Been Verified — checked 23 September 2026
  18. Twilio — Error 30034: US A2P 10DLC, message from an unregistered number — checked 23 September 2026
  19. twilio-node README (auto-retry) — checked 23 September 2026
  20. twilio-python README (supported Python versions) — checked 23 September 2026
  21. npm — twilio 6.1.1 — checked 23 September 2026
  22. PyPI — twilio 9.11.1 — checked 23 September 2026
  23. Telnyx — Send your first message — checked 23 September 2026
  24. Telnyx — Receiving webhooks for messaging — checked 23 September 2026
  25. Telnyx — API retries and reliability — checked 23 September 2026
  26. Telnyx — Trial account privileges and limitations — checked 23 September 2026
  27. Telnyx — SMS API pricing — checked 23 September 2026
  28. Telnyx — Pricing (developer docs: trial credit is not a sandbox) — checked 23 September 2026
  29. Telnyx — Opt-in/out management — checked 23 September 2026
  30. Telnyx — Getting started with 10DLC — checked 23 September 2026
  31. telnyx-node README (webhooks, retries) — checked 23 September 2026
  32. telnyx-node — webhook verification example — checked 23 September 2026
  33. telnyx-python README (retries, requirements) — checked 23 September 2026
  34. telnyx-python — webhooks resource source (unwrap) — checked 23 September 2026
  35. npm — telnyx 7.23.0 — checked 23 September 2026
  36. PyPI — telnyx 4.181.0 — checked 23 September 2026
  37. AWS — SendTextMessage (SMS and Voice v2 API reference) — checked 23 September 2026
  38. AWS End User Messaging — SMS sandbox — checked 23 September 2026
  39. AWS End User Messaging — Simulator phone numbers — checked 23 September 2026
  40. AWS End User Messaging — pricing — checked 23 September 2026
  41. Plivo — Plans and pricing (Professional vs Enterprise) — checked 23 September 2026
  42. Plivo — Messages API — checked 23 September 2026
  43. Plivo — Signature validation (messaging, V2) — checked 23 September 2026
  44. Vonage — SMS API overview — checked 23 September 2026
  45. Vonage — Limitations of a trial account — checked 23 September 2026
  46. Vonage — Signing messages — checked 23 September 2026
  47. Sinch — Getting started with an SMS channel (Conversation API) — checked 23 September 2026
  48. Sinch — Conversation API callbacks — checked 23 September 2026
  49. Sinch — SMS pricing — checked 23 September 2026