Skip to content

Recipe: verify a webhook

Never process a webhook without verifying x-scrapersocial-signature. The header is t=<unix>,v1=<hex> where v1 is HMAC-SHA256 of "{t}.{raw_body}" using your whsec_ secret.

Node (Express)

import crypto from "node:crypto";
import express from "express";
const app = express();
app.post("/hooks/ss", express.raw({ type: "application/json" }), (req, res) => {
const header = req.header("x-scrapersocial-signature") ?? "";
const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
const { t, v1 } = parts;
if (!t || !v1 || Math.abs(Date.now() / 1000 - Number(t)) > 300) {
return res.status(400).send("stale or malformed signature");
}
const expected = crypto
.createHmac("sha256", process.env.SS_WEBHOOK_SECRET)
.update(`${t}.${req.body}`)
.digest("hex");
if (!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(v1))) {
return res.status(401).send("bad signature");
}
const event = JSON.parse(req.body);
// handle event.type: job.completed | credits.low | plan.changed
res.sendStatus(200);
});

Python (FastAPI)

import hashlib, hmac, json, os, time
from fastapi import FastAPI, Header, HTTPException, Request
app = FastAPI()
SECRET = os.environ["SS_WEBHOOK_SECRET"].encode()
@app.post("/hooks/ss")
async def hook(request: Request, x_scrapersocial_signature: str = Header("")):
raw = await request.body()
parts = dict(p.split("=") for p in x_scrapersocial_signature.split(",") if "=" in p)
t, v1 = parts.get("t", ""), parts.get("v1", "")
if not t or abs(time.time() - int(t)) > 300:
raise HTTPException(400, "stale signature")
expected = hmac.new(SECRET, f"{t}.".encode() + raw, hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, v1):
raise HTTPException(401, "bad signature")
event = json.loads(raw)
return {"ok": True}

Checklist

  • Compute over the raw body, before any JSON parsing or re-serialization.
  • Use a constant-time comparison (timingSafeEqual / compare_digest).
  • Reject timestamps older than ~5 minutes to block replays.
  • Respond 2xx fast; do slow work async — deliveries retry on non-2xx.

Related: Webhooks guide · Webhooks API