Playground

What a Salesforce webhook actually looks like.

A real SFHooks delivery - payload, headers, and HMAC-SHA256 signature - computed live in your browser. Edit anything. Nothing leaves this page. Then send yourself a simulated delivery with one curl.

The delivery

When a record changes in Salesforce, SFHooks POSTs a JSON body like the one below to your endpoint, with two extra headers: X-SFHooks-Timestamp (unix seconds) and X-SFHooks-Signature (t=<ts>,v1=<hex>). The signature is the HMAC-SHA256 of "<timestamp>.<raw body>" under your webhook's signing secret. Change the payload or the secret and watch the signature update.

Request body editable - the exact bytes that get signed

Not valid JSON - SFHooks always sends valid JSON, but the signature below is still computed over exactly what you typed.

What your endpoint receives recomputed live

Computing…

The signature is recomputed in your browser with the Web Crypto API on every keystroke. Neither the secret nor the payload is sent anywhere - view source if you like.

Computing…

Paste a request-bin URL (or your staging endpoint) and run the curl - you'll receive a byte-for-byte simulation of an SFHooks delivery, signature and all.

Verify it in four lines

Recompute the HMAC over the raw body and compare in constant time. A delivery may carry multiple v1 values during a zero-downtime secret rotation - accept the request if any of them matches.

import crypto from "node:crypto" // rawBody must be the exact bytes you received - verify BEFORE JSON.parse. function verify(rawBody, headers, secret) { const ts = headers["x-sfhooks-timestamp"] const sig = headers["x-sfhooks-signature"] // "t=<ts>,v1=<hex>[,v1=<hex>]" // Reject stale timestamps (replay protection) - e.g. older than 5 minutes. if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false const expected = crypto .createHmac("sha256", secret) .update(`${ts}.${rawBody}`) .digest("hex") // A delivery may carry multiple v1 values during a secret rotation. return sig .split(",") .filter((p) => p.startsWith("v1=")) .map((p) => p.slice(3)) .some((v1) => crypto.timingSafeEqual(Buffer.from(v1), Buffer.from(expected)), ) }

Full details - rotation overlaps, gap reconciliation, and the "test": true marker on dashboard-sent test events - are in the signature docs.

This is a simulation - the real thing takes minutes

Connect your Salesforce org over OAuth, point a webhook at your endpoint, and press Send test event in the dashboard: the same payload shape arrives at your endpoint through the real pipeline - signed, retried if your endpoint blips, and logged per attempt. No managed package, no Apex.