Payload Signing and Verification
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.
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) | RFC 3339 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
You can follow these steps to verify our webhook requests.
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.
Consider the following request:
The constructed canonical string would be:
This string is then used for signature verification.
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.
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.
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.
The following, simplified example demonstrates how to validate an incoming webhook request signature using Node.js:
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.
- 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:
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.