For Coding Agents: Start Here
Before reading any examples or framework guides, you MUST read:
This file defines the payment safety contract and execution order that must be followed to prevent fraud and incorrect implementations. Start by identifying what you are trying to implement:
- •If implementing frontend popup/redirect → read references/one-time-payments.md
- •If implementing backend verification → read references/express-implementation.md or references/nextjs-implementation.md
- •If implementing webhooks → read references/webhooks.md
- •If implementing subscriptions → read references/subscriptions.md
- •If implementing mobile money (M-Pesa, MTN, Airtel) → read references/mobile-money.md
- •If implementing bank transfers/payouts → read references/bank-transfers.md
- •If unsure about responsibilities or safety → read references/AGENT_EXECUTION_SPEC.md first
- •If debugging issues → read references/troubleshooting.md
- •If you need v3 vs v4 migration or enckey handling → read references/flutterwave-specific.md
If you are unsure about responsibilities, safety, or execution order, read: references/AGENT_EXECUTION_SPEC.md first.
Condensed Payment Safety Rules
- •Generate unique
tx_refFIRST, store in DB with status=pending, THEN call Flutterwave. - •Always convert and store amounts in smallest currency unit (kobo/pesewas/cents).
- •Never trust client-side success callbacks; always verify on backend.
- •Webhook handlers must verify signature (
verif-hashheader OR HMAC-SHA256) before processing. - •Verify that the verified amount exactly matches the expected DB amount before fulfilling.
- •Handle
success-pending-validationstatus (wait for webhook confirmation). - •Ensure idempotency: if order.status == 'paid', exit immediately.
Table of Contents (all contained in ./references/)
- •Agent Execution Spec - READ FIRST
- •Quick Reference
- •Payment Flow Overview
- •Core Implementation
- •Webhook Essentials
- •Framework Guides
- •Deployment Checklist
- •Quick Troubleshooting
- •Database example
- •Local webhook testing with ngrok
Quick Reference
Environment Variables
# Backend (NEVER expose these) FLW_SECRET_KEY=FLWSECK_TEST-xxxx # or FLWSECK-xxxx for production FLW_SECRET_HASH=your_webhook_hash # Set in Dashboard → Settings → Webhooks # Frontend (safe to expose) FLW_PUBLIC_KEY=FLWPUBK_TEST-xxxx # or FLWPUBK-xxxx for production # Optional FLW_ENCRYPTION_KEY=FLWSECK_TESTxxxx # For direct card charges only
API Configuration
const FLW_BASE_URL = 'https://api.flutterwave.com/v3';
const headers = {
Authorization: `Bearer ${process.env.FLW_SECRET_KEY}`,
'Content-Type': 'application/json',
};
tx_ref Generation (CRITICAL)
import crypto from 'crypto';
// ALWAYS generate tx_ref BEFORE calling Flutterwave
function generateTxRef(prefix = 'FLW'): string {
const timestamp = Date.now().toString(36);
const random = crypto.randomBytes(4).toString('hex');
return `${prefix}_${timestamp}_${random}`.toUpperCase();
}
// Example: FLW_LK5J2M8_A1B2C3D4
Flow:
- •Generate unique
tx_ref - •Store in database with status=pending
- •Call Flutterwave
/v3/paymentswith thistx_ref - •Redirect user to returned
link - •Webhook events will contain this same
tx_ref
Unlike Paystack, YOU generate the reference before calling the API.
Currency Units (CRITICAL)
| Currency | Country | Smallest Unit | Multiplier |
|---|---|---|---|
| NGN | Nigeria | kobo | 100 |
| GHS | Ghana | pesewas | 100 |
| KES | Kenya | cents | 100 |
| ZAR | South Africa | cents | 100 |
| UGX | Uganda | cents | 100 |
| XOF | Francophone | francs | 100 |
| XAF | Central Africa | francs | 100 |
| USD | International | cents | 100 |
function toSmallestUnit(amount: number): number {
return Math.round(amount * 100);
}
Supported Payment Channels
type FlutterwaveChannel = | 'card' | 'banktransfer' | 'ussd' | 'credit' | 'mobilemoneyghana' | 'mobilemoneyuganda' | 'mobilemoneyrwanda' | 'mobilemoneyzambia' | 'mobilemoneytanzania' | 'mobilemoneyfranco' | 'mpesa' | 'barter';
Payment Flow Overview
One-Time Payment Flow
┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Client │────▶│ Backend │────▶│ Flutterwave │────▶│ Backend │
│ (initiate) │ │ (initialize)│ │ (payment) │ │ (verify) │
└─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘
│ │ │ │
│ 1. Request │ 2. POST │ 3. Pay via │ 4. GET
│ payment │ /v3/payments │ hosted page │ /v3/transactions
│ │ │ │ /:id/verify
│ │ Returns: │ Returns: │
│ │ link (hosted) │ tx_ref, flw_ref │ Returns:
│ │ │ transaction_id │ status: successful
Decision: Hosted vs Inline
| Method | Use When | Pros | Cons |
|---|---|---|---|
| Hosted (Redirect) | Simple integration, server-rendered | No JS required, PCI compliant | User leaves your site |
| Inline (Popup) | SPA, better UX | User stays on site | Requires Inline.js SDK |
Core Implementation
TypeScript Interfaces
interface FlutterwaveResponse<T> {
status: 'success' | 'error';
message: string;
data: T;
}
interface InitializePaymentData {
link: string; // Hosted payment page URL
}
interface VerifyTransactionData {
id: number;
tx_ref: string;
flw_ref: string;
status: 'successful' | 'pending' | 'failed' | 'success-pending-validation';
amount: number;
currency: string;
charged_amount: number;
app_fee: number;
customer: {
id: number;
email: string;
name: string;
phone_number: string;
};
card?: {
first_6digits: string;
last_4digits: string;
type: string;
expiry: string;
};
meta?: Record<string, unknown>;
created_at: string;
}
interface WebhookEvent {
event: string;
data: VerifyTransactionData;
}
Initialize Transaction
interface InitializeParams {
tx_ref: string; // YOUR unique reference (required)
amount: number; // In smallest unit
currency: string; // NGN, GHS, KES, etc.
redirect_url: string; // Where to redirect after payment
customer: {
email: string;
name?: string;
phonenumber?: string;
};
customizations?: {
title?: string;
description?: string;
logo?: string;
};
payment_options?: string; // Comma-separated: "card,banktransfer,ussd"
meta?: Record<string, unknown>;
}
async function initializePayment(params: InitializeParams): Promise<string> {
const response = await fetch(`${FLW_BASE_URL}/payments`, {
method: 'POST',
headers,
body: JSON.stringify(params),
});
const result: FlutterwaveResponse<InitializePaymentData> = await response.json();
if (result.status !== 'success') {
throw new Error(result.message);
}
return result.data.link; // Redirect URL
}
Verify Transaction (CRITICAL)
async function verifyTransaction(
transactionId: number,
expectedAmount: number,
expectedCurrency: string
): Promise<VerifyTransactionData> {
const response = await fetch(
`${FLW_BASE_URL}/transactions/${transactionId}/verify`,
{ headers }
);
const result: FlutterwaveResponse<VerifyTransactionData> = await response.json();
if (result.status !== 'success') {
throw new Error(result.message);
}
// CRITICAL: Verify payment status
if (result.data.status !== 'successful') {
throw new Error(`Payment not successful: ${result.data.status}`);
}
// CRITICAL: Verify amount matches expected
if (result.data.amount !== expectedAmount) {
throw new Error(`Amount mismatch: expected ${expectedAmount}, got ${result.data.amount}`);
}
// CRITICAL: Verify currency matches
if (result.data.currency !== expectedCurrency) {
throw new Error(`Currency mismatch: expected ${expectedCurrency}, got ${result.data.currency}`);
}
return result.data;
}
Webhook Essentials
Webhooks notify your server of payment events. Signature verification is mandatory for security.
Webhook Verification (Two Methods)
Method 1: Simple verif-hash (Recommended)
function handleWebhook(req: Request): Response {
const signature = req.headers.get('verif-hash');
const secretHash = process.env.FLW_SECRET_HASH;
if (!signature || signature !== secretHash) {
return new Response('Invalid signature', { status: 401 });
}
const event: WebhookEvent = await req.json();
await processWebhookEvent(event);
return new Response('OK', { status: 200 });
}
Method 2: HMAC-SHA256 (More Secure)
import crypto from 'crypto';
function verifyFlutterwaveSignature(
payload: string,
signature: string,
secretHash: string
): boolean {
const hash = crypto
.createHmac('sha256', secretHash)
.update(payload)
.digest('base64');
// Use timing-safe comparison
try {
return crypto.timingSafeEqual(
Buffer.from(hash),
Buffer.from(signature)
);
} catch {
return false;
}
}
Key Webhook Events
| Event | When | Action |
|---|---|---|
charge.completed | Payment completed | Fulfill order, update database |
charge.failed | Payment failed | Notify user, log for analysis |
transfer.completed | Payout completed | Update transfer status |
transfer.failed | Payout failed | Retry or notify admin |
subscription.cancelled | Subscription cancelled | Revoke access |
Idempotency
async function processWebhookEvent(event: WebhookEvent): Promise<void> {
const txRef = event.data.tx_ref;
// Check if already processed
const order = await db.orders.findUnique({ where: { txRef } });
if (!order) {
console.log(`Order not found for tx_ref: ${txRef}`);
return;
}
if (order.status === 'paid') {
console.log(`Order ${txRef} already paid, skipping`);
return;
}
// Process based on event type
if (event.event === 'charge.completed' && event.data.status === 'successful') {
await fulfillOrder(order, event.data);
}
}
See references/webhooks.md for complete event handling patterns.
Framework Guides
Next.js (App Router)
Recommended file structure:
app/api/flutterwave/ ├── initialize/route.ts # POST - Initialize payment ├── verify/route.ts # GET - Verify payment └── webhook/route.ts # POST - Handle webhooks components/ └── FlutterwaveButton.tsx # Client component lib/ └── flutterwave.ts # Utility functions
See references/nextjs-implementation.md for complete implementation.
Express.js
Recommended structure:
routes/ └── flutterwave.routes.ts controllers/ └── flutterwave.controller.ts middleware/ └── flutterwave.middleware.ts # Signature verification services/ └── flutterwave.service.ts # API calls
See references/express-implementation.md for complete implementation.
Deployment Checklist
Before Going Live
- •
Environment Variables
- •
FLW_SECRET_KEYset (FLWSECK-xxxx for production) - •
FLW_PUBLIC_KEYset (FLWPUBK-xxxx for production) - •
FLW_SECRET_HASHset (from Dashboard) - • Keys not committed to version control
- •
- •
Webhook Configuration
- • Webhook URL configured in Flutterwave Dashboard
- • HTTPS endpoint (required for production)
- • Signature verification implemented
- • Return 200 OK within 30 seconds
- •
Payment Verification
- • Amount verification implemented
- • Currency verification implemented
- • Status check implemented (only process
successful) - • Handle
success-pending-validationstatus - • Idempotency for webhook handling
- •
Testing
- • Test with Flutterwave test cards
- • Test mobile money flows
- • Test webhook with ngrok (local)
- • Test failed payment scenarios
Test Cards
| Card Number | CVV | Expiry | PIN | OTP | Result |
|---|---|---|---|---|---|
5531886652142950 | 564 | 09/32 | 3310 | 12345 | Success |
5258585922666506 | 883 | 09/31 | 3310 | 12345 | Insufficient Funds |
5399838383838381 | 470 | 10/31 | 3310 | 12345 | Declined |
Test Mobile Money
- •Phone:
0551234987(Ghana) - •OTP:
123456
Quick Troubleshooting
Common Errors
| Error | Cause | Solution |
|---|---|---|
Invalid API key | Wrong or missing key | Check FLW_SECRET_KEY env var |
Invalid tx_ref | Duplicate or malformed | Generate unique tx_ref per transaction |
Amount mismatch | Amount verification failed | Store and compare in smallest unit |
Invalid signature | Webhook verification failed | Check FLW_SECRET_HASH matches dashboard |
Transaction not found | Wrong transaction ID | Use correct id from callback/webhook |
Debug Checklist
- •
API calls failing?
- •Check Authorization header format:
Bearer FLWSECK-xxx - •Verify base URL:
https://api.flutterwave.com/v3 - •Check test vs live keys match environment
- •Check Authorization header format:
- •
Webhook not received?
- •Verify URL is publicly accessible (HTTPS)
- •Check Flutterwave Dashboard for delivery logs
- •Ensure you return 200 OK quickly
- •
Status issues?
- •Handle
success-pending-validationstatus - •Poll for status or wait for webhook
- •Handle
See references/troubleshooting.md for comprehensive debugging guide.