Superwall
Webhooks

Verify Webhook Requests

Learn how to verify webhook requests using the signing secret to ensure authenticity and security.

Why Verify Webhooks?

Verifying webhook requests is crucial for security. It ensures that:

  • Requests are actually coming from Superwall's servers
  • The payload hasn't been tampered with in transit
  • Replay attacks are prevented through timestamp validation

Without verification, malicious actors could send fake webhook events to your endpoint.

Getting Your Signing Secret

Every webhook endpoint has a unique signing secret that's used to verify requests. You can find this secret in your webhook details:

Copy webhook signing secret

Click the Copy Secret button to copy your webhook's signing secret to your clipboard.

Keep your signing secret secure. Never commit it to version control or expose it in client-side code. Store it as an environment variable like SUPERWALL_WEBHOOK_SECRET.

Verification Methods

Superwall uses Svix for webhook delivery, which provides robust verification libraries for multiple languages.

Install the Svix library:

npm install svix
# or
yarn add svix
# or
pnpm add svix

Verify incoming requests:

import { Webhook } from 'svix';

export async function POST(request) {
  // Get the raw body as a string
  const payload = await request.text();

  // Get the Svix headers
  const headers = {
    'svix-id': request.headers.get('svix-id'),
    'svix-timestamp': request.headers.get('svix-timestamp'),
    'svix-signature': request.headers.get('svix-signature'),
  };

  // Create a new Webhook instance with your secret
  const wh = new Webhook(process.env.SUPERWALL_WEBHOOK_SECRET);

  let event;

  try {
    // Verify the webhook
    event = wh.verify(payload, headers);
  } catch (err) {
    console.error('Webhook verification failed:', err.message);
    return new Response('Webhook verification failed', { status: 400 });
  }

  // Webhook is verified - process the event
  console.log('Verified event:', event);

  // Process your event here
  // ...

  return new Response('Success', { status: 200 });
}

Option 2: Manual Verification

If you prefer not to use the Svix library, you can manually verify webhooks using the HMAC signature:

import crypto from 'crypto';

function verifyWebhook(payload, headers, secret) {
  const msgId = headers['svix-id'];
  const msgTimestamp = headers['svix-timestamp'];
  const msgSignature = headers['svix-signature'];

  // Verify timestamp to prevent replay attacks
  const timestamp = parseInt(msgTimestamp, 10);
  const now = Math.floor(Date.now() / 1000);

  if (now - timestamp > 300) { // 5 minutes
    throw new Error('Webhook timestamp too old');
  }

  // Create the signed content
  const signedContent = `${msgId}.${msgTimestamp}.${payload}`;

  // Compute the expected signature
  const secretBytes = Buffer.from(secret.split('_')[1], 'base64');
  const signature = crypto
    .createHmac('sha256', secretBytes)
    .update(signedContent)
    .digest('base64');

  // Compare signatures
  const expectedSignature = `v1,${signature}`;

  // Extract all signatures from the header
  const passedSignatures = msgSignature.split(' ');

  // Check if any signature matches
  const signatureMatch = passedSignatures.some(sig =>
    crypto.timingSafeEqual(
      Buffer.from(sig),
      Buffer.from(expectedSignature)
    )
  );

  if (!signatureMatch) {
    throw new Error('Webhook signature verification failed');
  }

  return JSON.parse(payload);
}

Important Implementation Notes

Use Raw Request Body

The signature is computed against the raw request body. Do not parse or modify the body before verification:

// ✅ Correct - use raw body
const payload = await request.text();
const event = wh.verify(payload, headers);

// ❌ Wrong - parsing changes the body
const payload = await request.json();
const event = wh.verify(JSON.stringify(payload), headers); // Will fail!

Required Headers

Three headers are required for verification:

HeaderDescription
svix-idUnique message ID
svix-timestampUnix timestamp when the webhook was sent
svix-signatureHMAC signature(s) of the message

Framework-Specific Examples

Next.js (App Router)

