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.
Dotted arrows emit no event at the moment they happen.
| 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. Not one-way — a case can drop back to pending without an event (below). | no |
collected | Fully collected (payments reached ≥ 99 % of the outstanding — 1 % tolerance), or closed by Fieldproof's collection team (a direct close, or a recorded payment they closed) | yes |
closed_unrecovered | Closed without full recovery — recall, borrower not available / refused, write-off, legal handover | yes, unless re-opened by Fieldproof operations (same caseId) |
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 partner gets the case: a field partner accepts it, or Fieldproof ops or your organisation dashboard assign / reassign one (fires on every reassignment) | case.assigned (opt-in) | in_progress |
A visit is submitted, any outcome — except a Mode A visit that uploads a payment proof: its only event is the payment.collected at verification | 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 |
| Fieldproof's collection team books a payment taken outside the partner flow — record payment | payment.collected (the recorded mode, or other for cash / IMPS / any mode outside the list); if it completes the case, then case.closed status: collected, reason: full_settlement (balance cleared) or closed_by_operations (closed with a balance left) | collected if completed, otherwise unchanged |
| Fieldproof's collection team closes the case directly | payment.collected mode: "other" if an amount was recorded, then case.closed status: collected, reason: full_settlement (balance cleared) or closed_by_operations (balance left, or no amount) | collected |
You call POST /cases/{loan}/recall on a non-terminal case | case.closed status: closed_unrecovered, reason: recalled | closed_unrecovered |
| Visit limit reached with the borrower not found / refusing | 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 |
| You re-push an active case, or edit it on your organisation dashboard | none — your own change | unchanged |
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 (Mode A once verified, Mode B on your confirm): the case
detaches from the partner and re-enters the marketplace after a cool-down
(default 48 h) — externally
in_progressuntil the cool-down ends, thenpendingwith no event. - Re-pushing an active case (
pending/in_progress) updates it in place — outstanding, DPD, address… — with no new case and no event. Use this to correct an address afterwrong_address/shifted. - Re-pushing a closed loan (
collected/closed_unrecovered) opens a fresh case: newcaseId,case.receivedagain (even if the batch is byte-identical to an earlier one), 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.
Returns to pending
A case can go back to pending (open on the marketplace, no partner)
silently:
- the partner's lease lapsed, or the partner gave the case back;
- a timeout: accepted but never visited, a visit started but never submitted, or the payment-pending window expired;
- the cool-down after a partial payment ended (released at the next 06:00 IST run after it, default 48 h), or a promise-to-pay date passed unpaid;
- Fieldproof ops or your organisation dashboard unassigned the partner;
- a Mode A payment proof was rejected;
- Fieldproof operations resolved an admin review, reset the case's visit
attempts or corrected its address — including re-opening a
closed_unrecoveredcase under the samecaseId. (If ops instead assign the re-opened case straight to a partner,case.assignedis sent at once and it never showspending.)
Only GET /api/v1/partner/cases/{sourceLoanNumber} shows these. Do not read
in_progress in your LMS as "a partner is working this right now"; reconcile
open loans with the snapshot on a schedule
(Updating your LMS).
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. With ptpDate set it is a promise to pay by that date |
collected / partial | Money collected (full / part) — the partner's claim; the booking arrives as payment.collected |
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) |
ptp is no longer sent. In visits[] only, you may also see null or
abandoned: a visit that was started but not (or never) submitted. No
visit.completed is emitted for those.
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.visits[].ptpDateis the IST-midnight instant of the promised day (a promise for 20 Aug reads2026-08-19T18:30:00.000Z), whilevisit.completedcarries the plain date2026-08-20.- 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). case.closedmeans "closed as of now". A later event for the samecaseId(highersequence) supersedes it — apply it like any other event. A re-opened case that closes again sends a newcase.closedwhen it closes a different way or when any other event was recorded since the last closure.