Hey folks, I see a lot of questions about receiving webhooks from services like Stripe, Shopify, or SendGrid, and the big worry is always: "How do I make sure this request is *actually* from them?" If you just check for a secret parameter, you're vulnerable to replay attacks. The gold standard is HMAC validation, and it's not as scary as it sounds.
Here’s my step-by-step mental model for implementing it on your endpoint:
* **Shared Secret:** You and the sender (e.g., the SaaS tool) both have the same secret key. You generate this in their dashboard, and you store it securely in *your* environment variables.
* **The Signature:** The sender creates a unique "signature" for each payload. They hash the request body (and sometimes a timestamp) using the secret and send it in a header (often `X-Webhook-Signature`).
* **Your Validation:** When you receive the webhook, you perform the *exact same* hashing operation on the incoming raw body using your stored secret.
* **The Match:** You compare the signature you computed with the one they sent. If they match, you can trust the payload. If not, you reject it immediately.
The critical bits are using the *raw* request body (before any JSON parsing) and a constant-time comparison function to avoid timing attacks. Most languages have libraries for this. For Node.js, you'd use `crypto.timingSafeEqual`; for Python, `hmac.compare_digest`.
I've used this pattern to secure webhooks from Marketo, HubSpot, and our own internal analytics pipelines. It's rock solid. Once you set it up, you can sleep easy knowing your automated lead scoring or customer journey triggers aren't being faked.
What tools are you all trying to secure? Have you run into any quirks with specific vendors' implementations?
Cheers, Henry
You've nailed the core process. One critical detail teams often miss is verifying the signature's *timing* to prevent replay attacks.
Many services include a timestamp in the signed payload or as a separate header. You need to check that the timestamp is within a reasonable window, like 5 minutes. If it's too old, you reject it even if the HMAC matches. This stops a captured valid request from being replayed later.
A pattern I use is to compute the expected signature first, do a constant-time comparison with the incoming one, *then* also validate the timestamp if present. It's a cheap extra security layer.
The timestamp check is good advice, but it's only as good as your system clock. I've seen this fail in containerized environments where the local clock drifts, causing valid requests to get bounced. You need to sync with NTP and have a slightly more lenient window in production than you'd think.
Also, the order of operations matters. Do the cheap timestamp check *first* before you even bother computing the HMAC. No point wasting CPU cycles on a signature you already know is stale. It's a small optimization, but when you're dealing with high-volume webhook endpoints, those cycles add up.
Finally, be aware that not all services include a timestamp in the signature. Some just sign the raw body. For those, you're stuck with replay windows on their side, or you have to implement your own nonce tracking, which is a whole other can of worms.