Interswitch Webhooks — receive real-time event notifications for transactions, subscriptions, payment links, and invoices. Validate signatures with HmacSHA512, handle retry logic, and process all event types. Use this skill whenever setting up webhook endpoints, validating X-Interswitch-Signature headers, handling async payment confirmations, processing subscription lifecycle events, or debugging webhook delivery. Also use when you see references to X-Interswitch-Signature, HmacSHA512, webhook event types, or Quickteller Business webhook configuration.
Receive real-time event notifications for transactions, subscriptions, payment links, and invoices via webhook callbacks.
Configure webhooks through the Quickteller Business dashboard:
| Event | Description |
|---|---|
TRANSACTION.CREATED | Transaction initiated |
TRANSACTION.UPDATED | Transaction status changed |
TRANSACTION.COMPLETED | Transaction finalized (success or failure) |
| Event | Description |
|---|---|
SUBSCRIPTION.CREATED | New subscription created |
SUBSCRIPTION.TRANSACTION_SUCCESSFUL | Subscription payment succeeded |
SUBSCRIPTION.TRANSACTION_FAILURE | Subscription payment failed |
SUBSCRIPTION.CANCELLED | Subscription cancelled |
| Event | Description |
|---|---|
PAYMENT_LINK.CREATED | Payment link generated |
PAYMENT_LINK.COMPLETED | Payment via link completed |
| Event | Description |
|---|---|
INVOICE.CREATED | Invoice generated |
INVOICE.PAID | Invoice payment received |
interface WebhookPayload {
eventType: string; // e.g., 'TRANSACTION.COMPLETED'
eventId: string; // Unique event identifier
timestamp: string; // ISO 8601 timestamp
data: {
transactionRef: string;
amount: number;
currency: string;
responseCode: string;
responseDescription: string;
paymentReference?: string;
customerEmail?: string;
[key: string]: unknown;
};
}
Every webhook request includes an X-Interswitch-Signature header containing an HmacSHA512 hash of the request body using your secret key.
Always verify the signature before processing any webhook event.
import crypto from 'crypto';
function verifyWebhookSignature(
body: string, // Raw request body (NOT parsed JSON)
signature: string, // X-Interswitch-Signature header
secretKey: string // Your webhook secret key
): boolean {
const hash = crypto
.createHmac('sha512', secretKey)
.update(body)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(hash),
Buffer.from(signature)
);
}
import express from 'express';
import crypto from 'crypto';
const app = express();
// CRITICAL: Use raw body for signature verification
app.post(
'/webhooks/interswitch',
express.raw({ type: 'application/json' }),
async (req, res) => {
const signature = req.headers['x-interswitch-signature'] as string;
const rawBody = req.body.toString();
// 1. Verify signature
if (!signature) {
return res.status(401).end();
}
const isValid = verifyWebhookSignature(
rawBody,
signature,
process.env.INTERSWITCH_WEBHOOK_SECRET!
);
if (!isValid) {
return res.status(401).end();
}
// 2. Return 200 IMMEDIATELY (before processing)
res.status(200).end();
// 3. Process event asynchronously
const event: WebhookPayload = JSON.parse(rawBody);
await processWebhookEvent(event);
}
);
// app/api/webhooks/interswitch/route.ts
import { NextRequest, NextResponse } from 'next/server';
import crypto from 'crypto';
export async function POST(request: NextRequest) {
const rawBody = await request.text();
const signature = request.headers.get('x-interswitch-signature');
// Verify signature
if (!signature) {
return NextResponse.json({ error: 'Missing signature' }, { status: 401 });
}
const hash = crypto
.createHmac('sha512', process.env.INTERSWITCH_WEBHOOK_SECRET!)
.update(rawBody)
.digest('hex');
const isValid = crypto.timingSafeEqual(
Buffer.from(hash),
Buffer.from(signature)
);
if (!isValid) {
return NextResponse.json({ error: 'Invalid signature' }, { status: 401 });
}
// Return 200 immediately
const event: WebhookPayload = JSON.parse(rawBody);
// Process asynchronously (queue recommended for production)
processWebhookEvent(event).catch(console.error);
return new NextResponse(null, { status: 200 });
}
async function processWebhookEvent(event: WebhookPayload): Promise<void> {
// Idempotency check — prevent duplicate processing
const alreadyProcessed = await checkEventProcessed(event.eventId);
if (alreadyProcessed) return;
switch (event.eventType) {
case 'TRANSACTION.COMPLETED':
await handleTransactionCompleted(event.data);
break;
case 'TRANSACTION.UPDATED':
await handleTransactionUpdated(event.data);
break;
case 'SUBSCRIPTION.TRANSACTION_SUCCESSFUL':
await handleSubscriptionPayment(event.data);
break;
case 'SUBSCRIPTION.CANCELLED':
await handleSubscriptionCancelled(event.data);
break;
case 'INVOICE.PAID':
await handleInvoicePaid(event.data);
break;
default:
console.log('Unhandled event type:', event.eventType);
}
// Mark event as processed for idempotency
await markEventProcessed(event.eventId);
}
async function handleTransactionCompleted(
data: WebhookPayload['data']
): Promise<void> {
if (data.responseCode === '00') {
// Payment successful — fulfill order
await fulfillOrder(data.transactionRef);
} else {
// Payment failed
await markOrderFailed(data.transactionRef, data.responseDescription);
}
}
| Behavior | Value |
|---|---|
| Max retries | 5 attempts |
| Retry condition | Non-200 HTTP response |
| Retry interval | Exponential backoff |
| Response requirement | HTTP 200 with no body |
Critical: Return HTTP 200 immediately with an empty body. Any other status code triggers retries.
crypto.timingSafeEqual prevents timing attacks