paybondpaybond
Sign in

Flutterwave settlement rail — implementation design

Engineer-ready design for Paybond’s Nigeria and multi-country Africa corridor using Flutterwave Virtual Accounts (VA) and Transfers. Control plane, webhook verify, Harbor driver, admin, migrations, and security.

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

ConcernV1 choiceRationale
Provider surfaceFlutterwave VA and Transfers API (v3)Clean VA inbound → webhook fund signal → transfer payout / reversal
GeographyNGN (Nigeria) and GHS (Ghana) initial corridorsKnown VA support; expand others in phase 2
Escrow holdFunds settle to merchant float managed by FlutterwaveTreat as operational escrow; Harbor enforces release/refund
Funding signalcharge.completed / VA credit webhookWebhook-based fund gate (verify signature)
Terminal releasePOST /v3/transfers (transfer/payout)Transfer lifecycle confirmed by transfer webhooks
Webhook verifyHMAC-SHA256 (base64) via flutterwave-signature OR legacy verif-hashVerified using tenant webhook secret; raw body HMAC; timing-safe compare
Auth UXVA account display / instructions; no HPP required for VA flowsVA 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
  end

Layer ownership

LayerOwns
GatewayDestination CRUD and vault, webhook ingress and HMAC verify, flutterwave_settlement job enqueue, config gate
HarborVA 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):

Environments

SandboxLive
API basehttps://developersandbox-api.flutterwave.com (sandbox)https://api.flutterwave.com (production)
AuthAuthorization: Bearer <SECRET_KEY> (v3 secret-key) — v4 OAuth2 client-credentials also availablev4 OAuth2 client-credentials and v3 secret-key bearer both supported
Webhook verifyflutterwave-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)

  1. Destination stores secret_key_encrypted and webhook_secret_encrypted (write-only secrets) in vault. For tenants preferring OAuth, also support storing client_id / client_secret (client-credentials) encrypted in the same vault.
  2. 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.
  3. 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:

FieldPurpose
tx_ref / meta_datainclude paybond_intent_id and paybond_tenant_id at create time
account_number / virtual_account_idused 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 signalHarbor actionIntent state
VA credited / charge.completedPersist provider IDs, fund gate checkfunded
charge.failedMark funding failedfailure / retry semantics

Terminal settlement

EvidenceHarbor driverFlutterwave APIWebhook finalize
passed: trueTransfer to payee bankPOST /v3/transferstransfer.successreleased
passed: falseRefund / reversalprovider refund/reversal APItransfer_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.sql
  • go/gateway/sql/migrations/20260731_flutterwave_webhook_job_kind.sql
  • go/gateway/sql/migrations/20260731_flutterwave_inactive_binding_retention.sql
  • go/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, process
  • go/gateway/internal/flutterwavesecretpurge/ — secret purge worker
  • go/gateway/internal/flutterwavemap/ — account_number / virtual_account_id → tenant lookup helpers
  • go/gateway/internal/db/settlement_flutterwave_destination_custom.go — vault CRUD
  • go/gateway/internal/db/settlement_flutterwave_inventory_custom.go — inventory helpers
  • go/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 read
  • POST /v1/admin/settlement/flutterwave/destination/disconnect — deactivate

Public webhook ingress:

  • POST /webhooks/flutterwave — compatibility
  • POST /webhooks/live/flutterwave
  • POST /webhooks/sandbox/flutterwave

Processing order (ship blockers):

  1. Read raw body; verify signature (see §7). Fail-closed on verification failure.
  2. Resolve destination via URL token AND payload tx_ref / virtual_account_id / meta_data binding. Both must agree.
  3. Max-age guard: use inactive-binding window (default 72h).
  4. Require binding metadata for money-state webhooks; unbound → manual review.
  5. Enqueue one flutterwave_settlement job 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 == Livebilling.ResolveCommercialSnapshotcommercial.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:

  1. Sandbox tenants may only save environment: "sandbox" Flutterwave destinations — reject live with 400 Bad Request (parity with the Adyen check on the same line).
  2. Non-sandbox tenants attempting to save a live Flutterwave destination MUST resolve the tenant's commercial/billing snapshot and reject with 403 Forbidden unless PlanID.AllowsLiveSettlement() — this is the same paid-plan gate already enforced for Adyen, Stripe, and Shopify live destinations (see settlementRailStatusPlanUpgradeNeeded / *_paid_plan_required reason codes in settlement_config.go). Add a flutterwave_paid_plan_required reason code for readiness responses, consistent with the other rails.
  3. 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 live Flutterwave 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 a kyb_status column (unstarted / submitted / approved) gated the same way AdyenEnvironmentLive is gated today, so the enforcement becomes automatic instead of relying on operator diligence.
  4. 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 a live Flutterwave 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 a contracts_status column 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 a contracts_status column (unreviewed / signed_off) alongside kyb_status, gated the same way.
  5. 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-hash header: 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-signature header. Verify against the raw bytes (do not re-serialize JSON) and use timing-safe comparison.

