πŸͺ

Supplier flow

BRD steps 1, 3, 4, 8, 10. The supplier is funded after delivery is confirmed and is structurally removed from the repayment path.

One-time onboarding β€” persona pick β†’ signup β†’ settlement

The same Terracore WhatsApp number serves all four personas. New users hit a one-tap persona picker that routes them to the right onboarding path. From there it's two WhatsApp Flow forms β€” signup (identity + business, BVN + CAC verified live) and settlement bank (NIBSS-verified). All native, no app, no webview. Total time-to-value: ~2 minutes from first message to ready-to-fund.

O1
First touch β€” persona picker
BRD Β§1 β†’ PERSONA_OFFERED
9:36

Why this is the very first screen

The same Terracore WhatsApp number serves suppliers and financing partners. (Buyers β€” manufacturers like Bokku β€” go through email and never appear in this picker. The procurement-manager WhatsApp nudge in the buyer flow is invoice-scoped and reaches a named individual; it is not a persona who chats us first.) We can't infer persona from a phone number for a brand-new chat user. One tap routes them to a tailored welcome.

Why value-led button copy, not role-led

"I sell β€” fund my invoices" beats "I'm a supplier". Users don't always know our internal taxonomy. We label by what they want, not what we call them. The role is inferred from the tap.

Three branches, each with a tailored next step

  • πŸ“„ I sell β†’ O2 supplier welcome β†’ signup Flow (the path shown in this strip)
  • 🏦 I have capital β†’ routes to the Financing Partner registration flow (FO1 β†’ FO2). Self-service WhatsApp Flow captures registered business name, CAC RC, authorised representative, designated settlement account (NIBSS-verified), and concentration limits. See Financing Partner flow.
  • ❓ Something else β†’ secondary picker: "I'm a procurement manager confirming a delivery" (continues a buyer-flow Phase-1 thread already in progress) Β· "I'm chasing an invoice we paid" (buyer-finance support β€” corporate AP teams normally email instead) Β· "I want to talk to support" (human handoff)

Speed budget ≀ 800ms

Pre-rendered template. Tap β†’ next screen renders inside ~200ms.

What we never do

We never ask "Are you a supplier?" as a yes/no. Forced binary makes users second-guess. Three concrete options with verbs reduce abandonment versus a yes/no by ~30% in pilots.

O2
Supplier welcome β€” signup CTA
BRD Β§1 β†’ SIGNUP_OFFERED
9:36

Why a separate screen instead of bundling with O1

O1 is generic to all personas. O2 is supplier-specific copy: "you don't chase them" is the load-bearing value prop for a supplier and would feel wrong/confusing if shown to a financier. Splitting also lets us tune copy per persona without affecting the picker.

What the financing-partner branch looks like instead

If the user tapped "🏦 I have capital β€” earn on it", this same slot renders: "Welcome β€” financing partners are onboarded by our partnerships team. Share a few details and we'll call within 1 business hour." + a Flow CTA called "Tell us about your fund" (capturing org name, contact, target exposure, target return). No self-service KYC; ops handle it.

Why we name "BVN + CAC" again

The user might have skimmed O1. Showing the requirements again right before the Flow CTA prevents the in-flow abandonment: opening the form and quitting because they don't have a doc.

Speed budget ≀ 600ms

Persona tap β†’ tailored welcome arrives. Pre-rendered template; one variable substitution (the persona).

O3
Signup Flow β€” identity + business
BRD Β§1 β†’ KYC_VERIFIED

What this is

A native WhatsApp Flow with two chained screens (rendered here as one for clarity): Screen 1 captures identity (name, email, BVN); Screen 2 captures business (CAC RC, with auto-filled business name + type). The user sees a "Next" button between them. Multi-screen Flows are first-class in Meta's spec.

Live verification β€” two parallel data-source hooks

BVN field on blur β†’ NIBSS BVN lookup; we match the resolved name against the typed first/last name (Levenshtein-normalised). CAC field on blur β†’ CAC API; business name + type auto-populate read-only. If either fails, the "Create account" CTA stays disabled with a helper line under the failing field β€” the user cannot proceed with bad KYC.

Speed budget ≀ 1.5s per data-source call

BVN lookup p95: ~900ms via NIBSS; CAC lookup p95: ~1.2s via CAC public registry. Both run client-side via Flow data-source endpoints; we proxy and cache.

