Webhooks

reauth sends webhook events to your server when things happen in your domain — user signups, subscription changes, payment outcomes, and more.

Setup

  1. Go to the domain webhooks settings in the dashboard
  2. Add an endpoint URL (must be HTTPS)
  3. Select which events to subscribe to
  4. Save — you'll receive a webhook secret (whsec_...)

Maximum 10 webhook endpoints per domain.

Signature Verification

Every webhook includes a signature for verification. Always verify signatures before processing events.

Headers

HeaderDescription
reauth-webhook-signatureSignature: t=<timestamp>,v1=<hmac-sha256>
reauth-webhook-idUnique event ID
reauth-webhook-timestampUnix timestamp (seconds)

Verification

import { verifyWebhookSignature, WebhookVerificationError } from '@reauth-dev/sdk/webhooks';

app.post('/webhooks/reauth', express.raw({ type: 'application/json' }), (req, res) => {
  try {
    const event = verifyWebhookSignature({
      payload: req.body.toString(),
      signature: req.headers['reauth-webhook-signature'] as string,
      secret: process.env.WEBHOOK_SECRET!, // "whsec_..."
      tolerance: 300, // Optional, default 300 seconds (5 minutes)
    });

    // event is verified and parsed
    console.log('Event type:', event.type);
    console.log('Event data:', event.data);

    // Process the event...

    res.status(200).json({ received: true });
  } catch (err) {
    if (err instanceof WebhookVerificationError) {
      console.error('Verification failed:', err.message);
      res.status(400).json({ error: 'Invalid signature' });
      return;
    }
    res.status(500).json({ error: 'Internal error' });
  }
});

Important: Use express.raw() (not express.json()) for the webhook route so you get the raw body for signature verification.

Signature Algorithm

  1. Parse the reauth-webhook-signature header: extract t (timestamp) and v1 (signature)
  2. Build signed payload: ${timestamp}.${rawBody}
  3. Compute HMAC-SHA256 of the signed payload using the webhook secret
  4. Compare with timing-safe equality
  5. Check timestamp is within tolerance (default 300 seconds)

The whsec_ prefix on the secret is stripped automatically.

Event Envelope

Every event has this structure:

type WebhookEvent = {
  id: string;                      // Unique event ID (UUID)
  type: string;                    // Event type (e.g., "user.created")
  api_version: string;             // API version (e.g., "2026-01-29")
  created_at: string;              // ISO 8601 timestamp
  domain_id: string;               // Your domain ID
  data: Record<string, unknown>;   // Event-specific payload
};

Event Types

User Events

EventTriggerData Fields
user.createdNew user signs upuser_id
user.deletedUser deletes accountuser_id
user.loginUser logs inuser_id, auth_method
user.frozenUser account frozenuser_id
user.unfrozenUser account unfrozenuser_id
user.whitelistedUser whitelisteduser_id
user.unwhitelistedUser unwhitelisteduser_id
user.roles_changedUser roles updateduser_id, old_roles, new_roles
user.invitedUser invited to domainuser_id

Subscription Events

EventTriggerData Fields
subscription.createdNew subscriptionuser_id, plan_id, status
subscription.updatedStatus or plan changedSee payload variants below
subscription.canceledSubscription canceleduser_id, subscription_id

subscription.updated payload variants:

The subscription.updated event is emitted for both status changes and plan changes. Inspect the data fields to determine which:

VariantData FieldsWhen
Status changeuser_id, plan_id, old_status, new_statusSubscription transitions (e.g., trialing → active)
Plan changeuser_id, from_plan_code, to_plan_code, change_typeUser upgrades/downgrades (change_type: "upgrade", "downgrade", or "lateral")

Payment Events

EventTriggerData Fields
payment.succeededPayment processeduser_id, amount_cents, currency, invoice_id
payment.failedPayment faileduser_id, amount_cents, currency, invoice_id

Test Events

EventTriggerData Fields
webhook.testTest from dashboardendpoint_id

Full Express Handler Example

import express from 'express';
import { verifyWebhookSignature, WebhookVerificationError } from '@reauth-dev/sdk/webhooks';

const app = express();

// IMPORTANT: Use raw body for webhooks
app.post(
  '/webhooks/reauth',
  express.raw({ type: 'application/json' }),
  async (req, res) => {
    let event;
    try {
      event = verifyWebhookSignature({
        payload: req.body.toString(),
        signature: req.headers['reauth-webhook-signature'] as string,
        secret: process.env.WEBHOOK_SECRET!,
      });
    } catch (err) {
      if (err instanceof WebhookVerificationError) {
        return res.status(400).json({ error: err.message });
      }
      return res.status(500).json({ error: 'Verification error' });
    }

    // Handle events
    switch (event.type) {
      case 'user.created':
        console.log('New user:', event.data.user_id);
        // Provision resources, send welcome email, etc.
        break;

      case 'subscription.created':
        console.log('New subscription:', event.data.plan_id);
        // Activate features, update limits, etc.
        break;

      case 'payment.succeeded':
        console.log('Payment received:', event.data.amount_cents, event.data.currency);
        break;

      case 'payment.failed':
        console.log('Payment failed:', event.data.user_id);
        // Notify user, pause features, etc.
        break;

      case 'webhook.test':
        console.log('Test webhook received');
        break;

      default:
        console.log('Unhandled event:', event.type);
    }

    res.status(200).json({ received: true });
  },
);

Retry Behavior

SettingValue
Max attempts5
Stale lock threshold5 minutes
Expected response2xx status code

If your endpoint returns a non-2xx status or times out, reauth retries the delivery. After 5 failed attempts, the delivery is marked as failed.

Best practices:

  • Return 200 immediately after verifying the signature
  • Process events asynchronously if they involve slow operations
  • Use the event.id for idempotent processing (events may be delivered more than once during retries)

Secret Rotation

Rotate webhook secrets from the dashboard. During rotation, both old and new secrets are valid temporarily. The SDK's verifyWebhookSignature checks all v1 signatures in the header, so rotation is seamless.