Building on the Platform
...
Services
Subscription Service

Payload Signing and Verification

16min
why this approach matters securing webhook communications in distributed systems is critical to prevent unauthorized access and ensure data integrity this document details our recommended approach for verifying webhook payloads and signatures to guarantee that data is both authentic and untampered our webhook verification method resolves these vulnerabilities by leveraging hmac (hash based message authentication code) signatures combined with strict timestamp validation webhook verification ensures three critical security guarantees message authenticity confirms webhooks come only from our systems, data integrity verifies content hasn't been modified, and timestamp validation prevents replay attacks by following the steps in this guide, you can confidently validate incoming webhook requests and prevent potential security threats http headers overview always validate that all three required headers ( founda signature , founda signed headers , and founda timestamp ) are present if any are missing, respond with http 400 header name purpose format example value founda signature contains the hmac sha256 signature sha256=\<base64 encoded signature> may include multiple signatures, separated by commas sha256=abcdef123…,sha256=ghijk456… founda signed headers lists headers used in signature calculation the header values are space delimited and must be provided in the exact order used for signature generation the founda timestamp header is always included founda signed headers is always the last item in the list founda timestamp founda signed headers founda timestamp provides the request timestamp ( rfc 3339 https //www rfc editor org/rfc/rfc3339 html ) rfc 3339 https //www rfc editor org/rfc/rfc3339 html date time string 2025 03 19t12 34 56z note while http headers are case insensitive, convert all header names to lowercase when constructing the canonical string verifying webhook requests you can follow these steps to verify our webhook requests creating the canonical string for verification to verify the request signature, reconstruct the canonical representation to be signed using the following steps begin with the exact request url (including query parameters, if applicable) append a newline character ( \n ) for each header listed in founda signed headers (in the given order) convert the header name to lowercase append a colon ( ) immediately followed by the header value (with no extra spaces) if a header appears multiple times, concatenate its values using , (comma + space), preserving the original order append a literal lf newline character (ascii 10, represented as \n ) do not use crlf or any other newline variant append the raw request body (if applicable) important most web frameworks modify the request body during parsing ensure you capture the original, unmodified request body string for signature verification the exact string representation is critical — any deviation in whitespace, casing, or newline characters will cause verification to fail example consider the following request post https //webhook server com/webhook/event http/1 1 founda timestamp 2025 03 19t12 34 56 083z founda signed headers founda timestamp founda signed headers content type application/json founda signature sha256=abcdef123 {"event" "user created", "id" "1234"} the constructed canonical string would be https //webhook server com/webhook/event founda timestamp 2025 03 19t12 34 56 083z founda signed headers\ founda timestamp founda signed headers {"event" "user created", "id" "1234"} this string is then used for signature verification signature generation once the payload string is constructed, generate the signature as follows compute the hmac signature apply the hmac sha256 algorithm to the constructed canonical string using the webhook secret key provided during integration ensure that the secret key remains confidential after applying hmac sha256, base64 encode the binary digest before comparison compare the signature extract the base64 encoded signature from the founda signature header extract the signature by removing the sha256= prefix (including the equals sign) and, if multiple signatures are present, split by commas validate against each signature perform a constant time comparison to prevent timing attacks if the signatures do not match, reject the request with an http 400 bad request response note use a constant time comparison function to compare the calculated signature with the received one to mitigate timing attacks timestamp validation to prevent replay attacks, validate the founda timestamp header parse the timestamp and compare it to the current time if the difference exceeds 5 minutes , reject the request with an http 400 bad request response this ensures that old (potentially intercepted) requests cannot be replayed handling multiple secrets in scenarios where multiple secret keys are active (e g , during a key rotation), requests are signed with each active key the founda signature header will contain multiple signatures, separated by commas when verifying the request compute the expected signature using each active secret accept the request if at least one of the signatures matches validating the signature (javascript example) the following, simplified example demonstrates how to validate an incoming webhook request signature using node js const crypto = require('crypto'); function isvalidsignature(requesturl, headers, requestbody, secrets) { const signedheaders = headers\['founda signed headers']; const timestamp = headers\['founda timestamp']; const signature = headers\['founda signature']; const headerslowercase = { &#x9; 'founda signed headers' signedheaders, &#x9; 'founda timestamp' timestamp, &#x9; 'founda signature' signature &#x9; }; // check for required headers if (!timestamp || !signedheaders || !signature) { &#x9; return false; &#x9; } // validate timestamp const fiveminutes = 5 60 1000; const requesttime = new date(timestamp) gettime(); const currenttime = date now(); if (math abs(currenttime requesttime) > fiveminutes) { return false; // timestamp outside the allowed window } // construct canonical string let payloadstring = requesturl + '\n'; for (let headername of signedheaders split(' ')) { &#x9; payloadstring += headername tolowercase() + ' ' + headerslowercase\[headername] + '\n'; } payloadstring += requestbody; // extract received signatures const receivedsignatures = signature &#x9; split(',') &#x9; map(sig => buffer from(sig replace('sha256=', '') trim())); // check against all active secrets for (const secret of secrets) { const hmac = crypto createhmac('sha256', secret); hmac update(payloadstring); const expectedsignature = buffer from(hmac digest('base64')); // check if any signature matches const signaturevalid = receivedsignatures some(sig => sig length === expectedsignature length && crypto timingsafeequal(sig, expectedsignature) ); if (signaturevalid) { return true; } } return false; } // isvalidsignature usage example isvalidsignature( 'https //webhook server com/webhook/v1', { 'founda signature' 'sha256=bsb ', 'founda signed headers' 'founda timestamp founda signed headers', 'founda timestamp' '2025 04 01t15 16 30 083z' }, '{"resourcetype" "bundle","type" "batch", ', \['5c1 '] ); to use this function extract the founda signature , founda signed headers , and founda timestamp headers pass the request path, headers, raw request body, and your webhook secret to isvalidsignature() if the function returns false , reject the request response codes http 200 ok you should return this response if all verification steps succeed required headers are present signature validation passes timestamp is within the allowed window http 400 bad request you should return this response if the request is invalid your response should include a json object specifying the error { "error" "invalid request", "message" "the 'founda signature' header is missing " } common reasons for rejecting a request a required header is missing signature verification fails the timestamp is outside the allowed time window important these error responses should be sent by your application to us when a request is rejected our system does not automatically generate these responses, so it is your responsibility to implement and return them when necessary