Home / integration / webhook-signature-verification
webhook-signature-verification
Accept inbound webhooks (Stripe, GitHub, Slack, partner) and verify HMAC signatures in Apex REST — Crypto.verifyHMac platform verification, secret in Protected Custom Metadata, replay-window rejection. NOT for designing the receiver end to end — Sites routing, guest-user access, idempotency, the 5-second response window — use integration/webhook-inbound-patterns. NOT for signing a webhook Salesforce sends out — use integration/outbound-webhook-from-salesforce.
Open the skill folder on GitHub
Webhook Signature Verification
An inbound webhook is an HTTP POST from a third party that Salesforce did not initiate and cannot authenticate by session. When the endpoint is exposed through a public Salesforce Site, the signature check is the entire authentication boundary — there is no user, no token, and no network control in front of it.
Three properties have to hold, and each fails independently:
| Property | Established by | Fails when |
|---|---|---|
| Authenticity — the sender holds the shared secret | HMAC over the payload | Secret is wrong, or the wrong encoding of the right secret |
| Integrity — the bytes were not altered in transit | HMAC over the raw body | The body was parsed and re-serialised before hashing |
| Freshness — this is not a replay of an earlier valid request | Timestamp tolerance + idempotency key | Neither is implemented — the signature alone never establishes this |
Most broken webhook endpoints have the first two and not the third.
Scope. This skill owns the verification step: which Apex crypto method to call, over which bytes, against which secret, and what to reject. Designing the receiver end to end — Sites routing, guest-user access model, the idempotency schema, the response-window budget — belongs to integration/webhook-inbound-patterns. Those subjects appear below only as far as they change the verification decision (for example: on a public Site the signature is the only gate, which raises the stakes on every gotcha here). Signing a webhook Salesforce sends is integration/outbound-webhook-from-salesforce.
Before Starting
- Identify the signature scheme. HMAC (shared secret) or asymmetric
(RSA/ECDSA against a published public key)? They need different Apex methods and different secret storage. Do not assume HMAC.
- Get the exact header name, value format, and signed payload. Not every
provider signs the body alone — Stripe signs
"{timestamp}.{body}". Getting this wrong produces 100% verification failure that looks like a key problem. - Decide how the request reaches Apex.
/services/apexrest/*requires anauthenticated session, which a webhook does not have. Either the provider obtains a token (Connected App, client credentials flow) or you expose the class through a Salesforce Site guest user. Pick before you write the class.
- Choose where the secret lives. Protected Custom Metadata, read via
getAll(). Not a literal, not a Custom Setting, not a Named Credential (which is for outbound).
Core Concepts
Crypto.verifyHMac is the method you want
public static Boolean Crypto.verifyHMac(
String algorithmName, // hmacMD5 | hmacSHA1 | hmacSHA256 | hmacSHA512
Blob data,
Blob privateKey, // max 4 KB
Blob macToVerify
)
Crypto.generateMac produces a MAC and is correct for building a signature (in tests, or when calling out). It is the wrong tool for checking one, because it forces you to write the comparison yourself — and String.equals short-circuits on the first differing character. Both Stripe and GitHub explicitly tell integrators to compare signatures in constant time.
What the Apex Reference Guide does and does not say. Its verifyHMac entry documents the signature, the four valid algorithm names, the 4 KB key cap, the Base64 symmetry rule, and the Boolean return. It makes no timing claim. So verifyHMac is the recommended method here because it keeps the comparison out of your code entirely, not because Salesforce documents it as constant-time — that property is undocumented, and this skill does not assert it. Detail in