Right abstraction: a PaymentGateway interface with methods matching your business operations (initializePayment, verifyPayment, refundPayment, createWebhookHandler). Each gateway is an adapter implementing this interface. Your application calls the interface; the adapter calls the gateway. Wrong abstraction: mapping every Paystack feature to a generic interface — you lose gateway-specific features (Paystack subaccounts, Paystack Plans) that do not map cleanly to a generic interface. Principle: abstract at the level of your business operations, not at the level of every API endpoint.
// Payment gateway interface — what your application code depends on
class PaymentGateway {
async initializePayment({ amount, email, reference, callbackUrl, metadata }) {
throw new Error('Not implemented');
}
async verifyPayment(reference) {
throw new Error('Not implemented');
}
async refundPayment({ transactionId, amount }) {
throw new Error('Not implemented');
}
validateWebhookSignature(rawBody, signature) {
throw new Error('Not implemented');
}
parseWebhookEvent(body) {
throw new Error('Not implemented');
}
}
// Paystack adapter
class PaystackAdapter extends PaymentGateway {
constructor(secretKey) {
super();
this.secretKey = secretKey;
this.baseUrl = 'https://api.paystack.co';
}
get headers() {
return { Authorization: 'Bearer ' + this.secretKey, 'Content-Type': 'application/json' };
}
async initializePayment({ amount, email, reference, callbackUrl, metadata }) {
var res = await fetch(this.baseUrl + '/transaction/initialize', {
method: 'POST',
headers: this.headers,
body: JSON.stringify({ amount, email, reference, callback_url: callbackUrl, metadata }),
});
var data = (await res.json()).data;
return {
authorizationUrl: data.authorization_url,
accessCode: data.access_code,
reference: data.reference,
};
}
async verifyPayment(reference) {
var res = await fetch(this.baseUrl + '/transaction/verify/' + reference, { headers: this.headers });
var data = (await res.json()).data;
return {
status: data.status === 'success' ? 'success' : 'failed',
amount: data.amount, // in kobo
reference: data.reference,
gatewayResponse: data.gateway_response,
};
}
async refundPayment({ transactionId, amount }) {
var res = await fetch(this.baseUrl + '/refund', {
method: 'POST',
headers: this.headers,
body: JSON.stringify({ transaction: transactionId, amount }),
});
return (await res.json()).data;
}
validateWebhookSignature(rawBody, signature) {
var crypto = require('crypto');
var expected = crypto.createHmac('sha512', process.env.PAYSTACK_WEBHOOK_SECRET)
.update(rawBody).digest('hex');
return signature === expected;
}
parseWebhookEvent(body) {
var event = JSON.parse(body);
return { eventType: event.event, data: event.data };
}
}
// Your application uses the interface, not the Paystack adapter directly
var gateway = new PaystackAdapter(process.env.PAYSTACK_SECRET_KEY);
var { authorizationUrl } = await gateway.initializePayment({ amount: 500000, email: 'user@example.com', reference: 'PAY_001', callbackUrl: 'https://yourapp.com/callback' });
See gateway failover design to understand the failover pattern this abstraction enables.
KES 2,999
The definitive guide to building payment systems with Paystack — from your first transaction to production-grade payment infrastructure across Africa. 9 modules, 47 lessons. Free preview available.
Start Paystack Course