Skip to main content

Event reference

Every event — delivered by webhook or returned by the poll API — uses the same envelope. The webhook body and the poll-API item are byte-for-byte the same shape.

The envelope​

{
"eventId": "evt_66a1f0c2d3e4f5a6b7c8d9e0",
"type": "payment.collected",
"occurredAt": "2026-08-17T09:31:00.000Z",
"version": 1,
"lenderCode": "YOURORG",
"sourceLoanNumber": "LN-2026-0001",
"sequence": 4,
"payload": { }
}
FieldTypeMeaning
eventIdstringevt_ + 24 hex chars (a ping uses evt_ping_ + 16 hex). Globally unique — your dedupe key.
typestringOne of the types below. Also sent as the X-Fieldproof-Event header.
occurredAtISO 8601 datetimeWhen the change happened. On payment.collected (and a case.closed it triggers) this is the payment time — the same value as payload.collectedAt — so it can be days earlier than the event's arrival.
versionnumberEnvelope version. Currently 1.
lenderCodestringYour organisation code.
sourceLoanNumberstring | nullYour loan number as pushed. null only on ping.
sequenceinteger | nullPer-case counter, starts at 1 for every new case, counts all event types (including ones you are not subscribed to). null only on ping.
payloadobjectType-specific, below.

All money values are whole INR rupees (numbers, no paise).

Subscription​

The default subscription is case.received, payment.collected, case.closed. case.assigned and visit.completed are opt-in — ask our integration team to enable them (CRM Integrations tab); there is no self-serve subscription API yet. Two consequences:

  • sequence still counts the events you don't receive, so a default subscriber sees gaps (1 → 4 → 5). That is normal — see Ordering.
  • The poll API returns all types regardless of your webhook subscription.

:::note Some changes emit no event Your own updates to an active case — a re-push, or an edit on your organisation dashboard — emit nothing. Nor does a case going back to pending (lease lapsed, timeouts, unassignment, a rejected payment proof…). GET /api/v1/partner/cases/{sourceLoanNumber} always shows the current state — see returns to pending. :::


case.received​

Your pushed row is a live case in the network. Also the first event of a fresh case when a loan whose previous case had closed is re-pushed — even in a batch byte-identical to one you sent before — the caseId changes and sequence restarts at 1.

payload
{ "sourceLoanNumber": "LN-2026-0001", "caseId": "CC-2026-2388ED42", "status": "pending" }
FieldValue
statusalways "pending"

case.assigned (opt-in)​

A partner now holds the case: a field partner accepted it, or Fieldproof operations or your organisation dashboard assigned or reassigned one. It fires again on every reassignment. External status becomes in_progress. Unassigning emits nothing — the case silently returns to pending.

payload
{ "sourceLoanNumber": "LN-2026-0001", "caseId": "CC-2026-2388ED42", "assignedAt": "2026-08-17T06:10:11.000Z" }

visit.completed (opt-in)​

A partner submitted a visit, whatever the outcome. The case stays in_progress unless a case.closed follows.

payload
{
"sourceLoanNumber": "LN-2026-0001",
"caseId": "CC-2026-2388ED42",
"outcome": "delivered_acknowledged",
"visitAt": "2026-08-17T08:45:02.000Z",
"ptpDate": "2026-08-20",
"notes": "Will pay after salary credit"
}
FieldTypeNotes
outcomeenumSee table below.
visitAtISO datetimeWhen the visit was submitted (partner check-out).
ptpDatestring | nullYYYY-MM-DD (IST). Set when the borrower promised to pay by that date (outcome: "delivered_acknowledged"), null otherwise.
notesstring | nullPartner's free-text note, truncated to 500 characters; may be null.

outcome values:

outcomeMeaning
delivered_acknowledgedPayment notice delivered and acknowledged by the borrower. With ptpDate set, this is a promise to pay by that date
collectedMoney collected in full during the visit
partialPart of the outstanding collected
rtpRefuse to pay — the borrower declined to pay
refusedBorrower refused (also a case.closed reason when the case ends this way)
not_availableBorrower not found at the address this visit
wrong_addressAddress does not lead to the borrower — push a corrected row
shiftedBorrower has moved — push a corrected row

There is no ptp outcome: it is no longer sent. A promise to pay is delivered_acknowledged with ptpDate.

A collected or partial outcome is the partner's claim; money is only announced by payment.collected once it is verified/booked (below). A Mode A visit that submits a payment proof emits no visit.completed at all — the payment.collected after verification is its signal (and a rejected proof emits nothing).

payment.collected​

Money received on your rails and booked against the case. Fires per payment, with both the incremental amount and the absolute case totals.

