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.
| Simulator | Financing | Who pays the merchant | Portal & console | API base |
|---|---|---|---|---|
| Shop na Pesi | Cart — the whole basket | Vuka Bridge, from the financier's virtual account (express m2 and pay-on-delivery m1) | /sample/fin/sample/fin/console | /v1/m2/*, /v1/m1/* |
| Ngao Credit | Asset — 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 Credit | Cart | The 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
- Apply migrations (
npm run migrate) and setSAMPLE_FIN_ADMIN_PASSWORDin.env. - Sign in to the operator console at /sample/fin/console/login as
admin@faraja.demowith that password (the first sign-in sets it). - Make sure Shop na Pesi is wired into the bridge and live — see Wiring a simulator into the bridge.
- 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.
- Top up Shop na Pesi's virtual account on /admin/disbursement-accounts so it can fund payouts.
- On the merchant simulator, check out with
credit and choose Shop na Pesi. Link the account and confirm the loan with OTP
222222. - 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
| Variable | What it's for |
|---|---|
EDOMX_FIN_SIM_HMAC_SECRET | The 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_VALUE | Settings 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_PASSWORD | Password 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_KEY | Key the simulator's connector and loan book sign their internal calls with. Has a dev default. |
CONN_DEFAULT_TZ / NGAO_CONN_DEFAULT_TZ | The connector's default timezone; operators can change it under console Settings. |
PUBLIC_APP_ORIGIN | Base URL the simulator's connector uses to reach its own loan book. |
FIN_PRELOAD_SIM_DEFAULT, FIN_SANDBOX_DEFAULT_TARGET, FIN_SANDBOX_ALLOWED_HOSTS | Pre-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_LEG | Turn on the current express (express_loan/*) and delivery (disburse_loan/*) call sequences. Keep both true. |
FARAJA_RAIL_ENABLED | Shows Faraja Credit at checkout and to customers and merchants. Default false. |
EDOMX_DATA_ENCRYPTION_KEY | Decrypts each financier's saved secrets. If it changes, re-save the financier's secret. |
PUBLIC_ENV=production | Turns 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/login | Customer 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/profile | The 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/login | Operator console sign-in. |
/console — connector view | Overview · 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 view | Customers · 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)
| Path | What it is |
|---|---|
/sample/faraja/customer/login | Customer sign-in. There is no self sign-up. |
/sample/faraja/admin | Add 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:
| Call | Path | Used for |
|---|---|---|
| Health | GET /_health (unsigned) | All |
| Link an account | /link_loan_account/request, /complete, /check | All |
| Available limit | /credit_limit/check | Express (m2), lender-settled (m3) |
| Express loan | /express_loan/request, /complete, /settle | Express (m2) |
| Book a loan | /book_loan/request, /complete | Pay on delivery (m1) |
| Disburse at delivery | /disburse_loan/request, /complete | Pay on delivery (m1) |
| Cancel | /loans/{id}/void | Pay on delivery (m1) |
| Reverse (refund) | /loans/{id}/reverse — optional | m1, m2 |
| Authorize, status | /loans/authorize, /loans/status | Lender-settled (m3) |
- Express is three calls:
requestsends the OTP,completeonly verifies it, andsettlebooks and cements the loan once the bridge has debited the financier's virtual account. Asettlewithout a verifiedcompleteis refused (unknown_response_idorawaiting_settle_expired). - Order id:
edomx_order_idis required onbook_loan/request,express_loan/requestand bothdisburse_loancalls. - 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
| Step | OTP | Limits |
|---|---|---|
| Link an account | 222222 | Express link: valid 10 minutes |
| Book or express loan | 222222 | Valid 5 minutes, 3 attempts; the request expires after 15 minutes |
| Delivery confirmation (pay on delivery) | 333333 | As 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_merchantis the merchant's stable short code;vb_categoryandvb_subcategoryare the bridge's master categories, not the merchant's own;vb_amount_centsis 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:
- Create the financier on /admin/financiers. It gets an organisation id (
FIN-XXXXXX) and, automatically, a virtual account. - Activate it. The financier's contact signs up at
/fin/auth/sign-upwith that id and completes/fin/activate; an admin approves it on/admin/financiers/[id]. - Certify in sandbox on
/fin/integration: save the endpoint URLs (pre-filled with the simulator's whenFIN_PRELOAD_SIM_DEFAULT=true), set the Security config — the HMAC secret must equal the simulator'sEDOMX_FIN_SIM_HMAC_SECRET(or the Ngao one) — test each endpoint and save. Optional calls such as reverse never block certification. - Go live: save production URLs, pass the health probe and submit; an admin approves it at
/admin/financiers/go-live-applications/[id]. - Commercial setup: the per-loan platform fee on
/admin/financiers/[id]/billing(express fails withfin_platform_fee_unconfiguredwithout it), one product per journey on/admin/financiers/[id]/products, and financing mode, rules and asset tariff on/fin/financing. - 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
| Symptom | Cause / fix |
|---|---|
Linking is rejected with no_active_account | The customer has no credit line. Add one (see Credit lines). |
Linking fails with customer_not_registered or kyc_mismatch | The 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 Inspector | The 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_failed | EDOMX_DATA_ENCRYPTION_KEY changed since the secret was saved. Re-save the financier's secret. |
The bridge logs url_not_configured | No URL is saved for that call on /fin/integration. |
Checkout stops with fin_insufficient_funds or fin_platform_fee_unconfigured | Top up the financier's virtual account, or set its platform fee on /admin/financiers/[id]/billing. |
| The financier is missing from the checkout list | It 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 rejected | Use 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_production | PUBLIC_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_*, plussim_ngao_asset_contract,sim_ngao_asset_scheduleandsim_ngao_asset_type. - Faraja:
sim_faraja_*— customers, sessions, link requests and history, authorize requests, allowed merchants, admin overrides, webhook deliveries. - Bridge side:
fin_integration_configandfin_integration_endpoints(per-financier secrets and URLs),fin_float_accountsand related float tables.
Helper libraries
src/lib/fin-api-spec.ts— the financier API contractsrc/lib/server/fin-runtime-client.ts— how the bridge calls a financiersrc/lib/server/sim-fin-store.ts— simulator auth, OTPs and request handlingsrc/lib/server/sim-fin-lms.ts,sim-fin-link.ts,sim-link-resolver.ts,conn-*.ts,lms-client.ts— connector and loan booksrc/lib/server/ngao/*— Ngao Credit, including asset contractssrc/lib/server/sim-faraja-store.ts— Faraja Credit