interswitch-webhooks
SKILL.md
Interswitch Webhooks
Receive real-time event notifications for transactions, subscriptions, payment links, and invoices via webhook callbacks.
Setup
Configure webhooks through the Quickteller Business dashboard:
- Log in to Quickteller Business
- Navigate to Settings → Webhooks
- Enter your webhook URL (must be HTTPS)
- Select event types to subscribe to
- Copy your webhook secret key for signature verification
Event Types
Transaction Events
| Event | Description |
|---|---|
TRANSACTION.CREATED |
Transaction initiated |
TRANSACTION.UPDATED |
Transaction status changed |
TRANSACTION.COMPLETED |
Transaction finalized (success or failure) |
Subscription Events
| Event | Description |
|---|---|
SUBSCRIPTION.CREATED |
New subscription created |
SUBSCRIPTION.TRANSACTION_SUCCESSFUL |
Subscription payment succeeded |
SUBSCRIPTION.TRANSACTION_FAILURE |
Subscription payment failed |
SUBSCRIPTION.CANCELLED |
Subscription cancelled |
Payment Link Events
| Event | Description |
|---|---|
PAYMENT_LINK.CREATED |
Payment link generated |
PAYMENT_LINK.COMPLETED |
Payment via link completed |
Invoice Events
| Event | Description |
|---|---|
INVOICE.CREATED |
Invoice generated |
INVOICE.PAID |
Invoice payment received |
Webhook Payload Structure
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;
};
}
Signature Verification (CRITICAL)
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)
);
}
Express.js Webhook Handler
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);
}
);
Next.js Route Handler
// 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 });
}
Event Processing
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);
}
}
Retry Policy
| 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.
Best Practices
- Return 200 immediately — Process events asynchronously
- Use raw body for signature verification — Do NOT parse before verifying
- Implement idempotency — Store processed event IDs to prevent duplicates
- Use timing-safe comparison —
crypto.timingSafeEqualprevents timing attacks - Queue processing — Use a job queue (Bull, BullMQ) for production workloads
- Log all events — Store webhook payloads for debugging and audit
- Handle all event types — Even if you only care about some, log the rest
- Set up monitoring — Alert on high failure rates or missing events
Weekly Installs
1
Repository
rexedge/interswitchFirst Seen
3 days ago
Security Audits
Installed on
amp1
cline1
opencode1
cursor1
kimi-cli1
codex1