Transaction Not Found on Paystack Verify
The "Transaction not found" error on /transaction/verify/:reference means Paystack cannot find a transaction matching the reference you sent, in the environment your API key belongs to. The most common cause is a test/live key mismatch: you initialized the transaction with a test key but are verifying with a live key (or vice versa). Other causes include a typo in the reference, URL-encoded special characters, using the transaction ID instead of the reference string, or the transaction being too old.
What the Error Looks Like
When you call the verify endpoint with a reference Paystack cannot find, you get:
GET /transaction/verify/my_ref_123
Authorization: Bearer sk_live_xxxxx
HTTP/1.1 404 Not Found
{
"status": false,
"message": "Transaction reference not found"
}
Some developers also see this as "Transaction not found" or "Invalid transaction reference" depending on the exact scenario. The HTTP status is 404 in most cases.
This error is frustrating because you know the transaction happened. The customer was charged. But your verification call returns not found. The transaction exists, you just cannot find it with the parameters you are using.
Cause 1: Test Key vs Live Key Mismatch
This is the most common cause. Your initialization call and your verification call use keys from different environments.
The scenario:
// Initialization (happens during checkout)
// Uses test key
const initResponse = await fetch('https://api.paystack.co/transaction/initialize', {
method: 'POST',
headers: {
Authorization: 'Bearer sk_test_abc123', // TEST key
'Content-Type': 'application/json',
},
body: JSON.stringify({
email: 'customer@example.com',
amount: 50000,
}),
});
// ... customer pays ...
// Verification (happens after payment)
// WRONG: Uses live key
const verifyResponse = await fetch(
'https://api.paystack.co/transaction/verify/ref_from_test',
{
headers: {
Authorization: 'Bearer sk_live_xyz789', // LIVE key - WRONG!
},
}
);
// Result: "Transaction not found"
The fix:
// Use the SAME key for initialization and verification
const PAYSTACK_SECRET_KEY = process.env.PAYSTACK_SECRET_KEY;
// Both calls use the same key
// Initialize
const initResponse = 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 }),
});
// Verify
const verifyResponse = await fetch(
`https://api.paystack.co/transaction/verify/${reference}`,
{
headers: {
Authorization: `Bearer ${PAYSTACK_SECRET_KEY}`,
},
}
);
Use a single environment variable for the key. In development, set it to your test key. In production, set it to your live key. Never hard-code keys.
Cause 2: Wrong Reference String
The reference you pass to verify does not match the reference used when the transaction was created.
This happens when:
- You generate the reference on the client side and the server generates a different one
- You have a typo in the reference
- You use a stale reference from a previous transaction
- Your code truncates or modifies the reference string somewhere in the pipeline
The fix: Store the reference in your database at initialization time, and read it back at verification time.
const crypto = require('crypto');
// Generate a reference
function generateReference() {
return 'txn_' + crypto.randomBytes(16).toString('hex');
}
// At initialization time
async function initializePayment(email, amount) {
const reference = generateReference();
// Store in database BEFORE calling Paystack
await db.query(
'INSERT INTO payments (reference, email, amount, status) VALUES ($1, $2, $3, $4)',
[reference, email, amount, 'pending']
);
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, amount, reference }),
});
return response.json();
}
// At verification time
async function verifyPayment(reference) {
// Check that this reference exists in YOUR database first
const payment = await db.query(
'SELECT * FROM payments WHERE reference = $1',
[reference]
);
if (!payment.rows.length) {
throw new Error('Unknown reference: ' + reference);
}
const response = await fetch(
`https://api.paystack.co/transaction/verify/${encodeURIComponent(reference)}`,
{
headers: {
Authorization: `Bearer ${process.env.PAYSTACK_SECRET_KEY}`,
},
}
);
return response.json();
}
Cause 3: URL Encoding Issues
If your reference contains special characters, they must be URL-encoded in the verify URL path. Without encoding, the URL breaks and Paystack cannot parse the reference.
Characters that cause problems:
| Character | URL encoded | Example reference | Correct URL |
|---|---|---|---|
| # | %23 | order#123 | /transaction/verify/order%23123 |
| / | %2F | 2026/07/001 | /transaction/verify/2026%2F07%2F001 |
| + | %2B | ref+extra | /transaction/verify/ref%2Bextra |
| space | %20 | ref 001 | /transaction/verify/ref%20001 |
| & | %26 | a&b | /transaction/verify/a%26b |
The fix: Always use encodeURIComponent() on the reference when building the URL:
// WRONG: special characters break the URL
const url = `https://api.paystack.co/transaction/verify/${reference}`;
// RIGHT: encode the reference
const url = `https://api.paystack.co/transaction/verify/${encodeURIComponent(reference)}`;
Better yet, avoid special characters in references entirely. Stick to alphanumeric characters, hyphens, and underscores:
// Safe reference format
function generateSafeReference() {
return 'txn_' + crypto.randomBytes(16).toString('hex');
// Example: txn_a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6
}
Cause 4: Using Transaction ID Instead of Reference
The verify endpoint takes the transaction reference (a string you provide or Paystack generates), not the transaction ID (a numeric value Paystack assigns).
// When you initialize a transaction, Paystack returns:
{
"status": true,
"data": {
"authorization_url": "https://checkout.paystack.com/abc123",
"access_code": "abc123",
"reference": "my_custom_ref_001" // <-- THIS is what you use for verify
}
}
// A webhook event includes:
{
"event": "charge.success",
"data": {
"id": 987654321, // <-- This is the ID. Do NOT use this for verify.
"reference": "my_custom_ref_001", // <-- Use THIS for verify.
"amount": 50000
}
}
// WRONG: using the numeric ID
const url = 'https://api.paystack.co/transaction/verify/987654321';
// Returns: "Transaction not found"
// RIGHT: using the reference string
const url = 'https://api.paystack.co/transaction/verify/my_custom_ref_001';
// Returns: the transaction details
If you only have the transaction ID and need to look up the transaction, use the /transaction/:id endpoint instead (GET request with your secret key). But for the standard verify flow, always use the reference.
Cause 5: Expired or Abandoned Transaction
When you initialize a transaction, Paystack creates a record. If the customer never completes payment and the transaction expires, the behavior depends on the transaction state:
- Abandoned transactions (customer never loaded checkout): May still be verifiable for some time. The status will show as
"abandoned". - Expired access codes: The checkout link expires, but the underlying transaction record usually persists. Verification should still work.
- Very old transactions: Paystack may archive old transactions. Verification of transactions from months ago should still work, but transactions from years ago might not be immediately available.
If you get "Transaction not found" for a recent transaction, the cause is almost certainly one of the other issues on this page (key mismatch, wrong reference, encoding). Expiration only matters for very old transactions.
Prevention: Verify transactions as soon as possible after payment. Do not wait days or weeks to verify. Your webhook handler should verify and process immediately when charge.success arrives.
Debugging Checklist
When you get "Transaction not found", work through this checklist:
- Print the exact reference you are sending. Log it right before the API call. Is it what you expect?
- Check which API key you are using. Print the first 10 characters of the key. Does it start with
sk_test_orsk_live_? Does it match the key you used during initialization? - Check your database. Look up the reference in your payments table. Does it exist? Does it match exactly (case-sensitive)?
- Try in the Paystack dashboard. Go to Transactions in the dashboard. Search for the reference. If it appears in the test environment but you are verifying with a live key, that confirms a key mismatch.
- Check for URL encoding. Print the full URL you are requesting. Are special characters encoded?
- Confirm you are using the reference, not the ID. The reference is a string (often starts with a prefix you chose). The ID is a large number.
async function verifyWithDebug(reference) {
const key = process.env.PAYSTACK_SECRET_KEY;
console.log('Verifying reference:', JSON.stringify(reference));
console.log('Key prefix:', key.substring(0, 12));
console.log('Reference type:', typeof reference);
console.log('Reference length:', reference.length);
const url = `https://api.paystack.co/transaction/verify/${encodeURIComponent(reference)}`;
console.log('Full URL:', url);
const response = await fetch(url, {
headers: { Authorization: `Bearer ${key}` },
});
const data = await response.json();
console.log('Response status:', response.status);
console.log('Response:', JSON.stringify(data, null, 2));
return data;
}
This debug function prints everything you need to diagnose the issue. Remove the verbose logging after you fix the problem.
Verification and Prevention
After fixing the error, confirm it works:
- Initialize a test transaction and note the reference
- Complete the payment in the Paystack test checkout
- Call the verify endpoint with the same reference and key
- Confirm you get a 200 response with the transaction details
To prevent this error in future integrations:
- Generate references server-side. Do not rely on the client to create or pass references.
- Store references in your database before calling Paystack. This creates a source of truth.
- Use a single environment variable for the API key. Never hard-code keys or mix keys from different environments.
- Use safe characters in references. Stick to alphanumeric, hyphens, and underscores.
- Always encodeURIComponent the reference in URLs. Even if you think your references are safe, encoding prevents edge cases.
For the complete error reference, see Paystack Errors and Troubleshooting: The Complete Reference. For more on transaction verification patterns, see Verify Transaction: The Paystack Call You Must Never Skip.
Key Takeaways
- ✓The verify endpoint uses the transaction reference, not the transaction ID. The reference is the string you passed when initializing the transaction (or the one Paystack generated). The ID is a numeric value Paystack assigns internally.
- ✓Test transactions exist only in the test environment. Live transactions exist only in the live environment. If you initialize with sk_test_xxx and verify with sk_live_xxx, the transaction will not be found.
- ✓Special characters in references must be URL-encoded in the verify URL path. A reference like "order#123" must be sent as "order%23123" in the URL.
- ✓If you let Paystack generate the reference, capture it from the initialization response. Do not try to guess or reconstruct it.
- ✓Very old transactions may be archived and not immediately available via the verify endpoint. For recent transactions (within months), this is not a concern.
- ✓Always store the reference in your database at initialization time, before redirecting the customer. This prevents mismatches and lost references.
Frequently Asked Questions
- Can I verify a transaction using the transaction ID instead of the reference?
- The /transaction/verify endpoint requires the reference string, not the numeric ID. If you only have the ID, use the /transaction/:id endpoint (GET request) to fetch the transaction details. But the standard verify flow should always use the reference.
- What if I did not provide a reference when initializing the transaction?
- If you do not provide a reference, Paystack generates one and returns it in the initialization response. You must capture and store this generated reference for later verification. Check the data.reference field in the initialization response.
- Are transaction references case-sensitive?
- Yes. "TXN_001" and "txn_001" are different references. When storing and comparing references, preserve the exact case. If you generate references, pick a consistent casing convention (lowercase is safest) and stick with it.
- Can I verify a test transaction with a live key?
- No. Test transactions exist only in the test environment and can only be verified with a test secret key. Live transactions exist only in the live environment and can only be verified with a live secret key. The two environments are completely separate.
- How long after payment can I call the verify endpoint?
- You can verify recent transactions (within months) reliably. For production use, verify immediately when the customer returns from checkout or when you receive the charge.success webhook. There is no benefit to delaying verification.
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