Integrations
Sokoni CRM Integration — Setup Guide
This guide configures the Sokoni ↔ Hiana evidence-driven lending loop for a tenant. Once configured, merchants on the Sokoni POS/CRM platform can apply for financing using signed operational evidence, and the full loan lifecycle — origination, disbursement, repayments, delinquency — mirrors back into Sokoni.
Use it alongside the Loan Product Setup Guide and the Loan Servicing Operations Guide.
What the integration does
- A Sokoni merchant consents to share a signed evidence package (POS sales, cash-flow discipline, receivables, audit trail) with a financial partner (this LMS).
- Sokoni creates a disclosure of that package to the partner and calls the LMS partner API to originate a loan application.
- Hiana creates the borrower + application, retrieves the disclosed evidence, verifies its signature/digest, and assesses it as a configured data source.
- The application runs through the tenant's normal underwriting → offer → agreement → approval → funding → disbursement pipeline.
- Loan lifecycle events (
loan.created,loan.disbursed,loan.activated,loan.payment_received,loan.written_off, …) stream to Sokoni over a signed webhook, where a mirror of the loan is maintained per merchant. - Sokoni POS sales can be configured to auto-deduct a percentage toward loan repayment; settled deductions post back to Hiana as repayments.
- If evidence is later corrected, Sokoni issues a correction notice; Hiana polls it and flags every application built on the superseded evidence.
Prerequisites
On the Hiana side
| Requirement | Where |
|---|---|
| Encryption service configured (provider secrets are stored encrypted; without it the Sokoni adapter does not register) | deployment env |
Staff account with product.read + system.config permissions for setup | Admin → Staff |
| A loan product published for Sokoni originations (see Product gates) | Products |
| Funding-workflow staffing: at least 4 distinct staff members covering LOAN_OFFICER, OPERATIONS_MANAGER, FINANCE_OFFICER ×2 — see Staffing | Admin → Staff |
On the Sokoni side
| Requirement | Where |
|---|---|
| Sokoni tenant + staff login with credit administration rights | Sokoni |
The merchant must have verified KYC (unverified merchants are rejected at origination with 422) | Sokoni merchant record |
| A registered financial partner record representing this LMS | POST /credit/financial-partners |
An skp_ partner credential for the LMS to call Sokoni with | POST /credit/financial-partners/credentials |
| An active merchant data grant covering the partner | POST /credit/merchants/:id/data-grants |
1. Sokoni — register the lender & consent
All calls below are staff-authenticated (Authorization: Bearer <sokoni staff JWT>,
X-Tenant-ID: <tenant uuid>).
1a. Financial partner (once per lender)
POST /credit/financial-partners
{ "legal_name": "Nakhalat Microfinance",
"partner_code": "nakhalat",
"jurisdiction": "ZW",
"permitted_purposes": ["credit_underwriting"],
"allowed_data_products": ["credit_evidence_v1"],
"agreement_version": "v1",
"lms_base_url": "https://<hiana-host>/api/v1",
"lms_api_key_env": "LMS_PARTNER_KEY" }
lms_base_urloverrides the deployment-wideLMS_BASE_URL— use it when one Sokoni instance serves multiple lenders.lms_api_key_envnames the environment variable holding the API key the partner presents on inbound calls — never the key itself.
1b. Partner credential (the skp_ key)
POST /credit/financial-partners/credentials
{ "partner_id": "<partner uuid>", "expires_at": "<RFC3339>" }
Response token is the skp_… credential — shown once. Hiana calls Sokoni
with it; store it as the provider bearer_token secret in step 2b.
1c. Merchant data grant (merchant consent)
POST /credit/merchants/:merchant_id/data-grants
{ "partner_id": "<partner uuid>",
"purpose": "credit_underwriting",
"data_product": "credit_evidence_v1",
"data_scopes": ["business_identity","sales_summary","cash_flow","inventory",
"receivables","financial_controls","audit_summary","credit_readiness"],
"consent_text_version": "v1",
"policy_version": "v1",
"expires_at": "<RFC3339>" }
The grant is the consent record — packages, disclosures, and corrections are all scoped to it. Grants can be suspended/resumed/revoked; origination and disclosure both 403 on an inactive or expired grant.
2. Hiana — provider connection & webhook
All calls are staff-authenticated under /api/v1.
2a. Provider connection
PUT /integrations/providers
{ "provider_key": "sokoni", "adapter_key": "sokoni",
"display_name": "Sokoni CRM", "enabled": true,
"settings": { "base_url": "https://<sokoni-host>",
"sokoni_tenant_id": "<sokoni tenant uuid>",
"default_jurisdiction_id": "<optional — applied when origination omits jurisdiction>" } }
settings is rewritten wholesale on save — resend the whole object on updates.
2b. Provider secrets
PUT /integrations/providers/:provider_id/secret
{ "values": { "bearer_token": "<skp_… credential from 1b>",
"inbound_partner_key": "<API key Sokoni presents on /partner/sokoni/* calls>" } }
bearer_token— outbound: Hiana → Sokoni partner API.inbound_partner_key— inbound: the key Sokoni sends onPOST /partner/sokoni/applicationsandPOST /partner/sokoni/repayments. Set it asLMS_PARTNER_KEY(or the partner'slms_api_key_env) on Sokoni.
2c. Enable the data source
PUT /admin/settings/data_sources.enable_sokoni
{ "value": "true" }
2d. Webhook endpoint → Sokoni loan mirror
POST /webhook-endpoints
{ "url": "https://<sokoni-host>/credit/webhooks/lms",
"description": "Sokoni LMS mirror",
"partner_id": "sokoni",
"events": ["loan.created","loan.disbursed","loan.activated",
"loan.payment_received","loan.written_off","loan.cancelled",
"loan.terminated","loan.restructured",
"loan.penalty_applied","loan.penalty_waived"],
"timeout_seconds": 10, "max_retries": 5 }
The response includes a signing secret (shown once) — set it as
LMS_WEBHOOK_SECRET on the Sokoni deployment, then:
POST /webhook-endpoints/:id/activate
Sokoni verifies X-Signature (HMAC-SHA256 of the raw body) on every delivery;
wrong or missing secrets are rejected before processing.
Partner scoping: partner_id restricts the subscription to loans whose
origination channel matches ("sokoni" = the Sokoni provider's adapter key).
Events for loans originated through other channels are never delivered to
this endpoint, and Sokoni's receiver hard-fails any event naming a
disclosure_id it cannot resolve for the merchant. Unscoped endpoints
(partner_id omitted) receive every event — use them for internal/logging
consumers only.
3. Sokoni — LMS connection settings
Set on the Sokoni deployment (env or partner record):
| Setting | Purpose |
|---|---|
LMS_BASE_URL | Hiana base URL without /api/v1 suffix — the client appends it. Per-partner override lives on the financial-partner record. |
LMS_PARTNER_KEY | = Hiana inbound_partner_key (2b). Per-partner: lms_api_key_env. |
LMS_WEBHOOK_SECRET | = webhook secret returned in 2d. |
CREDIT_EVIDENCE_SIGNING_KEY | ≥32 chars in production — signs evidence packages; rotating requires CREDIT_EVIDENCE_SIGNING_KEY_ID bump. |
4. Product gates
Sokoni-originated applications are validated against the referenced
product_id exactly like staff-entered ones. Required configuration:
- Credit-risk data sources — the product's
credit_risk.data_sourcesmust includeSOKONI, otherwise evidence is never loaded as an input. - Published workflow covering the full chain the pilot uses: underwriting → offer proposal → offer acceptance → agreement generation → approval. Agreement generation auto-creates the agreement document on offer acceptance; the document hash is produced asynchronously before signing.
- Accounting — if the workflow posts accrual journals, the product needs
an
INTEREST_ACCRUALaccounting step and resolvable GL mappings (product → jurisdiction → tenant → system default priority). - Business borrowers — Sokoni merchants map to business borrowers. Scoring uses the business model (KYC/standing/history) and updates target base fields; individual-profile fields on a business borrower are rejected rather than silently dropped.
Staffing — segregation of duties
The funding workflow enforces maker-checker: the actor who performed a step is excluded from later assignments. One person cannot run a disbursement end-to-end. Minimum staffing:
| Step | Role that performs it |
|---|---|
| Create disbursement | LOAN_OFFICER |
| Verify | OPERATIONS_MANAGER |
| Process / release funds | FINANCE_OFFICER #1 |
| Confirm transfer | FINANCE_OFFICER #2 (or admin — but distinct from earlier actors) |
Confirmation additionally requires a DISBURSEMENT_CONFIRMATION evidence
document uploaded against the loan (POST /documents/upload).
5. Operating the loop
Origination (merchant applies)
POST <sokoni>/credit/merchants/:merchant_id/lms-applications
{ "grant_id": "…", "package_id": "…", "product_id": "<hiana product uuid>",
"requested_amount": "1000.00", "requested_term": 6,
"requested_term_unit": "MONTHS", "purpose": "working capital" }
Creates a disclosure, calls POST /partner/sokoni/applications on Hiana with
the inbound key, and returns lms_application_id + lms_borrower_id.
Idempotent on idempotency_key.
Evidence assessment (ad-hoc)
POST /api/v1/integrations/sokoni/assessments
{ "disclosure_id": "…", "submit_outcome": true,
"outcome_event_type": "credit.decision", "partner_reference_id": "…" }
Retrieves the disclosure, verifies digest + signature, ingests the outcome,
and acknowledges the delivery. Disclosures are one-shot — once retrieved
they move to delivered; each assessment needs its own disclosure.
Loan mirror
GET <sokoni>/credit/merchants/:merchant_id/loans shows every mirrored loan
with status, schedule, totals. Mirror status transitions are monotonic —
late/out-of-order deliveries cannot regress a loan (e.g. a replayed
loan.created won't downgrade an active loan).
Lifecycle events and their mirror effect:
| Event | Mirror effect |
|---|---|
loan.created / loan.disbursed / loan.activated | upsert + status (pending→disbursed→active), term/rate/currency synced |
loan.payment_received | repayment row (idempotent by payment_id), total_paid/outstanding_balance updated |
loan.restructured | term_months/interest_rate_pct updated; status unchanged |
loan.cancelled | status cancelled + close_reason |
loan.terminated | normalized to cancelled + close_reason |
loan.written_off | status written_off + close_reason |
loan.penalty_applied / loan.penalty_waived | receipt recorded; informational (no balance mutation) |
loan.terminated is what Hiana emits for a forced post-disbursement
termination (POST /loans/:id/terminate); the loan row itself lands in
CANCELLED. Make sure the webhook subscription includes it (see §2).
Failed webhook receipts do retry: redelivering an event whose receipt is
failed reprocesses it instead of treating it as a duplicate.
Repayments
- Partner-initiated:
POST /partner/sokoni/repaymentswith the inbound key and a stableidempotency_key— replays return the same processed payment. - POS deduction sweep: configure
POST /credit/loans/:loan_id/deduction-config {"auto_deduct_pct":10}on Sokoni, thenPOST /credit/loan-deductions/runaggregates eligible sales, pushes a NetOne C2B collection, and the settled webhook posts the repayment to Hiana automatically. Runs are once-per-day per loan and idempotent.
Evidence corrections
When merchant evidence turns out wrong (e.g. a duplicated till sync):
POST <sokoni>/credit/merchants/:merchant_id/evidence-packages # fresh package
POST <sokoni>/credit/merchants/:merchant_id/data-grants/:grant_id/evidence-packages/:old_package_id/corrections
{ "replacement_package_id": "<new package>", "reason": "…" }
This supersedes the old disclosure (dead-lettering undelivered copies), issues
a replacement disclosure, and records a notice. Hiana polls
/partner/v1/tenants/:id/correction-notices every 15 minutes
(SOKONI_CORRECTION_SYNC_INTERVAL env, e.g. 5m, overrides) and sets on every
application that consumed the superseded disclosure:
sokoni_correction_pending=truesokoni_correction_reason=<reason>sokoni_replacement_disclosure_id=<new disclosure>
Borrowers whose linked disclosure was superseded are rotated to the replacement. The flags are a staff-review signal — nothing blocks automatically. Review flagged applications, reassess against the corrected evidence, and clear the fields when resolved.
6. Verification & troubleshooting
| Symptom | Likely cause |
|---|---|
Invalid or expired partner credential (Sokoni inbound calls) | bearer_token secret missing/mismatched |
Missing or invalid partner key on /partner/sokoni/* | inbound_partner_key vs LMS_PARTNER_KEY mismatch |
Webhook 401/signature failures in delivery log | LMS_WEBHOOK_SECRET doesn't match the endpoint secret — endpoint secrets are server-generated, re-register and re-copy |
| Application created but no SOKONI evidence on assessment | data_sources.enable_sokoni not true, or product's credit_risk.data_sources lacks SOKONI |
Origination 422 on a merchant | Merchant KYC not verified |
Package is not covered by an active grant | Grant suspended/expired, or package belongs to a different grant |
| Disclosure unavailable on retrieve | Already consumed (one-shot) or superseded by a correction |
Funding step no assignable user | Earlier actor excluded — need a distinct staff member with the next role |
a current generated agreement is required | Document hash still generating — poll GET /agreements/:id |
| Correction flags never appear | SOKONI_CORRECTION_SYNC_INTERVAL default is 15m; check service logs for sokoni correction notice applied |
The end-to-end rehearsal script scripts/sokoni_e2e_rehearsal.sh exercises
this entire guide against a live stack and captures per-step request/response
artifacts — run it first on a staging tenant before onboarding merchants.