HMAC Outbound Signing
Enable HMAC signatures on webhook outputs with automatic secret rotation support.
Use this guide to enable HMAC signing on outbound webhook deliveries and implement verification in your receivers.
Purpose
This guide helps you:
- Enable per-output HMAC signing for webhook targets.
- Configure signature algorithm and custom headers.
- Rotate signing secrets with zero-downtime grace periods.
- Implement verification logic in downstream receivers.
How outbound signing works
When enabled on a webhook output, PayloadRelay computes an HMAC signature over timestamp + "." + body and sends two headers with every delivery:
X-PayloadRelay-Signature— Base64-encoded HMAC of the payloadX-PayloadRelay-Timestamp— Unix timestamp (seconds) when the request was signed
Recipients verify the signature using the shared secret to confirm the webhook originated from PayloadRelay.
Prerequisites and permissions
- Endpoint edit access.
- Ability to deploy verification logic to your webhook receiver.
Step-by-step workflow
1. Enable signing on a webhook output
- Open the endpoint edit page.
- Navigate to the Target destinations tab.
- Select a webhook output.
- Enable
HMAC signing. - Choose an algorithm (
SHA256,SHA1,SHA512). Default isSHA256. - Enter a signing secret, or use the UI Generate button to create one in your browser.
- Optionally customize the signature and timestamp header names (defaults:
X-PayloadRelay-SignatureandX-PayloadRelay-Timestamp) and a signature prefix prepended to the Base64 signature value. The signature header, timestamp header, and derived<signature-header>-Previousrotation header must be distinct and cannot use restricted HTTP header names. - Save.
PayloadRelay stores the secret encrypted and begins signing all deliveries. The API never returns the raw secret after it is saved.
2. Store the secret
Copy the secret to a secure location (vault, password manager). You'll need it to verify signatures in your receiver.
If you lose the secret, enter a new one and rotate it (see step 4).
3. Implement verification in your receiver
Your webhook endpoint must:
- Extract the
X-PayloadRelay-SignatureandX-PayloadRelay-Timestampheaders. - Reconstruct the signed payload:
timestamp + "." + raw_body. - Compute the HMAC using your algorithm and secret.
- Compare the computed signature to the received signature using constant-time comparison.
- Optionally enforce a replay window by checking the timestamp is within ±5 minutes of the current time.
Verification recipe (Node.js):
const crypto = require('crypto');
function verifyPayloadRelaySignature(req, secret) {
const signature = req.headers['x-payloadrelay-signature'];
const timestamp = req.headers['x-payloadrelay-timestamp'];
const body = req.rawBody; // raw request body string
if (!signature || !timestamp) {
return false;
}
const signedPayload = `${timestamp}.${body}`;
const computed = crypto
.createHmac('sha256', secret)
.update(signedPayload, 'utf8')
.digest('base64');
if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(computed))) {
return false;
}
// Optional: enforce replay window (±5 minutes)
const now = Math.floor(Date.now() / 1000);
const timestampInt = parseInt(timestamp, 10);
if (Math.abs(now - timestampInt) > 300) {
return false; // timestamp out of skew
}
return true;
}
// Usage
if (verifyPayloadRelaySignature(req, process.env.PAYLOADRELAY_SECRET)) {
console.log('Valid signature');
} else {
console.log('Invalid signature');
}Verification recipe (Python):
import hmac
import hashlib
import base64
import time
def verify_payloadrelay_signature(request, secret):
signature = request.headers.get('X-PayloadRelay-Signature')
timestamp = request.headers.get('X-PayloadRelay-Timestamp')
body = request.body # raw request body bytes
if not signature or not timestamp:
return False
signed_payload = f'{timestamp}.{body.decode()}'.encode()
computed = base64.b64encode(
hmac.new(secret.encode(), signed_payload, hashlib.sha256).digest()
).decode()
if not hmac.compare_digest(signature, computed):
return False
# Optional: enforce replay window (±5 minutes)
now = int(time.time())
timestamp_int = int(timestamp)
if abs(now - timestamp_int) > 300:
return False # timestamp out of skew
return True
# Usage
if verify_payloadrelay_signature(request, os.environ['PAYLOADRELAY_SECRET']):
print('Valid signature')
else:
print('Invalid signature')4. Rotate secrets
To rotate a signing secret:
- Open the endpoint edit page → Target destinations tab.
- Select the webhook output with signing enabled.
- Enter or generate the new secret.
- Select
Rotate secretand save.
Grace period behavior:
When you rotate, PayloadRelay:
- Generates a new secret (becomes the current secret).
- Preserves the old secret for 7 days as the previous secret.
- Sends both signatures on every delivery:
X-PayloadRelay-Signature— signed with the current secretX-PayloadRelay-Signature-Previous— signed with the previous secret
This dual-signature approach allows you to deploy the new secret to all receivers within 7 days without any downtime.
Receiver implementation for rotation:
Update your verification function to accept either signature:
function verifyPayloadRelaySignature(req, secret, previousSecret) {
const signature = req.headers['x-payloadrelay-signature'];
const previousSignature = req.headers['x-payloadrelay-signature-previous'];
const timestamp = req.headers['x-payloadrelay-timestamp'];
const body = req.rawBody;
if (!timestamp) {
return false;
}
const signedPayload = `${timestamp}.${body}`;
// Try current secret
if (signature) {
const computed = crypto
.createHmac('sha256', secret)
.update(signedPayload, 'utf8')
.digest('base64');
if (crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(computed))) {
return true;
}
}
// Try previous secret
if (previousSignature && previousSecret) {
const computedPrevious = crypto
.createHmac('sha256', previousSecret)
.update(signedPayload, 'utf8')
.digest('base64');
if (crypto.timingSafeEqual(Buffer.from(previousSignature), Buffer.from(computedPrevious))) {
return true;
}
}
return false;
}After 7 days, the X-PayloadRelay-Signature-Previous header is no longer sent. Ensure all receivers are updated to use the new secret before the grace period expires.
Header collision rules
Outbound signing reserves the configured signature header, timestamp header, and derived previous-signature header (<signature-header>-Previous). PayloadRelay rejects configurations where:
- The signature and timestamp header names are duplicates.
- A custom outbound header uses any of those HMAC header names.
- Outbound API-key auth uses one of those HMAC header names.
- A signing header uses restricted HTTP names such as
Authorization,Cookie,Host,Content-Type,Content-Length,Transfer-Encoding, orConnection.
Algorithm support
PayloadRelay always Base64-encodes outbound HMAC signatures. PayloadRelay supports three HMAC algorithms:
| Algorithm | Security | Notes |
|---|---|---|
SHA256 | Strong | Default. Recommended for new integrations. |
SHA1 | Weak | Supported for legacy compatibility only. |
SHA512 | Strong | Higher computational cost; use when required by receiver policy. |
Replay protection
Receivers should validate the X-PayloadRelay-Timestamp to reject replayed requests:
- Parse the timestamp as a Unix timestamp (integer seconds).
- Compare to the current server time.
- Reject if the difference exceeds your allowed skew (suggested: ±5 minutes / 300 seconds).
This protects against attackers capturing and re-sending valid requests.
Clock skew considerations:
- PayloadRelay signs each request with the timestamp sent in the signature header.
- If your server clock drifts significantly, valid requests may be rejected.
- Use NTP or equivalent to keep your server time accurate.
Expected result and verification checks
- Webhook deliveries include
X-PayloadRelay-SignatureandX-PayloadRelay-Timestampheaders. - Receivers successfully verify signatures using the shared secret.
- During rotation, receivers accept either the current or previous signature for 7 days.
- After 7 days, only the current signature is sent.
Common issues and fixes
- Signature mismatch: Verify the secret is exact. Ensure you're verifying the raw request body, not parsed JSON.
- Timestamp out of skew: Check server clock synchronization. Increase your replay window if appropriate (trade-off: larger window = less protection).
- Missing headers after rotation: Wait for deliveries to flow. New requests are signed immediately after rotation.
- Old secret still accepted after 7 days: PayloadRelay only sends the previous signature for 7 days. Your receiver may be explicitly accepting both — update to only trust the current secret after the grace period.