Simulator · financiers

Financier simulators

Three simulated credit providers, each with its own customers, credit lines, operator console and an implementation of the financier API that Vuka Bridge calls at checkout: Shop na Pesi (cart credit), Ngao Credit (asset finance) and Faraja Credit (lender-settled).

What they are

Each simulator plays the financier's side of a credit checkout: it links a shopper's account, sends the OTP, books the loan and tracks repayments. Vuka Bridge calls it exactly as it would a real financier — at the endpoint URLs saved for that financier, signed with that financier's secret.

SimulatorFinancingWho pays the merchantPortal & consoleAPI base
Shop na PesiCart — the whole basketVuka Bridge, from the financier's virtual account (express m2 and pay-on-delivery m1)/sample/fin
/sample/fin/console
/v1/m2/*, /v1/m1/*
Ngao CreditAsset — one item over a set number of months ("Lipa pole pole")Vuka Bridge, as for Shop na Pesi/sample/ngao
/sample/ngao/console
/ngao/v1/m2/*, /ngao/v1/m1/*
Faraja CreditCartThe lender itself (m3), then it notifies the bridge/sample/faraja
/sample/faraja/admin
/v1/m3/*

Faraja Credit appears at checkout only when FARAJA_RAIL_ENABLED=true (off by default). Its own pages and API work either way.

Quick start — a Shop na Pesi checkout

  1. Apply migrations (npm run migrate) and set SAMPLE_FIN_ADMIN_PASSWORD in .env.
  2. Sign in to the operator console at /sample/fin/console/login as admin@faraja.demo with that password (the first sign-in sets it).
  3. Make sure Shop na Pesi is wired into the bridge and live — see Wiring a simulator into the bridge.
  4. Create a test customer: sign up at /sample/fin/customer/sign-up with the same ID number, full name and date of birth as the shopper's Vuka Bridge profile — the simulator matches on those, not on phone. Then give the customer a credit line (see Credit lines); a new sign-up has none.
  5. Top up Shop na Pesi's virtual account on /admin/disbursement-accounts so it can fund payouts.
  6. On the merchant simulator, check out with credit and choose Shop na Pesi. Link the account and confirm the loan with OTP 222222.
  7. Follow the calls in the console's Wire Inspector, and the credit line's new drawdown under Credit Lines. The payout continues on the M-Pesa or PESI rail simulator.

Environment variables

VariableWhat it's for
EDOMX_FIN_SIM_HMAC_SECRETThe secret Shop na Pesi verifies EDOMX-Signature with. It must equal the HMAC secret saved for the financier on /fin/integration (Security), because the bridge signs with that saved secret. The integration screens use this value as their default. Unset, the simulator falls back to a built-in dev-only secret.
EDOMX_FIN_SIM_JWT_SECRET, …_OAUTH_CLIENT_ID, …_OAUTH_CLIENT_SECRET, …_JWT_TOKEN_URL, …_API_KEY_HEADER, …_API_KEY_VALUESettings for the alternative auth methods (bearer token, API key) when a financier is configured to use them instead of HMAC.
EDOMX_NGAO_SIM_*The same set for Ngao Credit (EDOMX_NGAO_SIM_HMAC_SECRET and so on).
SAMPLE_FIN_ADMIN_PASSWORD / SAMPLE_NGAO_ADMIN_PASSWORDPassword set on the console's seeded operator (admin@faraja.demo for Shop na Pesi, admin@ngao.demo for Ngao) at first sign-in.
CONN_LMS_HMAC_KEY / NGAO_CONN_LMS_HMAC_KEYKey the simulator's connector and loan book sign their internal calls with. Has a dev default.
CONN_DEFAULT_TZ / NGAO_CONN_DEFAULT_TZThe connector's default timezone; operators can change it under console Settings.
PUBLIC_APP_ORIGINBase URL the simulator's connector uses to reach its own loan book.
FIN_PRELOAD_SIM_DEFAULT, FIN_SANDBOX_DEFAULT_TARGET, FIN_SANDBOX_ALLOWED_HOSTSPre-fill the simulator URLs on a financier's sandbox integration tab (FIN_SANDBOX_DEFAULT_TARGET=inmem points them at /v1/m1, /v1/m2, /v1/m3) and limit which hosts the sandbox tab accepts.
EDOMX_M2_DISBURSE_2_LEG, EDOMX_M1_DELIVER_2_LEGTurn on the current express (express_loan/*) and delivery (disburse_loan/*) call sequences. Keep both true.
FARAJA_RAIL_ENABLEDShows Faraja Credit at checkout and to customers and merchants. Default false.
EDOMX_DATA_ENCRYPTION_KEYDecrypts each financier's saved secrets. If it changes, re-save the financier's secret.
PUBLIC_ENV=productionTurns every financier simulator off (HTTP 503 simulator_disabled_in_production) and hides simulator financiers from checkout.

Copy-paste block for your .env

# ─── Financier simulators ──────────────────────────────────────
EDOMX_FIN_SIM_HMAC_SECRET="whsec_local_dev_only"
EDOMX_NGAO_SIM_HMAC_SECRET="whsec_local_dev_only"
SAMPLE_FIN_ADMIN_PASSWORD="changeme-dev-only"
SAMPLE_NGAO_ADMIN_PASSWORD="changeme-dev-only"

# Current express + delivery call sequences
EDOMX_M2_DISBURSE_2_LEG="true"
EDOMX_M1_DELIVER_2_LEG="true"

# Faraja Credit at checkout (off by default)
FARAJA_RAIL_ENABLED="false"

Route map

Shop na Pesi (/sample/fin) and Ngao Credit (/sample/ngao)

Path (under the simulator's base)What it is
/customer/sign-up, /customer/loginCustomer sign-up and sign-in (email or phone). The session travels as ?sid=. Reached from a store's Get credit →, Ngao's pages show a "You're financing" card with the item and amount the store sent (see Item context).
/customer/dashboard, /customer/profileThe customer's credit lines, loans and repayments; repay or unlink an account; KYC details.
/customer/assets (Ngao only)Financed assets, installment schedules and installment payment.
/console/loginOperator console sign-in.
/console — connector viewOverview · Credit Lines (per line: book, disburse, cancel, express, reverse, repay, adjust limit, set status, unlink) · Bookings · Links & OTP · Wire Inspector (every call from the bridge) · Scenarios · Settings.
/console/lms — loan-book viewCustomers · Contracts · Drawdowns · Repayments · Loan types · Asset book (Ngao only).
/admin/*Older admin pages that show only the retired first-generation tables. Use the console instead.

Faraja Credit (/sample/faraja)

PathWhat it is
/sample/faraja/customer/loginCustomer sign-in. There is no self sign-up.
/sample/faraja/adminAdd customers (default limit KES 50,000) and merchants, set limits, record repayments, back-date a loan, reset a customer.

The financier API

The contract is defined in src/lib/fin-api-spec.ts (spec version 4.6.0) and shown to each financier in its portal at /fin/integration. The bridge appends these paths to the URLs saved for the financier:

CallPathUsed for
HealthGET /_health (unsigned)All
Link an account/link_loan_account/request, /complete, /checkAll
Available limit/credit_limit/checkExpress (m2), lender-settled (m3)
Express loan/express_loan/request, /complete, /settleExpress (m2)
Book a loan/book_loan/request, /completePay on delivery (m1)
Disburse at delivery/disburse_loan/request, /completePay on delivery (m1)
Cancel/loans/{id}/voidPay on delivery (m1)
Reverse (refund)/loans/{id}/reverse — optionalm1, m2
Authorize, status/loans/authorize, /loans/statusLender-settled (m3)
  • Express is three calls: request sends the OTP, complete only verifies it, and settle books and cements the loan once the bridge has debited the financier's virtual account. A settle without a verified complete is refused (unknown_response_id or awaiting_settle_expired).
  • Order id: edomx_order_id is required on book_loan/request, express_loan/request and both disburse_loan calls.
  • Lender-settled: Faraja Credit notifies the bridge with a signed webhook to /v1/m3/_webhooks/fin/{short_code}.

Headers and signing

Every call from the bridge carries:

Content-Type: application/json
X-EDOMX-Spec-Version: 4.7.0
Idempotency-Key: <per call>
X-Request-ID: <per call>
EDOMX-Signature: <hex HMAC-SHA256 of the raw body>   (HMAC, the default)

A financier can be set to use a bearer token or an API key instead of HMAC; those are configured per financier on /fin/integration. The simulators reject a missing or wrong signature with HTTP 401 and { "error": { "code": "signature_mismatch" }, "reason": "missing | malformed | mismatch | stale" }.

Try it with curl

The seeded Scenario Sandbox credit line is linked with the token sandbox_link_tok (scenario runs may change it — check Credit Lines in the console). Ask Shop na Pesi for its available limit:

BODY='{"request_id":"lim_req_demo_1","link_token":"sandbox_link_tok"}'
SIG=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$EDOMX_FIN_SIM_HMAC_SECRET" | sed 's/^.* //')

curl -s http://localhost:5173/v1/m2/credit_limit/check \
  -H 'Content-Type: application/json' \
  -H "EDOMX-Signature: $SIG" \
  -d "$BODY"

200 OK
{
  "request_id": "lim_req_demo_1",
  "link_token": "sandbox_link_tok",
  "available_limit_cents": 5000000,
  "currency": "KES",
  "customer_status": "active",
  "as_of": "2026-10-06T09:00:00.000Z",
  "outstanding_loan_count": 0
}

Sign the exact bytes you send. The call then appears in the console's Wire Inspector. GET /v1/m2/_health needs no signature.

Simulator behaviour

OTPs

StepOTPLimits
Link an account222222Express link: valid 10 minutes
Book or express loan222222Valid 5 minutes, 3 attempts; the request expires after 15 minutes
Delivery confirmation (pay on delivery)333333As above

OTPs are never returned in API responses. Every challenge is listed in the console under Links & OTP.

Linking

The simulator finds the customer by ID document type and number, then checks the full name and date of birth. Phone is not compared. The OTP goes to the phone on the simulator's own record, shown masked. Errors: customer_not_registered, account_suspended, kyc_mismatch, and no_active_account when the customer has no credit line.

Credit lines

What a customer can borrow comes from their credit line (sim_fin_loan_accounts, sim_ngao_loan_accounts): its limit, minus what is outstanding, minus open reservations. The limit shown at sign-up is not used.

Migrations give a credit line to the customers that existed when they ran (KES 50,000 on Shop na Pesi, KES 100,000 on Ngao) and to the Scenario Sandbox customer. A customer who signs up later has no credit line, so linking is rejected with no_active_account. Give them one:

-- Shop na Pesi: KES 50,000 credit line for a signed-up customer
INSERT INTO sim_fin_loan_accounts
       (customer_id, loan_type_id, credit_limit_cents, currency, status)
SELECT c.id, lt.loan_type_id, 5000000, 'KES', 'active'
  FROM sim_fin_customers c
  JOIN sim_fin_loan_types lt ON lt.name = 'Online Checkout Loan Type'
 WHERE c.email = 'shopper@example.com';

-- Ngao Credit: same, against the sim_ngao_* tables (KES 100,000)
INSERT INTO sim_ngao_loan_accounts
       (customer_id, loan_type_id, credit_limit_cents, currency, status)
SELECT c.id, lt.loan_type_id, 10000000, 'KES', 'active'
  FROM sim_ngao_customers c
  JOIN sim_ngao_loan_types lt ON lt.name = 'Online Checkout Loan Type'
 WHERE c.email = 'shopper@example.com';

Once a line exists, adjust its limit or status from the console (Credit Lines).

Connector and loan book

Each simulator is split the way a real financier deployment would be. The connector faces the bridge: it owns link tokens, OTPs, reservations, idempotency and in-progress calls, and survives restarts by reloading them from the database. The loan book holds customers, credit lines, contracts, drawdowns and repayments; the connector reaches it only over signed internal HTTP. The console's two views follow the same split.

Ngao asset finance

For asset orders, book_loan/complete and express_loan/settle carry an asset_finance block with the bridge-quoted terms. Ngao records an asset contract and its installment schedule, and the contract becomes active when the loan is disbursed. What each asset category supports (IMEI, remote lock, repossession) is set per category. Customers see their assets at /sample/ngao/customer/assets; operators use Asset book.

Item context on sign-up

When a shopper clicks Get credit → for an item, the bridge sends them to the financier's registered onboarding page with the item's details appended. It does this only for financiers that accept them; both simulators do. For the Tecno Spark 20 sold by Jumia:

/sample/ngao/customer/sign-up?vb_v=1&vb_mode=asset&vb_merchant=MER-XJS67S&vb_merchant_name=Jumia
  &vb_sku=TEC-SP20-128&vb_item_name=Tecno+Spark+20+6.6%22%2C+8GB%284%2B4%29%2B128GB%2C+Dual+SIM
  &vb_category=phones-tablets&vb_amount_cents=1299000&vb_currency=KES&vb_return=…
  • vb_merchant is the merchant's stable short code; vb_category and vb_subcategory are the bridge's master categories, not the merchant's own; vb_amount_cents is the item price when the shopper clicked.
  • The values are hints for display. The amount and terms that bind are set at checkout. No customer personal data is ever included.
  • Ngao's sign-up and sign-in pages show the item in a "You're financing" card and keep it when the shopper switches between them. Signing up and signing in work as before.

Wiring a simulator into the bridge

A simulator is onboarded like any financier:

  1. Create the financier on /admin/financiers. It gets an organisation id (FIN-XXXXXX) and, automatically, a virtual account.
  2. Activate it. The financier's contact signs up at /fin/auth/sign-up with that id and completes /fin/activate; an admin approves it on /admin/financiers/[id].
  3. Certify in sandbox on /fin/integration: save the endpoint URLs (pre-filled with the simulator's when FIN_PRELOAD_SIM_DEFAULT=true), set the Security config — the HMAC secret must equal the simulator's EDOMX_FIN_SIM_HMAC_SECRET (or the Ngao one) — test each endpoint and save. Optional calls such as reverse never block certification.
  4. Go live: save production URLs, pass the health probe and submit; an admin approves it at /admin/financiers/go-live-applications/[id].
  5. Commercial setup: the per-loan platform fee on /admin/financiers/[id]/billing (express fails with fin_platform_fee_unconfigured without it), one product per journey on /admin/financiers/[id]/products, and financing mode, rules and asset tariff on /fin/financing.
  6. Fund it: top up the virtual account on /admin/disbursement-accounts. The financier sees its balance at /fin/disbursement-float.

The production tab rejects simulator and same-origin URLs, so a simulator cannot be taken live through the portal. For a database that already has the simulator financiers, two scripts run the whole sequence — endpoints, secret, certification, live state, fee and products:

node --import tsx scripts/seed-shop-na-pesi-live.ts   # Shop na Pesi (FIN-NRJBD4)
node --import tsx scripts/seed-ngao-live.ts           # Ngao Credit
# SIM_BASE_URL defaults to http://localhost:5173

Troubleshooting

SymptomCause / fix
Linking is rejected with no_active_accountThe customer has no credit line. Add one (see Credit lines).
Linking fails with customer_not_registered or kyc_mismatchThe ID type and number, full name and date of birth on the shopper's Vuka Bridge profile must match the simulator customer exactly.
401 signature_mismatch in the Wire InspectorThe secret saved for the financier on /fin/integration differs from the simulator's EDOMX_FIN_SIM_HMAC_SECRET (Ngao: EDOMX_NGAO_SIM_HMAC_SECRET). Make them equal.
The bridge logs hmac_secret_decrypt_failedEDOMX_DATA_ENCRYPTION_KEY changed since the secret was saved. Re-save the financier's secret.
The bridge logs url_not_configuredNo URL is saved for that call on /fin/integration.
Checkout stops with fin_insufficient_funds or fin_platform_fee_unconfiguredTop up the financier's virtual account, or set its platform fee on /admin/financiers/[id]/billing.
The financier is missing from the checkout listIt is not live, has no product for the journey, or is a simulator on a deployment with PUBLIC_ENV=production. Faraja Credit also needs FARAJA_RAIL_ENABLED=true.
The OTP is rejectedUse 222222 for linking, booking and express, 333333 for delivery. Book and delivery OTPs last 5 minutes and allow 3 attempts.
Every simulator call returns 503 simulator_disabled_in_productionPUBLIC_ENV=production turns the simulators off.

To debug a call, compare the bridge's record at /fin/logs with the simulator's Wire Inspector.

Reference

Schema

  • Loan book: sim_fin_customers, sim_fin_loan_types, sim_fin_loan_accounts, sim_fin_cycles, sim_fin_drawdowns, sim_fin_repayments, sim_fin_reversals.
  • Connector: sim_fin_conn_* — link tokens and requests, OTP challenges, bookings, express pending, idempotency, limit ledger, call log (Wire Inspector), pending legs, settings, activity.
  • Ngao: the same tables as sim_ngao_*, plus sim_ngao_asset_contract, sim_ngao_asset_schedule and sim_ngao_asset_type.
  • Faraja: sim_faraja_* — customers, sessions, link requests and history, authorize requests, allowed merchants, admin overrides, webhook deliveries.
  • Bridge side: fin_integration_config and fin_integration_endpoints (per-financier secrets and URLs), fin_float_accounts and related float tables.

Helper libraries

  • src/lib/fin-api-spec.ts — the financier API contract
  • src/lib/server/fin-runtime-client.ts — how the bridge calls a financier
  • src/lib/server/sim-fin-store.ts — simulator auth, OTPs and request handling
  • src/lib/server/sim-fin-lms.ts, sim-fin-link.ts, sim-link-resolver.ts, conn-*.ts, lms-client.ts — connector and loan book
  • src/lib/server/ngao/* — Ngao Credit, including asset contracts
  • src/lib/server/sim-faraja-store.ts — Faraja Credit