SMS to Webhook — receive text messages as HTTP POST
Every inbound SMS to a DIDHub number can fire a signed HTTPS webhook to your endpoint within milliseconds. JSON payload, HMAC signature, configurable retries, and per-DID delivery routing. The foundation for two-way SMS, AI auto-responders, CRM/help-desk integration, and any custom messaging workflow. Free with the DID rental.
Why route SMS to a webhook
Webhooks are the high-leverage delivery mode — everything you can imagine doing with an inbound SMS becomes possible once it lands as a POST request on your server. Two-way conversations, AI agents, CRM auto-tickets, internal alerting, multi-step workflows, custom analytics — all just code, not phone-system configuration.
Common use cases
- Two-way SMS — webhook fires → your handler decides what to reply → calls the DIDHub send-SMS API. Real conversations from a single SaaS UI.
- AI auto-responder — route the message body into an LLM (OpenAI, Anthropic, your own), generate a reply, send it back — all under 2 seconds.
- CRM ticket creator — webhook handler creates a ticket in Zendesk/Freshdesk/Salesforce/HubSpot with the SMS body, sender, and DID context.
- Slack / Teams / Discord — pretty-print the SMS into a channel message with a "Reply" button that calls back to your handler.
- Survey / poll responses — SMS reply ("YES", "NO", "STOP") routes to a tally store via webhook.
- Two-factor / OTP capture — receive verification codes, parse with regex, trigger downstream auth flow.
- Voice agent escalation — AI voice agent emits "I'll text you the details" — webhook delivers the customer's reply back into the agent's conversation context.
- Compliance archival — every inbound SMS auto-logged to S3/Postgres for SOC 2 / HIPAA / FINRA retention.
Webhook payload schema
DIDHub POSTs a JSON body to your endpoint. The shape is stable — new fields are additive. The full payload:
{
"event": "sms.received",
"id": "msg_a4f9c2b1d8e3f5",
"timestamp": "2026-05-07T14:32:18.412Z",
"to": "+14155550123",
"from": "+14155554567",
"country": "US",
"body": "Hi, I'm looking to port 12 numbers.",
"encoding": "GSM-7",
"segments": 1,
"mms": [
{
"url": "https://media.didhub.io/m/abc123.jpg?sig=...",
"mime": "image/jpeg",
"size": 348291,
"expires_at": "2026-05-14T14:32:18Z"
}
],
"meta": {
"did_id": "did_3f8c1a2",
"trunk_id": "trunk_a4f9c2",
"upstream_carrier": "voxbone",
"sender_country": "US",
"smpp_message_id": "4831923859"
}
}
Field reference
| Field | Type | Description |
|---|---|---|
event | string | Always sms.received for inbound. Other events: sms.delivered, sms.failed for outbound DLRs. |
id | string | DIDHub-assigned unique message ID. Stable, idempotent — safe to use as a primary key. |
timestamp | RFC 3339 | When DIDHub received the SMS from the upstream carrier (UTC). |
to | E.164 | Your DIDHub number that received the message. |
from | E.164 or alphanumeric | Sender. Numeric for normal mobile-originated SMS; alphanumeric for sender-ID messages (some markets). |
country | ISO 3166-1 alpha-2 | Country of the receiving DID. |
body | string (UTF-8) | Decoded message text. Always UTF-8; the source encoding is in encoding. |
encoding | string | GSM-7, UCS-2, or Latin-1. Useful for billing math and encoding-aware features. |
segments | integer | How many SMS segments the message used. Long messages span multiple segments — concatenated transparently. |
mms | array | MMS attachments (zero or more). Each has url (signed, expires after 7 days), mime, size, expires_at. |
meta | object | Extended fields (DID/trunk IDs, upstream carrier, raw SMPP/SS7 IDs). Use for debugging and advanced routing. |
Code samples
Node.js (Express)
import express from 'express'; import crypto from 'node:crypto'; const app = express(); app.use(express.json({ verify: (req, res, buf) => { req.rawBody = buf; } })); app.post('/sms/inbound', (req, res) => { // 1. Verify HMAC signature const sig = req.headers['x-didhub-signature']; const expected = crypto.createHmac('sha256', process.env.DIDHUB_WEBHOOK_SECRET) .update(req.rawBody).digest('hex'); if (!crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) { return res.status(401).end(); } // 2. Process the SMS const { id, from, to, body, mms } = req.body; console.log(`SMS from ${from} to ${to}: ${body}`); // 3. Acknowledge fast (handle work async) res.status(200).json({ ok: true }); }); app.listen(3000);
Python (FastAPI)
from fastapi import FastAPI, Request, HTTPException import hmac, hashlib, os app = FastAPI() SECRET = os.environ["DIDHUB_WEBHOOK_SECRET"].encode() @app.post("/sms/inbound") async def inbound_sms(request: Request): raw = await request.body() sig = request.headers.get("x-didhub-signature", "") expected = hmac.new(SECRET, raw, hashlib.sha256).hexdigest() if not hmac.compare_digest(sig, expected): raise HTTPException(401) payload = await request.json() # payload["from"], payload["to"], payload["body"], payload["mms"] return {"ok": True}
Go (net/http)
package main import ( "crypto/hmac" "crypto/sha256" "encoding/hex" "encoding/json" "io" "net/http" "os" ) type SMS struct { ID, From, To, Body string } func handler(w http.ResponseWriter, r *http.Request) { body, _ := io.ReadAll(r.Body) mac := hmac.New(sha256.New, []byte(os.Getenv("DIDHUB_WEBHOOK_SECRET"))) mac.Write(body) if hex.EncodeToString(mac.Sum(nil)) != r.Header.Get("X-DIDHub-Signature") { http.Error(w, "bad sig", 401); return } var sms SMS json.Unmarshal(body, &sms) // process sms.From, sms.Body, etc. w.WriteHeader(200) }
The DIDHub send-SMS API uses the same JSON shape in reverse — once you've got the inbound handler working, sending a reply is a single POST /v1/sms with {from, to, body}.
Webhook security
HMAC-SHA256 signatures
Every webhook DIDHub sends is signed with HMAC-SHA256 using a per-account secret. The signature is in the X-DIDHub-Signature header, computed over the raw request body. Verify it before trusting any payload — this prevents anyone who knows your URL from impersonating DIDHub.
IP allowlist
If your firewall supports it, restrict inbound to DIDHub's webhook origin IP ranges (published at https://didhub.io/.well-known/webhook-origins). Combined with HMAC verification this is defense-in-depth — spoofed source IPs still fail signature check, but allowlisting eliminates the need to even process spoofed traffic.
HTTPS only
DIDHub refuses to deliver to plain-HTTP endpoints. Use a real TLS cert (Let's Encrypt is fine). Self-signed certs are accepted in test mode only.
Replay protection
The id field is unique per message. Maintain a recent-message-IDs cache (Redis, in-memory LRU) and ignore duplicate IDs; DIDHub may resend a message if your server didn't ACK with 2xx in time, and you don't want to double-process.
Per-DID secrets (optional)
By default a single account-level secret signs all webhooks. For multi-tenant systems, set per-DID secrets so each customer's webhook is signed differently — a leaked secret then only affects one customer's traffic.
Retries, idempotency, and timeouts
DIDHub waits up to 5 seconds for your webhook to respond with HTTP 2xx. If it times out, returns 5xx, or fails to connect, DIDHub retries with exponential backoff:
| Attempt | Delay |
|---|---|
| 1 (initial) | 0s |
| 2 | +5s |
| 3 | +30s |
| 4 | +2 min |
| 5 | +10 min |
| 6 | +1 hour |
| 7-12 | +6 hours each (up to 36h total) |
After all retries fail the message is marked delivery_failed in the dashboard and you can manually re-trigger from the message log. 4xx responses (your handler explicitly rejecting the message) do not retry — treat them as final.
Best practice: respond 200 immediately and queue the SMS for async processing. A handler that takes 5 seconds to do real work blocks the next inbound from the same trunk — a fast-ACK + worker queue pattern keeps throughput high under load.
Outbound delivery receipts
If you also use DIDHub for outbound SMS (POST /v1/sms), you get matching delivery-receipt webhooks for free:
{
"event": "sms.delivered",
"id": "msg_b8e2d1f4",
"reference": "your-internal-id-here",
"to": "+14155554567",
"status": "delivered",
"delivered_at": "2026-05-07T14:32:23.812Z",
"latency_ms": 1247
}
Status values: queued, sent, delivered, failed, rejected. The same webhook URL receives both inbound and DLR events — differentiate via the event field.
Country availability
Webhook delivery works on every DID where DIDHub supports inbound SMS — same coverage as SMS-to-email. 60+ countries, all major markets.
Latency varies by upstream carrier and origination network: typical end-to-end is 200-800ms in North America and Europe, 500-2000ms in MENA / India / APAC, depending on the SS7 / SMPP route.
Pricing
Free. Webhook delivery, retries, signed payloads, MMS URL hosting (signed CDN with 7-day TTL), DLR callbacks, and the per-DID configuration UI are all included in the monthly DID rental. There are no per-message charges for receiving SMS, no charges for fired webhooks, and no charges for the storage of the message log.
Outbound SMS sent in response is billed per message at carrier wholesale plus DIDHub markup. See the calling rates page for per-country SMS rates.
FAQ
How fast does a webhook fire after the SMS arrives?
Typical latency from sender's tap to your endpoint receiving the POST is 200-800ms in North America/Europe, 500-2000ms in MENA/India/APAC. The bottleneck is the upstream SS7/SMPP carrier — DIDHub's webhook dispatch adds <50ms.
What if my server is down when an SMS arrives?
DIDHub retries with exponential backoff for up to 36 hours. After 12 attempts the message is marked failed and you can re-trigger from the dashboard. No SMS is lost — just delivery is delayed until your server's back.
How do I avoid double-processing a retried webhook?
The id field is stable across retries. Cache the last N message IDs (Redis, LRU, or a unique constraint on a DB column) and skip duplicates. Most teams use a 24-hour window since DIDHub stops retrying at 36h.
What's the max payload size?
SMS body is at most 1600 characters (10 segments × 160 chars). MMS attachments are up to 10 per message, max 5 MB each — the URL is in the payload, you fetch on demand. Total JSON payload size is bounded at ~2 KB for SMS-only, <5 KB with MMS metadata.
Can I have different webhook URLs per DID?
Yes. Each DID has its own routing config — useful for multi-tenant systems where each customer's number routes to their own dedicated handler. You can also do account-wide default + per-DID overrides for simpler setups.
How do I test the webhook before going live?
The DIDHub dashboard has a Send test webhook button on the per-DID routing tab — fires a sample sms.received payload to your URL with a real signature so you can validate the handler end-to-end. Replays of historical messages are also supported from the message log.
Does the webhook work with serverless functions?
Yes — AWS Lambda, Cloudflare Workers, Vercel Functions, Google Cloud Run all work fine. Cold-start latency is the only consideration: aim for <5 second total response time including cold start, otherwise DIDHub retries.
Can I use SMS-to-webhook AND SMS-to-email on the same number?
Yes — both can be configured simultaneously. Inbound SMS fans out to both your webhook AND the email recipients in parallel. Common pattern for keeping a human-readable inbox while feeding a CRM.
Is there an SDK?
Not currently — the webhook is plain JSON over HTTPS, so any HTTP framework in any language works. We may publish thin wrappers (Node, Python, Go) once API surface stabilises; the underlying contract won't change.
What about WebSocket / streaming delivery?
Not supported today — webhooks are the only push mode. If you need true real-time bidirectional connectivity (sub-100ms turn-around for AI voice agents that also handle SMS), pair SMS-to-webhook with the DIDHub voice trunk and run your bridge layer (LiveKit, Pipecat) as the stateful component.
Adjacent reading
SMS to Email
For low-volume inboxes that don't need code: forward inbound SMS straight to one or more email addresses. No server required.
SMS & MMS overview
Full DIDHub messaging surface area — A2P 10DLC, India DLT, EU sender-ID, country availability.
Glossary: SMS Webhook
Plain-English definition with payload reference and security best practices.
Tutorial: receive SMS via webhook
5-minute walkthrough with a working Node + Python + Go handler and Slack integration.
Ready to get a number?
Pick a DID in 130+ countries from $1.99/month. Activates instantly on most numbers.