Skip to main content
Use vault.handleWebhook(provider, payload, headers) to verify provider signatures and normalize provider-specific event payloads into a canonical VaultEvent format.

Overview

Webhook handling provides:
  • Signature Verification: Cryptographic verification of webhook authenticity
  • Event Normalization: Consistent event shape across all providers
  • Automatic Parsing: Raw payload parsing and type conversion

Provider Support

Each provider adapter implements webhook signature verification:
ProviderHeaderConfig Required
Stripestripe-signaturewebhookSecret
dLocalx-dlocal-signature or x-signaturewebhookSecret or secretKey
Paystackx-paystack-signaturewebhookSecret or secretKey
Critical: Pass the raw request body exactly as received (string or Buffer). Do not parse and re-serialize JSON before verification, as this will cause signature validation to fail.

Basic Usage

import { VaultClient, WebhookVerificationError } from '@vaultsaas/core';

const vault = new VaultClient({
  providers: {
    stripe: {
      adapter: StripeAdapter,
      config: {
        apiKey: process.env.STRIPE_API_KEY,
        webhookSecret: process.env.STRIPE_WEBHOOK_SECRET,
      },
    },
  },
  routing: {
    rules: [{ match: { default: true }, provider: 'stripe' }],
  },
});

try {
  const event = await vault.handleWebhook(
    'stripe',
    rawBody,  // Buffer or string - don't parse!
    headers   // Request headers
  );
  
  console.log('Event type:', event.type);
  console.log('Transaction ID:', event.transactionId);
  console.log('Provider event ID:', event.providerEventId);
} catch (error) {
  if (error instanceof WebhookVerificationError) {
    // Signature verification failed
    return { status: 400, body: error.message };
  }
  throw error;
}

VaultEvent Structure

All webhook events are normalized to this shape:
src/types/events.ts:13
export interface VaultEvent {
  id: string;                  // Vault-generated event ID
  type: VaultEventType;        // Canonical event type
  provider: string;            // Provider name (e.g., "stripe")
  transactionId?: string;      // Related transaction ID
  providerEventId: string;     // Original provider event ID
  data: Record<string, unknown>; // Event-specific data
  rawPayload: unknown;         // Original webhook payload
  timestamp: string;           // ISO 8601 timestamp
}

Event Types

src/types/events.ts:1
export type VaultEventType =
  | 'payment.completed'
  | 'payment.failed'
  | 'payment.pending'
  | 'payment.requires_action'
  | 'payment.refunded'
  | 'payment.partially_refunded'
  | 'payment.disputed'
  | 'payment.dispute_resolved'
  | 'payout.completed'
  | 'payout.failed';

Framework Examples

import { createServer } from 'node:http';
import { StripeAdapter, VaultClient, WebhookVerificationError } from '@vaultsaas/core';

function mustEnv(name: string): string {
  const value = process.env[name];
  if (!value) throw new Error(`Missing env var: ${name}`);
  return value;
}

const vault = new VaultClient({
  providers: {
    stripe: {
      adapter: StripeAdapter,
      config: {
        apiKey: mustEnv('STRIPE_API_KEY'),
        webhookSecret: mustEnv('STRIPE_WEBHOOK_SECRET'),
      },
    },
  },
  routing: {
    rules: [{ match: { default: true }, provider: 'stripe' }],
  },
});

const server = createServer(async (req, res) => {
  if (req.method !== 'POST' || req.url !== '/webhooks/stripe') {
    res.statusCode = 404;
    res.end('not found');
    return;
  }

  const chunks: Buffer[] = [];
  req.on('data', (chunk) => chunks.push(Buffer.from(chunk)));

  req.on('end', async () => {
    try {
      const payload = Buffer.concat(chunks);
      const headers = Object.fromEntries(
        Object.entries(req.headers)
          .filter((entry): entry is [string, string] => typeof entry[1] === 'string'),
      );

      const event = await vault.handleWebhook('stripe', payload, headers);
      console.log('Normalized event:', event.type, event.providerEventId);
      res.statusCode = 200;
      res.end('ok');
    } catch (error) {
      if (error instanceof WebhookVerificationError) {
        res.statusCode = 400;
        res.end(`${error.code}: ${error.message}`);
        return;
      }
      res.statusCode = 500;
      res.end('internal_error');
    }
  });
});

