149 countries · crypto-native · no KYC

Securing SMS Webhooks: Signature Verification and Replay Defense

An unverified webhook endpoint is a public URL anyone can POST fake delivery data to. Here's how to prove a callback really came from your provider — and why it matters.

$0.035/msg from sub-100ms median 98.6% delivered
Securing SMS Webhooks: Signature Verification and Replay Defense — smsroute
$0.004
per SMS from
149
countries
60s
to first message
6
crypto rails
SMS webhook security starts from an uncomfortable fact: when you expose a webhook for delivery receipts or inbound SMS, you publish a URL that accepts POST requests from the internet. Your provider calls it, but so can anyone who finds it. Without verification, an attacker can POST fabricated delivery statuses, fake inbound messages, or replay old callbacks, and your application will trust every one. For a flow that drives business logic (marking messages delivered, routing 'STOP' opt-outs, triggering fallbacks), that is a real hole, not a theoretical one. For the authoritative reference, see the TCPA.

Your webhook is a public door

Threat Without verification Defense
Forged callback Attacker POSTs fake 'delivered' or 'STOP' HMAC signature check
Replayed callback Old signed callback re-sent, still trusted Signed timestamp + freshness window
Duplicate processing Same event acted on twice Idempotent dedupe on event id
Timing attack on signature Byte-by-byte signature guessing Constant-time comparison

Signature verification with HMAC

Your webhook is a public door — comparison diagram

Providers sign each webhook with a shared secret, typically an HMAC of the request body, sent in a header — the same scheme documented by Stripe, Twilio, and the standard webhook-signing conventions the industry has converged on. You recompute the HMAC with your copy of the secret and compare. Match means authentic; mismatch means drop.

import hmac, hashlib

def verify(body: bytes, signature: str, secret: str) -> bool:
    expected = hmac.new(secret.encode(), body, hashlib.sha256).hexdigest()
    # constant-time compare — never use == on secrets
    return hmac.compare_digest(expected, signature)

Replay protection

A valid signature proves the payload is authentic, but a captured, genuinely-signed callback can be replayed. If an attacker records a real 'delivered' or 'STOP' callback and re-sends it later, the signature still checks out. Timestamps close this.

  1. Include a timestamp in the signed dataProviders typically sign a timestamp alongside the body. Verify the signature covers both, so the timestamp can't be altered.
  2. Reject stale callbacksIf the timestamp is older than a small window (say 5 minutes), drop the request. A legitimate callback arrives promptly; an old one is a replay.
  3. Dedupe on an event idTrack processed webhook/event ids and ignore repeats. This also handles the legitimate case where a provider redelivers. The idempotency your DLR handler needs anyway does double duty here.
  4. Combine, don't chooseSignature proves authenticity; timestamp plus dedupe proves freshness. You need both. Either alone leaves a gap.

Idempotency is doing two jobs at once: it keeps duplicate legitimate DLRs from double-counting, and it neutralizes replay attacks that resend a captured, validly-signed callback.

The full secure handler

Assembled, a secure webhook does five things in order: capture the raw body, verify the HMAC signature in constant time, check the timestamp is fresh, dedupe on the event id, then (and only then) act on the payload and return 200. Anything that fails a check gets a 401 or 400 and touches no business logic.

SMSRoute is a no-KYC SMS API with crypto billing (BTC, ETH, USDT, XMR, LTC, and SOL), and webhook signing is exactly the kind of thing that's invisible until it isn't. Wire verification in when you build the DLR reconciliation and two-way inbound routing, not after an incident. It's a few lines, it costs nothing at runtime, and it's the difference between a webhook that drives your business logic safely and a public URL anyone can lie to. Treat every unverified callback as hostile until its signature and timestamp say otherwise. SMSRoute's published route pages list delivery from $0.004/message (premium direct-carrier corridors up to $0.035) with sub-100ms median submission and ~98.6% delivered success (smsroute.cc route pages, 2026).

FAQ

How do I verify an SMS webhook is really from my provider?
Recompute the HMAC signature the provider sends: hash the raw request body with your shared secret using the provider's algorithm (commonly SHA-256), and compare it to the signature header using a constant-time comparison. A match proves authenticity; reject anything that doesn't match with a 401.
Why do I need to secure my SMS delivery-receipt webhook?
Because it's a public URL that accepts POST requests from anyone who finds it. Without verification, an attacker can POST fake delivery statuses, forge inbound messages, or replay old callbacks — and since webhooks often drive business logic like opt-out handling and fallback triggers, that's a real security hole.
What is webhook replay protection?
Defense against an attacker re-sending a genuinely-signed callback they captured earlier. Even with a valid signature, a replayed message would be trusted — so you also verify a signed timestamp is recent (within a few minutes) and dedupe on an event id, rejecting stale or already-processed callbacks.
Why use constant-time comparison for webhook signatures?
A normal equality check (==) can leak, through timing differences, how much of the signature an attacker got right, letting them guess it byte by byte. Constant-time comparison (like hmac.compare_digest) takes the same time regardless of where a mismatch occurs, closing that side channel.

Send your first SMS in 5 minutes

No KYC. Pay with BTC, ETH, USDT, XMR, LTC, and SOL. Live routes to 149 countries.

Get an API key →