Skip to main content

Webhook Handlers

The SDK includes ready-made webhook handlers for Express and Next.js that handle signature verification, JSON parsing, deduplication, and event routing automatically.

processWebhook

The low-level webhook processor. Framework handlers build on top of this.
import { processWebhook } from '@zendfi/sdk';

const result = await processWebhook({
  signature: req.headers['x-zendfi-signature'],
  body: rawBody,
  handlers: {
    'payment.confirmed': async (payment) => {
      await fulfillOrder(payment);
    },
  },
  config: {
    secret: process.env.ZENDFI_WEBHOOK_SECRET!,
  },
});

WebhookResult

interface WebhookResult {
  success: boolean;     // did the webhook process without error
  processed: boolean;   // was a handler actually invoked
  error?: string;       // error message if failed
  event?: WebhookEvent; // which event type was received
  statusCode?: number;  // suggested HTTP status code
}

Express Handler

Import from @zendfi/sdk/express:
import express from 'express';
import { createExpressWebhookHandler } from '@zendfi/sdk/express';

const app = express();

app.post('/webhooks/zendfi',
  express.raw({ type: 'application/json' }),
  createExpressWebhookHandler({
    secret: process.env.ZENDFI_WEBHOOK_SECRET!,
    handlers: {
      'payment.confirmed': async (payment) => {
        await db.orders.update(
          { status: 'paid' },
          { where: { id: payment.metadata.orderId } }
        );
      },
      'payment.failed': async (payment) => {
        await sendFailureNotification(payment);
      },
      'subscription.canceled': async (subscription) => {
        await revokeAccess(subscription.customer_email);
      },
    },
  })
);
You must use express.raw({ type: 'application/json' }) before the webhook handler. The handler needs the raw body string for signature verification. If you use express.json() instead, the signature check will fail.

Next.js Handler

Import from @zendfi/sdk/nextjs. Works with the App Router:
// app/api/webhooks/zendfi/route.ts
import { createNextWebhookHandler } from '@zendfi/sdk/nextjs';

export const POST = createNextWebhookHandler({
  secret: process.env.ZENDFI_WEBHOOK_SECRET!,
  handlers: {
    'payment.confirmed': async (payment) => {
      await prisma.order.update({
        where: { id: payment.metadata.orderId },
        data: { status: 'paid', paidAt: new Date() },
      });
    },
    'invoice.paid': async (data) => {
      await markInvoiceFulfilled(data);
    },
  },
});

Handler Config

Both framework handlers accept a shared configuration:
interface WebhookHandlerConfig {
  /** Your webhook secret */
  secret: string;

  /** Called after successful processing (for deduplication) */
  onProcessed?: (webhookId: string) => Promise<void>;

  /** Check if webhook was already processed */
  isProcessed?: (webhookId: string) => Promise<boolean>;

  /** Global error handler */
  onError?: (error: Error, event?: WebhookEvent) => void | Promise<void>;
}
The handlers map is passed alongside the config when creating framework-specific handlers (see Express/Next.js examples above).

Handler Callback Signature

Each handler receives two arguments — the event-specific data object and the full webhook payload:
type WebhookEventHandler<T = any> = (
  data: T,
  event: WebhookPayload
) => void | Promise<void>;

Available Event Handlers

type WebhookHandlers = Partial<{
  'payment.created': WebhookEventHandler;
  'payment.confirmed': WebhookEventHandler;
  'payment.failed': WebhookEventHandler;
  'payment.expired': WebhookEventHandler;
  'subscription.created': WebhookEventHandler;
  'subscription.activated': WebhookEventHandler;
  'subscription.canceled': WebhookEventHandler;
  'subscription.payment_failed': WebhookEventHandler;
  'split.completed': WebhookEventHandler;
  'split.failed': WebhookEventHandler;
  'installment.due': WebhookEventHandler;
  'installment.paid': WebhookEventHandler;
  'installment.late': WebhookEventHandler;
  'invoice.sent': WebhookEventHandler;
  'invoice.paid': WebhookEventHandler;
}>;
You only need to register handlers for events you care about. Unhandled events return a 200 response automatically.

Deduplication

The SDK includes built-in deduplication to handle webhook retries. By default, it uses an in-memory set (auto-pruned at 10,000 entries). For production, provide your own storage:
createExpressWebhookHandler({
  secret: process.env.ZENDFI_WEBHOOK_SECRET!,
  handlers: { /* ... */ },

  // Use Redis for production deduplication
  isProcessed: async (webhookId) => {
    const exists = await redis.exists(`webhook:${webhookId}`);
    return exists === 1;
  },
  onProcessed: async (webhookId) => {
    await redis.set(`webhook:${webhookId}`, '1', 'EX', 86400); // 24h TTL
  },
});

Error Handling

Register a global error handler:
createNextWebhookHandler({
  secret: process.env.ZENDFI_WEBHOOK_SECRET!,
  handlers: { /* ... */ },
  onError: async (error, event) => {
    console.error(`Webhook error for ${event}:`, error);
    await alertOps({ error: error.message, event });
  },
});