Paystack 401 Unauthorized on /transaction/initialize
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.
What the Error Looks Like
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.
Cause 1: Missing Authorization Header
If Paystack says "No Authorization Header was found", your HTTP request literally did not include an Authorization header. This is common when:
- You forgot to add the header to your fetch or axios call
- You are using a shared HTTP client and the headers object is not being merged correctly
- Your framework strips certain headers (rare, but happens with some proxy setups)
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.
Cause 2: Public Key Instead of Secret Key
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:
- You named your env var
PAYSTACK_KEYand accidentally put the public key there - You copied the wrong key from the dashboard (the public key is shown first)
- You have
PAYSTACK_PUBLIC_KEYandPAYSTACK_SECRET_KEYand used the wrong variable name
Diagnosis:
// 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.
Cause 3: Missing "Bearer " Prefix
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,
});
Cause 4: Environment Variable Not Loaded
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
}
Step-by-Step Debug Checklist
Run through these in order. Most 401 errors are solved by step 3.
Step 1: Check the error message.
- "No Authorization Header was found" = header is missing entirely. Go to Cause 1.
- "Invalid key" = header is present but the key is wrong. Continue to step 2.
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
Complete Working Implementation
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']})
Verification Step
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.
Preventing This Error
Build these habits and this error will not catch you again:
- Validate keys at server startup. If
PAYSTACK_SECRET_KEYis missing or has the wrong prefix, crash with a clear error message instead of booting a server that will 401 on every payment attempt. - Use a shared HTTP client. Create an axios instance or fetch wrapper with the Authorization header set once. Every Paystack call goes through this client. One place to get it right, zero places to get it wrong.
- Name env vars unambiguously.
PAYSTACK_SECRET_KEYandPAYSTACK_PUBLIC_KEY. NotPAYSTACK_KEY, notAPI_KEY. - Never log the full key. Log the prefix (
sk_test_) and the last 4 characters. Enough to diagnose, not enough to compromise. - Add a health check. A
/healthendpoint 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.
Key Takeaways
- ✓The 401 on /transaction/initialize fires before Paystack reads your request body. It is purely an authentication failure, meaning your Authorization header is wrong or missing.
- ✓The Authorization header must be exactly "Bearer sk_test_xxx" or "Bearer sk_live_xxx". There is a space after "Bearer". The key must be a secret key, not a public key.
- ✓If your environment variable is not loaded, the header becomes "Bearer undefined" or "Bearer null", both of which return 401. Check that your .env file is loaded before the API call.
- ✓In Express, make sure you are not accidentally overwriting the Authorization header with middleware. In Next.js API routes, confirm the env var is available server-side.
- ✓The quickest debug step: log the full Authorization header value (minus the last 20 characters for security) right before the fetch call. This immediately reveals missing prefixes, wrong keys, or undefined values.
- ✓A 401 is different from a 400. A 400 means Paystack authenticated you but your request body is wrong (missing email, missing amount). A 401 means it never got past authentication.
Frequently Asked Questions
- Why does Paystack return 401 even though my key looks correct?
- The most common invisible cause is trailing whitespace or newline characters in your environment variable. Copy the key fresh from the Paystack dashboard, trim it, and make sure your .env file does not have extra spaces after the value. Also check that you are using the secret key (sk_), not the public key (pk_).
- Do I need the "Bearer " prefix in the Paystack Authorization header?
- Yes. The header must be exactly "Authorization: Bearer sk_test_xxx" or "Authorization: Bearer sk_live_xxx". The word Bearer, a space, then the key. Without the prefix, Paystack cannot parse the header and returns 401.
- Why does my key work in curl but not in my Next.js app?
- Your environment variable is likely not loaded in your Next.js server context. In Next.js, only variables prefixed with NEXT_PUBLIC_ are available in the browser. Server-side code (API routes and Route Handlers) can access all env vars, but you must have a .env.local file in the project root or set them in your hosting platform. Confirm with console.log(!!process.env.PAYSTACK_SECRET_KEY) in your route handler.
- Can I use the same Paystack key for test and live transactions?
- No. Test keys (sk_test_) work only in test mode. Live keys (sk_live_) work only in live mode. Both hit the same base URL (api.paystack.co), so you cannot tell the difference from the URL. You must use the right key for the right mode.
- What is the difference between a 400 and a 401 from Paystack?
- A 401 means authentication failed. Paystack did not recognise your key and rejected the request before reading the body. A 400 means authentication passed but your request body is invalid, such as a missing email field, an amount of zero, or an unsupported currency. Fix 401 first (correct key), then fix 400 (correct payload).
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