AgentSkillsCN

flutterwave-integration

在非洲市场(34+个国家)实施Flutterwave支付处理。适用于一次性支付(信用卡、银行转账、USSD、移动支付)、订阅/定期账单,或Webhook处理的场景。涵盖使用TypeScript的Next.js与Express实现。可通过“支付集成”“Flutterwave设置”“接受非洲支付”“NGN/GHS/KES/ZAR支付”、“移动支付”、“M-Pesa”、“MTN”、“订阅账单”、“支付Webhook”、“Flutterwave API”、“tx_ref”、“verif-hash”等指令触发。

SKILL.md
--- frontmatter
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/)

  1. Agent Execution Spec - READ FIRST
  2. Quick Reference
  3. Payment Flow Overview
  4. Core Implementation
  5. Webhook Essentials
  6. Framework Guides
  7. Deployment Checklist
  8. Quick Troubleshooting
  9. Database example
  10. Local webhook testing with ngrok

Quick Reference

Environment Variables

bash
# 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

typescript
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)

typescript
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:

  1. Generate unique tx_ref
  2. Store in database with status=pending
  3. Call Flutterwave /v3/payments with this tx_ref
  4. Redirect user to returned link
  5. Webhook events will contain this same tx_ref

Unlike Paystack, YOU generate the reference before calling the API.

Currency Units (CRITICAL)

CurrencyCountrySmallest UnitMultiplier
NGNNigeriakobo100
GHSGhanapesewas100
KESKenyacents100
ZARSouth Africacents100
UGXUgandacents100
XOFFrancophonefrancs100
XAFCentral Africafrancs100
USDInternationalcents100
typescript
function toSmallestUnit(amount: number): number {
  return Math.round(amount * 100);
}

Supported Payment Channels

typescript
type FlutterwaveChannel =
  | 'card'
  | 'banktransfer'
  | 'ussd'
  | 'credit'
  | 'mobilemoneyghana'
  | 'mobilemoneyuganda'
  | 'mobilemoneyrwanda'
  | 'mobilemoneyzambia'
  | 'mobilemoneytanzania'
  | 'mobilemoneyfranco'
  | 'mpesa'
  | 'barter';

Payment Flow Overview

One-Time Payment Flow

code
┌─────────────┐     ┌─────────────┐     ┌─────────────┐     ┌─────────────┐
│   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

MethodUse WhenProsCons
Hosted (Redirect)Simple integration, server-renderedNo JS required, PCI compliantUser leaves your site
Inline (Popup)SPA, better UXUser stays on siteRequires Inline.js SDK

Core Implementation

TypeScript Interfaces

typescript
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

typescript
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)

typescript
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)

typescript
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)

typescript
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

EventWhenAction
charge.completedPayment completedFulfill order, update database
charge.failedPayment failedNotify user, log for analysis
transfer.completedPayout completedUpdate transfer status
transfer.failedPayout failedRetry or notify admin
subscription.cancelledSubscription cancelledRevoke access

Idempotency

typescript
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:

code
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:

code
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_KEY set (FLWSECK-xxxx for production)
    • FLW_PUBLIC_KEY set (FLWPUBK-xxxx for production)
    • FLW_SECRET_HASH set (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-validation status
    • 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 NumberCVVExpiryPINOTPResult
553188665214295056409/32331012345Success
525858592266650688309/31331012345Insufficient Funds
539983838383838147010/31331012345Declined

Test Mobile Money

  • Phone: 0551234987 (Ghana)
  • OTP: 123456

Quick Troubleshooting

Common Errors

ErrorCauseSolution
Invalid API keyWrong or missing keyCheck FLW_SECRET_KEY env var
Invalid tx_refDuplicate or malformedGenerate unique tx_ref per transaction
Amount mismatchAmount verification failedStore and compare in smallest unit
Invalid signatureWebhook verification failedCheck FLW_SECRET_HASH matches dashboard
Transaction not foundWrong transaction IDUse correct id from callback/webhook

Debug Checklist

  1. 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
  2. Webhook not received?

    • Verify URL is publicly accessible (HTTPS)
    • Check Flutterwave Dashboard for delivery logs
    • Ensure you return 200 OK quickly
  3. Status issues?

    • Handle success-pending-validation status
    • Poll for status or wait for webhook

See references/troubleshooting.md for comprehensive debugging guide.


Official Documentation