Recommended verification flow:

  1. Read raw request body bytes as received; do not re-serialize or alter JSON or whitespace.
  2. Compute expected = base64(HMAC-SHA256(webhook_secret, raw_body)).
  3. If flutterwave-signature header present, compare expected to flutterwave-signature using a timing-safe equality check.
  4. If flutterwave-signature is missing but verif-hash is present, fall back to a strict equality check against verif-hash.
  5. 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, refunds
  • crates/harbor-intent-escrow/src/flutterwave_sync.rs — webhook ingest and fund/terminal converge
  • crates/harbor-intent-escrow/src/internal_flutterwave.rs — internal endpoints for Harbor ingest

Idempotency keys:

OperationKey example
Create VA / allocatepaybond:fw:va:{intent_id}
Transferpaybond:fw:transfer:{intent_id}:{attempt}
Refundpaybond: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.mdxpublished (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)

  1. Webhook verification: support BOTH flutterwave-signature (HMAC-SHA256 over the raw body, base64) and the legacy verif-hash equality check. Use HMAC as the primary, fall back to verif-hash only for tenants still on legacy dashboard config. (Source: https://developer.flutterwave.com/v3.0/docs/webhooks)
  2. 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)
  3. 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)
  4. Idempotency: accept and persist X-Idempotency-Key; use provider event event_id-based keys and dedupe. Before gating funds as funded, re-query the provider transaction/transfer API to confirm settled state (defensive read-after-webhook).
  5. 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)
  6. Sandbox base URL: use https://developersandbox-api.flutterwave.com for sandbox E2E testing; production uses https://api.flutterwave.com. Don't assume sandbox == production host.
  7. BYO vault model: store tenant secret_key or client_id/client_secret (for OAuth) plus webhook_secret (the raw secret or hash used for verif-hash) in the vault. Treat these as write-only, purge per retention policy.
  8. Rail identifier: keep flutterwave_virtual_account (do not churn). This name conveys the VA+transfer shape and aligns with existing recommendations.
  9. 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.sql
  • go/gateway/sql/migrations/20260731_flutterwave_webhook_job_kind.sql
  • go/gateway/sql/migrations/20260731_flutterwave_inactive_binding_retention.sql
  • go/gateway/sql/migrations/20260731_flutterwave_destination_secret_purge.sql
  • go/gateway/internal/flutterwavewebhook/*
  • go/gateway/internal/flutterwavemap/*
  • go/gateway/internal/db/settlement_flutterwave_destination_custom.go and inventory helpers
  • go/gateway/internal/flutterwavesecretpurge/*
  • go/gateway/internal/httpserver/settlement_flutterwave_destination*.go and 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 errors
  • crates/harbor-intent-escrow/src/flutterwave_sync.rs — ingest, fund-gate, terminal converge, pending-transfer classify/reconcile
  • crates/harbor-intent-escrow/src/internal_flutterwave.rsPOST /internal/v1/flutterwave/events ({tenant,event}) and reconcile; mTLS and forward bearer
  • crates/paybond-types rail enum FlutterwaveVirtualAccount (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

Implementation TODOs

Grouped, actionable checklist to start implementation (mirror file/name parity using flutterwave_* prefixes).

  1. 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 as flutterwave_commercial_sandbox_prep
    • Status: Documented, ready to execute. Tenant-admin steps to collect sandbox secret_key and webhook secret per market (NGN, GHS) and register the Gateway webhook URL are in the public guide apps/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.
  • 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):

      FieldValue
      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):

      FieldValue
      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.
  1. 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
  1. 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}/flutterwave and ensure durable enqueue-before-2xx
  • Add webhook integration tests go/gateway/internal/httpserver/flutterwave_webhook_integration_test.go (idempotency and max-age)
  • Wire settlementconfig enum/const settlementconfig.RailFlutterwaveVirtualAccount and 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)
  1. 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.rs for 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 outbox flutterwave:{environment}:{event_id})
  • Add unit & integration tests: VA lifecycle (payments), transfer success/failure and refund paths (Harbor API and payments wiremock)
  1. 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.ts to assert redaction and UX flows
  1. 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.json entries for Flutterwave parity
  • Add python SDK parity kit/python/... examples and tests
  1. 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.md and env-var classification for secrets (mark FLUTTERWAVE_SECRET_KEY, FLUTTERWAVE_CLIENT_ID etc.)
  • Add threat-model test cases (replay, forged-webhook, late-binding) to CI
  1. 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.yaml to include new admin endpoints and webhook ingress shapes (flutterwave_* tags)
  • Add public docs references and example cURL snippets for webhook signing verification
  1. 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 for flutterwave_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. Public configure-flutterwave-settlement.mdx intentionally 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_migrations completed: added the destination, webhook-job-kind, inactive-binding-retention, and destination-secret-purge migrations; synchronized schema.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, flutterwavesecretpurge and integration tests, admin destination upsert/get/disconnect with flutterwave_live_destination_gate paid-plan fail-closed, public webhook routes with durable flutterwave_settlement enqueue-before-2xx, settlementconfig.RailFlutterwaveVirtualAccount and 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_sync fund-gate and terminal converge, internal {tenant,event} ingest and reconcile, settlement/recovery pending-transfer paths, rail enum flutterwave_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