Build your payment integration to detect Paystack outages, inform customers honestly, queue orders for later processing, and optionally fail over to an alternative payment provider. Start with a health check that pings the Paystack API periodically. When it detects downtime, switch your checkout to a degraded mode: hide payment, show a "we are experiencing payment issues" message, or offer an alternative provider like M-Pesa Daraja for Kenyan customers. Queue orders so customers do not lose their cart. When Paystack recovers, process the queue.
Paystack processes millions of transactions across Nigeria, Ghana, Kenya, South Africa, and other markets. Their infrastructure is solid, but no system has 100% uptime. Even 99.9% uptime means about 8.7 hours of downtime per year. That downtime will not be evenly distributed; it will come in chunks during the worst possible moments.
Common causes of Paystack downtime:
Most Paystack outages are partial. Maybe card payments work but bank transfer is down. Maybe the API responds but with high latency. Maybe initialization works but verification is failing. Your degradation strategy should handle partial failures, not just total outages.
The first step is knowing that Paystack is down before your customers tell you. Build a health check that pings the Paystack API at regular intervals and tracks the response.
const axios = require('axios');
const EventEmitter = require('events');
class PaystackHealthCheck extends EventEmitter {
constructor(secretKey, options = {}) {
super();
this.secretKey = secretKey;
this.interval = options.interval || 30000; // Check every 30 seconds
this.timeout = options.timeout || 5000; // 5 second timeout
this.failThreshold = options.failThreshold || 3; // 3 failures before declaring outage
this.consecutiveFailures = 0;
this.isHealthy = true;
this.lastChecked = null;
this.lastLatency = null;
}
start() {
this.check(); // Check immediately
this.timer = setInterval(() => this.check(), this.interval);
console.log(`Paystack health check started (every ${this.interval / 1000}s)`);
}
stop() {
if (this.timer) clearInterval(this.timer);
}
async check() {
const start = Date.now();
try {
// Use a lightweight endpoint that confirms the API is responding
const response = await axios.get('https://api.paystack.co/bank', {
headers: {
Authorization: `Bearer ${this.secretKey}`,
},
timeout: this.timeout,
});
this.lastLatency = Date.now() - start;
this.lastChecked = new Date();
this.consecutiveFailures = 0;
if (!this.isHealthy) {
this.isHealthy = true;
this.emit('recovered', { latency: this.lastLatency });
console.log(`Paystack recovered. Latency: ${this.lastLatency}ms`);
}
// Warn if latency is high (over 2 seconds)
if (this.lastLatency > 2000) {
this.emit('degraded', { latency: this.lastLatency });
}
} catch (error) {
this.lastLatency = Date.now() - start;
this.lastChecked = new Date();
this.consecutiveFailures++;
console.warn(
`Paystack health check failed (${this.consecutiveFailures}/${this.failThreshold}): ` +
(error.code || error.message)
);
if (this.consecutiveFailures >= this.failThreshold && this.isHealthy) {
this.isHealthy = false;
this.emit('down', {
consecutiveFailures: this.consecutiveFailures,
error: error.message,
});
console.error('Paystack declared DOWN after', this.consecutiveFailures, 'consecutive failures');
}
}
}
getStatus() {
return {
healthy: this.isHealthy,
lastChecked: this.lastChecked,
lastLatency: this.lastLatency,
consecutiveFailures: this.consecutiveFailures,
};
}
}
// Usage
const healthCheck = new PaystackHealthCheck(process.env.PAYSTACK_SECRET_KEY);
healthCheck.on('down', (info) => {
// Send alert to your team (Slack, email, PagerDuty)
console.error('ALERT: Paystack is down.', info);
// Enable degraded mode
});
healthCheck.on('recovered', (info) => {
console.log('Paystack is back up.', info);
// Disable degraded mode, process queued orders
});
healthCheck.on('degraded', (info) => {
console.warn('Paystack is slow. Latency:', info.latency, 'ms');
});
healthCheck.start();
module.exports = healthCheck;
The health check uses a lightweight endpoint (/bank) to confirm the API is responding. It requires 3 consecutive failures before declaring an outage to avoid false positives from transient network hiccups. When the API recovers, it emits a "recovered" event so you can re-enable normal checkout.
Get weekly developer tips
Join 25,000+ developers. Practical guides, job tips, and new content — straight to your inbox.
No spam. Unsubscribe anytime.
Instead of checking Paystack health in every route handler, build a middleware that intercepts payment requests and routes them appropriately based on the current health status.
const healthCheck = require('./paystack-health-check');
function paystackDegradationMiddleware(req, res, next) {
const status = healthCheck.getStatus();
// Attach health status to the request for downstream use
req.paystackHealthy = status.healthy;
req.paystackLatency = status.lastLatency;
// If Paystack is down and this is a payment endpoint, handle gracefully
if (!status.healthy && isPaymentEndpoint(req.path)) {
// Option 1: Return a structured error the frontend can handle
return res.status(503).json({
error: 'payment_provider_unavailable',
message: 'Our payment system is temporarily unavailable. Please try again in a few minutes.',
retryAfter: 60, // Suggest retry in 60 seconds
alternatives: getAlternativePaymentMethods(),
});
}
// If Paystack is degraded (high latency), set a longer timeout
if (status.lastLatency > 2000) {
req.paystackTimeout = 15000; // Give Paystack more time when it is slow
}
next();
}
function isPaymentEndpoint(path) {
const paymentPaths = ['/api/pay', '/api/payment', '/api/checkout', '/api/subscribe'];
return paymentPaths.some(p => path.startsWith(p));
}
function getAlternativePaymentMethods() {
// Return available alternatives based on your setup
return [
{
method: 'mpesa',
name: 'M-Pesa',
description: 'Pay directly via M-Pesa STK Push',
available: true, // Check M-Pesa health separately
},
{
method: 'bank_transfer',
name: 'Direct Bank Transfer',
description: 'Transfer to our bank account and upload proof',
available: true, // Always available
},
];
}
// Apply to your Express app
const app = require('express')();
app.use(paystackDegradationMiddleware);
This middleware does three things: it attaches health status to every request (so any route can check it), it blocks payment requests with a structured error when Paystack is down, and it adjusts timeouts when Paystack is slow. The frontend can read the error code and alternatives array to show the customer appropriate options.
The frontend experience during an outage matters enormously. A good degraded experience keeps customers on your site. A bad one sends them to a competitor.
What NOT to do:
What to do instead:
// Frontend: handle payment provider unavailability
async function handleCheckout(orderData) {
try {
const response = await fetch('/api/pay', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(orderData),
});
if (response.status === 503) {
const data = await response.json();
if (data.error === 'payment_provider_unavailable') {
showDegradedCheckout({
message: data.message,
alternatives: data.alternatives,
retryAfter: data.retryAfter,
});
return;
}
}
if (!response.ok) {
throw new Error('Payment initialization failed');
}
const data = await response.json();
// Normal payment flow
redirectToPaystack(data.authorization_url);
} catch (error) {
showDegradedCheckout({
message: 'We are having trouble connecting to our payment system. Your order is saved.',
alternatives: [],
retryAfter: 120,
});
}
}
function showDegradedCheckout({ message, alternatives, retryAfter }) {
// Show a friendly message with alternatives
const container = document.getElementById('checkout-container');
container.innerHTML = `
Payment Temporarily Unavailable
${message}
Your cart is saved. You can:
${alternatives.map(alt => `
-
${alt.description}
`).join('')}
-
`;
}
The key principles: tell the customer what is happening (honestly), reassure them their order/cart is not lost, give them options, and make retrying easy. Never blame the customer for a provider outage.
If a customer wants to buy and your payment provider is down, you have a choice: turn them away, or accept the order and process payment when the provider recovers. For many businesses, the second option is better.
const express = require('express');
const app = express();
const healthCheck = require('./paystack-health-check');
// In-memory queue for demo. Use Redis, BullMQ, or a database in production.
const paymentQueue = [];
app.post('/api/pay', async (req, res) => {
const { email, amount, orderId, items } = req.body;
if (!healthCheck.getStatus().healthy) {
// Queue the order for later payment processing
const queueEntry = {
id: 'q_' + Date.now(),
email,
amount,
orderId,
items,
queuedAt: new Date().toISOString(),
status: 'queued',
};
paymentQueue.push(queueEntry);
// Save the order in your database with status "payment_pending"
await db.query(
'INSERT INTO orders (id, email, amount, status) VALUES ($1, $2, $3, $4)',
[orderId, email, amount, 'payment_pending']
);
console.log('Order queued for later payment:', orderId);
return res.status(202).json({
message: 'Your order has been saved. We will send you a payment link as soon as our payment system is back online.',
orderId: orderId,
queuePosition: paymentQueue.length,
});
}
// Normal payment flow when Paystack is healthy
try {
const response = await initializePaystackTransaction(email, amount);
res.json({
authorization_url: response.authorization_url,
reference: response.reference,
});
} catch (error) {
res.status(500).json({ error: 'Payment initialization failed' });
}
});
// Process queued orders when Paystack recovers
healthCheck.on('recovered', async () => {
console.log(`Processing ${paymentQueue.length} queued orders...`);
while (paymentQueue.length > 0) {
const entry = paymentQueue.shift();
try {
// Initialize payment for the queued order
const response = await initializePaystackTransaction(entry.email, entry.amount);
// Send the customer a payment link via email or SMS
await sendPaymentLink(entry.email, response.authorization_url, entry.orderId);
console.log(`Payment link sent for queued order ${entry.orderId}`);
} catch (error) {
console.error(`Failed to process queued order ${entry.orderId}:`, error.message);
// Re-queue or flag for manual handling
paymentQueue.push(entry);
break; // Stop if Paystack is failing again
}
// Small delay between requests to avoid hammering Paystack right after recovery
await new Promise(resolve => setTimeout(resolve, 1000));
}
});
app.listen(3000);
This approach saves the customer's intent even when you cannot process payment. When Paystack recovers, you send them a payment link. This works well for e-commerce (customer gets the order after paying) but not for instant-delivery products (where payment must happen before delivery).
If your product serves Kenyan customers, you have an ace up your sleeve: M-Pesa Daraja API. M-Pesa infrastructure is completely independent of Paystack. When Paystack is down, M-Pesa is almost certainly still working (and vice versa).
This is not about replacing Paystack. It is about having a fallback that lets you keep processing payments during outages.
const axios = require('axios');
class MpesaFallback {
constructor(config) {
this.consumerKey = config.consumerKey;
this.consumerSecret = config.consumerSecret;
this.shortcode = config.shortcode;
this.passkey = config.passkey;
this.callbackUrl = config.callbackUrl;
this.accessToken = null;
this.tokenExpiry = null;
}
async getAccessToken() {
if (this.accessToken && this.tokenExpiry > Date.now()) {
return this.accessToken;
}
const auth = Buffer.from(
`${this.consumerKey}:${this.consumerSecret}`
).toString('base64');
const response = await axios.get(
'https://api.safaricom.co.ke/oauth/v1/generate?grant_type=client_credentials',
{ headers: { Authorization: `Basic ${auth}` } }
);
this.accessToken = response.data.access_token;
this.tokenExpiry = Date.now() + 3500 * 1000; // Expires in ~1 hour
return this.accessToken;
}
async stkPush(phoneNumber, amountKES, accountReference) {
const token = await this.getAccessToken();
const timestamp = new Date()
.toISOString()
.replace(/[-:T]/g, '')
.slice(0, 14);
const password = Buffer.from(
`${this.shortcode}${this.passkey}${timestamp}`
).toString('base64');
const response = await axios.post(
'https://api.safaricom.co.ke/mpesa/stkpush/v1/processrequest',
{
BusinessShortCode: this.shortcode,
Password: password,
Timestamp: timestamp,
TransactionType: 'CustomerPayBillOnline',
Amount: amountKES,
PartyA: phoneNumber,
PartyB: this.shortcode,
PhoneNumber: phoneNumber,
CallBackURL: this.callbackUrl,
AccountReference: accountReference,
TransactionDesc: 'Payment',
},
{
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
}
);
return {
success: response.data.ResponseCode === '0',
checkoutRequestId: response.data.CheckoutRequestID,
merchantRequestId: response.data.MerchantRequestID,
};
}
}
// Usage in your payment router
const mpesaFallback = new MpesaFallback({
consumerKey: process.env.MPESA_CONSUMER_KEY,
consumerSecret: process.env.MPESA_CONSUMER_SECRET,
shortcode: process.env.MPESA_SHORTCODE,
passkey: process.env.MPESA_PASSKEY,
callbackUrl: process.env.MPESA_CALLBACK_URL,
});
// In your payment endpoint
async function processPayment(req, res) {
const { email, amount, phone, orderId } = req.body;
if (healthCheck.getStatus().healthy) {
// Primary: Paystack
return processPaystackPayment(email, amount, orderId, res);
}
if (phone && phone.startsWith('+254')) {
// Fallback: M-Pesa for Kenyan customers
try {
const result = await mpesaFallback.stkPush(phone, amount / 100, orderId);
return res.json({
provider: 'mpesa',
message: 'Check your phone for the M-Pesa prompt',
checkoutRequestId: result.checkoutRequestId,
});
} catch (error) {
console.error('M-Pesa fallback failed:', error.message);
}
}
// No fallback available
return res.status(503).json({
error: 'payment_provider_unavailable',
message: 'Payment is temporarily unavailable. Your order has been saved.',
});
}
The M-Pesa fallback adds complexity (you now have two payment providers to maintain, two webhook handlers, two reconciliation processes), but for Kenyan products it is worth it. When Paystack is down, you can still process payments. When M-Pesa is down (rare, but it happens), Paystack covers you.
The health check above is a basic version of the circuit breaker pattern. A proper circuit breaker has three states: closed (normal), open (outage detected), and half-open (testing recovery).
class PaystackCircuitBreaker {
constructor(options = {}) {
this.state = 'closed'; // closed, open, half-open
this.failureCount = 0;
this.successCount = 0;
this.failureThreshold = options.failureThreshold || 5;
this.recoveryTimeout = options.recoveryTimeout || 60000; // 1 minute
this.halfOpenMaxAttempts = options.halfOpenMaxAttempts || 3;
this.lastFailureTime = null;
}
async execute(apiCall) {
if (this.state === 'open') {
// Check if recovery timeout has passed
if (Date.now() - this.lastFailureTime > this.recoveryTimeout) {
this.state = 'half-open';
this.successCount = 0;
console.log('Circuit breaker: half-open, testing recovery...');
} else {
throw new Error('Circuit breaker is open. Paystack is unavailable.');
}
}
try {
const result = await apiCall();
if (this.state === 'half-open') {
this.successCount++;
if (this.successCount >= this.halfOpenMaxAttempts) {
this.state = 'closed';
this.failureCount = 0;
console.log('Circuit breaker: closed. Paystack recovered.');
}
}
this.failureCount = 0;
return result;
} catch (error) {
this.failureCount++;
this.lastFailureTime = Date.now();
if (this.state === 'half-open') {
this.state = 'open';
console.log('Circuit breaker: re-opened. Recovery attempt failed.');
} else if (this.failureCount >= this.failureThreshold) {
this.state = 'open';
console.error(`Circuit breaker: opened after ${this.failureCount} failures.`);
}
throw error;
}
}
getState() {
return this.state;
}
}
// Usage
const breaker = new PaystackCircuitBreaker({
failureThreshold: 3,
recoveryTimeout: 30000,
});
async function initializePayment(email, amount) {
return breaker.execute(async () => {
const response = await axios.post(
'https://api.paystack.co/transaction/initialize',
{ email, amount },
{
headers: {
Authorization: `Bearer ${process.env.PAYSTACK_SECRET_KEY}`,
'Content-Type': 'application/json',
},
timeout: 10000,
}
);
return response.data;
});
}
module.exports = { breaker, initializePayment };
The circuit breaker prevents your app from hammering a dead API. When Paystack is down, the circuit opens and all payment requests fail immediately with a clear error instead of waiting for a timeout. After the recovery timeout, the breaker enters half-open state and tests with a few requests. If those succeed, it closes and normal traffic resumes.
You should know about a Paystack outage within minutes, not when a customer complains. Set up monitoring that alerts your team immediately.
What to monitor:
// Simple monitoring that sends alerts
const healthCheck = require('./paystack-health-check');
healthCheck.on('down', async (info) => {
// Send to Slack
await axios.post(process.env.SLACK_WEBHOOK_URL, {
text: `Payment Alert: Paystack is DOWN. ${info.consecutiveFailures} consecutive failures. Error: ${info.error}. Degraded mode activated.`,
});
// Send to PagerDuty, email, SMS, etc.
});
healthCheck.on('recovered', async (info) => {
await axios.post(process.env.SLACK_WEBHOOK_URL, {
text: `Payment Recovery: Paystack is back UP. Latency: ${info.latency}ms. Processing queued orders.`,
});
});
healthCheck.on('degraded', async (info) => {
// Only alert if latency is significantly high
if (info.latency > 5000) {
await axios.post(process.env.SLACK_WEBHOOK_URL, {
text: `Payment Warning: Paystack latency is ${info.latency}ms (threshold: 5000ms). Payments may be slow.`,
});
}
});
Also subscribe to the Paystack status page (status.paystack.com) for email or SMS notifications. This gives you Paystack's own view of their system health, which complements your external monitoring.
You cannot wait for a real outage to test your degradation code. Simulate outages in your staging environment.
tc (traffic control on Linux) or a proxy like Toxiproxy to add 5+ seconds of latency to Paystack API calls. Confirm your circuit breaker adjusts timeouts and your monitoring alerts fire.Run these simulations quarterly, or at least before major sales events (Black Friday, product launches) when the stakes are highest. Your degradation code is like an insurance policy. You hope you never need it, but when you do, it has to work.
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