SMS webhook
Get the SMS your phone receives
as a webhook, on your own number.
Inbound SMS APIs usually start with renting a new number. Install a free Android app instead: each text your SIM receives reaches your URL as signed JSON, and you answer from that same number.
In a hurry? Summarize this article with an AI
The short answer
Can you receive SMS on your own number by webhook?
Yes, with an Android phone. The free Unlimited Messaging app links your phone's SIM to your account, and every SMS it receives after sign-in is posted to your URL as a signed JSON event, message.received. People keep texting the number they know, and you pay no fee per incoming SMS. Unlike Twilio, there is no number to rent. Webhooks are on the paid Medium and Max plans.
The classic way to receive SMS by API is an A2P provider. With Twilio you lease a number ($1.15 a month for a US local number), point its webhook at your server, and pay $0.0083 per inbound segment plus a carrier fee, $0.0035 from AT&T or $0.0025 from T-Mobile. The number is new, so your customers have to learn it, and Twilio posts form-encoded parameters signed with HMAC-SHA1.
An Android gateway skips the rental. Unlimited Messaging for Android is free and open source (MIT): you install the APK, sign in, and each SIM becomes an SMS account (dual SIM, two accounts). The phone keeps a connection to the API, uploads each SMS it receives, and the API announces it to your webhook in the same JSON envelope it uses for WhatsApp and email. A retried upload is recognized and never announced twice.
The catch is physical: the phone must stay on, online and allowed to run in the background. It handles the texts of one business at a human pace, not the inbound traffic of a national short code.
Compare first: the four Android SMS gateway apps, and what a free SMS API really costs.

