Complete Webhook Handler
Full examples of webhook handlers in various languages and frameworks.Copy
// app/api/webhooks/affonso/route.ts
import { NextRequest, NextResponse } from 'next/server';
import crypto from 'crypto';
const WEBHOOK_SECRET = process.env.AFFONSO_WEBHOOK_SECRET!;
function verifySignature(payload: string, signature: string): boolean {
const [timestampPart, signaturePart] = signature.split(',');
const timestamp = timestampPart.replace('t=', '');
const expectedSignature = signaturePart.replace('v1=', '');
const currentTime = Math.floor(Date.now() / 1000);
if (Math.abs(currentTime - parseInt(timestamp)) > 300) {
return false;
}
const signedPayload = `${timestamp}.${payload}`;
const computedSignature = crypto
.createHmac('sha256', WEBHOOK_SECRET)
.update(signedPayload)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(expectedSignature),
Buffer.from(computedSignature)
);
}
export async function POST(req: NextRequest) {
const payload = await req.text();
const signature = req.headers.get('x-affonso-signature');
if (!signature || !verifySignature(payload, signature)) {
return NextResponse.json(
{ error: 'Invalid signature' },
{ status: 401 }
);
}
const event = JSON.parse(payload);
switch (event.type) {
case 'affiliate.created':
await handleAffiliateCreated(event.data);
break;
case 'referral.converted':
await handleReferralConverted(event.data);
break;
case 'transaction.paid':
await handleTransactionPaid(event.data);
break;
case 'payout.paid':
await handlePayoutPaid(event.data);
break;
default:
console.log(`Unhandled event type: ${event.type}`);
}
return NextResponse.json({ received: true });
}
async function handleAffiliateCreated(data: any) {
// Send welcome email
await sendEmail({
to: data.email,
subject: 'Welcome to our affiliate program!',
template: 'affiliate-welcome',
data: { name: data.name, trackingId: data.trackingId }
});
}
async function handleReferralConverted(data: any) {
// Update analytics
await analytics.track('referral_converted', {
affiliateId: data.affiliateId,
referralId: data.referralId
});
}
async function handleTransactionPaid(data: any) {
// Notify affiliate
await notifyAffiliate(data.affiliateId, {
type: 'commission_paid',
amount: data.commissionAmount,
currency: data.commissionCurrency
});
}
async function handlePayoutPaid(data: any) {
// Create accounting entry
await createAccountingEntry({
type: 'affiliate_payout',
amount: data.amount,
affiliateId: data.affiliateId,
invoiceNumber: data.invoiceNumber
});
}
Event-Specific Examples
Sending Welcome Emails
When a new affiliate joins, send them a welcome email:Copy
async function handleAffiliateCreated(data: {
affiliateId: string;
email: string;
name: string;
trackingId: string;
}) {
await resend.emails.send({
from: '[email protected]',
to: data.email,
subject: 'Welcome to our Affiliate Program!',
react: WelcomeEmail({
name: data.name,
trackingLink: `https://yoursite.com?ref=${data.trackingId}`,
dashboardLink: 'https://affiliates.yoursite.com'
})
});
}
Syncing with CRM
Update your CRM when referrals convert:Copy
async function handleReferralConverted(data: {
referralId: string;
affiliateId: string;
email: string;
customerId: string;
}) {
// Update HubSpot contact
await hubspot.contacts.update(data.email, {
properties: {
referred_by: data.affiliateId,
referral_id: data.referralId,
customer_id: data.customerId,
conversion_date: new Date().toISOString()
}
});
}
Slack Notifications
Send Slack alerts for new sales:Copy
async function handleTransactionCreated(data: {
transactionId: string;
affiliateId: string;
saleAmount: number;
saleCurrency: string;
commissionAmount: number;
}) {
await slack.chat.postMessage({
channel: '#affiliate-sales',
blocks: [
{
type: 'section',
text: {
type: 'mrkdwn',
text: `🎉 *New Affiliate Sale!*\n` +
`Amount: ${data.saleCurrency} ${data.saleAmount}\n` +
`Commission: ${data.saleCurrency} ${data.commissionAmount}\n` +
`Affiliate: ${data.affiliateId}`
}
}
]
});
}
Accounting Integration
Create accounting entries when payouts complete:Copy
async function handlePayoutPaid(data: {
payoutId: string;
affiliateId: string;
amount: number;
invoiceNumber: number;
paidAt: string;
}) {
// Create expense in QuickBooks
await quickbooks.expense.create({
amount: data.amount,
account: 'Affiliate Commissions',
vendor: data.affiliateId,
reference: `Payout #${data.invoiceNumber}`,
date: new Date(data.paidAt)
});
}
Idempotency
Ensure you handle duplicate webhooks gracefully:Copy
const processedEvents = new Set<string>();
async function handleWebhook(event: { id: string; type: string; data: any }) {
// Check if already processed
if (processedEvents.has(event.id)) {
console.log(`Event ${event.id} already processed, skipping`);
return;
}
// Or use database for persistence
const existing = await db.webhookEvent.findUnique({
where: { eventId: event.id }
});
if (existing) {
return; // Already processed
}
// Process the event
await processEvent(event);
// Mark as processed
await db.webhookEvent.create({
data: {
eventId: event.id,
type: event.type,
processedAt: new Date()
}
});
}
Error Handling
Handle errors gracefully without blocking webhook acknowledgment:Copy
app.post('/webhooks/affonso', async (req, res) => {
// Always acknowledge quickly
res.status(200).json({ received: true });
try {
await processWebhook(req.body);
} catch (error) {
// Log error but don't fail the webhook
console.error('Webhook processing error:', error);
// Queue for retry
await retryQueue.add('webhook-retry', {
event: req.body,
attempt: 1,
error: error.message
});
// Alert your team
await alerting.notify({
severity: 'warning',
message: `Webhook processing failed: ${error.message}`,
eventId: req.body.id
});
}
});
