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

  1. A Sokoni merchant consents to share a signed evidence package (POS sales, cash-flow discipline, receivables, audit trail) with a financial partner (this LMS).
  2. Sokoni creates a disclosure of that package to the partner and calls the LMS partner API to originate a loan application.
  3. Hiana creates the borrower + application, retrieves the disclosed evidence, verifies its signature/digest, and assesses it as a configured data source.
  4. The application runs through the tenant's normal underwriting → offer → agreement → approval → funding → disbursement pipeline.
  5. 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.
  6. Sokoni POS sales can be configured to auto-deduct a percentage toward loan repayment; settled deductions post back to Hiana as repayments.
  7. 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

RequirementWhere
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 setupAdmin → 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 StaffingAdmin → Staff

On the Sokoni side

RequirementWhere
Sokoni tenant + staff login with credit administration rightsSokoni
The merchant must have verified KYC (unverified merchants are rejected at origination with 422)Sokoni merchant record
A registered financial partner record representing this LMSPOST /credit/financial-partners
An skp_ partner credential for the LMS to call Sokoni withPOST /credit/financial-partners/credentials
An active merchant data grant covering the partnerPOST /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_url overrides the deployment-wide LMS_BASE_URL — use it when one Sokoni instance serves multiple lenders.
  • lms_api_key_env names 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 on POST /partner/sokoni/applications and POST /partner/sokoni/repayments. Set it as LMS_PARTNER_KEY (or the partner's lms_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):

SettingPurpose
LMS_BASE_URLHiana 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:

  1. Credit-risk data sources — the product's credit_risk.data_sources must include SOKONI, otherwise evidence is never loaded as an input.
  2. 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.
  3. Accounting — if the workflow posts accrual journals, the product needs an INTEREST_ACCRUAL accounting step and resolvable GL mappings (product → jurisdiction → tenant → system default priority).
  4. 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:

StepRole that performs it
Create disbursementLOAN_OFFICER
VerifyOPERATIONS_MANAGER
Process / release fundsFINANCE_OFFICER #1
Confirm transferFINANCE_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:

EventMirror effect
loan.created / loan.disbursed / loan.activatedupsert + status (pending→disbursed→active), term/rate/currency synced
loan.payment_receivedrepayment row (idempotent by payment_id), total_paid/outstanding_balance updated
loan.restructuredterm_months/interest_rate_pct updated; status unchanged
loan.cancelledstatus cancelled + close_reason
loan.terminatednormalized to cancelled + close_reason
loan.written_offstatus written_off + close_reason
loan.penalty_applied / loan.penalty_waivedreceipt 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/repayments with the inbound key and a stable idempotency_key — replays return the same processed payment.
  • POS deduction sweep: configure POST /credit/loans/:loan_id/deduction-config {"auto_deduct_pct":10} on Sokoni, then POST /credit/loan-deductions/run aggregates 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=true
  • sokoni_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

SymptomLikely 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 logLMS_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 assessmentdata_sources.enable_sokoni not true, or product's credit_risk.data_sources lacks SOKONI
Origination 422 on a merchantMerchant KYC not verified
Package is not covered by an active grantGrant suspended/expired, or package belongs to a different grant
Disclosure unavailable on retrieveAlready consumed (one-shot) or superseded by a correction
Funding step no assignable userEarlier actor excluded — need a distinct staff member with the next role
a current generated agreement is requiredDocument hash still generating — poll GET /agreements/:id
Correction flags never appearSOKONI_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.