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)).
- Order transfer —
GET /ezp/send.cgiper loan withMIN(CIN),INVOICE(unique, digits — one INVOICE ⇒ one transfer, retry-safe),AMOUNT,RCPT_NAME, and recipient ID as a mandatory group:RCPT_PID(ЕГН) and/orRCPT_ID_NO(+RCPT_ID_DATE). Response:SYS_CODE=…orERR=…(empty ⇒ retry with identical data). No IBAN required. - Payout notification — on collection (or expiry) ePay POSTs
INVOICE:STATUS:PAY_TIME:STAN:BCODEto your registered URL. STATUS=PAID→ loan →Active.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.- 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-kb → simulator/),
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]¶
- IRIS agent credentials + EasyPay merchant registration (sandbox → prod).
- Outbound SCA/consent for bank disbursement from Kaptl's funding account.
- Money Send currency = EUR (confirm; docs predate euro adoption) and the notification retry/expiry window (doc says 30 days, public page 14).
- 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.
- IRIS commercial offer expires 31 July 2026 — lock the channel decision before then (PM checklist).