Loading...
Loading...
Complete Flutterwave integration skill supporting both v3 Production and v4 Public Beta APIs across checkout, cards, bank transfers, mobile money, transfers, virtual accounts, and webhooks.
npx skill4agent add devmubarak1/flutterwave flutterwave.mdhttps://developer.flutterwave.com/docs/getting-started.md| Feature | v3 (Production Stable) | v4 (Next-Gen Public Beta) |
|---|---|---|
| Status | Globally deployed, battle-tested | Public Beta (high performance, microservices) |
| Base URL | | Sandbox: |
| Authentication | Static Secret Key Bearer ( | OAuth 2.0 OpenID Connect Client Credentials ( |
| Token Endpoint | N/A | |
| Tracing Header | Optional | |
| Idempotency | Custom | |
| Primary Inflow | | |
| Primary Outflow | | |
import crypto from "node:crypto";
export interface FlutterwaveConfig {
secretKey?: string; // v3 secret key (e.g., FLWSECK_TEST-... or FLWSECK-...)
encryptionKey?: string; // v3 3DES encryption key for direct card charges
v4ClientId?: string; // v4 OAuth2 client_id
v4ClientSecret?: string; // v4 OAuth2 client_secret
v3BaseUrl?: string; // default: https://api.flutterwave.com/v3
v4BaseUrl?: string; // sandbox: https://developersandbox-api.flutterwave.com | live: https://f4bexperience.flutterwave.com/
v4IdpUrl?: string; // default: https://idp.flutterwave.com/realms/flutterwave/protocol/openid-connect/token
}
export interface FlwResponse<T = unknown> {
status: "success" | "error";
message: string;
data: T;
meta?: Record<string, unknown>;
}
export class FlutterwaveApiError extends Error {
constructor(
message: string,
public readonly statusCode: number,
public readonly payload?: unknown,
) {
super(message);
this.name = "FlutterwaveApiError";
}
}
export class Flutterwave {
private readonly config: FlutterwaveConfig;
private v4TokenCache: { token: string; expiresAt: number } | null = null;
constructor(config: FlutterwaveConfig = {}) {
this.config = {
secretKey: config.secretKey ?? process.env.FLW_SECRET_KEY,
encryptionKey: config.encryptionKey ?? process.env.FLW_ENCRYPTION_KEY,
v4ClientId: config.v4ClientId ?? process.env.FLW_V4_CLIENT_ID,
v4ClientSecret: config.v4ClientSecret ?? process.env.FLW_V4_CLIENT_SECRET,
v3BaseUrl: config.v3BaseUrl ?? process.env.FLW_V3_BASE_URL ?? "https://api.flutterwave.com/v3",
v4BaseUrl: config.v4BaseUrl ?? process.env.FLW_V4_BASE_URL ?? "https://developersandbox-api.flutterwave.com",
v4IdpUrl: config.v4IdpUrl ?? "https://idp.flutterwave.com/realms/flutterwave/protocol/openid-connect/token",
};
}
/**
* Retrieves or refreshes OAuth 2.0 access token for v4 requests.
*/
private async getV4Token(): Promise<string> {
const now = Date.now();
if (this.v4TokenCache && this.v4TokenCache.expiresAt > now + 60_000) {
return this.v4TokenCache.token;
}
if (!this.config.v4ClientId || !this.config.v4ClientSecret) {
throw new Error("Missing v4 credentials: FLW_V4_CLIENT_ID and FLW_V4_CLIENT_SECRET are required for v4 calls.");
}
const res = await fetch(this.config.v4IdpUrl!, {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
grant_type: "client_credentials",
client_id: this.config.v4ClientId,
client_secret: this.config.v4ClientSecret,
}).toString(),
});
if (!res.ok) {
throw new FlutterwaveApiError(`Failed to fetch v4 OAuth token: ${res.statusText}`, res.status);
}
const data = (await res.json()) as { access_token: string; expires_in: number };
this.v4TokenCache = {
token: data.access_token,
expiresAt: now + data.expires_in * 1000,
};
return data.access_token;
}
/**
* Executes a typed API request to either v3 or v4.
*/
async request<T = unknown>(
version: "v3" | "v4",
path: string,
options: RequestInit & { idempotencyKey?: string } = {},
): Promise<FlwResponse<T>> {
const { idempotencyKey, headers: customHeaders, ...init } = options;
const headers: Record<string, string> = {
"Content-Type": "application/json",
...(customHeaders as Record<string, string>),
};
let baseUrl: string;
if (version === "v3") {
if (!this.config.secretKey) {
throw new Error("Missing FLW_SECRET_KEY for v3 request");
}
baseUrl = this.config.v3BaseUrl!;
headers["Authorization"] = `Bearer ${this.config.secretKey}`;
} else {
baseUrl = this.config.v4BaseUrl!;
const token = await this.getV4Token();
headers["Authorization"] = `Bearer ${token}`;
headers["X-Trace-Id"] = crypto.randomUUID();
if (idempotencyKey) {
headers["X-Idempotency-Key"] = idempotencyKey;
}
}
const url = `${baseUrl}${path.startsWith("/") ? path : `/${path}`}`;
const response = await fetch(url, { ...init, headers });
const body = (await response.json()) as FlwResponse<T>;
if (!response.ok || body.status === "error") {
throw new FlutterwaveApiError(
body.message || `Flutterwave ${version} request failed with status ${response.status}`,
response.status,
body,
);
}
return body;
}
}| Action | Method | v3 Route | v4 Beta Route | Key Parameters / Description |
|---|---|---|---|---|
| Standard Hosted Checkout | | | | |
| Verify Transaction (by ID) | | | | Confirms final payment status, settled amount, and card token. Mandatory before order fulfillment. |
| Verify Transaction (by ref) | | | | Queries payment using the merchant's custom order/transaction reference. |
| List Transactions | | | | Filters by |
| Direct Card Charge | | | | v3 uses 3DES encrypted payload; v4 uses |
| Validate Charge (OTP) | | | | Submits customer OTP challenge code ( |
| Bank Transfer Collection | | | | Generates dynamic dedicated Nigerian NIP bank account with expiry timer. |
| Mobile Money (M-Pesa) | | | | Triggers instant STK push prompt on customer mobile device in Kenya ( |
| Mobile Money (Ghana) | | | | Charges MTN, Telecel (Vodafone voucher), or AirtelTigo wallets ( |
| USSD Payment | | | | Returns bank-specific interactive dial code (e.g. |
| Tokenized Charge | | | | Re-bills stored |
| Initiate Transfer (Payout) | | | | Disburses funds to bank accounts or mobile wallets. v4 uses |
| Get Transfer Status | | | | Inspects payout status ( |
| Retry Transfer | | | | Resubmits failed transfer for reprocessing. |
| List Beneficiaries | | | | Lists saved bank account and mobile wallet payout contacts. |
| Create Beneficiary | | | | Saves recipient ( |
| Create Virtual Account | | | | Provisions permanent or dynamic virtual bank accounts for direct customer wallet funding. |
| Bulk Virtual Accounts | | | | Batches creation of dedicated virtual accounts for fintech platforms. |
| Create Subaccount (Split) | | | Split configuration | Configures vendor subaccount for marketplace splits ( |
| Create Payment Plan | | | Subscription plans | Sets automated subscription billing intervals ( |
| Create Virtual Card | | | Card issuing | Issues digital dollar ( |
| Resolve Bank Account | | | | Verifies Nigerian bank account number and resolves official registered account holder name. |
| Validate BVN | | | KYC validation | Confirms Bank Verification Number matches customer legal identity for compliance. |
| Issue Refund | | | | Issues full or partial refunds directly to customer's source account. |
| Manage Chargebacks | | | | Queries open dispute claims and submits defense evidence before the due date. |
| List Settlements | | | | Reconciles bank deposits, gross transactions, and processor fee deductions. |
| Pay Bills & Airtime | | | Utility bills | Purchases airtime, mobile data bundles, power tokens, and renews cable TV subscriptions. |
const flw = new Flutterwave();
const session = await flw.request<{ link: string }>("v3", "/payments", {
method: "POST",
body: JSON.stringify({
tx_ref: `ORD-${Date.now()}`,
amount: "7500.00",
currency: "NGN",
redirect_url: "https://yourdomain.com/checkout/callback",
customer: {
email: "buyer@example.com",
name: "Amina Bello",
phonenumber: "+2348012345678",
},
customizations: {
title: "Store Checkout",
description: "Payment for order #1082",
},
}),
});
console.log("Customer Payment URL:", session.data.link);const flw = new Flutterwave();
// v3 verification by Transaction ID
const v3Tx = await flw.request<{
status: string;
amount: number;
currency: string;
tx_ref: string;
card?: { token: string };
}>("v3", `/transactions/${transactionId}/verify`, { method: "GET" });
if (v3Tx.data.status === "successful" && v3Tx.data.amount >= orderAmount) {
// Safe to credit account or ship goods
const savedCardToken = v3Tx.data.card?.token;
}
// v4 Next-Gen verification by Charge ID
const v4Charge = await flw.request<{
status: string;
amount: number;
currency: string;
reference: string;
}>("v4", `/charges/${chargeId}`, { method: "GET" });const flw = new Flutterwave();
// v3 Payout
const v3Payout = await flw.request("v3", "/transfers", {
method: "POST",
body: JSON.stringify({
account_bank: "058", // GTBank
account_number: "0123456789",
amount: 50000,
currency: "NGN",
reference: `PAYOUT-${Date.now()}`,
narration: "Vendor disbursement",
}),
});
// v4 Next-Gen Transfer Orchestrator
const v4Payout = await flw.request("v4", "/transfers", {
method: "POST",
idempotencyKey: `IDEMP-${Date.now()}`,
body: JSON.stringify({
action: "instant",
reference: `V4-PAYOUT-${Date.now()}`,
narration: "Vendor payment",
payment_instruction: {
amount: 50000,
currency: "NGN",
recipient_id: "rcb_B9aAgsdzzl",
},
}),
});verif-hashimport crypto from "node:crypto";
export function handleFlutterwaveWebhook(req: Request) {
const incomingHash = req.headers.get("verif-hash");
const secretHash = process.env.FLW_WEBHOOK_HASH;
if (!incomingHash || !secretHash) {
return new Response("Unauthorized", { status: 401 });
}
// Timing-safe constant-time comparison to prevent side-channel leaks
const a = Buffer.from(incomingHash, "utf8");
const b = Buffer.from(secretHash, "utf8");
if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
return new Response("Forbidden", { status: 403 });
}
// Respond HTTP 200 immediately to meet Flutterwave's 60-second SLA
// Process the event asynchronously in the background
return new Response(JSON.stringify({ status: "success" }), { status: 200 });
}| Rail | Test Input | Expected Behavior |
|---|---|---|
| 3DS Mastercard | Card: | Requires OTP -> Enter |
| Verve Card (PIN) | Card: | Requires PIN |
| Decline Simulation | Card: | Direct decline ( |
| Kenya M-Pesa | Phone: | Auto-approves simulated STK push in sandbox |
| Bank Resolution | Account: | Resolves name: |
customer.emailpayment_instructioncustomer_idredirect_url?status=successfultx_refreferenceFLW_SECRET_KEYFLW_V4_CLIENT_IDFLW_V4_CLIENT_SECRETFLW_WEBHOOK_HASH