Date: 2026-07-14
Status: Gateway control plane and Harbor / paybond-payments implemented (Admin console / Kit pending)
Decision: Prefer Flutterwave as next multi-country African rail (BYO tenant credentials). Paystack remains an alternate; TrueLayer/Wise deferred until demand.
Not legal advice — commercial counsel should review partner contracts, KYB, and AML exposure before enabling live tenant destinations.
1. Executive decision
Why Flutterwave
- Flutterwave offers Virtual Accounts (VA) and Transfers semantics across multiple African corridors (NGN, GHS, etc.), matching Paybond's required fund → evidence → release/refund escrow pattern.
- BYO tenant credentials (per-tenant secret keys / webhook secret) keeps tenant isolation and legal posture identical to Adyen / TrueLayer designs.
V1 product slice
| Concern | V1 choice | Rationale |
|---|---|---|
| Provider surface | Flutterwave VA and Transfers API (v3) | Clean VA inbound → webhook fund signal → transfer payout / reversal |
| Geography | NGN (Nigeria) and GHS (Ghana) initial corridors | Known VA support; expand others in phase 2 |
| Escrow hold | Funds settle to merchant float managed by Flutterwave | Treat as operational escrow; Harbor enforces release/refund |
| Funding signal | charge.completed / VA credit webhook | Webhook-based fund gate (verify signature) |
| Terminal release | POST /v3/transfers (transfer/payout) | Transfer lifecycle confirmed by transfer webhooks |
| Webhook verify | HMAC-SHA256 (base64) via flutterwave-signature OR legacy verif-hash | Verified using tenant webhook secret; raw body HMAC; timing-safe compare |
| Auth UX | VA account display / instructions; no HPP required for VA flows | VA numbers presented to payer to instruct bank transfer |
2. Architecture
Reuse Paybond's three-layer settlement split (Gateway control plane, Harbor money movement, Admin/Kit). Gateway ingests Flutterwave webhooks, verifies HMAC, attributes tenant, and enqueues durable webhook jobs. Harbor executes VA provisioning (if needed), fund-gate logic, transfers on evidence pass, and terminal converge.
sequenceDiagram
autonumber
actor Admin as Tenant admin
participant GW as Gateway
participant PG as Postgres
participant HB as Harbor
participant FW as Flutterwave API
actor Buyer as Buyer bank transfer
actor App as Kit / application
Admin->>GW: POST /v1/admin/settlement/flutterwave/destination (upsert secrets and webhook token)
GW->>PG: Encrypt secret_key and webhook_secret, store destination snapshot
App->>GW: POST /harbor/intents (rail=flutterwave_virtual_account)
GW->>HB: Forward create (mTLS)
HB->>PG: Snapshot destination and payee beneficiary
HB->>FW: (optional) Create VA or allocate per-intent VA
HB-->>App: intent created / instructions (display VA account number)
Buyer->>FW: Bank transfer into VA
FW-->>GW: Webhook (VA credited) with `flutterwave-signature` or `verif-hash`
GW->>GW: Verify signature, bind tx_ref/meta -> tenant, enqueue `flutterwave_settlement`
GW->>HB: POST /internal/v1/flutterwave/events (tenant and webhook)
HB-->>HB: Fund gate -> funded
App->>GW: evidence and settlement decision
alt evidence passed
HB->>FW: POST /v3/transfers (transfer to payee bank)
FW-->>GW: transfer success / failed webhooks
HB-->>HB: released / manual_review
else evidence failed
HB->>FW: Refund / reversal per provider docs
FW-->>GW: refund/transfer_failed webhook
HB-->>HB: refunded
endLayer ownership
| Layer | Owns |
|---|---|
| Gateway | Destination CRUD and vault, webhook ingress and HMAC verify, flutterwave_settlement job enqueue, config gate |
| Harbor | VA provisioning (optional), fund gate, transfer/refund drivers, recovery/reconcile |
| Postgres (Gateway) | paybond_tenant_flutterwave_destination, webhook outbox, inventory |
Rail identifier (recommendation)
I propose the settlement rail string: flutterwave_virtual_account
Justification: this name clearly communicates the provider (Flutterwave) and the primary funding primitive (Virtual Account). It maps naturally to code/package names (flutterwave_*) and avoids ambiguity with pure transfer-only rails — the VA and Transfer shape is central to the escrow model.
Webhook job kind: flutterwave_settlement
Rust enum: SettlementRail::FlutterwaveVirtualAccount
Go const: settlementconfig.RailFlutterwaveVirtualAccount
3. Flutterwave product contract (v1)
Authoritative docs (examples):
- Webhooks / signature verification: https://developer.flutterwave.com/v3.0/docs/webhooks (HMAC-SHA256 /
flutterwave-signatureheader) - Virtual Accounts: https://developer.flutterwave.com/v3.0/docs/virtual-account (VA create / metadata / tx_ref)
- Transfers: https://flutterwaveinc.mintlify.app/api-reference/transfers/initiate-a-transfer (create/get transfer)
Environments
| Sandbox | Live | |
|---|---|---|
| API base | https://developersandbox-api.flutterwave.com (sandbox) | https://api.flutterwave.com (production) |
| Auth | Authorization: Bearer <SECRET_KEY> (v3 secret-key) — v4 OAuth2 client-credentials also available | v4 OAuth2 client-credentials and v3 secret-key bearer both supported |
| Webhook verify | flutterwave-signature (HMAC-SHA256 base64 over raw body) or legacy verif-hash header (legacy equality) | same |
Engineer access & credentials
- To start sandbox E2E: tenant sandbox
secret_key, webhook secret (set in Flutterwave dashboard), and optionally sandbox VA provisioning access. - Live requires tenant KYB / onboarding per Flutterwave commercial process (production ship blocker).
Auth model (backend-only)
- Destination stores
secret_key_encryptedandwebhook_secret_encrypted(write-only secrets) in vault. For tenants preferring OAuth, also support storingclient_id/client_secret(client-credentials) encrypted in the same vault. - Harbor obtains tenant credentials (v3 secret key or v4 client credentials token) for outbound API calls; Gateway uses webhook secret to verify inbound webhooks. Harbor should prefer short-lived tokens when using OAuth2, but v3 secret-key bearer (BYO secret key) is acceptable for V1 to reduce rollout friction and speed tenant onboarding — this keeps parity with other rails and reduces operator UX friction while KYB is completed. If higher security is required later, migrate tenants to OAuth2 (v4) client-credentials.
- Webhook verification MUST be performed on the raw request body; verification result dictates enqueue vs. reject.
Binding metadata (mandatory)
All provider-side transactions must include a stable reference for attribution:
| Field | Purpose |
|---|---|
tx_ref / meta_data | include paybond_intent_id and paybond_tenant_id at create time |
account_number / virtual_account_id | used by gateway inventory lookup to resolve tenant |
Gateway preflight rejects events that cannot be attributed to a tenant by token and payload binding.
Amount / currency rules
- V1: NGN / GHS per-destination currency; intent budget must match destination currency.
- Minor units: keep Harbor amounts in integer minor units; convert at API boundary with explicit scale checks.
4. Lifecycle state machine (high-level)
Pay-in (VA credit)
| Flutterwave signal | Harbor action | Intent state |
|---|---|---|
VA credited / charge.completed | Persist provider IDs, fund gate check | funded |
charge.failed | Mark funding failed | failure / retry semantics |
Terminal settlement
| Evidence | Harbor driver | Flutterwave API | Webhook finalize |
|---|---|---|---|
passed: true | Transfer to payee bank | POST /v3/transfers | transfer.success → released |
passed: false | Refund / reversal | provider refund/reversal API | transfer_failed / refund_* → refunded |
Async terminal rule: treat create-transfer/refund responses as request-accepted only; wait for webhook confirmation to mark terminal state.
5. SQL schema and migrations
Suggested migration filenames (mirror Adyen / Truelayer pattern):
go/gateway/sql/migrations/20260731_flutterwave_destination.sqlgo/gateway/sql/migrations/20260731_flutterwave_webhook_job_kind.sqlgo/gateway/sql/migrations/20260731_flutterwave_inactive_binding_retention.sqlgo/gateway/sql/migrations/20260731_flutterwave_destination_secret_purge.sql
Destination table sketch
CREATE TABLE paybond_tenant_flutterwave_destination ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id uuid NOT NULL REFERENCES paybond_tenant (id) ON DELETE CASCADE, secret_key_encrypted text, webhook_secret_encrypted text, virtual_account_template jsonb, -- optional template / static VA hints webhook_uri_token text NOT NULL, environment text NOT NULL CHECK (environment IN ('sandbox', 'live')), status text NOT NULL DEFAULT 'active' CHECK (status IN ('active','inactive')), deactivated_at timestamptz, secrets_purged_at timestamptz, label text NOT NULL DEFAULT '', currency text NOT NULL DEFAULT 'NGN' CHECK (currency ~ '^[A-Z]{3}$'), created_at timestamptz NOT NULL DEFAULT now(), updated_at timestamptz NOT NULL DEFAULT now(), UNIQUE (tenant_id, environment) ); CREATE UNIQUE INDEX paybond_tenant_flutterwave_destination_active_uidx ON paybond_tenant_flutterwave_destination (tenant_id) WHERE status = 'active'; ALTER TABLE paybond_tenant_flutterwave_destination ENABLE ROW LEVEL SECURITY;
Webhook job kind
Extend paybond_webhook_job.job_kind CHECK to include 'flutterwave_settlement'.
Idempotency key suggestion:
flutterwave:{environment}:{event_id}
Fallback: flutterwave:{environment}:{type}:{resource_id}:{status} when event_id missing.
Operational note: Gateway should persist and respect X-Idempotency-Key when supplied by Harbor or upstream callers. Additionally, before moving an intent to funded based solely on a webhook delivery, re-query the provider's transaction/transfer GET endpoint using the tenant credentials to confirm final settled state — this avoids races where the webhook arrives before the provider's read API reflects the completed settlement.
Secret purge worker: purge after 2160h inactive, retain client_id→tenant binding for 72h inactive-binding window (configurable).
6. Gateway implementation
Packages / files (parity with Adyen / Paystack)
go/gateway/internal/flutterwavewebhook/— HMAC verify, max-age, binding, processgo/gateway/internal/flutterwavesecretpurge/— secret purge workergo/gateway/internal/flutterwavemap/— account_number / virtual_account_id → tenant lookup helpersgo/gateway/internal/db/settlement_flutterwave_destination_custom.go— vault CRUDgo/gateway/internal/db/settlement_flutterwave_inventory_custom.go— inventory helpersgo/gateway/internal/httpserver/settlement_flutterwave_destination.go— Admin HTTP handlers (upsert, disconnect)go/gateway/internal/httpserver/flutterwave_webhook_integration_test.go— webhook integration tests
Admin HTTP APIs:
POST /v1/admin/settlement/flutterwave/destination— upsert secrets (write-only)GET /v1/admin/settlement/flutterwave/destination— redacted readPOST /v1/admin/settlement/flutterwave/destination/disconnect— deactivate
Public webhook ingress:
POST /webhooks/flutterwave— compatibilityPOST /webhooks/live/flutterwavePOST /webhooks/sandbox/flutterwave
Processing order (ship blockers):
- Read raw body; verify signature (see §7). Fail-closed on verification failure.
- Resolve destination via URL token AND payload
tx_ref/virtual_account_id/meta_databinding. Both must agree. - Max-age guard: use inactive-binding window (default 72h).
- Require binding metadata for money-state webhooks; unbound → manual review.
- Enqueue one
flutterwave_settlementjob and return 2xx quickly.
Gateway must never forward plaintext secrets to Harbor; Harbor looks up destination via tenant+snapshot.
Live-destination fail-closed gate — acceptance criteria (flutterwave_live_destination_gate)
Status: implemented (paid-plan and sandbox env gates). Enforced in go/gateway/internal/httpserver/settlement_flutterwave_destination.go mirroring Adyen (!isSandboxTenant && environment == Live → billing.ResolveCommercialSnapshot → commercial.PlanID.AllowsLiveSettlement(), else 403 forbidden). Readiness uses flutterwave_paid_plan_required. KYB/contracts remain operational controls (see items 3–4) until kyb_status / contracts_status schema columns land — do not ship fake confirmation flags:
- Sandbox tenants may only save
environment: "sandbox"Flutterwave destinations — rejectlivewith400 Bad Request(parity with the Adyen check on the same line). - Non-sandbox tenants attempting to save a
liveFlutterwave destination MUST resolve the tenant's commercial/billing snapshot and reject with403 ForbiddenunlessPlanID.AllowsLiveSettlement()— this is the same paid-plan gate already enforced for Adyen, Stripe, and Shopify live destinations (seesettlementRailStatusPlanUpgradeNeeded/*_paid_plan_requiredreason codes insettlement_config.go). Add aflutterwave_paid_plan_requiredreason code for readiness responses, consistent with the other rails. - In addition to the paid-plan gate, the live Flutterwave path MUST fail closed on the commercial KYB gate: the upsert handler (or the rail-enable step in settlement config) MUST refuse to activate a
liveFlutterwave destination unless an operator has recorded that KYB was approved for that tenant/corridor. Until a KYB-status field exists in the schema, this is an operational control (operator runbook and Console support review before flipping any tenant to a live Flutterwave destination) rather than a database-enforced one — do not ship a fake "kyb_confirmed" flag with no verification behind it. When the destination table lands, evaluate adding akyb_statuscolumn (unstarted/submitted/approved) gated the same wayAdyenEnvironmentLiveis gated today, so the enforcement becomes automatic instead of relying on operator diligence. - Also in addition to the paid-plan gate, the live path MUST fail closed on the commercial contracts gate (
flutterwave_commercial_contracts): the same upsert/enable path MUST refuse to activate aliveFlutterwave destination unless an operator has recorded that legal/compliance signed off on partner contract terms, refund/reversal SLAs, and liability/custodial posture — tracked under Implementation todos in this doc (flutterwave_commercial_contracts). Like the KYB gate above, this is an operational control until acontracts_statuscolumn exists in the schema; do not ship a fake "contracts_confirmed" flag with no sign-off behind it. When the destination table lands, evaluate adding acontracts_statuscolumn (unreviewed/signed_off) alongsidekyb_status, gated the same way. - Webhook ingress and secret purge must remain fail-closed regardless of KYB/contracts/plan status (§7, §5) — those gates protect against forged webhooks and stale secrets and are independent of the commercial gates above.
7. Webhook verification (engineer notes)
Flutterwave exposes two webhook verification patterns; Gateway must support both to remain compatible with tenant dashboard configurations:
- Legacy
verif-hashheader: a simple legacy dashboard-configured value. When present, validate by direct equality: compare the header value to the stored webhook secret/hash (string equality). This is maintained for backwards compatibility but is less robust than HMAC verification. - HMAC-SHA256 signature (recommended): Flutterwave computes HMAC-SHA256 over the raw request body using the configured webhook secret and returns a base64-encoded digest in the
flutterwave-signatureheader. Verify against the raw bytes (do not re-serialize JSON) and use timing-safe comparison.
Recommended verification flow:
- Read raw request body bytes as received; do not re-serialize or alter JSON or whitespace.
- Compute expected = base64(HMAC-SHA256(webhook_secret, raw_body)).
- If
flutterwave-signatureheader present, compare expected toflutterwave-signatureusing a timing-safe equality check. - If
flutterwave-signatureis missing butverif-hashis present, fall back to a strict equality check againstverif-hash. - On mismatch, return 401, log audit context (including headers and truncated payload), and do not process the webhook.
Example pseudocode (node-ish):
const expected = createHmac('sha256', WEBHOOK_SECRET).update(rawBody).digest('base64'); if (!timingSafeEqual(Buffer.from(expected,'utf8'), Buffer.from(signatureHeader,'utf8'))) { return res.status(401).end(); }
Webhook delivery & retries:
- Provider delivery timeouts are typically ~60s; Flutterwave may retry failed deliveries. In practice expect ~3 retries over a window of up to ~30 minutes if retries are enabled. Gateway handlers must therefore be idempotent and able to deduplicate delivered events (see §5 for idempotency key patterns). When possible, design the handler to acknowledge (2xx) only after the webhook is durably queued to the outbox to avoid loss on transient failures.
Citations: Flutterwave Webhooks docs (see §13) and provider sandbox notes.
8. Harbor / payments crate implementation
Suggested modules (parity with other rails):
crates/paybond-payments/src/flutterwave.rs— tenant-scoped client (token if needed), VA create, transfers, get transfer, refundscrates/harbor-intent-escrow/src/flutterwave_sync.rs— webhook ingest and fund/terminal convergecrates/harbor-intent-escrow/src/internal_flutterwave.rs— internal endpoints for Harbor ingest
Idempotency keys:
| Operation | Key example |
|---|---|
| Create VA / allocate | paybond:fw:va:{intent_id} |
| Transfer | paybond:fw:transfer:{intent_id}:{attempt} |
| Refund | paybond:fw:refund:{intent_id}:{attempt} |
Tenant isolation invariants, recovery, tests, and internal endpoint hardening follow the same rules applied to Adyen / TrueLayer (deny_unknown_fields, snapshot beneficiary, strict tenant-scoped loads).
9. Admin console and guides
Console form: client secret_key, webhook_secret, environment, currency, label, webhook URL copy button, disconnect/rotate buttons. Return webhook_url with embedded webhook_uri_token for Console configuration.
Files (parity):
apps/admin/app/api/settlement/flutterwave/destination/route.ts(+ disconnect)apps/admin/guides/configure-flutterwave-settlement.mdx— published (operator runbook; wired into guides loader and SEO)- Tests under
apps/admin/tests/lib/settlement-config-*.test.ts
Kit CLI parity: kit/ts/src/cli/commands/flutterwave.ts, kit/python/...
10. Multi-country notes (v1 vs later)
V1 corridors: NGN (Nigeria), GHS (Ghana) — both support Flutterwave VAs today. Expand in Phase 2 to KE (Kenya), UG (Uganda), ZA (South Africa) where Flutterwave (or local partners) support virtual accounts/transfers or where Flutterwave offers payout rails.
Cross-border transfers: Phase 2 — evaluate FX and cross-border transfer costs, or complement release leg with Wise / other FX partner for guaranteed delivered amounts.
11. Security, threat model, and phases
Severity-zero (must before production):
- Fail-closed webhook verification (HMAC/legacy) at Gateway.
- Durable outbox-before-2xx ingestion and idempotent job enqueue.
- Secret-box encryption for tenant secrets; purge only after retention and no pending jobs.
- Snapshot beneficiary at create; never accept runtime beneficiary changes for payout.
- Strict host allow-list for provider API egress in HTTP client (no tenant-supplied base URL).
Phases (summary)
- Phase 0 — Design & commercial: sandbox and live KYB (production ship blocker).
- Phase 1 — MVP: NGN and GHS VA fund → evidence → transfer/refund sandbox E2E.
- Phase 2 — Expand corridors and cross‑border / FX optimizations.
Decisions log (selected)
- Webhook verification: support BOTH
flutterwave-signature(HMAC-SHA256 over the raw body, base64) and the legacyverif-hashequality check. Use HMAC as the primary, fall back toverif-hashonly for tenants still on legacy dashboard config. (Source: https://developer.flutterwave.com/v3.0/docs/webhooks) - VA inbound (V1): limit initial rollout to NGN and GHS virtual accounts only; plan to expand other currencies/markets in Phase 2 as provider support and commercial terms allow. (Product research: c33cd960)
- Auth: support v3 secret-key bearer (Authorization: Bearer <SECRET_KEY>) and note v4 OAuth2 client-credentials as the future path. Recommendation: start with v3 BYO secret key for V1 to speed tenant onboarding and parity with other rails; migrate tenants to v4 OAuth2 when operationally feasible. (Docs: Flutterwave auth patterns; research c33cd960)
- Idempotency: accept and persist
X-Idempotency-Key; use provider eventevent_id-based keys and dedupe. Before gating funds asfunded, re-query the provider transaction/transfer API to confirm settled state (defensive read-after-webhook). - Webhook retries & timeouts: expect provider delivery timeout ~60s and retry behavior (commonly ~3 retries over a ~30 minute window when enabled); design handlers to be idempotent and durable (outbox-before-2xx). (Source: provider delivery semantics; see webhooks docs)
- Sandbox base URL: use
https://developersandbox-api.flutterwave.comfor sandbox E2E testing; production useshttps://api.flutterwave.com. Don't assume sandbox == production host. - BYO vault model: store tenant
secret_keyorclient_id/client_secret(for OAuth) pluswebhook_secret(the raw secret or hash used forverif-hash) in the vault. Treat these as write-only, purge per retention policy. - Rail identifier: keep
flutterwave_virtual_account(do not churn). This name conveys the VA+transfer shape and aligns with existing recommendations. - Reconciliations / contradictions: this document reconciles prior briefings — see research snapshot c33cd960 and the Flutterwave webhooks docs for verification specifics; where prior notes conflicted about sandbox hosts or legacy headers, prefer the authoritative developer docs and the sandbox host listed above.
12. File checklist (implementation inventory)
Gateway
go/gateway/sql/migrations/20260731_flutterwave_destination.sqlgo/gateway/sql/migrations/20260731_flutterwave_webhook_job_kind.sqlgo/gateway/sql/migrations/20260731_flutterwave_inactive_binding_retention.sqlgo/gateway/sql/migrations/20260731_flutterwave_destination_secret_purge.sqlgo/gateway/internal/flutterwavewebhook/*go/gateway/internal/flutterwavemap/*go/gateway/internal/db/settlement_flutterwave_destination_custom.goand inventory helpersgo/gateway/internal/flutterwavesecretpurge/*go/gateway/internal/httpserver/settlement_flutterwave_destination*.goand webhook/purge integration tests
Harbor / payments
crates/paybond-payments/src/flutterwave.rs— VA create, transfer, get transfer/transaction, refund; host allow via sandbox/live/custom bases; secret redaction in API errorscrates/harbor-intent-escrow/src/flutterwave_sync.rs— ingest, fund-gate, terminal converge, pending-transfer classify/reconcilecrates/harbor-intent-escrow/src/internal_flutterwave.rs—POST /internal/v1/flutterwave/events({tenant,event}) and reconcile; mTLS and forward bearercrates/paybond-typesrail enumFlutterwaveVirtualAccount(flutterwave_virtual_account)- Settlement runtime and recovery pending-transfer reconcile/stale flag; Harbor API tests for fund / transfer / refund paths
Admin / Kit / docs
apps/admin/guides/configure-flutterwave-settlement.mdx(operator runbook published and wired)- Admin routes & tests (
apps/admin/app/api/settlement/flutterwave/*) - Kit CLI parity commands
13. References
- Flutterwave Webhooks: https://developer.flutterwave.com/v3.0/docs/webhooks
- Flutterwave Virtual Accounts: https://developer.flutterwave.com/v3.0/docs/virtual-account
- Flutterwave Transfers: https://flutterwaveinc.mintlify.app/api-reference/transfers/initiate-a-transfer
Implementation TODOs
Grouped, actionable checklist to start implementation (mirror file/name parity using flutterwave_* prefixes).
- Commercial / access prerequisites
Status legend: [x] = deliverable complete in-repo; [ ] = external commercial action (owner and status tracked below). Documentation deliverables are marked complete when shipped; true commercial actions (obtaining real credentials, filing KYB, legal sign-off) are not marked complete here until the responsible owner executes them outside the repo.
- Obtain sandbox credentials for NGN and GHS tenants (tenant sandbox
secret_key, webhook secret) — label asflutterwave_commercial_sandbox_prep- Status: Documented, ready to execute. Tenant-admin steps to collect sandbox
secret_keyand webhook secret per market (NGN, GHS) and register the Gateway webhook URL are in the public guideapps/admin/guides/configure-flutterwave-settlement.mdx. Actually provisioning real Flutterwave sandbox credentials is an external step for the onboarding owner — no credentials are fabricated in this repo. - Owner / next step: settlement onboarding owner creates a Flutterwave sandbox account per corridor and follows the public guide.
- Status: Documented, ready to execute. Tenant-admin steps to collect sandbox
- Start commercial KYB for live tenant onboarding (production ship blocker) —
flutterwave_commercial_kyb-
Status: Blocked on commercial (external). KYB itself has not been started with Flutterwave for any live tenant. This remains an internal production ship blocker (tracked here only — not exposed on the public configure guide). Live secret keys are only issued after Flutterwave KYB.
-
Owner / next step: commercial/BD owner initiates Flutterwave KYB for the first live tenant (confirm current NGN/GHS document requirements in the Flutterwave dashboard before filing), then records the case id / submission date here (not fabricated — leave blank until Flutterwave issues one).
-
Tracking table (fill in only with real values once the case exists — do not fabricate):
Field Value KYB case reference (not yet filed) Submission date (not yet filed) Corridor(s) (NGN and/or GHS — TBD) Approval date (pending) Live merchant/account id (pending) Conditions / limits imposed (pending)
-
- Confirm contract terms, refund/reversal SLAs, and liability with legal/compliance —
flutterwave_commercial_contracts-
Status: Blocked on commercial (external). Not confirmed. Internal production ship blocker (tracked here only — not exposed on the public configure guide).
-
Owner / next step: legal/compliance reviews partner contract, refund/reversal SLAs for the VA-credit and Transfer model, liability allocation, and custodial/AML posture before any live destination is enabled, then records reviewer, date, contract version/ref, and sign-off status in the tracking table below (not fabricated — leave blank until legal/compliance actually signs off).
-
Tracking table (fill in only with real values once legal/compliance has reviewed — do not fabricate):
Field Value Reviewer (legal/compliance) (pending) Review date (pending) Contract version / reference (pending) Refund/reversal SLA summary accepted (pending) Open conditions / follow-ups (pending) Sign-off status (not started)
-
- Publish tenant onboarding runbook for operator (steps to collect secrets, set webhook URL) —
apps/admin/guides/configure-flutterwave-settlement.mdx- Status: Complete. Public guide published (BYO self-serve parity with Adyen: Console write-only secrets, dual webhook verification, per-market NGN/GHS setup, Gateway webhook URL registration, async terminal settlement, ops guidance). Commercial KYB (
flutterwave_commercial_kyb) and contracts (flutterwave_commercial_contracts) remain internal ops gates tracked in this doc only — not on the public MDX guide.
- Status: Complete. Public guide published (BYO self-serve parity with Adyen: Console write-only secrets, dual webhook verification, per-market NGN/GHS setup, Gateway webhook URL registration, async terminal settlement, ops guidance). Commercial KYB (
- SQL / migrations
- Add destination table migration
go/gateway/sql/migrations/20260731_flutterwave_destination.sql - Add webhook job kind migration
go/gateway/sql/migrations/20260731_flutterwave_webhook_job_kind.sql - Add inactive-binding retention migration
go/gateway/sql/migrations/20260731_flutterwave_inactive_binding_retention.sql - Add destination secret purge migration
go/gateway/sql/migrations/20260731_flutterwave_destination_secret_purge.sql - Add RLS and active-unique index guards; include idempotency-key format docs in migration comments —
flutterwave_sql_migrations
- Gateway (destination, webhook, purge, settlementconfig, tests)
- Implement webhook ingress and HMAC verify package
go/gateway/internal/flutterwavewebhook/*(raw-body HMAC-SHA256 base64) - Implement gateway mapping helpers
go/gateway/internal/flutterwavemap/*(account_number / virtual_account_id -> tenant) - Implement destination vault CRUD
go/gateway/internal/db/settlement_flutterwave_destination_custom.go - Implement secret purge worker
go/gateway/internal/flutterwavesecretpurge/*and integration test - Add Admin HTTP handlers
go/gateway/internal/httpserver/settlement_flutterwave_destination*.go(upsert, disconnect, redacted read) - Add public webhook routes
POST /webhooks/{sandbox,live}/flutterwaveand ensure durable enqueue-before-2xx - Add webhook integration tests
go/gateway/internal/httpserver/flutterwave_webhook_integration_test.go(idempotency and max-age) - Wire
settlementconfigenum/constsettlementconfig.RailFlutterwaveVirtualAccountand gateway feature-flag - Enforce the live-destination fail-closed gate —
flutterwave_live_destination_gate(paid-plan and sandbox-tenant env gates shipped; KYB/contracts remain operational until schema columns land)
- Harbor / paybond-payments
- Implement tenant-scoped client in
crates/paybond-payments/src/flutterwave.rs(VA create, transfer, get, refund) - Implement Harbor webhook sync
crates/harbor-intent-escrow/src/flutterwave_sync.rs(ingest, fund-gate, terminal converge) - Implement internal Harbor endpoints
crates/harbor-intent-escrow/src/internal_flutterwave.rsfor internal event ingestion ({tenant, event}Gateway contract) - Add idempotency keys and retry/recovery semantics consistent with
paybond:key patterns (paybond:fw:va|transfer|refund:{intent_id}…; Gateway outboxflutterwave:{environment}:{event_id}) - Add unit & integration tests: VA lifecycle (payments), transfer success/failure and refund paths (Harbor API and payments wiremock)
- Admin console and guides
- Add Admin route file
apps/admin/app/api/settlement/flutterwave/destination/route.ts(+ disconnect) - Add admin guide
apps/admin/guides/configure-flutterwave-settlement.mdx(step-by-step: sandbox, webhook copy, rotate/disconnect) - Add console form UX for secrets, webhook URL copy, currency selection and label
- Add admin tests
apps/admin/tests/lib/settlement-config-*.test.tsto assert redaction and UX flows
- Kit CLI and cli-parity
- Add Kit CLI command
kit/ts/src/cli/commands/flutterwave.ts(create/destroy destination, show webhook URL) - Update
kit/cli-parity/commands.json&kit/cli-parity/contract.jsonentries for Flutterwave parity - Add python SDK parity
kit/python/...examples and tests
- Security / hardening / env-var classification
- Implement fail-closed webhook verification and timing-safe compare at Gateway (HMAC primary, legacy fallback)
- Add secret-box encryption policy & secret purge policy enforcement (purge only when no pending jobs)
- Add egress allow-list for provider API hosts and document firewall/DNS expectations
- Add
docs/security/flutterwave-hardening.mdand env-var classification for secrets (markFLUTTERWAVE_SECRET_KEY,FLUTTERWAVE_CLIENT_IDetc.) - Add threat-model test cases (replay, forged-webhook, late-binding) to CI
- Docs / OpenAPI
- Add Admin guide MDX (
apps/admin/guides/configure-flutterwave-settlement.mdx) and include example webhook header verification steps - Update
docs/api/gateway-openapi.yamlto include new admin endpoints and webhook ingress shapes (flutterwave_*tags) - Add public docs references and example cURL snippets for webhook signing verification
- Verification exit criteria (sandbox E2E and live gates)
- Sandbox E2E test: tenant sandbox secret and webhook secret → VA create (optional) → VA credited webhook → Harbor fund → evidence → transfer (sandbox) → transfer webhook → released
- Harbor unit/API coverage: payments wiremock VA/transfer/refund and Harbor internal fund/transfer/refund ingest and simulated terminal drivers
- Security exit (Gateway slice): fail-closed verification and secret purge policy tested; env-var classification entries added for Flutterwave purge/retention
- Live-destination gate exit (Gateway slice): paid-plan and sandbox-tenant gates implemented (
flutterwave_live_destination_gate); unit coverage forflutterwave_paid_plan_required; KYB/contracts remain operational - Commercial exit: KYB initiated for at least one live tenant (
flutterwave_commercial_kyb— process kit published, case not yet filed) and legal sign-off on contracts (flutterwave_commercial_contracts) - Operator runbook / rollback documented and tested
Notes:
- Keep
flutterwave_*filename and migration prefixes for parity with other rails. - V1 acceptance scope: NGN and GHS VA fund → evidence → transfer/refund.
Changelog
- 2026-07-14 — created flutterwave-settlement-implementation.md (initial design, webhook & VA and transfer v1).
- 2026-07-14 — enriched with Flutterwave research (c33cd960): added dual-header webhook verification details, sandbox base URL, v1 auth recommendation (v3 secret-key BYO), idempotency guidance, webhook retry semantics, VA v1 scope (NGN/GHS), and BYO vault guidance.
- 2026-07-14 —
flutterwave_commercial_kyb/flutterwave_commercial_contracts: operational gates and tracking tables live in this implementation doc only. Publicconfigure-flutterwave-settlement.mdxintentionally omits "Live onboarding gates" / commercial KYB+contracts copy (BYO self-serve parity with Adyen's public configure guide). Paid-plan live gate remains documented publicly where Adyen does (CLI readiness). - 2026-07-15 — Scrubbed public Flutterwave "Live onboarding gates" content from admin guides and
llms.ts; retargeted commercial KYB/contracts references here. - 2026-07-14 —
flutterwave_sql_migrationscompleted: added the destination, webhook-job-kind, inactive-binding-retention, and destination-secret-purge migrations; synchronizedschema.sql; and documented Flutterwave webhook idempotency keys in the webhook-job migration. - 2026-07-14 — Gateway workstream completed:
flutterwavewebhook(HMAC and legacy verif-hash, max-age, binding, Harbor forward),flutterwavemap, destination vault and inventory CRUD,flutterwavesecretpurgeand integration tests, admin destination upsert/get/disconnect withflutterwave_live_destination_gatepaid-plan fail-closed, public webhook routes with durableflutterwave_settlementenqueue-before-2xx,settlementconfig.RailFlutterwaveVirtualAccountand rail readiness (flutterwave_paid_plan_required). Harbor / Admin console / Kit remain open. - 2026-07-14 — Harbor / paybond-payments workstream completed: tenant-scoped Flutterwave client (VA/transfer/get/refund),
flutterwave_syncfund-gate and terminal converge, internal{tenant,event}ingest and reconcile, settlement/recovery pending-transfer paths, rail enumflutterwave_virtual_account, and focused Rust tests. Admin console destination UI and Kit CLI remain open.
Recommended rail string: flutterwave_virtual_account
Recommended webhook job kind: flutterwave_settlement