| name | flutterwave-integration |
| description | Implement Flutterwave payment processing for African markets (34+ countries). Use when integrating one-time payments (card, bank transfer, USSD, mobile money), subscriptions/recurring billing, or webhook handling. Covers Next.js and Express implementations with TypeScript. Triggers: payment integration, Flutterwave setup, accept payments Africa, NGN/GHS/KES/ZAR payments, mobile money, M-Pesa, MTN, subscription billing, payment webhooks, Flutterwave API, tx_ref, verif-hash.
|
For Coding Agents: Start Here
Before reading any examples or framework guides, you MUST read:
Agent Execution Spec
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 you are unsure about responsibilities, safety, or execution order, read: references/AGENT_EXECUTION_SPEC.md first.
Condensed Payment Safety Rules
- Generate unique
tx_ref FIRST, 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-hash header OR HMAC-SHA256) before processing.
- Verify that the verified amount exactly matches the expected DB amount before fulfilling.
- Handle
success-pending-validation status (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
FLW_SECRET_KEY=FLWSECK_TEST-xxxx
FLW_SECRET_HASH=your_webhook_hash
FLW_PUBLIC_KEY=FLWPUBK_TEST-xxxx
FLW_ENCRYPTION_KEY=FLWSECK_TESTxxxx
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';
function generateTxRef(prefix = 'FLW'): string {
const timestamp = Date.now().toString(36);
const random = crypto.randomBytes(4).toString('hex');
return `${prefix}_${timestamp}_${random}`.toUpperCase();
}
Flow:
- Generate unique
tx_ref
- Store in database with status=pending
- Call Flutterwave
/v3/payments with this tx_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;
}
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;
amount: number;
currency: string;
redirect_url: string;
customer: {
email: string;
name?: string;
phonenumber?: string;
};
customizations?: {
title?: string;
description?: string;
logo?: string;
};
payment_options?: string;
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;
}
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);
}
if (result.data.status !== 'successful') {
throw new Error(`Payment not successful: ${result.data.status}`);
}
if (result.data.amount !== expectedAmount) {
throw new Error(`Amount mismatch: expected ${expectedAmount}, got ${result.data.amount}`);
}
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');
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;
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;
}
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
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
-
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-validation status
- Poll for status or wait for webhook
See references/troubleshooting.md for comprehensive debugging guide.
Official Documentation