What the submission produces

One signed JSON: name, email, WhatsApp ID, BVN (hashed at rest, never logged in plaintext), BVN match score, CAC RC, fetched business name + type, T&C version, timestamp, device fingerprint. This is the KYC consent record.

What we never do here

We never let the user edit the auto-filled business name or business type β€” both come from CAC, not the user. Same anti-tamper principle as the settlement-account name field.

O4
KYC verified β€” settlement CTA
BRD Β§1 β†’ FLOW_OFFERED
9:39

This is a WhatsApp Flow CTA

A native Meta primitive β€” not a chat reply button, not a list message, not a webview. Tapping it opens a structured form sheet (next screen) rendered by WhatsApp itself.

Why Flow over chat for this step

Bank linking is structured data β€” bank code + 10-digit account number + confirmation. Chat-based capture means parsing free text, handling typos, retries, and redoing partial submissions. A Flow form captures it all in one submit, with native validation, autofill, and a single audit-able payload.

Speed budget ≀ 200ms

The CTA bubble is part of a pre-rendered template. Tap β†’ modal opens locally on the device with no network round-trip.

O5
Settlement Flow β€” bank account
BRD Β§1 β†’ BANK_LINKED

What this is

A native WhatsApp Flow modal rendered by Meta's client. The form definition is JSON; we host it on Meta's Flow endpoint. The user never leaves WhatsApp; no webview, no browser, no redirect.

Live NIBSS lookup as the user types

The "Account number" field has a data-source hook on blur (after 10 digits): we call NIBSS NameEnquiry, fuzzy-match the result against the BVN name, and if it passes, the "Account name" field auto-populates green and the "Link this account" CTA enables. If the match fails, the CTA stays disabled and helper text reads "This account is registered to a different name. Try another account."

Speed budget ≀ 1.2s NIBSS round-trip

NIBSS NameEnquiry p95 in production is ~700ms. The Flow's data-source endpoint is ours; we proxy and cache (5-min TTL on the bank-code+account tuple) to keep below budget.

What the form submission produces

One signed JSON payload with: bank code, account number, NIBSS-resolved name, BVN match score, T&C version, timestamp, device fingerprint, and the user's WhatsApp ID. This is the consent record β€” no separate "Yes, link it" tap chain to log.

Where else this same Flow primitive is used in the rail

  • Financier limits setup β€” exposure cap, max-outstanding, max-per-invoice, alert preferences. Same Flow pattern: one form, validated, submitted.
  • Buyer "claim unreferenced payment" β€” when a buyer pays without our reference, the recovery flow (E2) opens a Flow with: invoice number, amount paid, paying bank, payer reference. Beats chat for structured claim data.
  • Buyer procurement "Dispute INV β€” explain" β€” when the procurement manager rejects a delivery, a follow-up Flow captures the reason (wrong invoice, wrong items, never received from this supplier) for ops triage.
  • Supplier "edit a field" β€” instead of free-text correction in chat (S3), a Flow with the parsed invoice fields editable.

What we never do here

We never let the user edit the auto-resolved "Account name" field. The name comes from NIBSS, not from the user. This closes the most common "switch the receiving account" attack.

Per-invoice flow

Once the bank is linked, every receivable cycles through the steps below. Step S2 (BRD Β§3 document submission) branches by the supplier's industry β€” captured at signup and stored on the supplier profile. General Trade suppliers submit a stamped invoice (S2a). Agro suppliers submit a validated weighbridge ticket or Goods-Received Note (S2b). Both paths converge at S3 (AI extraction confirmation) and proceed identically thereafter.

1
Onboarding complete
BRD Β§1 β†’ SUPPLIER_READY
9:41

Trigger

KYC service emits supplier.kyc.verified after CAC + BVN/NIN passes. The internal Terracore transit wallet is provisioned silently behind the scenes; this template fires only after both KYC and provisioning succeed (parallel-await; ~600ms p95).

What the supplier sees vs. doesn't

The supplier only sees their own settlement bank account. The Terracore transit wallet exists in our infrastructure to route funds between financier and supplier settlement bank, but it is never exposed in any supplier-facing message. To the supplier, money simply lands in their bank.

Speed budget ≀ 1s

Template arrives within 1s of KYC completion. Quick-reply buttons load with the message β€” no extra round trip.

Why three buttons

