← Back to documentation

HMAC Outbound Signing

Enable HMAC signatures on webhook outputs with automatic secret rotation support.

10 min read

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 payload
  • X-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

  1. Open the endpoint edit page.
  2. Navigate to the Target destinations tab.
  3. Select a webhook output.
  4. Enable HMAC signing.
  5. Choose an algorithm (SHA256, SHA1, SHA512). Default is SHA256.
  6. Enter a signing secret, or use the UI Generate button to create one in your browser.
  7. Optionally customize the signature and timestamp header names (defaults: X-PayloadRelay-Signature and X-PayloadRelay-Timestamp) and a signature prefix prepended to the Base64 signature value. The signature header, timestamp header, and derived <signature-header>-Previous rotation header must be distinct and cannot use restricted HTTP header names.
  8. 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:

  1. Extract the X-PayloadRelay-Signature and X-PayloadRelay-Timestamp headers.
  2. Reconstruct the signed payload: timestamp + "." + raw_body.
  3. Compute the HMAC using your algorithm and secret.
  4. Compare the computed signature to the received signature using constant-time comparison.
  5. Optionally enforce a replay window by checking the timestamp is within ±5 minutes of the current time.

Verification recipe (Node.js):

Code Example
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):

Code Example
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:

  1. Open the endpoint edit page → Target destinations tab.
  2. Select the webhook output with signing enabled.
  3. Enter or generate the new secret.
  4. Select Rotate secret and 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 secret
    • X-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:

Code Example
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, or Connection.

Algorithm support

PayloadRelay always Base64-encodes outbound HMAC signatures. PayloadRelay supports three HMAC algorithms:

AlgorithmSecurityNotes
SHA256StrongDefault. Recommended for new integrations.
SHA1WeakSupported for legacy compatibility only.
SHA512StrongHigher computational cost; use when required by receiver policy.

Replay protection

Receivers should validate the X-PayloadRelay-Timestamp to reject replayed requests:

  1. Parse the timestamp as a Unix timestamp (integer seconds).
  2. Compare to the current server time.
  3. 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-Signature and X-PayloadRelay-Timestamp headers.
  • 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.

Related guides