Your /transaction/initialize call returns 401 because the Authorization header is missing, contains a public key instead of a secret key, is missing the "Bearer " prefix, or the environment variable holding the key is not loaded. Add the header as Authorization: Bearer sk_test_YOUR_SECRET_KEY, confirm the key starts with sk_, and verify your .env file is loaded before the API call runs.
You call POST https://api.paystack.co/transaction/initialize and get back:
HTTP/1.1 401 Unauthorized
Content-Type: application/json
{
"status": false,
"message": "Invalid key"
}
Or sometimes:
{
"status": false,
"message": "No Authorization Header was found"
}
Both are 401 responses. The first means you sent a key but Paystack rejected it. The second means you did not send an Authorization header at all. The fix path is different for each, so note which message you are getting.
This error fires before Paystack looks at your request body. Even if your email and amount are perfectly formatted, a 401 means your credentials failed. Fix the authentication first, then worry about the payload.
If Paystack says "No Authorization Header was found", your HTTP request literally did not include an Authorization header. This is common when:
Broken code:
// Missing the headers entirely
const response = await fetch('https://api.paystack.co/transaction/initialize', {
method: 'POST',
body: JSON.stringify({ email: 'user@example.com', amount: 50000 }),
});
// 401: "No Authorization Header was found"
Fixed code:
const response = await fetch('https://api.paystack.co/transaction/initialize', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.PAYSTACK_SECRET_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ email: 'user@example.com', amount: 50000 }),
});
Also note the Content-Type: application/json header. It is not related to the 401, but if you leave it out, Paystack will not parse your JSON body and you will get a 400 after fixing the auth.
Get weekly developer tips
Join 25,000+ developers. Practical guides, job tips, and new content — straight to your inbox.
No spam. Unsubscribe anytime.
Paystack public keys (pk_test_, pk_live_) are for the frontend checkout popup. They cannot authenticate server-side API calls. If you use a public key in your Authorization header, Paystack returns 401 with "Invalid key".
This usually happens because:
PAYSTACK_KEY and accidentally put the public key therePAYSTACK_PUBLIC_KEY and PAYSTACK_SECRET_KEY and used the wrong variable nameDiagnosis:
// Add this before your API call to check
const key = process.env.PAYSTACK_SECRET_KEY;
console.log('Key prefix:', key?.substring(0, 8));
// Should print "sk_test_" or "sk_live_"
// If it prints "pk_test_" or "pk_live_", that is your problem
Fix: Go to your Paystack dashboard, Settings > API Keys & Webhooks, copy the Secret Key (not the Public Key), and update your environment variable.
The Authorization header must follow the format Bearer <key>. The word "Bearer", a single space, then the key. If you send just the key without "Bearer ", Paystack cannot parse it and returns 401.
// BROKEN: no Bearer prefix
headers: {
Authorization: process.env.PAYSTACK_SECRET_KEY, // "sk_test_xxx"
}
// Paystack sees: Authorization: sk_test_xxx
// Expected: Authorization: Bearer sk_test_xxx
// BROKEN: wrong capitalisation
headers: {
Authorization: `bearer ${process.env.PAYSTACK_SECRET_KEY}`, // lowercase b
}
// BROKEN: no space after Bearer
headers: {
Authorization: `Bearer${process.env.PAYSTACK_SECRET_KEY}`, // "Bearersk_test_xxx"
}
// CORRECT
headers: {
Authorization: `Bearer ${process.env.PAYSTACK_SECRET_KEY}`,
}
This is a common copy-paste error. Some API providers (like Stripe) accept the key directly with basic auth. Paystack uses Bearer token authentication, so you need the prefix.
If you are using axios, you can set the header globally:
import axios from 'axios';
const paystack = axios.create({
baseURL: 'https://api.paystack.co',
headers: {
Authorization: `Bearer ${process.env.PAYSTACK_SECRET_KEY}`,
'Content-Type': 'application/json',
},
});
// Now every call through this instance has the correct auth
const { data } = await paystack.post('/transaction/initialize', {
email: 'user@example.com',
amount: 50000,
});
Your code says process.env.PAYSTACK_SECRET_KEY, but the variable is not actually in the environment when the code runs. The header becomes Authorization: Bearer undefined, and Paystack returns 401.
This happens in these scenarios:
Scenario A: You did not load dotenv.
// You have a .env file but never called dotenv.config()
// process.env.PAYSTACK_SECRET_KEY is undefined
// Fix for Node.js:
import 'dotenv/config'; // Add this at the very top of your entry file
// Or with require:
require('dotenv').config();
Scenario B: The .env file is in the wrong directory.
# dotenv loads from the current working directory by default
# If your app runs from /app but your .env is in /app/src, it will not find it
# Fix: specify the path
import dotenv from 'dotenv';
dotenv.config({ path: '/app/.env' });
Scenario C: Next.js and the NEXT_PUBLIC_ prefix confusion.
# In Next.js, only variables prefixed with NEXT_PUBLIC_ are available in the browser
# But server-side code (API routes, Server Components) can access all env vars
# .env.local
PAYSTACK_SECRET_KEY=sk_test_xxx # Available server-side only (correct)
NEXT_PUBLIC_PAYSTACK_KEY=pk_test_xxx # Available everywhere (for frontend)
# WRONG: Do not prefix your secret key with NEXT_PUBLIC_
# NEXT_PUBLIC_PAYSTACK_SECRET_KEY=sk_test_xxx # This exposes your secret key to the browser!
Scenario D: The deployment platform did not receive the variable.
# Vercel: Settings > Environment Variables
# Railway: Variables tab in your service
# Render: Environment tab
# After adding the variable, you must redeploy. Existing deployments do not pick up
# new environment variables automatically.
Diagnostic: confirm the variable is present.
// Add this to your API route temporarily
export async function POST(request: Request) {
const key = process.env.PAYSTACK_SECRET_KEY;
console.log('Key present:', !!key);
console.log('Key type:', typeof key);
console.log('Key prefix:', key?.substring(0, 8) || 'MISSING');
// ... rest of your handler
}
Run through these in order. Most 401 errors are solved by step 3.
Step 1: Check the error message.
Step 2: Log the Authorization header.
const authHeader = `Bearer ${process.env.PAYSTACK_SECRET_KEY}`;
console.log('Auth header:', authHeader.substring(0, 20) + '...');
// Should show: "Auth header: Bearer sk_test_xxx..."
// "Bearer undefined..." = env var not loaded (Cause 4)
// "Bearer pk_test_..." = wrong key type (Cause 2)
// "sk_test_..." (no Bearer) = missing prefix (Cause 3)
Step 3: Verify the key itself.
# Use curl to test the key directly, bypassing your code
curl -s -w "\nHTTP Status: %{http_code}\n" \
-H "Authorization: Bearer sk_test_YOUR_KEY" \
https://api.paystack.co/transaction?perPage=1
# 200 = key works, problem is in your code's header construction
# 401 = key itself is invalid, get a new one from the dashboard
Step 4: Check for middleware interference.
If your key works in curl but not in your app, something in your middleware stack is modifying or stripping the Authorization header before it reaches the outgoing fetch call. This is rare but happens with proxy middleware or custom request interceptors.
Step 5: Test in isolation.
// Create a standalone test file with zero middleware
// test-paystack.js
const key = 'sk_test_paste_your_actual_key_here';
fetch('https://api.paystack.co/transaction/initialize', {
method: 'POST',
headers: {
Authorization: `Bearer ${key}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ email: 'test@example.com', amount: 10000 }),
})
.then(r => r.json())
.then(d => console.log(d))
.catch(e => console.error(e));
// Run: node test-paystack.js
// If this works, the problem is in your app's environment or middleware
Here is a properly authenticated /transaction/initialize call in three common setups.
Node.js with Express:
// Ensure dotenv is loaded at the very top of your entry file
import 'dotenv/config';
import express from 'express';
const app = express();
app.use(express.json());
// Validate key at startup, not at request time
const PAYSTACK_SECRET_KEY = process.env.PAYSTACK_SECRET_KEY?.trim();
if (!PAYSTACK_SECRET_KEY || !PAYSTACK_SECRET_KEY.startsWith('sk_')) {
console.error('Invalid or missing PAYSTACK_SECRET_KEY');
process.exit(1);
}
app.post('/api/pay', async (req, res) => {
const { email, amount } = req.body;
const response = await fetch('https://api.paystack.co/transaction/initialize', {
method: 'POST',
headers: {
Authorization: `Bearer ${PAYSTACK_SECRET_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ email, amount }),
});
const data = await response.json();
if (!response.ok) {
return res.status(response.status).json({
error: data.message || 'Payment initialization failed',
});
}
return res.json({ authorization_url: data.data.authorization_url });
});
Next.js App Router (Route Handler):
// app/api/pay/route.ts
import { NextResponse } from 'next/server';
export async function POST(request: Request) {
const { email, amount } = await request.json();
// process.env is available in Route Handlers without dotenv
const key = process.env.PAYSTACK_SECRET_KEY;
if (!key) {
return NextResponse.json(
{ error: 'Payment service not configured' },
{ status: 500 }
);
}
const response = await fetch('https://api.paystack.co/transaction/initialize', {
method: 'POST',
headers: {
Authorization: `Bearer ${key}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ email, amount }),
});
const data = await response.json();
if (!response.ok) {
return NextResponse.json({ error: data.message }, { status: response.status });
}
return NextResponse.json({ authorization_url: data.data.authorization_url });
}
Python with Django:
import os
import json
import requests
from django.http import JsonResponse
from django.views.decorators.http import require_POST
from django.views.decorators.csrf import csrf_protect
PAYSTACK_SECRET_KEY = os.environ.get('PAYSTACK_SECRET_KEY', '').strip()
@require_POST
@csrf_protect
def initialize_payment(request):
body = json.loads(request.body)
email = body.get('email')
amount = body.get('amount')
response = requests.post(
'https://api.paystack.co/transaction/initialize',
json={'email': email, 'amount': amount},
headers={
'Authorization': f'Bearer {PAYSTACK_SECRET_KEY}',
'Content-Type': 'application/json',
},
)
data = response.json()
if response.status_code != 200:
return JsonResponse({'error': data.get('message')}, status=response.status_code)
return JsonResponse({'authorization_url': data['data']['authorization_url']})
After fixing the 401, verify the entire flow works:
# 1. Verify the key authenticates
curl -s -H "Authorization: Bearer $PAYSTACK_SECRET_KEY" \
https://api.paystack.co/transaction?perPage=1 | jq '.status'
# Expected: true
# 2. Verify transaction/initialize works
curl -s -X POST \
-H "Authorization: Bearer $PAYSTACK_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{"email":"test@example.com","amount":10000}' \
https://api.paystack.co/transaction/initialize | jq '.data.authorization_url'
# Expected: a URL starting with https://checkout.paystack.com/
# 3. Verify from your app
# Hit your own /api/pay endpoint and confirm it returns an authorization_url
If step 1 works but step 2 fails with a 400, your auth is fixed but your request body has issues. That is a different problem (likely missing email or amount in kobo). If step 2 works but step 3 fails, the issue is in your app code, not the key.
Build these habits and this error will not catch you again:
PAYSTACK_SECRET_KEY is missing or has the wrong prefix, crash with a clear error message instead of booting a server that will 401 on every payment attempt.PAYSTACK_SECRET_KEY and PAYSTACK_PUBLIC_KEY. Not PAYSTACK_KEY, not API_KEY.sk_test_) and the last 4 characters. Enough to diagnose, not enough to compromise./health endpoint that calls Paystack's API and reports whether auth succeeds. Your monitoring can hit this and alert you before customers do.For the complete list of Paystack errors beyond authentication, see the Paystack Errors and Troubleshooting: The Complete Reference. For understanding how the payment initialization flow works end to end, see How Paystack Accept Payments Actually Works Under the Hood.
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