Webhooks
Webhooks
Webhooks are HTTP callbacks that payment providers send to notify your application about events like successful payments, subscription changes, and disputes.
Why Webhooks Matter
Webhooks are essential for:
- Subscription status changes - Know when a subscription is cancelled
- Payment confirmations - Don’t rely on client-side success
- Dispute notifications - Respond to chargebacks quickly
- Invoice updates - Track invoice payment status
Setting Up Webhooks
-
Create a webhook endpoint
import { Hono } from 'hono';import { createWebhookRouter } from '@qazuor/qzpay-hono';import { stripeAdapter } from './billing';const app = new Hono();const webhookRouter = createWebhookRouter({billing,paymentAdapter: stripeAdapter,handlers: {'customer.subscription.created': async (c, event) => {console.log('New subscription:', event.data);}}});app.route('/webhooks/stripe', webhookRouter); -
Configure in provider dashboard
Go to Stripe Dashboard → Developers → Webhooks and add:
- URL:
https://yourdomain.com/webhooks/stripe - Events: Select all billing-related events
- URL:
-
Store the webhook secret
Terminal window STRIPE_WEBHOOK_SECRET=whsec_xxx
Webhook Security
QZPay automatically handles:
Signature Verification
// Signature is verified before any processing// Invalid signatures throw WebhookSignatureErrorReplay Attack Prevention
// Timestamps are validated within tolerance window// Default: 300 seconds (5 minutes)Idempotency
// Duplicate events are detected and ignored// Events are processed exactly onceHandling Events
Built-in Processing
QZPay processes common webhook events automatically:
| Provider Event | QZPay Event | Action |
|---|---|---|
customer.subscription.created | subscription.created | Sync subscription |
customer.subscription.updated | subscription.updated | Update status |
customer.subscription.deleted | subscription.canceled | Mark canceled |
invoice.payment_succeeded | payment.succeeded | Record payment |
invoice.payment_failed | payment.failed | Update status |
Custom Event Handlers
billing.on('subscription.canceled', async (event) => { // Your custom logic await sendCancellationEmail(event.data.customerId); await revokeAccess(event.data.customerId);});
billing.on('payment.failed', async (event) => { await notifyPaymentFailure(event.data); await scheduleRetryReminder(event.data.customerId);});Provider-Specific Configuration
Stripe
import { createWebhookRouter } from '@qazuor/qzpay-hono';import { createQZPayStripeAdapter } from '@qazuor/qzpay-stripe';
const stripeAdapter = createQZPayStripeAdapter({ secretKey: process.env.STRIPE_SECRET_KEY!, webhookSecret: process.env.STRIPE_WEBHOOK_SECRET!});
const stripeWebhook = createWebhookRouter({ billing, paymentAdapter: stripeAdapter});
app.route('/webhooks/stripe', stripeWebhook);MercadoPago
import { createWebhookRouter } from '@qazuor/qzpay-hono';import { createQZPayMercadoPagoAdapter } from '@qazuor/qzpay-mercadopago';
const mpAdapter = createQZPayMercadoPagoAdapter({ accessToken: process.env.MP_ACCESS_TOKEN!});
const mpWebhook = createWebhookRouter({ billing, paymentAdapter: mpAdapter});
app.route('/webhooks/mercadopago', mpWebhook);Testing Webhooks
Local Development
Use Stripe CLI for local testing:
stripe listen --forward-to localhost:3000/webhooks/stripeTest Events
// In tests, use the mock webhook creatorimport { createMockWebhookEvent } from '@qazuor/qzpay-core/testing';
const event = createMockWebhookEvent('subscription.created', { customerId: 'cus_123', planId: 'pro_monthly'});Error Handling
import { WebhookSignatureError, WebhookProcessingError} from '@qazuor/qzpay-core';
app.post('/webhooks/stripe', async (c) => { try { await webhookHandler(c); return c.json({ received: true }); } catch (error) { if (error instanceof WebhookSignatureError) { return c.json({ error: 'Invalid signature' }, 400); } // Log and return 500 to trigger retry console.error('Webhook error:', error); return c.json({ error: 'Processing failed' }, 500); }});Best Practices
- Always verify signatures before processing
- Process webhooks idempotently - same event may arrive multiple times
- Return quickly - do heavy processing asynchronously
- Log all webhook events for debugging
- Monitor webhook failures and set up alerts
- Use webhook secrets from environment variables