Tutorial · Inbound SMS

Receive SMS via webhook in 5 minutes

Stand up a webhook handler that receives inbound SMS from a DIDHub number, verifies the HMAC signature, and posts a formatted card to Slack. Working code in Node and Python; deployable as a Cloudflare Worker, AWS Lambda, or any HTTP server.

Prerequisites: a DIDHub account with one SMS-capable DID, a Slack workspace with an Incoming Webhook URL, and any HTTPS endpoint that you can deploy code to.

Step 1: Get your DIDHub webhook secret

Dashboard → Settings → API & Webhooks → Webhook secret. Copy the value — you'll need it to verify incoming requests. Treat it like a password; rotate any time without affecting message delivery.

Step 2: Write the handler

Node.js (Cloudflare Worker)

export default {
  async fetch(req, env) {
    if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });

    // 1. Verify HMAC-SHA256 signature
    const raw = await req.text();
    const sig = req.headers.get('x-didhub-signature') || '';
    const key = await crypto.subtle.importKey(
      'raw', new TextEncoder().encode(env.DIDHUB_WEBHOOK_SECRET),
      { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']
    );
    const mac = await crypto.subtle.sign('HMAC', key, new TextEncoder().encode(raw));
    const expected = [...new Uint8Array(mac)].map(b => b.toString(16).padStart(2, '0')).join('');
    if (sig !== expected) return new Response('Bad signature', { status: 401 });

    // 2. Parse + post to Slack
    const { from, to, body, country } = JSON.parse(raw);
    await fetch(env.SLACK_WEBHOOK_URL, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        text: `SMS to ${to}`,
        blocks: [
          { type: 'section', text: { type: 'mrkdwn',
            text: `*${from}* (${country}) → *${to}*\n> ${body}` } }
        ]
      })
    });

    return new Response('ok', { status: 200 });
  }
};

Python (FastAPI on Lambda)

from fastapi import FastAPI, Request, HTTPException
import hmac, hashlib, os, httpx

app = FastAPI()
SECRET = os.environ["DIDHUB_WEBHOOK_SECRET"].encode()
SLACK_URL = os.environ["SLACK_WEBHOOK_URL"]

@app.post("/sms")
async def handler(req: Request):
    raw = await req.body()
    sig = req.headers.get("x-didhub-signature", "")
    expected = hmac.new(SECRET, raw, hashlib.sha256).hexdigest()
    if not hmac.compare_digest(sig, expected):
        raise HTTPException(401)

    msg = await req.json()
    text = f"*{msg['from']}* ({msg['country']}) → *{msg['to']}*\n> {msg['body']}"
    async with httpx.AsyncClient() as c:
        await c.post(SLACK_URL, json={"text": msg["to"],
                                    "blocks": [{"type":"section",
                                                 "text": {"type":"mrkdwn","text": text}}]})
    return {"ok": True}

Step 3: Deploy

Deploy the handler to a public HTTPS URL. Examples:

Step 4: Wire it to your DID

Dashboard → Numbers → pick the DID → Inbound routingSMS deliveryWebhook → paste your URL → Save.

Tick Forward delivery receipts if you also send outbound SMS from this DID and want DLR callbacks at the same URL.

Step 5: Test

Two ways to verify before sending a real SMS:

  1. Dashboard test button. Routing tab → Send test webhook → DIDHub fires a sample sms.received payload with a real signature. Your handler runs end-to-end. Slack should show the test message.
  2. Real SMS. Text your DIDHub number from any phone — the message appears in Slack within ~1 second.
Common gotcha: if you're using a body-parsing middleware (express.json, bodyParser) in Node, the raw bytes that the HMAC signs aren't available by default. Either pass {verify: (req,res,buf) => req.rawBody = buf} to express.json (Node example above) or compute the HMAC over a re-serialised JSON. The latter is brittle — always sign the raw bytes.

Production checklist

What you just built

A production-ready SMS-to-Slack pipeline with HMAC-verified delivery, sub-second latency, and zero infrastructure overhead. The same handler skeleton extends to Zendesk ticket creation, Salesforce activity logging, AI auto-responders, or any custom flow — just swap the Slack POST for your real downstream system.

More tutorials

Ready to get a number?

Pick a DID in 130+ countries from $1.99/month. Activates instantly on most numbers.