The events
Which SMS events does the webhook send?
SMS uses the same event types as the other channels. An endpoint only receives the types in its eventTypes list.
message.received
A text arrived on one of your SIMs. data.channel.type is SMS, content holds the text, sender the number in +E.164. A sender the carrier only names (a bank, a delivery service) comes with sender null and that name in senderId.
message.sent
An SMS you sent through the API left the phone. Carries the text and the recipient, fired once, so your inbox can show both sides.
message.status
An SMS you sent moved to SENT, DELIVERED (when the carrier sends delivery reports), FAILED or UNDELIVERABLE, with error saying why. SMS has no read receipts, so never READ.
account.status_changed
A SIM account changed status, for instance disconnected when the app was signed out. Subscribe to it to know when the phone stops relaying.
{
"type": "message.received",
"eventId": "whd_2b9e7c41-0d3a-4f6e-8c52-91a7d4e0b6f3",
"occurredAt": "2026-11-09T08:42:06.000Z",
"data": {
"message": {
"id": "msg_7d2c9a10-5e4b-4b8f-a1c3-0f6e2d9b8a47",
"externalId": "c1f4a9e27b3d4e6f9a0b",
"direction": "IN",
"fromMe": false,
"content": "Running 10 min late for my 9:00, sorry!",
"sentAt": "2026-11-09T08:42:03.000Z",
"receivedAt": "2026-11-09T08:42:06.000Z",
"sender": "+447700900456",
"senderId": "+447700900456",
"senderName": null,
"participant": null,
"replyTo": null,
"transcript": null
},
"channel": {
"type": "SMS",
"account": { "id": "acc_5e8d1f2a-3c4b-4d6e-9f70-1a2b3c4d5e6f", "name": "Front desk SIM" },
"conversation": { "id": null, "isGroup": false, "address": "+447700900456" }
}
}
}Set it up
How do you receive incoming SMS in Node.js?
Install the app, register an endpoint, verify each POST. The same handler works for WhatsApp and email later.
- 01
Install the Android app
Download the APK from GitHub (Android 8.0 or newer), sign in, grant SMS access and unrestricted battery. Each SIM appears as an SMS account.
- 02
Register the endpoint
POST /webhooks with your URL and eventTypes, or add it on the Webhooks page of the dashboard. The secret (whsec_...) is shown once.
- 03
Verify, acknowledge, act
Check the webhook-signature header, return 2xx within 10 seconds, deduplicate on eventId, then reply or store.
curl https://api.unlimitedmessaging.app/webhooks \
-H "Authorization: Bearer $UM_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com/webhooks/um",
"eventTypes": ["message.received", "message.status"],
"description": "Incoming SMS"
}'
# The response carries "secret": "whsec_..." once. Store it as UM_WEBHOOK_SECRET.import crypto from 'node:crypto';
import express from 'express';
import { UnlimitedMessagingApiClient } from '@unlimited-messaging/sdk';
const um = new UnlimitedMessagingApiClient({ token: process.env.UM_API_KEY! });
const key = Buffer.from(process.env.UM_WEBHOOK_SECRET!.replace(/^whsec_/, ''), 'base64');
const seen = new Set<string>(); // use your database in production
// Standard Webhooks: HMAC-SHA256 of "id.timestamp.rawBody", base64.
function verify(raw: string, id: string, ts: string, header: string) {
if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;
const expected = crypto.createHmac('sha256', key).update(`${id}.${ts}.${raw}`).digest('base64');
return header.split(' ').some((entry) => {
const sig = entry.startsWith('v1,') ? entry.slice(3) : '';
return sig.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
});
}
const app = express();
app.post('/webhooks/um', express.raw({ type: 'application/json' }), async (req, res) => {
const raw = req.body.toString('utf8');
const ok = verify(raw, req.get('webhook-id') ?? '', req.get('webhook-timestamp') ?? '',
req.get('webhook-signature') ?? '');
if (!ok) return res.sendStatus(401);
res.sendStatus(200); // answer within 10 s, work afterwards
const event = JSON.parse(raw);
if (seen.has(event.eventId)) return; // at-least-once delivery
seen.add(event.eventId);
if (event.type === 'message.received' && event.data.channel.type === 'SMS') {
const { sender, content } = event.data.message;
if (content?.trim().toUpperCase() === 'C') {
// Texts back from the same SIM that received the SMS.
await um.messages.messageSend({
to: { phone: sender },
channels: [{ channel: 'SMS', accountId: event.data.channel.account.id }],
text: 'Thanks, your appointment is confirmed.',
});
}
}
});
app.listen(3000);Vibe coding
Building with Cursor, Claude Code, Lovable or Bolt? Paste this
Your AI editor gets the route, the signature scheme and the SMS event shape in one go.
Add an Unlimited Messaging webhook to this app so it receives the SMS my own Android phone receives.
1. Create a POST route at /webhooks/um that reads the RAW request body.
2. Verify the Standard Webhooks signature: key = base64-decode(UM_WEBHOOK_SECRET without "whsec_"); expected = base64(HMAC-SHA256(key, `${webhook-id}.${webhook-timestamp}.${rawBody}`)); accept if any space-separated entry of the webhook-signature header equals `v1,${expected}` (constant-time compare); reject timestamps more than 5 minutes off; return 401 otherwise.
3. Return 200 at once, then process. Deduplicate on body.eventId.
4. On type "message.received" with data.channel.type "SMS", save data.message.sender (+E164, or null with the name in senderId), data.message.content and data.message.sentAt. conversation.id is null for SMS: group by sender.
5. To reply by SMS, install @unlimited-messaging/sdk, read UM_API_KEY from env, and call `client.messages.messageSend({ to: { phone: sender }, channels: [{ channel: 'SMS', accountId: data.channel.account.id }], text })`.
6. Put UM_API_KEY and UM_WEBHOOK_SECRET in .env and .env.example, never in code.
Docs: https://docs.unlimitedmessaging.app (Webhooks). Register the URL with POST https://api.unlimitedmessaging.app/webhooks and eventTypes ["message.received", "message.status"].What people build
What can you do with incoming SMS on your own number?
Whatever starts with a customer texting the number on your van, your flyer or your invoice.
Reply-to-confirm reminders
Send a reminder by SMS, read the C or the R that comes back, update the booking and confirm, all from your usual number.
SMS into your CRM
Attach each incoming text to the contact by sender, next to their WhatsApp and email, since all three arrive in the same envelope.
STOP handling
Catch STOP or UNSUBSCRIBE in content and flag the contact before your next send.
AI first answer
Hand the text to an LLM for a draft, send it back by SMS or route it to a human.
Alerts from machines and services
Forward texts from an alarm, a sensor SIM or a supplier to Slack or email.
No-code flows
Point the webhook at n8n, Make or Zapier and branch on content without a server.
The table
Receiving SMS by webhook: Twilio vs Android gateways vs Unlimited Messaging
A rented number from a provider, or the SIM already in your phone.
| TwilioRented number (US) | SMSGate / httpSMSOpen source Android gateways | Unlimited MessagingAndroid app, paid plans | |
|---|---|---|---|
| Number that receives | A number you lease from Twilio | Your phone's SIM | Your phone's SIM |
| Monthly number cost | $1.15 local, $2.15 toll-free | None beyond your carrier plan | None beyond your carrier plan; webhooks need Medium or Max |
| Cost per incoming SMS | $0.0083 per segment plus a carrier fee | None | None from Unlimited Messaging |
| Payload | Form-encoded, X-Twilio-Signature (HMAC-SHA1) | Webhook or callback URL (SMSGate: sms:received event) | JSON, Standard Webhooks signature (HMAC-SHA256) |
| Hardware | None | An Android phone kept on | An Android 8.0+ phone kept on |
| WhatsApp and email in the same webhook | WhatsApp via a registered sender; email via SendGrid, separately | SMS only | Yes, same envelope, same signature |
| Volume | Built for high volume | One phone's pace | One phone's pace |
Checked on 9 November 2026 on Twilio's US SMS pricing, its incoming message and webhook security docs, and the SMSGate and httpSMS repositories (links in the sources). Carrier fees vary by sending carrier.
The catch
What are the limits of an SMS webhook from your phone?
No rented number and no per-SMS fee, but a phone is a phone.
Webhooks are a paid feature
On the Free plan, incoming SMS show up in the web app inbox. Pushing them to your server needs Medium (€19 a month, or €17 a month billed yearly) or Max.
The phone must stay on and online
If it is off, out of signal or killed by battery saving, nothing is relayed until it is back. Keep the app's battery unrestricted, and allow auto-start on Samsung, Xiaomi, Huawei, Oppo and Vivo.
Android only, outside Google Play
Google Play grants SMS permissions only to the default SMS app, so the app ships as a signed APK from GitHub. There is no iPhone version.
Text only, from sign-in onward
SMS is text only (no MMS). Only messages received after the app was signed in are uploaded: the texts already on the phone are never read.
A personal line, a personal pace
Consumer plans are sold for person-to-person use, and replies count against your plan. For thousands of inbound texts an hour or a short code, use an A2P provider.
FAQ
Questions about SMS webhooks
Sources
Sources & references
To check the information in this article for yourself:
- SMS pricing in the United States (inbound, numbers, carrier fees)Twilio
- Incoming message webhook request (form-encoded parameters)Twilio Docs
- Webhooks security (X-Twilio-Signature, HMAC-SHA1)Twilio Docs
- SMS Gateway for Android (sms:received webhook, Apache-2.0)GitHub - capcom6
- httpSMS (forwards received SMS to your server, AGPL-3.0)GitHub - NdoleStudio
- Unlimited Messaging for Android (MIT)GitHub - UnlimitedMessaging
- Standard Webhooks specificationstandardwebhooks.com
- Unlimited Messaging API reference - WebhooksUnlimited Messaging

Written by
Ilian · Senior Software Engineer
A senior software engineer, he founds and runs several tech companies. He builds products around messaging APIs, automation and AI integration, and shares here what he learns while building.
Your own number, wired to your code.
Create an account, install the free Android app, and turn on webhooks with Medium or Max. Sending SMS stays free on every plan.