server.listen(3000, () => {
  console.log('Listening on http://localhost:3000/webhooks/stripe');
});

Event Processing

async function processWebhookEvent(event: VaultEvent) {
  console.log(`Received ${event.type} from ${event.provider}`);
  
  switch (event.type) {
    case 'payment.completed':
      await handlePaymentCompleted(event);
      break;
    
    case 'payment.failed':
      await handlePaymentFailed(event);
      break;
    
    case 'payment.refunded':
      await handlePaymentRefunded(event);
      break;
    
    default:
      console.log('Unhandled event type:', event.type);
  }
}

Signature Verification Details

The SDK verifies webhook signatures when adapters support it:
src/client/vault-client.ts:343
async handleWebhook(
  provider: string,
  payload: Buffer | string,
  headers: Record<string, string>,
): Promise<VaultEvent> {
  const adapter = this.getAdapter(provider);

  if (adapter.handleWebhook) {
    const handler = adapter.handleWebhook;
    const event = await this.wrapProviderCall(provider, 'handleWebhook', () =>
      Promise.resolve(handler.call(adapter, payload, headers)),
    );

    if (event.transactionId) {
      this.transactionProviderIndex.set(event.transactionId, provider);
    }

    this.queueWebhookEvent(event);
    return event;
  }

  const parsedPayload = this.parseWebhookPayload(payload);
  const event = normalizeWebhookEvent(provider, parsedPayload, payload);

  if (event.transactionId) {
    this.transactionProviderIndex.set(event.transactionId, provider);
  }

  this.queueWebhookEvent(event);
  return event;
}
If signature verification fails, a WebhookVerificationError is thrown with code WEBHOOK_SIGNATURE_INVALID.

Best Practices

Always use raw bodyConfigure your framework to preserve the raw request body for webhook routes:
// Express
app.use('/webhooks', express.raw({ type: 'application/json' }));

// Next.js - use request.text() instead of request.json()
const body = await request.text();
Implement idempotent processingProviders may send duplicate webhooks. Track processed event IDs to prevent duplicate actions.
Return 200 quicklyAcknowledge receipt immediately and process asynchronously:
app.post('/webhooks/stripe', async (req, res) => {
  const event = await vault.handleWebhook('stripe', req.body, req.headers);
  
  res.json({ received: true }); // Return 200 immediately
  
  // Process asynchronously
  processWebhookEvent(event).catch(console.error);
});
Secure your webhook endpoints
  • Always verify signatures (don’t skip handleWebhook)
  • Use HTTPS in production
  • Consider IP allowlisting for additional security
  • Don’t expose webhook URLs publicly
Log webhook eventsLog all webhook events for debugging and audit trails:
logger.info('Webhook received', {
  provider: event.provider,
  type: event.type,
  providerEventId: event.providerEventId,
  transactionId: event.transactionId,
});

Testing Webhooks

Use provider CLI tools to test webhook handling:
# Install Stripe CLI
stripe listen --forward-to localhost:3000/webhooks/stripe

# Trigger test events
stripe trigger payment_intent.succeeded

Platform Integration

When Platform Connector is enabled, webhook events are automatically queued for analytics:
src/client/vault-client.ts:573
private queueWebhookEvent(event: VaultEvent): void {
  if (!this.platformConnector) {
    return;
  }

  this.platformConnector.queueWebhookEvent({
    id: event.id,
    type: event.type,
    provider: event.provider,
    transactionId: event.transactionId,
    providerEventId: event.providerEventId,
    data: event.data,
    timestamp: event.timestamp,
  });
}
No additional code required - webhook events are automatically sent to VaultSaaS platform when platformApiKey is configured.

Next Steps

Error Handling

Handle webhook verification errors

Architecture

Learn about webhook normalization

Build docs developers (and LLMs) love