Webhook Handling
This guide shows you how to handle SolvaPay webhooks to keep your application in sync with subscription and payment events.
Table of Contents
Overview
SolvaPay sends webhooks to notify your application about important events:
- Subscription Created - New subscription activated
- Subscription Updated - Subscription plan or status changed
- Subscription Cancelled - Subscription cancelled
- Payment Succeeded - Payment processed successfully
- Payment Failed - Payment processing failed
Webhook Flow
SolvaPay Backend
↓
Webhook Event
↓
Your Webhook Endpoint
↓
Verify Signature
↓
Process Event
↓
Update Your Database
Webhook Setup
1. Configure Webhook Endpoint
Set your webhook endpoint in the SolvaPay dashboard:
https://your-app.com/api/webhooks/solvapay
2. Create Webhook Endpoint
Create an endpoint to receive webhooks:
// app/api/webhooks/solvapay/route.ts (Next.js)
import { NextRequest, NextResponse } from 'next/server';
import { verifyWebhook } from '@solvapay/server';
export async function POST(request: NextRequest) {
try {
// Verify webhook signature
const payload = await request.text();
const signature = request.headers.get('x-solvapay-signature');
if (!signature) {
return NextResponse.json(
{ error: 'Missing signature' },
{ status: 401 }
);
}
const verified = verifyWebhook(payload, signature, {
secret: process.env.SOLVAPAY_WEBHOOK_SECRET!,
});
if (!verified) {
return NextResponse.json(
{ error: 'Invalid signature' },
{ status: 401 }
);
}
// Parse event
const event = JSON.parse(payload);
// Handle event
await handleWebhookEvent(event);
return NextResponse.json({ received: true });
} catch (error) {
console.error('Webhook error:', error);
return NextResponse.json(
{ error: 'Webhook processing failed' },
{ status: 500 }
);
}
}
Express.js Webhook Endpoint
// routes/webhooks.ts
import express from 'express';
import { verifyWebhook } from '@solvapay/server';
const router = express.Router();
router.post('/solvapay', express.raw({ type: 'application/json' }), async (req, res) => {
try {
const signature = req.headers['x-solvapay-signature'] as string;
if (!signature) {
return res.status(401).json({ error: 'Missing signature' });
}
const payload = req.body.toString();
const verified = verifyWebhook(payload, signature, {
secret: process.env.SOLVAPAY_WEBHOOK_SECRET!,
});
if (!verified) {
return res.status(401).json({ error: 'Invalid signature' });
}
const event = JSON.parse(payload);
await handleWebhookEvent(event);
res.json({ received: true });
} catch (error) {
console.error('Webhook error:', error);
res.status(500).json({ error: 'Webhook processing failed' });
}
});
export default router;
Webhook Verification
Verify Webhook Signature
Always verify webhook signatures to ensure requests are from SolvaPay:
import { verifyWebhook } from '@solvapay/server';
const verified = verifyWebhook(payload, signature, {
secret: process.env.SOLVAPAY_WEBHOOK_SECRET!,
});
if (!verified) {
// Reject webhook
return res.status(401).json({ error: 'Invalid signature' });
}
Environment Variables
Set your webhook secret:
SOLVAPAY_WEBHOOK_SECRET=whsec_...
Get your webhook secret from the SolvaPay dashboard.
Event Handling
Event Types
Handle different event types:
interface WebhookEvent {
type: string;
data: {
customerRef: string;
subscriptionId?: string;
planRef?: string;
status?: string;
// ... other event data
};
timestamp: string;
}
Handle Subscription Events
async function handleWebhookEvent(event: WebhookEvent) {
switch (event.type) {
case 'subscription.created':
await handleSubscriptionCreated(event.data);
break;
case 'subscription.updated':
await handleSubscriptionUpdated(event.data);
break;
case 'subscription.cancelled':
await handleSubscriptionCancelled(event.data);
break;
case 'payment.succeeded':
await handlePaymentSucceeded(event.data);
break;
case 'payment.failed':
await handlePaymentFailed(event.data);
break;
default:
console.log('Unknown event type:', event.type);
}
}
Subscription Created
async function handleSubscriptionCreated(data: any) {
const { customerRef, subscriptionId, planRef } = data;
// Update your database
await db.subscriptions.create({
customerRef,
subscriptionId,
planRef,
status: 'active',
});
// Clear subscription cache
await clearSubscriptionCache(customerRef);
// Send welcome email, etc.
await sendWelcomeEmail(customerRef);
}
Subscription Updated
async function handleSubscriptionUpdated(data: any) {
const { customerRef, subscriptionId, planRef, status } = data;
// Update subscription in database
await db.subscriptions.update({
where: { subscriptionId },
data: { planRef, status },
});
// Clear cache
await clearSubscriptionCache(customerRef);
}
Subscription Cancelled
async function handleSubscriptionCancelled(data: any) {
const { customerRef, subscriptionId } = data;
// Update subscription status
await db.subscriptions.update({
where: { subscriptionId },
data: { status: 'cancelled', cancelledAt: new Date() },
});
// Clear cache
await clearSubscriptionCache(customerRef);
// Send cancellation email
await sendCancellationEmail(customerRef);
}
Payment Succeeded
async function handlePaymentSucceeded(data: any) {
const { customerRef, paymentIntentId, amount } = data;
// Record payment
await db.payments.create({
customerRef,
paymentIntentId,
amount,
status: 'succeeded',
});
// Clear cache
await clearSubscriptionCache(customerRef);
}
Payment Failed
async function handlePaymentFailed(data: any) {
const { customerRef, paymentIntentId, error } = data;
// Record failed payment
await db.payments.create({
customerRef,
paymentIntentId,
status: 'failed',
error: error.message,
});
// Notify user
await sendPaymentFailedEmail(customerRef, error);
}
Complete Examples
Next.js Complete Example
// app/api/webhooks/solvapay/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { verifyWebhook } from '@solvapay/server';
import { clearSubscriptionCache } from '@solvapay/next';
export async function POST(request: NextRequest) {
try {
// Get raw body for signature verification
const payload = await request.text();
const signature = request.headers.get('x-solvapay-signature');
if (!signature) {
return NextResponse.json(
{ error: 'Missing signature' },
{ status: 401 }
);
}
// Verify webhook signature
const verified = verifyWebhook(payload, signature, {
secret: process.env.SOLVAPAY_WEBHOOK_SECRET!,
});
if (!verified) {
return NextResponse.json(
{ error: 'Invalid signature' },
{ status: 401 }
);
}
// Parse event
const event = JSON.parse(payload);
// Handle event
await handleWebhookEvent(event);
return NextResponse.json({ received: true });
} catch (error) {
console.error('Webhook error:', error);
return NextResponse.json(
{ error: 'Webhook processing failed' },
{ status: 500 }
);
}
}
async function handleWebhookEvent(event: any) {
const { type, data } = event;
switch (type) {
case 'subscription.created':
case 'subscription.updated':
case 'subscription.cancelled':
// Clear subscription cache
if (data.customerRef) {
await clearSubscriptionCache(data.customerRef);
}
// Update database
await updateSubscriptionInDatabase(data);
break;
case 'payment.succeeded':
case 'payment.failed':
// Handle payment events
await handlePaymentEvent(type, data);
break;
default:
console.log('Unknown event type:', type);
}
}
Express.js Complete Example
// routes/webhooks.ts
import express from 'express';
import { verifyWebhook } from '@solvapay/server';
import { clearSubscriptionCache } from '@solvapay/next';
const router = express.Router();
// Important: Use express.raw() to get raw body for signature verification
router.post(
'/solvapay',
express.raw({ type: 'application/json' }),
async (req, res) => {
try {
const signature = req.headers['x-solvapay-signature'] as string;
if (!signature) {
return res.status(401).json({ error: 'Missing signature' });
}
const payload = req.body.toString();
const verified = verifyWebhook(payload, signature, {
secret: process.env.SOLVAPAY_WEBHOOK_SECRET!,
});
if (!verified) {
return res.status(401).json({ error: 'Invalid signature' });
}
const event = JSON.parse(payload);
await handleWebhookEvent(event);
res.json({ received: true });
} catch (error) {
console.error('Webhook error:', error);
res.status(500).json({ error: 'Webhook processing failed' });
}
}
);
async function handleWebhookEvent(event: any) {
const { type, data } = event;
switch (type) {
case 'subscription.created':
case 'subscription.updated':
case 'subscription.cancelled':
if (data.customerRef) {
await clearSubscriptionCache(data.customerRef);
}
await updateSubscriptionInDatabase(data);
break;
case 'payment.succeeded':
case 'payment.failed':
await handlePaymentEvent(type, data);
break;
}
}
export default router;
Best Practices
-
Always Verify Signatures: Never process webhooks without verifying the signature.
-
Use Idempotency: Handle duplicate webhook deliveries gracefully.
-
Process Asynchronously: Process webhooks asynchronously to respond quickly.
-
Log Events: Log all webhook events for debugging and auditing.
-
Handle Errors Gracefully: Return appropriate status codes and log errors.
-
Clear Caches: Clear subscription caches when subscription events occur.
-
Update Database: Keep your database in sync with webhook events.
Testing Webhooks
Local Testing with ngrok
# Start your local server
npm run dev
# In another terminal, expose with ngrok
ngrok http 3000
# Use ngrok URL in SolvaPay dashboard webhook settings
Test Webhook Payload
// Test webhook locally
const testEvent = {
type: 'subscription.created',
data: {
customerRef: 'user_123',
subscriptionId: 'sub_123',
planRef: 'pln_premium',
},
timestamp: new Date().toISOString(),
};
await handleWebhookEvent(testEvent);
Next Steps
- Error Handling Strategies - Handle webhook errors
- Next.js Integration Guide - Next.js webhook setup
- API Reference - Full API documentation