Developer API · updated 2026-08-22
Every webhook delivery is signed so your server can prove it came from Dumpster Controls and was not tampered with. Verification is a dozen lines of code in any language. Never process an unverified webhook.
Each delivery carries:
X-DC-Signature: t=1755900000,v1=a1b2c3...
t is the unix timestamp when we signed, and v1 is the hex HMAC-SHA256 of the string "<t>.<raw body>" computed with your endpoint secret (dcwh_...).
1. Read the RAW request body (before any JSON parsing; whitespace matters). 2. Parse t and v1 from the header. 3. Compute HMAC-SHA256(secret, t + "." + rawBody) and hex-encode it. 4. Compare with v1 using a constant-time comparison. 5. Reject if |now - t| is more than about 5 minutes (replay protection).
const crypto = require("crypto");
function verify(rawBody, header, secret) { const parts = Object.fromEntries(header.split(",").map(p => p.split("="))); const expected = crypto.createHmac("sha256", secret) .update(parts.t + "." + rawBody).digest("hex"); const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) < 300; const sig = Buffer.from(parts.v1 || ""); // timingSafeEqual THROWS on unequal lengths: guard first return fresh && sig.length === 64 && crypto.timingSafeEqual(Buffer.from(expected), sig); }
import hmac, hashlib, time
def verify(raw_body: bytes, header: str, secret: str) -> bool: parts = dict(p.split("=") for p in header.split(",")) expected = hmac.new(secret.encode(), f"{parts['t']}.".encode() + raw_body, hashlib.sha256).hexdigest() fresh = abs(time.time() - int(parts["t"])) < 300 return fresh and hmac.compare_digest(expected, parts["v1"])
Parsing the JSON and re-serializing it before hashing (key order changes, verification fails): always hash the raw bytes. Using == instead of a constant-time compare. Skipping the timestamp check, which allows replays of old captured deliveries.
© 2026 Dumpster Controls. All rights reserved. Made in the USA.