Reconciliation
Fieldproof never holds money, so there is no settlement to reconcile with us.
Reconciliation means one thing: every payment.collected event pairs with
exactly one credit in your own bank or gateway records, and vice versa. The
API gives you three independent lanes to prove it, from real-time to
month-end.
1. Event ↔ statement pairing (continuous)
Every payment.collected carries what you need to find the matching credit:
| Field | Meaning | Pair it against |
|---|---|---|
amount | Incremental INR rupees booked by this event | The credit amount |
mode | upi, card, netbanking, wallet, online, link, neft, nach, cheque, razorpay or other — the partner-recorded mode in Mode A (usually upi), the mode you sent in Mode B (default link); for a payment booked by Fieldproof's collection team, the mode they recorded, or other (a direct close; cash, IMPS or any mode outside this list) | Payment channel on your statement |
reference | Mode A: the UTR where extracted, otherwise null. Mode B: your gateway's payment id — the reference you confirmed. Collection team: the UTR / reference they entered; null on a direct close or a cash entry without one | UTR / gateway payment id |
totalCollected | Absolute total collected for the case so far | Running total for the loan |
outstandingAfter | Absolute remaining outstanding for the case | Your loan balance |
collectedAt | When the payment happened according to its source — not when we booked it. Mode A: the partner's visit check-out time; Mode B: your paidAt (or when we received the confirm, if omitted); collection team: the payment date they entered (direct close, or none entered: when they recorded it) | Value date, when reference is null |
eventId (envelope) | Unique per event — evt_<24hex> | Your dedupe key |
Rules that keep this lane clean:
- Dedupe on
eventId. Delivery is at-least-once; the same event can arrive twice by webhook and again from the poll feed. One booking pereventId. - Never skip a
payment.collectedbecause of ordering. Sequences have legitimate gaps (they count event types you are not subscribed to). Book the money — it is idempotent oneventId/reference— and gate only your case-state fields on sequence. See Updating your LMS. - Mode B: your own confirm log is the first thing to reconcile. The event
mirrors the
payments/confirmyou made, so a captured payment with no event means a confirm you never sent — or one that returned a409telling you to refund. Reconcile gateway captures → confirm log → events, in that order. Note that a409 DUPLICATE_CONFIRMATIONreturns onlycaseId, not the original booking; the booking of record is the event or the case snapshot, never the 409 body. - Mode A: the event may not equal the partner's claim. The operator-verified
amount is what is booked. A proof that is rejected produces no event at all.
Verification can come days after the visit, so the event can arrive well
after its
collectedAt.
2. Case snapshot (on demand)
GET /api/v1/partner/cases/{sourceLoanNumber} is the authoritative
per-loan view — use it to settle any single-loan question instantly, or to
re-sync a loan after a sequence gap.
{
"success": true,
"case": {
"sourceLoanNumber": "LN-2026-0001",
"caseId": "CC-2026-2388ED42",
"status": "in_progress",
"isActive": true,
"assignedAt": "2026-08-12T04:10:00.000Z",
"totalCollected": 3000,
"outstandingAmount": 9500,
"visits": [
{ "visitAt": "2026-08-18T09:20:00.000Z", "outcome": "partial", "amountCollected": 3000,
"paymentMode": "upi", "paymentReference": "UTR2026081812345", "ptpDate": null }
],
"createdAt": "2026-08-11T18:30:00.000Z",
"updatedAt": "2026-08-18T09:30:12.000Z"
}
}
statusis one ofpending,in_progress,collected,closed_unrecovered;isActivetells you whether the case is still in the field.totalCollectedandoutstandingAmountare absolute, in whole INR rupees.visitsholds the last 20 visits. In Mode A, a visit'samountCollected/paymentReferencebefore operations verify the proof is the partner's unverified claim — informative, not bookable. Book from events only.- If a loan has had more than one case (closed, then re-pushed), the snapshot prefers the active case, otherwise the most recent one.
404 CASE_NOT_FOUNDfor a loan you never pushed — and equally for another organisation's loan; the endpoint is not an existence oracle.
3. The poll feed as an independent audit lane
GET /api/v1/partner/cases/updates returns the same event envelopes your
webhook receives — every event type, whether or not you subscribed to it
by webhook, oldest first, with all fields identical. That makes it the natural
lane for a second, independent pass over any window:
GET /api/v1/partner/cases/updates?since=2026-08-01T00:00:00Z&limit=500
→ { "success": true, "events": [ … ], "nextCursor": "…", "hasMore": true }
GET /api/v1/partner/cases/updates?cursor=<nextCursor>&limit=500
…until hasMore is false
since(ISO datetime) is honoured on the first call only and filters by the time the event was received by the server; after that, follownextCursor(opaque). If both are sent,cursorwins.limitdefaults to 100, maximum 500.- Persist
nextCursoronly after you have processed the page. - Because the feed contains everything, it also lets you check for a
payment.collectedyou fear a webhook lost — no replay needed. Details in Poll API.
4. Book-level (daily or weekly): fullSnapshot: true
Push your complete open book with fullSnapshot: true
(push API). Any case Fieldproof still holds active
that is missing from your file is flagged for Fieldproof operations review
(snapshot_dropped); you receive only the count — summary.flaggedNeedsReview
— not the list. This catches loans that were settled directly with you and
never recalled.
Send it as one single batch: chunking a full snapshot would flag every case absent from the chunk. Do it daily or weekly, never incrementally.
Month-end
Your monthly bank or gateway statement is the audit pair for the month; the Fieldproof side of the pairing comes straight from the API — there is no separate statement file to wait for:
- After the month ends, pull events from the poll feed with
sinceat or before the IST start of the month, written in UTC — for September,since=2026-08-31T18:30:00Z(or simply a day earlier; step 2 filters oncollectedAt). If you send an offset such as+05:30, percent-encode the+as%2B, or the request fails. Page to the end (today). - Keep the
payment.collectedevents whosepayload.collectedAtfalls inside the month — not the ones received in it:sincefilters on when our server stored the event, and a Mode A payment verified in the first days of the next month still belongs to this one. We bill on IST calendar months; use the same boundaries. A proof verified after you run arrives later with an in-monthcollectedAt— carry it into your next run. - Sum
amountpersourceLoanNumberover those events (deduped oneventId); compare against the credits on your statement. - Spot-check the last
totalCollectedper loan againstGET /cases/{sourceLoanNumber}for anything that does not agree. - Anything you cannot pair — an event without a credit, or a credit without an
event — goes to [email protected] with the
eventId(or the UTR / gateway payment id) attached. That is a support escalation, not a normal month.