β€œSubmit an invoice” is the dominant CTA. The other two exist because new users want to verify their in-flight deals and read the financing model in plain language before they trust the rail with the next invoice.

2a
Submit stamped invoice General Trade
BRD Β§3 β†’ INVOICE_SUBMITTED
11:43

What runs here

PDF lands on the WhatsApp Cloud media endpoint. We download in parallel with sending the typing indicator. AI extraction (OpenAI mini, vision) starts immediately β€” target p95: 1.8s.

Authenticity & data-extraction checks (BRD Β§3 & Β§6)

The vision pass returns four authenticity signals plus structured invoice data. All four authenticity signals must pass for the invoice to advance. The structured data is what we render back to the supplier for confirmation in S3.

  • buyer_stamp_present β€” vision detects the buyer's company stamp on the invoice (proof the buyer received and acknowledged the goods, per BRD Β§3 "Stamped invoice"). Absent stamp β†’ reject, ask for re-upload.
  • buyer_signature_present β€” vision detects an authorising signature alongside or near the buyer's stamp. Absent signature β†’ reject. We fingerprint the signature and store the hash so a repeat-buyer's signature can be verified against prior invoices (BRD Β§6 fraud signals).
  • products_extracted β€” line-items: description, quantity, unit, unit price. Used downstream to flag duplicate invoices and to cross-check against the buyer-procurement waybill at B3.
  • total_amount β€” extracted invoice total in ₦. Recomputed from line-items as a consistency check; mismatch > tolerance β†’ reject (BRD Β§6 "Consistency").

If any of the four fail, we skip S3 and reply: "I couldn't confirm [stamp / signature / total] on this invoice. Please send a clearer photo, or re-upload after the buyer stamps and signs it." Caught here, not at the financier desk.

Why a typing indicator

Sets perceived speed expectation. The supplier never sees a "loading" spinner because WhatsApp doesn't allow custom UI; the typing dot is the native equivalent.

2b
Submit validated ticket Agro
BRD Β§3 β†’ INVOICE_SUBMITTED
11:43

Why agro uses a weighbridge ticket / GRN, not an invoice

In agro trade, goods are commodities priced by weight at the moment of offload β€” paddy rice, maize, soya, palm oil, etc. The economic terms are not finalised until the buyer's weighbridge measures net weight, applies any moisture / grade adjustments, and the buyer's representative signs off. The validated weighbridge ticket (or Goods-Received Note for warehoused stock) is therefore the equivalent of a stamped invoice: it is the document the buyer cannot dispute. We finance off the ticket, not off a pre-trade invoice.

Authenticity & data-extraction checks (BRD Β§3 & Β§6)

Vision pass returns:

  • buyer_stamp_present β€” buyer-side validation stamp on the ticket. Absent β†’ reject.
  • buyer_signature_present β€” receiver's signature on the ticket. Absent β†’ reject. Signature fingerprinted as in the General Trade path.
  • weighbridge_id + ticket_number β€” unique identifier; checked for duplicates against prior tickets from the same weighbridge.
  • net_weight + unit + grade β€” measured payload (e.g. 28.4 t paddy, Grade A). Grade governs unit price.
  • vehicle_plate + driver_name β€” chain-of-custody data; cross-checked against the supplier's logistics record if available.
  • computed_total β€” net weight Γ— buyer's price-list for that grade (price list is set at the buyer-supplier relationship level). Mismatch with any printed total on the ticket > tolerance β†’ reject.

If any check fails, we skip S3 and reply: "I couldn't confirm [validation stamp / weight / signature]. Please send a clearer photo of the ticket, or re-upload once the buyer's rep has signed."

Same downstream flow as General Trade

Once the ticket is accepted at S2b, the receivable proceeds through S3 (confirm extracted data β€” see next screen, with agro-specific fields) β†’ S4 (terms) β†’ S5 β†’ S6 β†’ S7 identically. The financier (F2) and buyer (B1–B8) flows do not branch on industry.

Speed budget ≀ 1.8s end-to-end

Same vision-extraction pipeline as the invoice path, different prompt template.

3
AI extraction β€” confirm
BRD Β§3 β†’ INVOICE_PARSED
11:43

What the three green ticks mean