payload
{
"sourceLoanNumber": "LN-2026-0001",
"caseId": "CC-2026-2388ED42",
"amount": 3000,
"mode": "upi",
"reference": "UTR2026081712345",
"totalCollected": 3000,
"outstandingAfter": 9500,
"collectedAt": "2026-08-17T09:30:12.000Z"
}
FieldTypeNotes
amountnumber (INR)Incremental — this payment only.
modeenumupi | card | netbanking | wallet | online | link | neft | nach | cheque | razorpay | other. Never cash: partners cannot take cash on your cases, and a cash or IMPS payment our collection team records is sent as other.
referencestring | nullUTR / gateway payment id. null when no reference could be captured.
totalCollectednumber (INR)Absolute total collected on this case after this payment.
outstandingAfternumber (INR)Absolute outstanding on this case after this payment.
collectedAtISO datetimeWhen the payment happened according to its source — not when we booked it. Mode A: the partner's visit check-out time. Mode B: the paidAt you sent (or when we received your confirm, if you omitted it). Recorded by our collection team: the payment date they entered (or when they recorded it, if none). Direct close: when our team recorded it.

When it fires and what mode / reference carry:

  • Mode A — static QR: after our operations team verifies the partner's payment proof. amount is the operator-confirmed figure — it may differ from what the partner claimed; trust the event. mode is the partner-recorded mode (usually upi); reference is the UTR where extracted. Rejected proofs emit nothing.
  • Mode B — your gateway: the event mirrors your own POST /cases/{loan}/payments/confirm. mode echoes the mode you sent (default link); reference is the gateway payment id you sent.
  • Booked by Fieldproof's collection team (a payment taken outside the partner flow). Record payment: mode is the mode they recorded when it is in the list above, otherwise "other" (e.g. cash, IMPS); reference is the UTR / reference they entered, or null. If that payment completes the case, a case.closed follows. Direct close: mode is "other" and reference is null, sent only if they recorded an amount; a case.closed always follows. In both, the payment.collected has the lower sequence.

Money is never announced before it is booked. Use the absolute values to set your loan record; use amount for your ledger row.

case.closed​

The case is closed as of now. Normally it is the last event of a case, but Fieldproof operations can re-open a closed_unrecovered case under the same caseId (resolving an admin review, resetting visit attempts or correcting the address), and more events can follow. Re-opening itself sends no event. If the case then closes again, you receive a new case.closed (new eventId, higher sequence) when it closes a different way or when any other event was recorded on the case since the last closure; a closure that repeats the previous one with nothing in between is treated as a retry. A closed_unrecovered case can also get a second case.closed without being re-opened, when Fieldproof later writes it off or hands it over for legal action (new reason, higher sequence). Apply any later event for the same caseId (higher sequence) like any other: it supersedes the closure.

payload
{
"sourceLoanNumber": "LN-2026-0001",
"caseId": "CC-2026-2388ED42",
"status": "collected",
"reason": "full_settlement",
"totalCollected": 12500
}
FieldTypeNotes
statusenumcollected | closed_unrecovered
reasonenumfull_settlement | closed_by_operations | recalled | not_available | refused | written_off | legal_handover
totalCollectednumber (INR)Absolute total collected on this case at closure.
reasonstatusTrigger
full_settlementcollectedPayments reached full clearance (≥ 99 % of the remaining outstanding — 1 % tolerance), including a payment our collection team recorded, or a direct close, that cleared the balance. Preceded by a payment.collected.
closed_by_operationscollectedFieldproof's collection team closed the case with a balance left (collected outside the partner flow): a direct close with a balance left or no amount recorded, or a recorded payment they closed short of full clearance. Preceded by a payment.collected only if an amount was recorded. Any remaining balance is not reported as collected — reconcile it against your own records. On a direct close, outstandingAfter (and the snapshot's outstandingAmount) still shows the balance left; on a recorded payment the team closed short, both are 0. No case fee is billed for it.
recalledclosed_unrecoveredYou called POST /cases/{loan}/recall.
not_availableclosed_unrecoveredThe visit limit was reached and the last visit could not find the borrower.
refusedclosed_unrecoveredThe visit limit was reached and the last visit found the borrower refusing.
written_offclosed_unrecoveredWritten off — an admin action on the Fieldproof side.
legal_handoverclosed_unrecoveredHanded over for legal action — an admin action on the Fieldproof side.

A closed loan can be re-pushed (even in a byte-identical batch): that opens a new case with a new caseId and sequence restarting at 1 (you will see case.received again).

verification.completed​

:::note Address-verification product only verification.completed belongs to the address-verification product. It is not emitted for collection cases — if you only run collections you will never receive it, and you can ignore the type. Its payload is documented with that product. :::

ping​

Sent only by the webhook self-test (POST /api/v1/partner/webhooks/test). The envelope is the same shape, but sourceLoanNumber and sequence are null and the eventId is evt_ping_ + 16 hex chars:

full ping body
{
"eventId": "evt_ping_9c4e21a7b3d05f18",
"type": "ping",
"occurredAt": "2026-08-17T09:00:00.000Z",
"version": 1,
"lenderCode": "YOURORG",
"sourceLoanNumber": null,
"sequence": null,
"payload": { "message": "<free text>" }
}

Verify the signature, return 2xx, apply nothing. Your JSON model must accept the nulls.


Ready to consume these? Updating your LMS from our events has the complete, transactional apply handler with SQL.