Skip to content

TurnKey ← IRIS + EasyPay — Mass Disbursement Integration Brief

Integration instructions for TurnKey Lender, who owns the disbursement-PSP integration under the Kaptl × TurnKey contract. Prepared by Kaptl; updated 16 July 2026.

1. Scope & ownership

Per the signed SOW, TurnKey integrates the payment providers server-side; Kaptl supplies credentials and approvals. Disbursement is routed by the method the borrower chose on the application:

Method Rail Mechanism Needs
Bank IRIS Account-to-account bulk-payment order Borrower IBAN
Cash EasyPay Money Send — collect at any EasyPay office Recipient name + national ID (no bank account)

Both settle in EUR. A single disbursement run may contain both — TurnKey splits it: bank loans into one IRIS bulk order, cash loans into EasyPay Money Send transfers (one per loan). The LOS front office only triggers and monitors; it never calls the providers directly.

Repayment is a separate concern (IRIS PayByLink/QR or EasyPay cash-office/card) and is out of scope for this brief. The EasyPay cash-office repayment path has its own brief: EasyPay Repayment.

2. End-to-end flow

sequenceDiagram
    participant BO as TurnKey Back Office (LOS)
    participant TK as TurnKey Server
    participant IRIS as IRIS PIS (bank)
    participant EP as EasyPay Money Send (cash)

    BO->>TK: Disburse selected approved loans
    Note over TK: split by loan.disbursementMethod
    par Bank loans
        TK->>IRIS: POST bulk-payment order [entries…] (x-agent-hash)
        IRIS-->>TK: { orderHash, entries:[{entryRef}] }
        IRIS-->>TK: webhook → entry CONFIRMED|FAILED (JWT via JWKS)
    and Cash loans
        TK->>EP: GET /ezp/send.cgi (per loan, ENCODED+CHECKSUM)
        EP-->>TK: SYS_CODE
        EP-->>TK: notification → STATUS=PAID|EXPIRED (ENCODED+CHECKSUM)
    end
    Note over TK: CONFIRMED/PAID → loan ACTIVE<br/>FAILED/EXPIRED → loan back to queue
    BO->>TK: poll disbursement status (both rails)

3. Bank rail — IRIS bulk-payment order

Auth

IRIS layered-hash headers (auth model): x-agent-hash (partner, initiates payments), JWKS (/api/6/.well-known/jwks.json) to verify inbound webhook JWTs. Sandbox developer.sandbox.irispay.bg, prod developer.irispay.bg.

Order

Submit one order carrying all bank loans as entries (debtor = Kaptl funding IBAN, creditor = borrower IBAN, amount = principal EUR, remittance ref = TurnKey loan PublicId). IRIS validates each line at submission — lines whose bank is unknown or not PIS-capable are rejected up front; the order still forms with the accepted lines. IRIS returns one orderHash and a per-entry reference; persist both. Settle via a JWT-verified, idempotent webhook (per-entry or whole-order).

Order status Meaning Loan effect
WAITING ≥1 entry unsettled loans stay DISBURSING
CONFIRMED all entries OK each loan → Active
PARTIAL mix, none waiting confirmed → Active; failed → re-queue
FAILED all entries failed all loans re-queued

4. Cash rail — EasyPay Money Send

Full spec: EasyPay → Money Send (Cash Disbursement). Same CIN + 64-char SECRET as all EasyPay (no separate contract); standard ePay signing (ENCODED = Base64 payload, CHECKSUM = hmac_sha1_hex(ENCODED, SECRET)).

  1. Order transferGET /ezp/send.cgi per loan with MIN (CIN), INVOICE (unique, digits — one INVOICE ⇒ one transfer, retry-safe), AMOUNT, RCPT_NAME, and recipient ID as a mandatory group: RCPT_PID (ЕГН) and/or RCPT_ID_NO (+RCPT_ID_DATE). Response: SYS_CODE=… or ERR=… (empty ⇒ retry with identical data). No IBAN required.
  2. Payout notification — on collection (or expiry) ePay POSTs INVOICE:STATUS:PAY_TIME:STAN:BCODE to your registered URL.
  3. STATUS=PAID → loan → Active.
  4. STATUS=EXPIRED → terminal non-payout (money-send transfers can expire uncollected) → loan back to the queue / re-attempt out of band. This path must be handled — unlike a card payment it is expected.
  5. Ack each notification: INVOICE=…:STATUS=OK (stop resending), …:STATUS=ERR (keep retrying), …:STATUS=NO (unknown invoice → stop). See the retry schedule in the EasyPay doc.

Currency is EUR. Bulgaria's euro adoption supersedes the older Money Send doc's "BGN only" note — confirm on onboarding, but build for EUR.

5. Reconciliation with TurnKey loan state

APPROVED ──disburse──▶ DISBURSING ──CONFIRMED (bank) / PAID (cash)──▶ ACTIVE
                            │
                            └──FAILED (bank) / EXPIRED (cash)────────▶ APPROVED (re-queue)

Map onto TurnKey's own disbursement operation (POST /Management/Loans/{loanId}/IssueDisbursement): the bulk order / money- send transfer is the funding mechanism behind that operation. A loan must not read as disbursed to downstream consumers (servicing, reports, the Public API Disbursements array) until its bank entry is CONFIRMED or its cash transfer is PAID. Run a reconciliation sweep for anything left WAITING past a timeout (query IRIS; read EasyPay Microaccount statuses) — don't rely on webhooks alone. Idempotency keys (IRIS submission id; EasyPay INVOICE) prevent double-disbursement on retry.

6. What Kaptl provides vs. what TurnKey builds

Kaptl provides TurnKey builds
IRIS agent + EasyPay merchant credentials (sandbox → prod) Method-routing (bank→IRIS order, cash→Money Send)
Funding-account IBAN + SCA/consent (bank); recipient-ID capture (cash) Both webhook receivers (JWT-verified IRIS; signed EasyPay ack OK/ERR/NO)
Loan → IBAN (bank) / recipient-ID (cash) mapping Status reconciliation + re-queue on FAILED/EXPIRED
Sign-off on the status→loan-state mapping (§5) Reconciliation sweep + idempotency keys

7. Reference implementation (behavioural contract)

Kaptl has built a working simulator of both rails — TurnKey can treat it as the behavioural contract and integration-test target (Simulator, kaptl-los-api-kbsimulator/), and the LOS prototype's Tools → Bulk disbursement console drives it:

Simulator endpoint Models
POST /disbursements/batch { loanIds } routes by method → { order:{…} \| null, cash:{transfers[]}, results[] }
GET /disbursements/order/:hash IRIS bank order aggregate + entries
GET /disbursements/cash EasyPay Money Send transfers
POST /webhooks/iris bank webhook — order-level or single-entry, idempotent
POST /webhooks/easypay-disbursement cash payout notification (INVOICE, STATUS=PAID\|EXPIRED) → ack OK/NO

The two-rail semantics (method routing, IRIS order lifecycle, Money Send PAID/EXPIRED, per-failure re-queue) are covered by the simulator's test suite.

8. Open items — [confirm on onboarding]

  1. IRIS agent credentials + EasyPay merchant registration (sandbox → prod).
  2. Outbound SCA/consent for bank disbursement from Kaptl's funding account.
  3. Money Send currency = EUR (confirm; docs predate euro adoption) and the notification retry/expiry window (doc says 30 days, public page 14).
  4. Exact request/response + webhook schemas against the live IRIS V6 spec and EasyPay Money Send — the simulator models the shapes, not the vendors' exact field names.
  5. IRIS commercial offer expires 31 July 2026 — lock the channel decision before then (PM checklist).