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(
rawBody: string,
timestampHeader: string | null,
signatureHeader: string | null
): boolean {
if (!timestampHeader || !signatureHeader) {
return false;
}
if (!/^\d+$/.test(timestampHeader)) {
return false;
}
const currentTime = Math.floor(Date.now() / 1000);
if (Math.abs(currentTime - parseInt(timestampHeader, 10)) > 300) {
return false;
}
const signedPayload = `${timestampHeader}.${rawBody}`;
const computedSignature = crypto
.createHmac('sha256', WEBHOOK_SECRET)
.update(signedPayload)
.digest('hex');
const expected = Buffer.from(signatureHeader);
const computed = Buffer.from(computedSignature);
if (expected.length !== computed.length) {
return false;
}
return crypto.timingSafeEqual(expected, computed);
}
export async function POST(req: NextRequest) {
const payload = await req.text();
const timestamp = req.headers.get('x-affonso-timestamp');
const signature = req.headers.get('x-affonso-signature');
if (!verifySignature(payload, timestamp, 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
});
}
# webhooks.py
from fastapi import FastAPI, Request, HTTPException
import hmac
import hashlib
import time
import json
import os
app = FastAPI()
WEBHOOK_SECRET = os.environ.get('AFFONSO_WEBHOOK_SECRET')
def verify_signature(
raw_body: str,
timestamp_header: str | None,
signature_header: str | None,
) -> bool:
if not timestamp_header or not signature_header:
return False
if not timestamp_header.isdigit():
return False
if abs(int(time.time()) - int(timestamp_header)) > 300:
return False
signed_payload = f"{timestamp_header}.{raw_body}"
computed_signature = hmac.new(
WEBHOOK_SECRET.encode("utf-8"),
signed_payload.encode("utf-8"),
hashlib.sha256,
).hexdigest()
return hmac.compare_digest(signature_header, computed_signature)
@app.post("/webhooks/affonso")
async def handle_webhook(request: Request):
payload = await request.body()
timestamp = request.headers.get('x-affonso-timestamp')
signature = request.headers.get('x-affonso-signature')
if not verify_signature(payload.decode(), timestamp, signature):
raise HTTPException(status_code=401, detail="Invalid signature")
event = json.loads(payload)
if event['type'] == 'affiliate.created':
await handle_affiliate_created(event['data'])
elif event['type'] == 'referral.converted':
await handle_referral_converted(event['data'])
elif event['type'] == 'transaction.paid':
await handle_transaction_paid(event['data'])
elif event['type'] == 'payout.paid':
await handle_payout_paid(event['data'])
return {"received": True}
async def handle_affiliate_created(data):
# Your logic here
pass
async def handle_referral_converted(data):
# Your logic here
pass
async def handle_transaction_paid(data):
# Your logic here
pass
async def handle_payout_paid(data):
# Your logic here
pass
// webhooks.go
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"io"
"net/http"
"os"
"strconv"
"time"
)
var webhookSecret = os.Getenv("AFFONSO_WEBHOOK_SECRET")
func abs(n int64) int64 {
if n < 0 {
return -n
}
return n
}
func verifySignature(rawBody, timestampHeader, signatureHeader string) bool {
if timestampHeader == "" || signatureHeader == "" {
return false
}
ts, err := strconv.ParseInt(timestampHeader, 10, 64)
if err != nil {
return false
}
if abs(time.Now().Unix()-ts) > 300 {
return false
}
signedPayload := timestampHeader + "." + rawBody
h := hmac.New(sha256.New, []byte(webhookSecret))
h.Write([]byte(signedPayload))
computedSig := hex.EncodeToString(h.Sum(nil))
return hmac.Equal([]byte(signatureHeader), []byte(computedSig))
}
func webhookHandler(w http.ResponseWriter, r *http.Request) {
body, _ := io.ReadAll(r.Body)
timestamp := r.Header.Get("X-Affonso-Timestamp")
signature := r.Header.Get("X-Affonso-Signature")
if !verifySignature(string(body), timestamp, signature) {
http.Error(w, "Invalid signature", http.StatusUnauthorized)
return
}
var event struct {
Type string `json:"type"`
Data json.RawMessage `json:"data"`
}
json.Unmarshal(body, &event)
switch event.Type {
case "affiliate.created":
handleAffiliateCreated(event.Data)
case "referral.converted":
handleReferralConverted(event.Data)
case "transaction.paid":
handleTransactionPaid(event.Data)
case "payout.paid":
handlePayoutPaid(event.Data)
}
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(map[string]bool{"received": true})
}
func main() {
http.HandleFunc("/webhooks/affonso", webhookHandler)
http.ListenAndServe(":8080", nil)
}
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: 'affiliates@yourcompany.com',
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
});
}
});