// app/api/webhooks/route.js
import { Webhook } from 'svix';

export async function POST(request) {
  const payload = await request.text();

  const headers = {
    'svix-id': request.headers.get('svix-id'),
    'svix-timestamp': request.headers.get('svix-timestamp'),
    'svix-signature': request.headers.get('svix-signature'),
  };

  const wh = new Webhook(process.env.SUPERWALL_WEBHOOK_SECRET);

  try {
    const event = wh.verify(payload, headers);

    // Handle the event
    switch (event.type) {
      case 'initial_purchase':
        // Handle initial purchase
        break;
      case 'renewal':
        // Handle renewal
        break;
      // ... other event types
    }

    return new Response('Success', { status: 200 });
  } catch (err) {
    return new Response('Webhook verification failed', { status: 400 });
  }
}

Express

import express from 'express';
import { Webhook } from 'svix';

const app = express();

// Important: Use raw body for webhook verification
app.post('/webhooks', express.raw({ type: 'application/json' }), (req, res) => {
  const payload = req.body.toString();

  const headers = {
    'svix-id': req.headers['svix-id'],
    'svix-timestamp': req.headers['svix-timestamp'],
    'svix-signature': req.headers['svix-signature'],
  };

  const wh = new Webhook(process.env.SUPERWALL_WEBHOOK_SECRET);

  try {
    const event = wh.verify(payload, headers);

    // Handle the event
    console.log('Verified event:', event);

    res.status(200).send('Success');
  } catch (err) {
    console.error('Webhook verification failed:', err.message);
    res.status(400).send('Verification failed');
  }
});

Python (FastAPI)

from fastapi import FastAPI, Request, HTTPException
from svix.webhooks import Webhook, WebhookVerificationError
import os

app = FastAPI()

@app.post("/webhooks")
async def handle_webhook(request: Request):
    payload = await request.body()
    headers = {
        "svix-id": request.headers.get("svix-id"),
        "svix-timestamp": request.headers.get("svix-timestamp"),
        "svix-signature": request.headers.get("svix-signature"),
    }

    wh = Webhook(os.environ["SUPERWALL_WEBHOOK_SECRET"])

    try:
        event = wh.verify(payload, headers)

        # Handle the event
        print(f"Verified event: {event}")

        return {"status": "success"}
    except WebhookVerificationError as e:
        print(f"Webhook verification failed: {e}")
        raise HTTPException(status_code=400, detail="Verification failed")

Testing Webhook Verification

During development, you can test webhook verification:

  1. Use the actual signing secret from your webhook endpoint
  2. Capture real webhook payloads by temporarily logging them
  3. Test with valid and invalid signatures to ensure your verification works

Never test with production webhooks in a development environment without proper safeguards. Consider creating a separate webhook endpoint for testing.

Security Best Practices

  1. Always verify webhooks - Never process unverified webhook data
  2. Use environment variables - Store your signing secret securely
  3. Check timestamps - Reject old webhooks to prevent replay attacks (Svix does this automatically)
  4. Return 200 quickly - Acknowledge receipt immediately, then process asynchronously
  5. Log verification failures - Monitor for potential attacks or configuration issues
  6. Rotate secrets periodically - Update your signing secret if it's ever compromised

Troubleshooting

Verification Always Fails

  • Ensure you're using the raw request body, not a parsed/stringified version
  • Check that all three required headers are present
  • Verify you're using the correct signing secret for this webhook endpoint
  • Make sure your secret includes the full value (it should start with whsec_)

"Timestamp too old" Errors

  • Your server's clock may be out of sync - verify your server time
  • Network delays may be too high - check your server's response time
  • The webhook may be a replay attack - this is working as intended

Advanced Usage

For advanced webhook verification scenarios, including signature rotation and custom verification logic, see the Svix documentation.


Webhooks Reference

For information about webhook events, payload structure, and handling different event types, see the main Webhooks documentation.

In the Webhooks section within Integrations, you can manage your webhooks with Superwall:

How is this guide?