What you'll build

A Node.js/Express backend that accepts payments across Africa using Flutterwave. Flutterwave supports 30+ African countries and automatically presents the right payment methods for each market — mobile money in Ghana and East Africa, bank transfers in Nigeria, cards everywhere. Your server initializes the payment, Flutterwave hosts the checkout page, and a webhook confirms the result.

By the end of this tutorial:

Prerequisites

How Flutterwave's hosted checkout works

  1. Your server sends the payment details (amount, currency, customer) to Flutterwave's API
  2. Flutterwave returns a hosted checkout URL
  3. You redirect the customer to that URL — Flutterwave shows the right payment methods for their country
  4. After payment, the customer is redirected back to your redirect_url
  5. Flutterwave sends a webhook to your server with the payment result
  6. Your server verifies the payment by calling Flutterwave's verify endpoint

Get your API key

  1. Go to dashboard.flutterwave.com and create an account
  2. Switch to Test Mode using the toggle in the top bar
  3. Navigate to Settings → API Keys and copy your Secret Key and Public Key
  4. Under Settings → Webhooks, set your webhook URL and copy the Secret Hash

Add these to your .env:

FLW_SECRET_KEY=FLWSECK_TEST-your_secret_key
FLW_PUBLIC_KEY=FLWPUBK_TEST-your_public_key
FLW_SECRET_HASH=your_webhook_secret_hash
FLW_REDIRECT_URL=https://your-domain.com/payment/complete
PORT=3000

Full API reference: developer.flutterwave.com

Initialize the project

mkdir flutterwave-app && cd flutterwave-app
npm init -y
npm install express dotenv axios
npm install -D typescript @types/express @types/node tsx
npx tsc --init

Add to package.json scripts:

"dev": "tsx watch src/index.ts"

Build the payment endpoint

BlocWeave prompt

Create a Flutterwave payment integration with Express. 1. src/flutterwave.ts — export two functions: - initializePayment(params: { amount: number, currency: string, email: string, name: string, txRef: string, redirectUrl: string, meta?: Record<string, any> }): POST to https://api.flutterwave.com/v3/payments with Authorization header Bearer ${FLW_SECRET_KEY} and body: { tx_ref: params.txRef, amount: params.amount, currency: params.currency, redirect_url: params.redirectUrl, customer: { email: params.email, name: params.name }, meta: params.meta || {}, customizations: { title: "Payment", logo: "" } }. Return the response data (contains link for hosted checkout). - verifyTransaction(transactionId: string): GET https://api.flutterwave.com/v3/transactions/${transactionId}/verify with the same Bearer auth. Return the response data. The caller must check that data.status === "successful" AND data.amount >= expected amount AND data.currency === expected currency. 2. src/index.ts — Express app with: - An in-memory Map<string, object> called payments to track payment status by tx_ref. - POST /pay: validate body { amount: number, currency: string, email: string, name: string }. Validate currency is one of ["GHS", "NGN", "KES", "UGX", "ZAR", "XOF"]. Generate tx_ref as "FLW-" + Date.now() + "-" + crypto.randomBytes(4).toString("hex"). Call initializePayment, store the tx_ref in the payments map with status "pending" and the expected amount and currency. Return 201 { txRef, paymentLink: data.link }. - GET /payments/:ref: look up the tx_ref in the payments map and return its current status, or 404 if not found. Use express.json() globally. Add error handling middleware. Load env vars with dotenv.

Handle the webhook

Flutterwave sends webhook events for payment state changes. The most important event is charge.completed. You must verify the webhook signature and then verify the transaction with Flutterwave's API — the webhook payload alone is not proof of payment.

BlocWeave prompt

