TurnKey ← EasyPay — Cash-Office Repayment Integration Brief¶
Integration instructions for TurnKey Lender, who owns the payment-provider integration under the Kaptl × TurnKey contract. Prepared by Kaptl; 16 July 2026, revised 31 July 2026.
1. Scope & ownership¶
This brief covers inbound repayment in cash at an EasyPay office — the borrower walks in, quotes a payment code, and pays their installment over the counter. It is the mirror image of the Mass Disbursement brief, which the mass-disbursement brief explicitly left out of scope.
| Disbursement (mass-disbursement brief) | Repayment (this brief) | |
|---|---|---|
| Direction | Lender pays out to borrower | Borrower pays in to lender |
| EasyPay product | Money Send (/ezp/send.cgi) |
Payment via EasyPay — 10-digit collection code |
| Money at the office | Recipient collects cash | Borrower hands over cash |
Per the signed SOW, TurnKey integrates EasyPay server-side; the LOS front office only registers the code and reconciles the payout. It never calls EasyPay directly. IRIS PayByLink/QR (bank repayment) is a separate rail and not covered here.
2. End-to-end flow¶
sequenceDiagram
participant BO as TurnKey Back Office (LOS)
participant TK as TurnKey Server
participant EP as EasyPay (collection)
participant B as Borrower @ office
BO->>TK: Repay loan L, installment N (or early payoff)
TK->>EP: register obligation (ENCODED+CHECKSUM) → 10-digit code
EP-->>TK: payment code + expiry
TK-->>BO: show code + payable-at window
Note over B: borrower goes to any EasyPay office, quotes the code
B->>EP: pays cash
EP->>TK: notification (ENCODED+CHECKSUM) STATUS=PAID
TK-->>EP: ack OK / ERR / NO
Note over TK: PAID → apply payment to schedule<br/>EXPIRED → drop code, borrower re-requests
3. Authentication & signing¶
Same EasyPay merchant identity as Money Send — no separate contract: the
same CIN + 64-char SECRET (auth model). Every request
and every notification uses standard ePay signing:
ENCODED= Base64 (RFC 3548,EOL='') of the newline-joinedKEY=VALUEpayload.CHECKSUM=hmac_sha1_hex(ENCODED, SECRET).
4. Registering the collection code¶
The borrower repays against a registered obligation carrying a 10-digit
identification code — the WEB API's "payment via EasyPay". TurnKey registers one
per repayment intent via GET /ezp/reg_bill.cgi (the standard
ENCODED+CHECKSUM envelope; payload is the EasyPayBillPayload model). Full
request/response is on the EasyPay API Swagger page.
Field (ENCODED payload) |
Required | Notes |
|---|---|---|
MIN |
Yes | Merchant CIN. |
INVOICE |
Yes | Digits, unique to merchant (spec) — use it as the idempotency key. Money Send guarantees a repeated request returns the same code rather than a second obligation; assume the same one-time rule here and confirm. |
AMOUNT |
Yes | > 0.01 — installment due, a partial amount, or the early-payoff settlement figure. |
EXP_TIME |
Yes | Payment end date/time, DD.MM.YYYY[hh:mm[:ss]]; on lapse the code expires. |
CURRENCY |
No | BGN, USD or EUR; defaults to BGN — set EUR explicitly (see §5). |
DESCR |
No | ≤100 chars — human-readable reference (loan PublicId + period). |
EMAIL |
No | Merchant email. |
ENCODING |
No | utf-8 for a non-CP1251 DESCR. |
A successful call returns the 10-digit code (IDN) the borrower quotes at the
office.
One code, one obligation. The code is bound to a fixed AMOUNT and a fixed
EXP_TIME. Each installment, each agreed partial payment and each early-payoff
figure is therefore a separate registration — there is no persistent
per-borrower code. An 18-month loan generates at least eighteen codes, and more
once expiries, partials and payoff quotes are counted.
5. Notification & reconciliation¶
On payment (or expiry) EasyPay POSTs a signed notification to TurnKey's
registered URL — the shared NotificationPayload
(INVOICE:STATUS:PAY_TIME:STAN:BCODE, STATUS ∈ {PAID, DENIED, EXPIRED}). Ack
each one:
STATUS=PAID→ apply the payment to the loan schedule (installment N settled, or early payoff), then ackINVOICE=…:STATUS=OKso EasyPay stops resending.STATUS=EXPIRED→ the code lapsed uncollected (EXP_TIMEpassed); drop it and let the borrower request a fresh one. Ack…:STATUS=OK.STATUS=DENIED→ the collection attempt was rejected; do not apply a payment. Ack…:STATUS=OKand surface it for follow-up.- Unknown
INVOICE→ ack…:STATUS=NO. - Transient failure applying the payment → ack
…:STATUS=ERRso EasyPay retries.
The colon-line notification shape, the
OK/ERR/NOack semantics and the resend schedule are written up in the KB for Money Send (disbursement §notification) — the bill/collection flow reuses the sameNotificationPayload, so this brief assumes parity. Confirm on onboarding, along with the resend window (the Money Send doc notes a 30-day vs 14-day discrepancy between the internal doc and the public page).
The notification carries INVOICE, not the 10-digit code. Reconciliation
therefore runs entirely on our own reference; the code is borrower-facing display
only and must never be used as a matching key.
Idempotency & guards TurnKey must own:
- Replay-safe application — a resent
PAIDfor an already-appliedINVOICEis a no-op (the simulator returnsreplayed: true). - Overpayment guard — reject/adjust an amount above the installment due or the outstanding balance.
- State guard — only
ACTIVEloans accept a repayment code;SETTLEDand not-yet-disbursed loans are refused. - Map onto TurnKey's own repayment operation so servicing, reports and the
Public API reflect the payment only once EasyPay confirms
PAID.
Currency is EUR.
EasyPayBillPayload.CURRENCYacceptsEURbut defaults toBGN— always setEURexplicitly on registration.
6. Mapping onto the TurnKey schedule¶
Checked against the v7.12 Public API spec (API); the mapping is less direct than it first appears.
RefNum is the reconciliation anchor, and it already exists.
POST /Management/Loans/Repayments accepts a free-text RefNum, and the value
round-trips — it is present on LoanRepayment when the loan is read back. The
EasyPay INVOICE goes here. No new field is required for the identifier that
actually links cash to loan.
There is no installment identifier. LoanDetails.Schedule is an array of
InstallmentDetails, whose properties are Total, Principal, Interest,
PastDue, Fee, Penalty, Leasing, Tax, DownPayment, Other,
Commissions, DueDate, IsClosed and Status — no Id. This is deliberate
rather than an omission: LoanDetails and LoanRepayment both carry Id.
Installments are exposed as a computed projection of the loan, not as addressable
entities, so the only handles available are DueDate and array position.
InstallmentStatus is one of Scheduled, PaidOnTime, PaidEarly, PaidLate,
Missed, Closed.
Repayments post at loan level, not installment level.
POST /Management/Loans/Repayments takes loanid, Date, Amount, RefNum,
Type, Mode, Service, Comments — no installment reference anywhere.
Allocation is performed by the calculation engine's repayment priority, steered
only by Mode (0 Regular, 1 Extra, 2 Payoff, 3 Initial/down-payment).
Type=1 (Cash) is the correct transaction type for an EasyPay counter payment.
Source of the amount to register.
GET /PaymentOptions/RepaymentAmounts/{loanId} returns CurrentInstallment,
BadDebt and Payoff — covering the scheduled-installment and early-payoff
cases directly.
Registration must be scheduled, not pre-generated. Because EXP_TIME is
mandatory and capped, a full amortisation schedule cannot be registered at
disbursement — later codes would expire long before they were needed. TurnKey
should generate each registration on a rolling basis, a configurable number of
days ahead of the relevant due date. Kaptl proposes 10 days, to be confirmed
by the Risk & Credit Manager with the Support & Collections Lead; build it as a
parameter rather than a constant.
Re-issue after expiry. An expired code leaves the obligation outstanding.
Re-registration is a new record against the same due date under a new
INVOICE — the previous reference cannot be reused.
7. Back office¶
Until the borrower app ships in Phase 2, the back office is the only channel through which a borrower receives a payment code. The loan screen therefore needs the active registration — code, amount and expiry — legible enough for an agent to read out over the phone, alongside the full registration history for the loan, including expired and cancelled rows, which is what dispute handling and collections review both depend on.
Most registrations come from the scheduler (§6) and need no human involvement. Two cases fall outside it:
Automatic invalidation. When a loan is paid off through another rail,
restructured, or written off, any live cash codes against it must be killed by
the system, by setting a past EXP_TIME. Without this a borrower can walk into
an EasyPay office and pay against an installment that no longer exists, leaving
Kaptl unwinding cash it should not have received. This is a system behaviour, not
an agent action.
Agent-created registrations. Partial payments are agreed on a call — a borrower offers €120 against a €200 installment — and because the code is amount-bound, the agent needs to create that registration while the borrower is on the line. The same applies to an early-payoff figure on request. Both are permission-controlled; Kaptl will supply the role matrix and amount limits.
Manual expiry extension is a lower priority. Its only real use is a borrower who already holds the digits and slips a few days past expiry: re-registering works but issues different digits, which the borrower may not have to hand. Worth including if it comes cheaply; not worth building if it does not.
8. What Kaptl provides vs. what TurnKey builds¶
| Kaptl provides | TurnKey builds |
|---|---|
EasyPay merchant credentials (CIN/SECRET, sandbox → prod) |
Code registration per repayment intent (installment / partial / early payoff) |
| Loan → amount-due / settlement-figure mapping | Rolling scheduler generating registrations ahead of each due date (§6) |
| Confirmation of the scheduler lead time (proposed 10 days) | Notification receiver (signed) + ack OK/ERR/NO |
| Sign-off on the status → schedule mapping (§5) | Payment application via RefNum + overpayment / replay / state guards |
| Back-office role matrix and amount limits for agent-created registrations | Back-office display of active code, amount, expiry + registration history (§7) |
| Office-network sign-off (informational directory) | Automatic invalidation of live codes on payoff / restructure / write-off |
| Reconciliation sweep for codes left unpaid past expiry |
9. Reference implementation (behavioural contract)¶
Kaptl's simulator models this flow end-to-end — TurnKey can integration-test
against it (Simulator, kaptl-los-api-kb → simulator/):
| Simulator endpoint | Models |
|---|---|
GET /easypay/offices |
the EasyPay office directory (sample of the network) |
POST /repayments/easypay/code { loanId, officeId?, period? \| amount? \| early } |
register a collection code → { code, payableAt, amountEur, … } (the response also carries amountBgn and an informational banner — ignore both; EUR is the working currency) |
POST /repayments/easypay/pay { code } |
models the borrower paying cash → applies the payment, idempotent (replayed) |
The LOS prototype's Tools → EasyPay repayment console drives these: pick an
office, generate a code for the next installment / a partial amount / an early
payoff, and settle it. The office picker, amount guards and schedule view show
the borrower-facing shape TurnKey reconciles against. See kaptl-los-prototype
→ src/api/easypay.ts.
10. Open items — [confirm on onboarding]¶
EXP_TIMEpolicy — how long an unpaid code stays payable — and the notification retry schedule onERRacks (Money Send doc says 30 days, public page 14). Confirm the bill/collection flow uses the same notification colon-line +OK/ERR/NOack as Money Send (assumed parity, §5).CURRENCY=EURoperationally confirmed with EasyPay (accepted by the spec; default isBGN).- Office choice is informational —
EasyPayBillPayloadhas no office field, so the code is payable at any EasyPay office. Confirm no separate steering mechanism is expected (the simulator accepts anofficeIdbut does not constrain collection). - Reminder-SMS trigger (via VertexSMS) the day before a due date, carrying the payment code — align with TurnKey's notification engine. With no borrower app in Phase 1, this and the back-office screen (§7) are the only two routes by which a borrower obtains the digits.
- Installment identity (§6) — a registration needs to be tied to a specific
scheduled installment, but the Public API exposes no installment
Id. Confirm whether TurnKey holds an internal identifier that can be surfaced, or whether the link should be modelled asloanId+DueDate. - Scheduler lead time — confirm the configurable number of days ahead of due date at which registrations are generated (Kaptl proposes 10).