Skip to content

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-joined KEY=VALUE payload.
  • 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 ack INVOICE=…:STATUS=OK so EasyPay stops resending.
  • STATUS=EXPIRED → the code lapsed uncollected (EXP_TIME passed); 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=OK and surface it for follow-up.
  • Unknown INVOICE → ack …:STATUS=NO.
  • Transient failure applying the payment → ack …:STATUS=ERR so EasyPay retries.

The colon-line notification shape, the OK/ERR/NO ack semantics and the resend schedule are written up in the KB for Money Send (disbursement §notification) — the bill/collection flow reuses the same NotificationPayload, 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 PAID for an already-applied INVOICE is a no-op (the simulator returns replayed: true).
  • Overpayment guard — reject/adjust an amount above the installment due or the outstanding balance.
  • State guard — only ACTIVE loans accept a repayment code; SETTLED and 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.CURRENCY accepts EUR but defaults to BGN — always set EUR explicitly 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-kbsimulator/):

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-prototypesrc/api/easypay.ts.

10. Open items — [confirm on onboarding]

  1. EXP_TIME policy — how long an unpaid code stays payable — and the notification retry schedule on ERR acks (Money Send doc says 30 days, public page 14). Confirm the bill/collection flow uses the same notification colon-line + OK/ERR/NO ack as Money Send (assumed parity, §5).
  2. CURRENCY=EUR operationally confirmed with EasyPay (accepted by the spec; default is BGN).
  3. Office choice is informationalEasyPayBillPayload has no office field, so the code is payable at any EasyPay office. Confirm no separate steering mechanism is expected (the simulator accepts an officeId but does not constrain collection).
  4. 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.
  5. 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 as loanId + DueDate.
  6. Scheduler lead time — confirm the configurable number of days ahead of due date at which registrations are generated (Kaptl proposes 10).