Add the Flutterwave webhook handler to the Express app in src/index.ts. POST /webhooks/flutterwave: - Read the verif-hash header (Flutterwave sends this as the header name). - Compare it to FLW_SECRET_HASH using crypto.timingSafeEqual. If they don't match, return 401 { error: "Invalid signature" }. - Parse the body. The event payload has { event, data } where event is like "charge.completed" and data contains the transaction details. - If event === "charge.completed" and data.status === "successful": - Call verifyTransaction(data.id) to double-check the payment with Flutterwave's API (never trust the webhook payload alone). - From the verification response, confirm: status === "successful", amount >= the expected amount stored in the payments map (look up by data.tx_ref), currency matches the expected currency. - If all checks pass, update the payment record with status "completed", transactionId: data.id, paidAt: new Date().toISOString(), paymentType: data.payment_type. - If verification fails, update the payment with status "failed" and reason explaining the mismatch. - Return 200 immediately. Important: Always verify with the API. A webhook alone is not proof of payment — the request could be forged even with the hash, or the amount could have been tampered with on the client side.

Split payments for marketplaces

If you run a marketplace where sellers receive a portion of each payment, Flutterwave's split payments handle this automatically. Add a subaccounts array when initializing the payment:

BlocWeave prompt

Add split payment support to the initializePayment function in src/flutterwave.ts. Add an optional splits parameter: Array<{ subaccountId: string, percentage: number }>. If splits is provided and non-empty, add a subaccounts field to the request body: subaccounts: splits.map(s => ({ id: s.subaccountId, transaction_split_ratio: s.percentage, transaction_charge_type: "percentage", transaction_charge: s.percentage })) Also update POST /pay to accept an optional splits array in the request body and pass it through to initializePayment. Add a comment explaining: the platform keeps the remainder after all splits are deducted. For example, if you split 80% to a seller, the platform keeps 20% minus Flutterwave's fees.

Test it

Start the server and expose it via ngrok for webhook delivery:

npm run dev
# In another terminal:
ngrok http 3000

Update FLW_REDIRECT_URL in .env and the webhook URL in the Flutterwave dashboard, then restart the server.

Create a test payment:

curl -X POST http://localhost:3000/pay \
  -H "Content-Type: application/json" \
  -d '{
    "amount": 100,
    "currency": "GHS",
    "email": "[email protected]",
    "name": "Kwame Test"
  }'

Open the paymentLink URL from the response. Use the Flutterwave test card:

After payment, check the webhook logs on your server and verify the payment status:

curl http://localhost:3000/payments/FLW-1719000000000-abcd1234

Testing with different currencies

Flutterwave automatically shows country-appropriate payment methods. Test with different currencies to see the checkout adapt:

# Nigerian Naira — shows card, bank transfer, USSD
curl -X POST http://localhost:3000/pay \
  -H "Content-Type: application/json" \
  -d '{"amount": 5000, "currency": "NGN", "email": "[email protected]", "name": "Test User"}'

# Kenyan Shilling — shows M-Pesa, card
curl -X POST http://localhost:3000/pay \
  -H "Content-Type: application/json" \
  -d '{"amount": 1000, "currency": "KES", "email": "[email protected]", "name": "Test User"}'

Supported currencies and payment methods

Currency Country Payment methods
GHS Ghana Mobile Money, Card, Bank Transfer
NGN Nigeria Card, Bank Transfer, USSD
KES Kenya M-Pesa, Card
UGX Uganda Mobile Money, Card
ZAR South Africa Card, EFT
XOF Francophone West Africa Mobile Money, Card

Production checklist: (1) Switch from test keys to live keys in the Flutterwave dashboard. (2) Store payments in a real database with idempotency on tx_ref. (3) Always verify the amount and currency server-side — clients can modify the checkout page. (4) Set up Flutterwave's IP allowlist for webhook endpoints. (5) Handle the transfer.completed event if you use split payments. (6) Implement retry logic for the verification API call — if it fails, queue it for retry rather than marking the payment as failed.

Project structure after this tutorial

src/
  flutterwave.ts    # Payment initialization + verification
  index.ts          # Express app, routes, webhook handler
.env                # FLW_SECRET_KEY, FLW_PUBLIC_KEY, FLW_SECRET_HASH
package.json