Case lifecycle & statuses
Your system sees a deliberately simple four-state model — internal operational sub-states are mapped onto it so we can evolve them without breaking you.
| External status | Meaning | Terminal? |
|---|---|---|
pending | Accepted, live in the network, awaiting a field partner | no |
in_progress | A partner is assigned / visiting / a payment is in flight. Also the state between a partial payment and re-assignment. | no |
collected | Fully collected (payments reached ≥ 99 % of the outstanding — 1 % tolerance) | yes |
closed_unrecovered | Closed without full recovery — recall, borrower not available / refused, write-off, legal handover | yes |
Visits and partial payments do not change the external status — the case
stays in_progress (each one arrives as its own visit.completed /
payment.collected event) until it fully clears or closes.
Which event fires when
| Moment | Event | Status after |
|---|---|---|
Your row is accepted (POST /cases, not a dry-run) | case.received | pending |
| A field partner accepts the case | case.assigned (opt-in) | in_progress |
| A visit is recorded, any outcome | visit.completed (opt-in) | in_progress |
Money booked — Mode A after our ops verifies the proof, Mode B when you call payments/confirm | payment.collected | in_progress (partial) — a full clear is followed by case.closed |
| Payments reach full clearance | payment.collected then case.closed status: collected, reason: full_settlement | collected |
You call POST /cases/{loan}/recall on a non-terminal case | case.closed status: closed_unrecovered, reason: recalled | closed_unrecovered |
| Field outcome ends the case (borrower not found / refuses) | case.closed reason: not_available or refused | closed_unrecovered |
| Fieldproof admin writes the case off | case.closed reason: written_off | closed_unrecovered |
| Fieldproof admin hands the case over for legal action | case.closed reason: legal_handover | closed_unrecovered |
Default webhook subscription is case.received, payment.collected,
case.closed; case.assigned and visit.completed are opt-in
(event reference). The
poll API always returns all types.
Details worth knowing:
- Recall is idempotent. On an already-terminal case it returns
alreadyClosed: truewith the existing status and emits nothing new. The assigned partner is told to stop. - Partial payment in Mode B (
org_gateway): the case detaches from the partner and re-enters the marketplace after a cool-down (default 48 h) — externally stillin_progress. - Re-pushing an active case (
pending/in_progress) updates it in place — outstanding, DPD, address… — with no new case. Use this to correct an address afterwrong_address/shifted. - Re-pushing a closed loan (
collected/closed_unrecovered) opens a fresh case: newcaseId,case.receivedagain, andsequencerestarts at 1. Watermark on(sourceLoanNumber, caseId)— see ordering. - One case per loan is active at a time; cases of the same loan never overlap.
Visit outcomes
Delivered in visit.completed events (opt-in) and in the visits[] array of
GET /cases/{loan}:
| Outcome | Meaning |
|---|---|
delivered_acknowledged | Payment notice delivered and acknowledged by the borrower |
collected / partial | Money collected (full / part) — the partner's claim; the booking arrives as payment.collected |
ptp | Promise to pay with a committed date (ptpDate) |
rtp | Refuse to pay — the borrower declined to pay |
refused / not_available | Borrower refused / not found this visit (also case.closed reasons when the case ends that way) |
wrong_address / shifted | Address problem — your cue to push a corrected address (just re-send the row) |
Reading state correctly
- The event stream is the narrative;
GET /api/v1/partner/cases/{sourceLoanNumber}is the authoritative snapshot — when in doubt (e.g. a sequence gap), fetch it. It returns the loan's active case if there is one, otherwise the most recent:{ sourceLoanNumber, caseId, status, isActive, assignedAt, totalCollected, outstandingAmount, visits[] (last 20), createdAt, updatedAt }. A404 CASE_NOT_FOUNDmeans we hold no case for that loan number under your org. visits[].amountCollectedis the partner's unverified claim until apayment.collectedconfirms it (Mode A: after ops verification, and the operator-confirmed amount may differ). Book money from events, not from visits.- Payment events carry absolute values (
totalCollected,outstandingAfter) — never accumulate deltas yourself. Amounts are whole INR rupees. - Never skip a
payment.collectedbecause it arrived out of order — book it and gate only your case-state fields onsequence(Updating your LMS).