dánwòdánwò

Payments

Flutterwave

Flutterwave's v3 API with its hosted checkout, test cards, refunds, payment plans, transfers and virtual accounts.

danwo's Flutterwave emulator answers api.flutterwave.com in test mode and serves the hosted checkout on checkout.flutterwave.com. Your app creates a Standard payment link, the browser pays on the hosted page, and the emulator sends charge.completed to your webhook.

Set up

Add flutterwaveService. Any test-mode key, FLWSECK_TEST-…, is accepted, so your app keeps its own. Seed the webhook with the secret hash your app checks:

danwo.config.ts
import { appEnvironment, defineConfig, flutterwaveService } from "@danwo/sdk"

const url = "http://localhost:3000"

export default defineConfig({
  testDir: "tests",
  readiness: { mode: "page+backend", settle: true },
  environment: () =>
    appEnvironment({
      app: { command: "npm run dev", url, env: { FLW_SECRET_HASH: "danwo-hash" } },
      services: [
        flutterwaveService({
          seed: { webhook: { url: `${url}/api/webhooks/flutterwave`, secretHash: "danwo-hash" } },
        }),
      ],
    }),
})

A key that isn't a test-mode key gets Flutterwave's 401, Invalid authorization key.

Seed it

danwo.config.ts · the seed
flutterwaveService({
  seed: {
    webhook: { url: "http://localhost:3000/api/webhooks/flutterwave", secretHash: "danwo-hash" },
    balances: { NGN: 250_000, USD: 1_000 },
    plans: [{ id: 101, name: "Pro", amount: 5_000, interval: "monthly", currency: "NGN" }],
    accounts: [{ bankCode: "044", accountNumber: "0690000099", accountName: "Ada Obi" }],
  },
})
FieldWhat it seeds
webhookThe dashboard's webhook URL and its secret hash.
balancesBalances by currency, in major units; NGN 1,000,000 by default.
plansPayment plans, hourly to yearly, with an optional number of charges.
accountsAccounts that resolve, beside Flutterwave's own test accounts; blacklisted refuses transfers to one.

Use it in a test

Pay on the hosted page, then check the transaction was recorded once:

tests/pay.danwo.ts
import { type FlutterwaveFixtures, flutterwaveLedger, test as base } from "@danwo/sdk"

const test = base.extend<FlutterwaveFixtures>()

test("pays for an order once", async ({ app, screen, expect, flutterwave }) => {
  await app.open("/checkout")
  await screen.getByRole("button", { name: "Pay with Flutterwave" }).click()

  await screen.getByLabel("Card number").fill("5377 2836 4507 7450")
  await screen.getByLabel("Expiry date").fill("09/32")
  await screen.getByLabel("CVV").fill("789")
  await screen.getByRole("button", { name: /^Pay / }).click()
  await expect(screen.getByText("Order paid")).toBeVisible()

  const paid = flutterwaveLedger(flutterwave).paid("order-1")

  expect(paid).toHaveLength(1)
  expect(paid[0]?.status).toBe("successful")
})
HelperWhat it's for
flutterwaveLedger(flutterwave)Transactions, refunds, transfers, subscriptions and balances.
flutterwaveLedger(…).renewDue()Charge every subscription that's due now.
flutterwaveBank(flutterwave)Pay into a virtual account, as the customer's bank would.

Test cards

Card numberWhat the hosted page does
5377 2836 4507 7450Pays with no further step.
5531 8866 5214 2950Asks for a PIN, then an OTP.
4187 4274 1556 4246Asks for the bank's OTP (3-D Secure).
4556 0527 0417 2643Asks for a billing address.
5258 5859 2266 6506Refused for insufficient funds.
5143 0105 2233 9965Refused with "Do Not Honour".

OTP 5548 ends in "Wrong OTP". The page also offers bank transfer ("I've sent the money") and mobile money, and "Cancel payment" goes back to your redirect_url.

What's covered

AreaCovered
PaymentsStandard payment links, the hosted page, disabling a link.
TransactionsVerify, list, events, fees, and sending the webhook again.
RefundsFull and partial refunds.
Plans and subscriptionsPlans, subscriptions, and renewals on the emulator's clock.
TransfersTransfers, fees and retries, settling as the test-mode references say.
Money inVirtual account numbers and bank transfers into them.
LookupsBanks, account resolution, beneficiaries and balances.

Faults

CallFaults the matrix tries
A write, such as POST /v3/payments429, 500, 503, a 10-second delay, connection reset, answer lost after Flutterwave acted
GET /v3/transactions/:id/verify429, 500, connection reset, and a 200 whose body says the payment failed
Every webhook deliveryarrives twice, arrives 10 minutes late, forged signature, 1 hour stale

Webhooks

Each delivery carries the secret hash in verif-hash and an HMAC of the body in flutterwave-signature, as Flutterwave sends them.

EventSent when
charge.completedA payment ends, paid or failed.
transfer.completedA transfer settles.
subscription.cancelledA subscription is cancelled.

Fidelity

Live. danwo fidelity flutterwave checks 3 scenarios against a test-mode account. 2 match; the bank list is skipped, since Flutterwave's test mode answers it with a provider error.

Not covered yet

  • Direct card charges (/v3/charges) and their encryption
  • Subaccounts and split payments
  • Bill payments, airtime and settlements reports

Next

On this page

Run it on every pull request

A GitHub check and one comment per pull request, with the flight records attached.

Set up CI