LostChurn Docs
Security & Compliance

Webhook Verification

How LostChurn uses HMAC-SHA256 to verify incoming webhook payloads and prevent spoofed events.

LostChurn verifies every incoming webhook payload using HMAC-SHA256 signatures. This ensures that only genuine events from your payment processor trigger recovery actions. Spoofed, tampered, or replayed payloads are rejected at the edge before they reach the recovery engine.

How It Works

When your payment processor sends a webhook to LostChurn, the event goes through a multi-layer verification process:

Step-by-Step

  1. Extract the signature: The edge worker reads the signature from the webhook header (e.g., Stripe-Signature for Stripe, X-Braintree-Signature for Braintree).
  2. Compute the expected hash: Using the signing secret stored in your LostChurn integration settings, the worker computes an HMAC-SHA256 hash of the raw request body.
  3. Compare signatures: The computed hash is compared to the signature in the header using a constant-time comparison to prevent timing attacks.
  4. Validate timestamp: The worker checks that the event timestamp is within a 5-minute tolerance window to prevent replay attacks.
  5. Forward or reject: If verification passes, the event is forwarded to the recovery module. If it fails, the request is rejected with a 401 Unauthorized response.

Signing Secret Configuration

Each payment processor integration has its own signing secret. You configure these when you connect your payment processor.

Stripe

Stripe provides a webhook signing secret when you create a webhook endpoint. It starts with whsec_.

  1. In the Stripe Dashboard, go to Developers > Webhooks.
  2. Select the LostChurn endpoint.
  3. Click Reveal under Signing secret.
  4. Copy the secret and paste it into Settings > Integrations > Stripe > Webhook Secret in your LostChurn dashboard.

If you connected Stripe via OAuth, the signing secret is configured automatically.

Braintree

Braintree uses a different verification model based on the gateway's public key and a challenge/response handshake. LostChurn handles this automatically during integration setup. No manual signing secret configuration is required.

Other Providers

For providers that support HMAC-SHA256 signing, you will find the signing secret in the webhook or notification settings of that provider's dashboard. Paste it into the corresponding field in your LostChurn integration settings.

Manual Verification

If you want to verify webhook payloads independently (e.g., for audit or debugging purposes), you can compute the HMAC-SHA256 signature yourself.

Node.js Example

import crypto from "node:crypto";

function verifyWebhookSignature(payload, signature, secret) {
  const computedSignature = crypto
    .createHmac("sha256", secret)
    .update(payload, "utf8")
    .digest("hex");

  return crypto.timingSafeEqual(
    Buffer.from(signature, "hex"),
    Buffer.from(computedSignature, "hex")
  );
}

// Usage
const rawBody = '{"id":"evt_123","type":"invoice.payment_failed",...}';
const headerSignature = "a1b2c3d4e5f6..."; // from the webhook header
const signingSecret = "whsec_your_signing_secret";

const isValid = verifyWebhookSignature(rawBody, headerSignature, signingSecret);

Python Example

import hmac
import hashlib

def verify_webhook_signature(payload: bytes, signature: str, secret: str) -> bool:
    computed = hmac.new(
        secret.encode("utf-8"),
        payload,
        hashlib.sha256
    ).hexdigest()

    return hmac.compare_digest(computed, signature)

# Usage
raw_body = b'{"id":"evt_123","type":"invoice.payment_failed",...}'
header_signature = "a1b2c3d4e5f6..."
signing_secret = "whsec_your_signing_secret"

is_valid = verify_webhook_signature(raw_body, header_signature, signing_secret)

cURL Example

echo -n '{"id":"evt_123","type":"invoice.payment_failed"}' \
  | openssl dgst -sha256 -hmac "whsec_your_signing_secret"

Always use the raw request body for HMAC computation. If you parse the JSON and re-serialize it, the signature will not match because whitespace or key ordering may differ.

Replay Attack Prevention

Even if an attacker obtains a valid signed webhook payload, they cannot replay it after the tolerance window expires. LostChurn enforces two protections:

Timestamp Tolerance

Each webhook event includes a timestamp. LostChurn rejects events where the timestamp is more than 5 minutes from the current server time. This prevents an attacker from capturing a valid payload and replaying it later.

Idempotency Keys

Every webhook event has a unique event ID (e.g., evt_1234567890 for Stripe). LostChurn tracks processed event IDs and silently discards duplicates. Even if the same event is delivered twice within the tolerance window (which payment processors sometimes do for reliability), it is only processed once.

Edge Deduplication Layer

After signature verification passes, the edge worker performs a deduplication check against Cloudflare KV before forwarding the event to the recovery module. The dedup key is the event ID from the payment processor, and it is retained for a 24-hour window. This covers all standard retry scenarios from payment processors — even if a processor retries delivery hours after the original attempt, the duplicate is caught and discarded at the edge.

The 24-hour dedup window is intentionally longer than the 5-minute timestamp tolerance. Timestamp validation prevents replay attacks (old payloads resent maliciously), while deduplication prevents processing errors from legitimate retries that arrive well within the timestamp window.

ProtectionWhat It Prevents
HMAC-SHA256 signatureForged or tampered payloads
Constant-time comparisonTiming-based signature guessing
Timestamp tolerance (5 min)Replay of captured payloads
Edge deduplication (24 hr)Duplicate processing from processor retries

Troubleshooting

Signature Mismatch

If webhooks are failing verification, check the following:

  1. Correct signing secret: Ensure the signing secret in LostChurn matches the one shown in your payment processor's webhook settings. Rotating the secret in one place without updating the other will cause all verifications to fail.
  2. Proxy interference: If a reverse proxy or middleware is modifying the request body before it reaches LostChurn (e.g., re-encoding JSON, adding headers), the HMAC will not match. Ensure the raw body is forwarded unmodified.
  3. Multiple endpoints: If you have multiple webhook endpoints configured for the same provider, each endpoint has its own signing secret. Make sure you are using the secret for the LostChurn endpoint, not a different one.

Clock Skew

If events are being rejected due to timestamp validation, the issue may be clock skew between your payment processor's servers and LostChurn's edge worker. The 5-minute tolerance window accounts for normal clock drift. If you are consistently seeing timestamp rejections, contact support@lostchurn.com.

Next Steps

On this page