Your webhook endpoint is a public API. Treat it like one

A webhook endpoint accepts data from the internet and acts on it. Beyond verifying the signature on raw bytes, four things get skipped: a freshness window, a size limit, a timeout, and SSRF on the URLs you accept.

1. Replay

A signature proves the payload came from the provider. It does not prove it came from them just now. Anyone who captures one request can send it again, and a signature check alone will accept it every time.

age = time.time() - int(headers["X-Njiwa-Timestamp"])
if abs(age) > 300:                      # five minutes
    raise HTTPException(401, "stale")
# Then verify the signature over timestamp + body, so the timestamp
# cannot be edited independently of the payload.

2. Size

Cap the body before parsing. An endpoint that reads an unbounded request into memory can be taken down by one large POST, and JSON parsers are happy to allocate whatever they are given.

3. Timeouts, on your own outbound calls

The handler that acknowledges quickly is only half of it. The worker behind it usually calls something - your database, an object store, the provider's media endpoint. Every one of those needs a timeout, or a slow dependency turns into an unbounded queue of stuck workers.

4. SSRF, in the URL you let customers set

If customers configure their own webhook URL, your server will make a request to whatever they type. Inside a cloud network that includes the metadata service and every internal host.

  1. Resolve the hostname and reject private, loopback, link-local and metadata addresses.
  2. Re-check after redirects. A public URL that 302s to 169.254.169.254 defeats a check done only on the original.
  3. Require HTTPS.
  4. Send from a network path that cannot reach anything internal, if you can. Belt and braces, because the checks above are a denylist and denylists leak.

The redirect step is the one that gets missed. Validating the URL at save time and following redirects at send time is a check that passes and a request that goes wherever it likes.

And log the rejections

Every 401 on a webhook endpoint is either an attack or your own integration broken. Both are worth knowing about, and neither shows up if rejections are silent.

Questions people actually ask

Is signature verification enough for a webhook?

No. A valid signature from an hour ago is still valid unless you check the timestamp, so a captured request can be replayed. Signatures plus a freshness window is the minimum.

Should I allow-list the sender's IP addresses?

As defence in depth if the provider publishes stable ranges, never as the only control. Ranges change, and an allow-list that silently stops matching produces exactly the silent failure webhooks are worst at.

Njiwa

One inbox for WhatsApp, Facebook and Instagram, and an API that sends over either WhatsApp rail. 7 days free, no card.

Start a trial See pricing