Skip to main content

Complete Webhook Handler

Full examples of webhook handlers in various languages and frameworks.
// 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:
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:
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:
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:
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:
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:
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
    });
  }
});