textbee Logotextbee.dev
Save 44% with yearly billing.View Plans
How to Integrate an SMS Gateway into Your App (Step by Step, 2026)

How to Integrate an SMS Gateway into Your App (Step by Step, 2026)

SMS gateway integration in six steps: pick a gateway, get an API key, send from your backend, receive replies by webhook, track delivery status, and go to production. Code in curl, Node.js and Python.

TT

textbee team

10 min read
Share

TL;DR

Integrating an SMS gateway means six things: choose a gateway, get credentials, send a message from your backend, receive replies and delivery events through a webhook, store the status, and harden the setup before production. Every gateway follows this shape, so the steps below are generic. The code uses textbee as the worked example because it needs no number rental, no carrier registration and no card, so you can finish the whole tutorial in an afternoon on the free tier.

What an SMS gateway integration looks like

An SMS gateway is the service that takes an HTTP request from your app and turns it into a text message on a phone network, then does the reverse for replies. From your code's point of view, every gateway is the same four surfaces:

SurfaceWhat it isWhere it lives in your app
Send endpointAn HTTPS call with recipients and a messageBackend, never the browser
WebhookThe gateway calls your URL when a reply arrives or a status changesA public route on your backend
Message historyAn endpoint to list or poll messagesBackend jobs, admin screens
CredentialsAn API key or tokenEnvironment variables

The difference between gateways is what sits behind the send endpoint. A cloud provider (Twilio, Vonage, Plivo, Sinch and similar) rents you a virtual number and bills per message plus carrier fees. A phone-based gateway like textbee runs an app on an Android phone with a SIM and sends from that number, so the message costs whatever your phone plan charges and there is no number to rent or register. Both expose the four surfaces above.

Step 1: Choose the gateway

Pick on four questions, in this order:

  1. Which countries do you send to? Cloud providers publish per-country rates and rules. A phone-based gateway works wherever the phone's SIM works.
  2. What volume? Under a few thousand messages a month, a flat plan or a free tier beats per-message billing. Above tens of thousands a month, or with strict delivery SLAs, a cloud provider's carrier routes are built for it.
  3. Do you need your own number? With a cloud provider the number is rented. With a phone-based gateway the sender is your SIM, so recipients see a number they can call back.
  4. Do you need two-way? Confirm the gateway can receive and can push inbound messages to a webhook, not only poll.

For the rest of this guide the answer is textbee: any country where you have a SIM, free for 300 messages a month, your own number, two-way with webhooks. The best SMS APIs for developers post compares the cloud options if your volume or SLA rules a phone out.

Step 2: Get credentials and a sender

Every gateway starts with two things: an API key, and a sender (a rented number, a registered sender ID, or a linked phone).

For textbee:

  1. Create an account at textbee.dev. No card.
  2. Install the Android app on a phone with a SIM and link it by scanning the QR code in the dashboard. That phone is your sender.
  3. In the dashboard, create an API key.

Put the key in an environment variable. Never ship it to a browser or a mobile client; anyone who can read it can send from your number.

Shell
export TEXTBEE_API_KEY="your_key_here"

Step 3: Send your first message from the backend

The send call is one POST. Recipients are an array in E.164 format (a plus sign, country code, number, no spaces).

Shell
curl -X POST https://api.textbee.dev/api/v1/gateway/send-sms \
  -H "x-api-key: $TEXTBEE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"recipients": ["+15550100123"], "message": "Your order 4821 has shipped."}'

The response returns a batch id you keep for status lookups:

JSON
{
  "data": {
    "success": true,
    "message": "SMS added to queue for processing",
    "smsBatchId": "abc123",
    "recipientCount": 1
  }
}

The same call in Node.js with the official SDK:

JavaScript
import { Textbee } from '@textbee/sdk'

const textbee = new Textbee({ apiKey: process.env.TEXTBEE_API_KEY })

const { smsBatchId } = await textbee.sendSms({
  recipients: ['+15550100123'],
  message: 'Your order 4821 has shipped.',
})

And in Python with requests:

Python
import os
import requests

resp = requests.post(
    "https://api.textbee.dev/api/v1/gateway/send-sms",
    headers={"x-api-key": os.environ["TEXTBEE_API_KEY"]},
    json={"recipients": ["+15550100123"], "message": "Your order 4821 has shipped."},
    timeout=10,
)
resp.raise_for_status()
batch_id = resp.json()["data"]["smsBatchId"]

Three habits that save you later, whatever the gateway:

  • Wrap the send in one function (sendSms(to, body)) that every part of your app calls. Swapping gateways then touches one file.
  • Store the batch or message id the gateway returns next to the record that caused the send (the order, the login attempt, the appointment). You will need it to reconcile status.
  • Accept that send is asynchronous. A 2xx means "queued", not "delivered". Delivery arrives later through step 4 or step 5.

Step 4: Receive replies and delivery events with a webhook

A webhook is a public URL on your backend that the gateway POSTs to. For textbee, create one in the dashboard (Webhooks, add a URL, pick the events) and copy the signing secret. There are four events:

EventWhen
MESSAGE_RECEIVEDA text arrived on the phone
MESSAGE_SENTThe phone handed a message to the network
MESSAGE_DELIVEREDThe network confirmed delivery
MESSAGE_FAILEDThe phone could not send it

An Express handler that verifies the signature, deduplicates on the idempotency key, and answers fast:

JavaScript
import crypto from 'node:crypto'
import express from 'express'

const app = express()
const seen = new Set()

app.post('/webhooks/sms', express.raw({ type: 'application/json' }), (req, res) => {
  const expected = crypto
    .createHmac('sha256', process.env.TEXTBEE_WEBHOOK_SECRET)
    .update(req.body)
    .digest('hex')
  if (req.get('x-signature') !== expected) return res.sendStatus(401)

  const event = JSON.parse(req.body)
  if (seen.has(event.idempotencyKey)) return res.sendStatus(200)
  seen.add(event.idempotencyKey)

  if (event.webhookEvent === 'MESSAGE_RECEIVED') {
    // event.sender, event.message, event.receivedAt
  } else {
    // MESSAGE_SENT, MESSAGE_DELIVERED, MESSAGE_FAILED: update the row by event.smsId
  }
  res.sendStatus(200)
})

Replace the in-memory Set with your database in production. Two rules apply to every gateway's webhooks:

  • Return 200 quickly and do the work after. Gateways retry on timeouts, so slow handlers get duplicate events.
  • Verify the signature on the raw body, before any JSON parsing. Re-serialized JSON can change key order and break the HMAC.

During development, expose your local server with ngrok or a similar tunnel and paste the tunnel URL into the webhook settings. The inbound SMS webhook guide walks through that and has a Flask version of the handler.

Step 5: Track delivery status

Webhooks tell you about status changes as they happen. You also want a way to ask, for reconciliation jobs, admin pages and the case where a webhook was missed. That is the message history endpoint.

Shell
curl "https://api.textbee.dev/api/v1/gateway/messages?smsBatchId=abc123" \
  -H "x-api-key: $TEXTBEE_API_KEY"

Each message carries a status: pending, dispatched, sent, delivered, failed, unknown for outbound, or received for inbound. Map them to three states in your own table: in flight (pending, dispatched, unknown), done (sent, delivered), and needs attention (failed). Do not treat sent as delivered; the first is the phone handing off to the network, the second is the network confirming.

For polling (an inbox screen, a job that syncs replies), follow the cursor instead of page numbers so a message that arrives mid-read is never skipped or repeated:

JavaScript
for await (const message of textbee.iterateMessages({ direction: 'received' })) {
  // process message, then persist the last seen id
}

Step 6: Go to production

A checklist that applies to any SMS gateway integration, with the textbee-specific notes:

  1. Idempotency on your side. If your app retries a failed HTTP call, it can send the same text twice. Generate a key per intended message and refuse to send it twice.
  2. Rate and quota handling. The gateway returns 429 when you exceed a limit (on textbee: 50 a day and 300 a month on the free tier, higher on paid plans). Queue and back off instead of retrying in a tight loop.
  3. Throughput. A phone sends roughly 10 to 15 messages a minute, one at a time. A batch of 500 takes most of an hour. Use it for transactional traffic and small campaigns; add devices on a paid plan to spread load.
  4. Consent and content. Transactional messages (codes, receipts, reminders) are fine. Marketing needs opt-in under local rules, and carriers filter spam-like patterns on consumer SIMs. Include your business name in the first message.
  5. Monitoring. Alert when failed rises, when the phone goes offline (textbee shows device status in the dashboard), and when webhook deliveries fail (the dashboard keeps a delivery log).
  6. Secrets and environments. One API key per environment, stored in a secrets manager, rotated when someone leaves.
  7. Fallback. If SMS is on the critical path (login codes), keep a second channel: email OTP, a TOTP app, or a second linked device.

Common mistakes

  • Sending from the frontend. The API key ends up in the browser. Route every send through your backend.
  • Treating the send response as delivery. Queued is not delivered. Wire the webhook or poll the status.
  • Ignoring number format. 5550100123 fails; +15550100123 works. Normalize to E.164 at the input boundary.
  • Skipping signature checks. An unverified webhook is a public endpoint that anyone can post fake replies to.
  • Blasting a consumer SIM. Thousands of identical messages in an hour get filtered. Pace them, vary the copy, and use the flat plan's extra devices.

Frequently asked questions

How long does an SMS gateway integration take?

Sending takes minutes once you have credentials. A production-grade integration with webhooks, status tracking and retries is a day or two of work for one developer with any gateway, and the code above covers most of it.

Do I need a virtual number to integrate an SMS gateway?

With cloud providers, yes: you rent a number or register a sender ID, and in the US you also register the number for A2P traffic. With a phone-based gateway you use the SIM already in the phone, so there is nothing to rent or register.

Can I integrate an SMS gateway without a backend?

Not safely. The API key must stay server-side. If you have no backend, use a no-code tool that stores the key for you; the n8n, Zapier and Make guide shows the same send and receive steps without code.

How do I receive SMS in my app?

Enable receiving on the linked phone, create a webhook for MESSAGE_RECEIVED, and handle the POST as in step 4. If you would rather pull than be pushed, poll GET /gateway/messages?direction=received with the cursor.

What does it cost to send SMS through the gateway?

On textbee, the platform side is $0 for up to 300 messages a month, then a flat $14.99 for up to 5,000. The messages themselves cost whatever your SIM plan charges for a text. Cloud providers bill per message plus number rental and carrier fees; the best SMS APIs comparison has current numbers.

Next steps

Get more textbee in Google Search

Add textbee.dev as a preferred source. Our posts show up more often in your Google results.

Add as a preferred source