Each tick maps to one extractor signal from S2, surfaced here so the supplier sees the system's authenticity verdict before approving:

  • Buyer's stamp detected β€” vision found the buyer's company stamp on the invoice (BRD Β§3).
  • Buyer's signature detected β€” vision found an authorising signature; signature fingerprint stored for cross-invoice verification (BRD Β§6 fraud signals).
  • Line items match total β€” sum of extracted line-items reconciles with the printed total within tolerance (BRD Β§6 consistency).

If any check had failed at S2, this screen would not have rendered β€” the supplier would have seen the failure prompt instead.

Speed budget ≀ 1.8s end-to-end

From upload acknowledgement to this confirmation message rendering in the supplier's chat.

Failure modes shown explicitly

"Edit a field" triggers a single follow-up flow per field (no free text β€” supplier can adjust buyer phone, items, total, tenor). The buyer's stamp, signature, and the underlying invoice number are not editable β€” they come from the document, not the user.

What we never do

We never auto-progress on AI confidence alone. Confirmation is always required because this row anchors a real money transfer downstream. Confidence threshold lives in logs, not UX.

4
Discount terms
BRD Β§4 β†’ TERMS_ACCEPTED
11:44

Why this is its own screen

BRD Β§4 explicitly requires the supplier to accept discounted terms. Combining this with the parse-confirmation in S3 would mix β€œis the data right” with β€œdo I accept the price” β€” two very different decisions that would create regret loops.

Speed budget ≀ 600ms

Pricing is computed deterministically from the financier’s rate card; no AI involved. Renders almost instantly.

Audit trail

The β€œAccept terms” tap is logged with the rate card version, the message ID, the device fingerprint, and the timestamp β€” that’s our consent record.

5
Awaiting buyer's procurement confirmation
BRD Β§5–§7 β†’ AWAITING_DELIVERY_CONFIRM
11:44

Two truth layers, same buyer, different channels

The trade chain is simply Prime Goods Ltd β†’ Bokku. Bokku is the buyer (also the manufacturer) β€” they purchased the goods. Bokku is contacted twice in the rail by Terracore, via different teams and different channels:

  • Layer 1 β€” Bokku's stamp + signature on the invoice (verified at S2/S3 via vision). The invoice is a paper artifact; a supplier could in theory forge a stamp.
  • Layer 2 β€” Bokku's procurement team confirms by DKIM-signed email (BRD Β§5, this screen). Independent channel, independent team, harder to forge.

Same entity (Bokku) β€” but two channels means a supplier cannot fake one and quietly bypass the other. Layer 2 also goes to procurement@bokku.ng, not finance@bokku.ng: the warehouse / procurement team who signs for goods is a different function from the AP team who pays invoices. Finance gets contacted separately for payment instructions later (BRD Β§9).

Critical async pattern

This is the only screen where the supplier waits. The bot promises a push update so the supplier never re-pings. Re-pinging is the #1 abandonment driver in pilots.

The status list bubble

It's sent once and updated by edits via Cloud API where supported, otherwise re-sent at each transition. We never let the chat go silent for >15 min before sending a heartbeat update.

6
Funding disbursed CRIT
BRD Β§8 β†’ FUNDED
11:52

Trigger

Wallet service emits disbursement.confirmed after the financier→supplier wallet transfer settles. The disbursement and this message are wrapped in the same DB transaction — if the message fails to render, the disbursement is reversed.

Speed budget ≀ 8 min from terms accepted

NIBSS instant settlement target. The supplier sees the money in their bank account before they re-open WhatsApp.

The reassurance line

β€œYou don’t need to chase them” is intentional. It is the load-bearing sentence in the entire supplier experience β€” it’s how we sell the value of the rail back to them in two seconds.

7
Cycle complete
BRD Β§10 β†’ CLOSED
14:18

What we deliberately do NOT show here

Internal admin metrics β€” credit standing, available headroom, exposure used, financier limits, performance scores β€” are never rendered on a supplier-facing screen. Those are underwriting and risk-management concepts owned by the financing partner and Terracore ops; they live in the dashboard, not in the supplier's chat. The supplier only ever sees what concerns them directly: the invoice closed, the buyer paid, and a prompt to submit the next one.

Money-control assertion

This message is gated on financier-wallet inbound webhook with matching reference TC-INV-23-A4F. The supplier cannot trigger this state by sending "buyer paid me" β€” there is no such intent in the bot.


Terracore Financing Bot Β· UI Flow Architecture Β· 2026-05-01
WhatsApp visual tokens reflect Meta UI as of Feb 2026. Β· Back to overview