Paystack SSL and Certificate Errors from Your Server
SSL errors when calling the Paystack API mean your server does not trust the certificate presented by api.paystack.co. The most common causes are an expired CA certificate bundle on your server, a missing root CA certificate, a corporate proxy intercepting TLS traffic, or a misconfigured Node.js environment. The fix is to update your CA bundle (update-ca-certificates on Linux or upgrade Node.js), not to set NODE_TLS_REJECT_UNAUTHORIZED=0. Disabling certificate verification on a payment API connection exposes you to man-in-the-middle attacks where an attacker can steal your API keys and customer data.
The Error Messages You Will See
SSL/TLS errors manifest differently depending on your language and HTTP library:
Node.js (native fetch or node-fetch):
Error: unable to verify the first certificate
Error: self signed certificate in certificate chain
Error: UNABLE_TO_VERIFY_LEAF_SIGNATURE
Error: certificate has expired
Node.js (axios):
Error: unable to verify the first certificate
at TLSSocket.onConnectSecure (node:tls:...)
code: 'UNABLE_TO_VERIFY_LEAF_SIGNATURE'
Python (requests):
requests.exceptions.SSLError: HTTPSConnectionPool(host='api.paystack.co', port=443):
Max retries exceeded with url: /transaction/initialize
(Caused by SSLError(SSLCertVerificationError(1,
'[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed:
unable to get local issuer certificate')))
PHP (cURL):
cURL error 60: SSL certificate problem: unable to get local issuer certificate
All of these mean the same thing: your server tried to establish a TLS connection to api.paystack.co and could not verify the certificate chain. The connection was refused by your own TLS library to protect you from a potential man-in-the-middle attack.
Cause 1: Outdated CA Certificate Bundle
Your operating system and runtime (Node.js, Python, PHP) ship with a bundle of trusted root CA certificates. When a CA issues new certificates or rotates their root certificate, your bundle needs to be updated. If it is not, connections to sites using the new certificates will fail.
This is common on:
- Long-running servers that have not been updated in months
- Docker images built from old base images
- Old versions of Node.js (anything before 18.x may have an outdated bundle)
- Linux distributions that are past end-of-life
Fix for Ubuntu/Debian:
# Update the CA certificate bundle
sudo apt-get update && sudo apt-get install -y ca-certificates
sudo update-ca-certificates
Fix for CentOS/RHEL:
sudo yum update ca-certificates
Fix for Alpine (Docker):
# In your Dockerfile
RUN apk add --no-cache ca-certificates
Fix for Node.js:
# Upgrade to the latest LTS version
nvm install --lts
nvm use --lts
# Node.js 18+ uses the operating system's CA store by default
# Older versions bundle their own, which may be outdated
After updating, restart your application and try the API call again.
Cause 2: Missing Intermediate Certificate
TLS certificates form a chain: your server trusts a root CA, the root CA signs an intermediate CA, and the intermediate CA signs the server certificate. If your CA bundle has the root but not the intermediate, the chain is broken.
This manifests as "unable to verify the first certificate" in Node.js. The "first certificate" is the server certificate (api.paystack.co), and it cannot be verified because the intermediate certificate that signed it is missing from your trust store.
Diagnosis:
# Check the certificate chain from your server
openssl s_client -connect api.paystack.co:443 -showcerts
This command shows the full certificate chain. If you see "verify return code: 0 (ok)", the chain is valid from the internet. The problem is on your server.
# Check what your server trusts
openssl s_client -connect api.paystack.co:443 -CApath /etc/ssl/certs/
If this fails but the previous command (without -CApath) succeeds, your /etc/ssl/certs/ directory is missing the required certificates.
Fix: Update your CA certificates as shown in Cause 1. If that does not work, you may need to manually install the intermediate certificate:
# Download the intermediate certificate and add it to your trust store
# The specific intermediate depends on Paystack's certificate issuer
# Check the openssl output above for the issuer details
sudo cp intermediate-cert.pem /usr/local/share/ca-certificates/
sudo update-ca-certificates
Cause 3: Corporate Proxy or VPN Intercepting TLS
Many corporate networks run a TLS inspection proxy. When your application connects to api.paystack.co, the proxy intercepts the connection, decrypts it, inspects the traffic, and re-encrypts it using its own certificate. Your application sees the proxy's certificate instead of Paystack's certificate and rejects it.
Signs that a proxy is the problem:
- The error only happens on your work network, not at home
- Other HTTPS APIs also fail with certificate errors
- The error started after your IT department changed the VPN or proxy configuration
- When you inspect the certificate, the issuer is your company name, not a public CA
Fix: Add your corporate proxy's CA certificate to your application's trust store.
// Node.js: Add a custom CA certificate
// Get the proxy CA cert from your IT department (usually a .pem or .crt file)
// Option 1: Environment variable (recommended)
// Set this before starting your Node.js app:
// NODE_EXTRA_CA_CERTS=/path/to/corporate-proxy-ca.pem node app.js
// Option 2: In code (less preferred but works)
var https = require('https');
var fs = require('fs');
var proxyCACert = fs.readFileSync('/path/to/corporate-proxy-ca.pem');
var agent = new https.Agent({
ca: proxyCACert,
});
The NODE_EXTRA_CA_CERTS environment variable is the cleanest approach. It adds the certificate to Node.js's trust store without modifying your application code.
Cause 4: Docker Container Missing CA Certificates
Minimal Docker base images (like node:alpine or python:slim) may not include a complete CA certificate bundle. Your application works fine on your development machine but fails with SSL errors in the container.
Fix for Node.js Alpine:
# Dockerfile
FROM node:20-alpine
# Add CA certificates
RUN apk add --no-cache ca-certificates
WORKDIR /app
COPY package*.json ./
RUN npm ci --production
COPY . .
CMD ["node", "server.js"]
Fix for Python slim:
# Dockerfile
FROM python:3.12-slim
# Add CA certificates
RUN apt-get update && apt-get install -y ca-certificates && rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .
CMD ["python", "app.py"]
Fix for Ubuntu-based images:
# Dockerfile
FROM ubuntu:22.04
RUN apt-get update && apt-get install -y ca-certificates && update-ca-certificates
# ... rest of your Dockerfile
Always include ca-certificates in your Dockerfile if your application makes HTTPS calls. This is not Paystack-specific. Any HTTPS connection will fail without it.
Why You Must Never Disable Certificate Verification
You will find Stack Overflow answers and blog posts suggesting these "fixes":
// NEVER DO THIS for a payment integration
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
// NEVER DO THIS
var agent = new https.Agent({ rejectUnauthorized: false });
# NEVER DO THIS (Python)
requests.post(url, verify=False)
// NEVER DO THIS (PHP)
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
These "solutions" disable certificate verification entirely. Here is what that means for a payment integration:
- Man-in-the-middle attacks become trivial. An attacker on the network (compromised Wi-Fi, rogue DNS, ISP-level interception) can present any certificate and your server will accept it. The attacker intercepts your API call, reads your secret key from the Authorization header, and reads the request body containing customer emails and payment amounts.
- Your Paystack secret key is exposed. With the secret key, the attacker can make API calls as your business: initialize transactions, create transfers from your balance, and read all your customer data.
- Your customers' data is exposed. Card details, email addresses, and payment metadata flow through the intercepted connection in plain text.
- You are violating PCI DSS requirements. If you process card payments, disabling certificate verification violates PCI DSS Requirement 4.1 (use strong cryptography and security protocols to safeguard sensitive cardholder data during transmission over open, public networks).
The "it works in development" argument is irrelevant. If this code ships to production, every API call your server makes is vulnerable. Fix the root cause (update CA bundle, add proxy cert) instead of disabling the security that protects your customers' money.
Cause 5: System Clock Is Wrong
SSL certificates have validity periods. If your server's clock is wrong (set to a date in the past or far future), a valid certificate may appear expired or not-yet-valid.
This is rare on cloud servers (AWS, GCP, Azure sync time automatically) but common on:
- Virtual machines where the hypervisor clock drifted
- Development machines after a long sleep/hibernate
- Raspberry Pi or embedded devices without RTC batteries
Diagnosis:
# Check your system time
date
# Should match the current time within a few seconds
# Check against an NTP server
ntpdate -q pool.ntp.org
Fix:
# Sync time with NTP
sudo ntpdate pool.ntp.org
# Or enable automatic time sync
sudo timedatectl set-ntp true
If your server clock was significantly off, restart your application after fixing the time. Some TLS libraries cache certificate validation results.
Code: Diagnosis Script
Run this script on your server to identify the exact SSL problem:
// diagnose-ssl.js
// Run: node diagnose-ssl.js
var https = require('https');
var tls = require('tls');
function diagnoseSsl() {
console.log('=== Paystack SSL Diagnosis ===\n');
// Check Node.js version
console.log('Node.js version: ' + process.version);
console.log('OpenSSL version: ' + process.versions.openssl);
console.log('TLS default min version: ' + tls.DEFAULT_MIN_VERSION);
console.log('');
// Check if NODE_TLS_REJECT_UNAUTHORIZED is set (bad!)
if (process.env.NODE_TLS_REJECT_UNAUTHORIZED === '0') {
console.error(
'WARNING: NODE_TLS_REJECT_UNAUTHORIZED is set to 0. ' +
'Certificate verification is DISABLED. This is a security risk.'
);
}
// Check NODE_EXTRA_CA_CERTS
if (process.env.NODE_EXTRA_CA_CERTS) {
console.log('NODE_EXTRA_CA_CERTS: ' + process.env.NODE_EXTRA_CA_CERTS);
} else {
console.log('NODE_EXTRA_CA_CERTS: not set');
}
console.log('');
// Try connecting to api.paystack.co
console.log('Testing connection to api.paystack.co:443...');
var options = {
host: 'api.paystack.co',
port: 443,
method: 'GET',
path: '/bank?perPage=1',
headers: {
Authorization: 'Bearer sk_test_placeholder',
},
};
var req = https.request(options, function(res) {
console.log('HTTP Status: ' + res.statusCode);
console.log('TLS connection successful.');
// Get certificate details
var socket = res.socket;
var cert = socket.getPeerCertificate();
console.log('\nCertificate details:');
console.log(' Subject: ' + (cert.subject ? cert.subject.CN : 'unknown'));
console.log(' Issuer: ' + (cert.issuer ? cert.issuer.O : 'unknown'));
console.log(' Valid from: ' + cert.valid_from);
console.log(' Valid to: ' + cert.valid_to);
console.log(' Protocol: ' + socket.getProtocol());
res.on('data', function() {});
res.on('end', function() {
console.log('\nResult: SSL is working correctly. No issues detected.');
});
});
req.on('error', function(err) {
console.error('\nConnection FAILED: ' + err.message);
console.error('Error code: ' + err.code);
if (err.code === 'UNABLE_TO_VERIFY_LEAF_SIGNATURE') {
console.error('\nDiagnosis: Missing intermediate CA certificate.');
console.error('Fix: Update your CA bundle with "sudo update-ca-certificates" (Debian/Ubuntu)');
console.error(' or "sudo yum update ca-certificates" (CentOS/RHEL)');
} else if (err.code === 'CERT_HAS_EXPIRED') {
console.error('\nDiagnosis: A certificate in the chain has expired.');
console.error('Fix: Update your CA bundle and check your system clock.');
} else if (err.code === 'SELF_SIGNED_CERT_IN_CHAIN') {
console.error('\nDiagnosis: A proxy or firewall is intercepting the connection.');
console.error('Fix: Add the proxy CA certificate to NODE_EXTRA_CA_CERTS.');
} else if (err.code === 'ERR_TLS_CERT_ALTNAME_INVALID') {
console.error('\nDiagnosis: Certificate hostname mismatch. A proxy may be intercepting.');
console.error('Fix: Check your DNS and proxy configuration.');
}
});
req.end();
}
diagnoseSsl();
Run this script on any server experiencing SSL errors with Paystack. It will tell you the exact problem and the fix.
Verification: Confirm SSL Is Working
After applying the fix, verify it works:
Step 1: Run the diagnosis script.
Execute the diagnosis script from the previous section. Confirm it shows "SSL is working correctly" with a valid certificate.
Step 2: Make a test API call.
Call a lightweight Paystack endpoint and confirm it succeeds.
// Quick verification: call the bank list endpoint
function verifySsl() {
fetch('https://api.paystack.co/bank?perPage=1', {
headers: {
'Authorization': 'Bearer ' + process.env.PAYSTACK_SECRET_KEY,
},
})
.then(function(res) { return res.json(); })
.then(function(data) {
if (data.status) {
console.log('SSL is working. API responded successfully.');
console.log('Bank count: ' + data.data.length);
} else {
console.error('API responded but with an error: ' + data.message);
}
})
.catch(function(err) {
if (err.message.includes('certificate') ||
err.message.includes('SSL') ||
err.message.includes('TLS')) {
console.error('SSL error still present: ' + err.message);
} else {
console.error('Non-SSL error: ' + err.message);
}
});
}
verifySsl();
Step 3: Check the certificate chain.
Run openssl s_client -connect api.paystack.co:443 from your server and confirm the verify return code is 0.
Step 4: Confirm NODE_TLS_REJECT_UNAUTHORIZED is not set.
Check your environment variables, .env file, Dockerfile, and deployment configuration. If it is set to 0 anywhere, remove it.
Step 5: Test from your Docker container.
If you deploy with Docker, build the updated image and run the verification script inside the container. Do not test only on your host machine.
For webhook-related SSL issues (Paystack cannot reach your webhook URL because of your server's SSL certificate), see Paystack Webhook Not Firing: Diagnostic Checklist. For mixed content errors on the frontend, see Paystack Mixed Content and HTTPS Errors.
Key Takeaways
- ✓SSL errors calling the Paystack API are always a problem on your server, not on Paystack. The Paystack API uses a valid certificate from a trusted CA. If your server cannot verify it, your server CA bundle or TLS configuration is broken.
- ✓The number one cause is an outdated CA certificate bundle. Linux servers, Docker containers, and old Node.js versions may have CA bundles that do not include newer root certificates. Update them.
- ✓Setting NODE_TLS_REJECT_UNAUTHORIZED=0 is never acceptable for a payment integration. It disables all certificate verification, meaning your server will trust any certificate, including one presented by an attacker performing a man-in-the-middle attack.
- ✓Corporate proxies and VPNs that intercept TLS traffic are a common cause of SSL errors in development. The proxy presents its own certificate that your application does not trust. The fix is to add the proxy CA to your trust store, not to disable verification.
- ✓Docker containers often have minimal CA bundles. Add the ca-certificates package to your Dockerfile.
- ✓After fixing the SSL issue, verify the fix by making a test API call and checking that the TLS handshake completes without errors.
Frequently Asked Questions
- Why does my Paystack API call fail with "unable to verify the first certificate"?
- Your server does not have the intermediate CA certificate that signed the Paystack API certificate. Update your CA certificate bundle by running "sudo apt-get install ca-certificates && sudo update-ca-certificates" on Debian/Ubuntu, or "sudo yum update ca-certificates" on CentOS/RHEL. If you are using Docker, add "RUN apk add --no-cache ca-certificates" to your Dockerfile.
- Is it safe to set NODE_TLS_REJECT_UNAUTHORIZED=0 for testing?
- It is technically possible for local development where no real data flows, but it is a bad habit that leads to this setting leaking into production. Use NODE_EXTRA_CA_CERTS to add your development certificates properly instead. If you are testing against the real Paystack API, fix the SSL issue properly rather than disabling verification.
- Why does the SSL error only happen on my server and not on my laptop?
- Your laptop likely has a more recent operating system with an up-to-date CA bundle. Servers, especially Docker containers and older Linux installations, may have outdated CA bundles. The difference is in the trusted root certificates available to the TLS library, not in the Paystack API itself.
- My SSL error started suddenly without any code changes. What happened?
- Either Paystack (or their CDN/certificate provider) rotated their certificate to one signed by a different CA, or a certificate in the chain expired and was replaced. If your CA bundle does not include the new CA, the verification fails. The fix is always the same: update your CA certificates. Certificate rotations happen routinely and are not a Paystack bug.
- How do I add a corporate proxy CA certificate to my Node.js application?
- Set the NODE_EXTRA_CA_CERTS environment variable to the path of your proxy CA certificate file (.pem format). For example: NODE_EXTRA_CA_CERTS=/etc/ssl/certs/corporate-proxy-ca.pem node app.js. Get the CA certificate from your IT department. This adds the proxy certificate to the Node.js trust store alongside the default certificates.
Ready to build real-world apps?
Join the McTaba Labs full-stack marathon (4 months full-time · 6 months part-time). Learn M-Pesa, USSD, and WhatsApp engineering while shipping 8 production apps.
Apply to the McTaba Marathon