Skip to main content

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 statusMeaningTerminal?
pendingAccepted, live in the network, awaiting a field partnerno
in_progressA 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
collectedFully 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_unrecoveredClosed without full recovery — recall, borrower not available / refused, write-off, legal handoveryes, 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​

MomentEventStatus after
Your row is accepted (POST /cases, not a dry-run)case.receivedpending
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 verificationvisit.completed (opt-in)in_progress
Money booked — Mode A after our ops verifies the proof, Mode B when you call payments/confirmpayment.collectedin_progress (partial) — a full clear is followed by case.closed
Payments reach full clearancepayment.collected then case.closed status: collected, reason: full_settlementcollected
Fieldproof's collection team books a payment taken outside the partner flow — record paymentpayment.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 directlypayment.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 casecase.closed status: closed_unrecovered, reason: recalledclosed_unrecovered
Visit limit reached with the borrower not found / refusingcase.closed reason: not_available or refusedclosed_unrecovered
Fieldproof admin writes the case offcase.closed reason: written_offclosed_unrecovered
Fieldproof admin hands the case over for legal actioncase.closed reason: legal_handoverclosed_unrecovered
You re-push an active case, or edit it on your organisation dashboardnone — your own changeunchanged

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: true with 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_progress until the cool-down ends, then pending with 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 after wrong_address / shifted.
  • Re-pushing a closed loan (collected / closed_unrecovered) opens a fresh case: new caseId, case.received again (even if the batch is byte-identical to an earlier one), and sequence restarts 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_unrecovered case under the same caseId. (If ops instead assign the re-opened case straight to a partner, case.assigned is sent at once and it never shows pending.)

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}:

OutcomeMeaning
delivered_acknowledgedPayment notice delivered and acknowledged by the borrower. With ptpDate set it is a promise to pay by that date
collected / partialMoney collected (full / part) — the partner's claim; the booking arrives as payment.collected
rtpRefuse to pay — the borrower declined to pay
refused / not_availableBorrower refused / not found this visit (also case.closed reasons when the case ends that way)
wrong_address / shiftedAddress 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 }. A 404 CASE_NOT_FOUND means we hold no case for that loan number under your org.
  • visits[].amountCollected is the partner's unverified claim until a payment.collected confirms it (Mode A: after ops verification, and the operator-confirmed amount may differ). Book money from events, not from visits.
  • visits[].ptpDate is the IST-midnight instant of the promised day (a promise for 20 Aug reads 2026-08-19T18:30:00.000Z), while visit.completed carries the plain date 2026-08-20.
  • Payment events carry absolute values (totalCollected, outstandingAfter) — never accumulate deltas yourself. Amounts are whole INR rupees.
  • Never skip a payment.collected because it arrived out of order — book it and gate only your case-state fields on sequence (Updating your LMS).
  • case.closed means "closed as of now". A later event for the same caseId (higher sequence) supersedes it — apply it like any other event. A re-opened case that closes again sends a new case.closed when it closes a different way or when any other event was recorded since the last closure.