||

Unified layer

Surface B — normalized, provider-agnostic operations with minor-unit Money, plus the capability matrix that tells you what each provider actually supports.

The unified layer (Surface B) gives a smaller set of high-traffic operations identical signatures across the providers that support them. Write your integration once against defaultProvider; pass { provider } per call to target another configured one.

const payweave = createPayweave({
  paystack: { secretKey: process.env.PAYSTACK_SECRET_KEY! },
})

const checkout = await payweave.checkout.create({
  amount: { value: 500_000, currency: "NGN" }, // ALWAYS minor units
  customer: { email: "ada@example.com" },
  reference: "order_8123",
  redirectUrl: "https://app.example.com/pay/callback",
})
// → { checkoutUrl, reference, providerRef, raw }

const result = await payweave.verify({ reference: "order_8123" })
// → { status, amount: { value, currency }, customer, paidAt, channel, raw }

await payweave.refunds.create({ reference })
await payweave.banks.list({ country: "NG" })
await payweave.banks.resolveAccount({ accountNumber: "0123456789", bankCode: "058" })

Amounts are always minor units

Every amount is { value: number, currency: string } where value is in integer minor units. The adapters convert:

  • Paystack already uses kobo → passed through.
  • Flutterwave v3 uses major units → divided/multiplied by the currency's subunit factor (100 for NGN/GHS/KES/ZAR/USD; unknown currencies default to 100 with a logged warning).
  • Stripe already uses cents-style minor units → passed through, where implemented (see capabilities).
// amount: { value: 500000, currency: "NGN" }
// Paystack outgoing body     → amount: 500000
// Flutterwave outgoing body  → amount: 5000

References

Your reference maps to Paystack reference and Flutterwave tx_ref. Omit it and the SDK generates one (pwv_<ulid>), returned on the result.

Status normalization

verify() returns a single normalized status, mapped from each provider's own strings:

Unified statusMeaning
successpayment completed
failedpayment failed / declined
pendingnot yet resolved (also the fallback for unknown statuses)
abandonedcustomer never completed
reversedfunds returned

Unknown statuses map to pending plus a logged warning — the SDK never throws on an unrecognized status.

Capabilities

Not every provider supports every unified operation, and the SDK never silently drops a call onto the wrong endpoint — it gates each one against a capability matrix and throws before making a request.

OperationStripePaystackFlutterwave
checkout.create
verify
refunds.create
transfers.create
banks.list
banks.resolveAccount

Stripe doesn't map transfers.create or banks.* — Stripe Connect payouts are semantically different from Paystack/Flutterwave bank transfers, and bank-account lookup is Nigeria-specific.

const payweave = createPayweave({
  stripe: { secretKey: process.env.STRIPE_SECRET_KEY! },
})

await payweave.transfers.create({ /* ... */ })
// throws PayweaveValidationError: "transfers are not supported on stripe" — zero HTTP calls made

Introspect the matrix at runtime instead of hardcoding it:

payweave.capabilities("stripe")
// → { "checkout.create": { supported: true }, verify: { supported: true },
//     "refunds.create": { supported: true },
//     "transfers.create": { supported: false, reason: "transfers are not supported on stripe" },
//     "banks.list": { supported: false, reason: "banks.list is not supported on stripe" },
//     "banks.resolveAccount": { supported: false, reason: "banks.resolveAccount is not supported on stripe" } }

payweave.capabilities() // the full matrix, keyed by every configured provider

checkout.create, verify, and refunds.create show supported: true for Stripe in the matrix above, but the runtime mapping hasn't shipped yet — calling them today on a Stripe-only client throws a plain "not implemented yet" error, not a capability error. Use Surface A (payweave.stripe.*) for Stripe until that lands.

The raw escape hatch

Every unified response includes the untouched provider response on raw, so the abstraction never hides data from you.

Did you like the content?