Skip to main content

Data contract

One row = one overdue loan. The same Row shape is used by the push API, the pull API and (as columns, under different header names) the CSV path. Your team can see every case you send, and correct its data, in the organisation dashboard; edits made there pass the same validation as this page.

Ground rules that apply everywhere:

  • Money is whole INR rupees, sent as a JSON number. Never paise. Any amount is rounded to an integer on our side.
  • Unknown fields are ignored, never fatal — you can send your full export and let us pick the fields we know.
  • Every row needs a complete address and the real coordinates of that address — line1, city, state, pincode, latitude, longitude. See address and coordinates.
  • Rows fail individually. A row that fails validation is reported in errors[] with its index and reason; the rest of the batch still commits. A row that imported but needs your attention is reported in warnings[].
  • Optional fields that are present must parse. An unparseable loan.dpd, loan.nextDueDate or loan.disbursedDate is a per-row error (visible on a dry-run) — it does not silently become 0 or an empty date. This holds on every path (push, pull and CSV). Only in a CSV cell or a field read through a pull column mapping does a placeholder (NA, N/A, -, null, none …) count as empty instead.

The Row​

FieldTypeRequiredNotes
sourceLoanNumberstring, ≤ 64 charsYesYour stable loan ID and the key for every later lookup, update, event and payment. Unique within your organisation. Appears in URL paths (GET /cases/{sourceLoanNumber}) — percent-encode it there.
borrower.namestringYesFull name.
borrower.phonestringYesIndian mobile. A +91, 91 or leading 0 prefix is stripped; the result must be exactly 10 digits or the row errors.
borrower.emailstringNoBorrower email.
borrower.languagestringNoBorrower's preferred language (free text, e.g. hindi). Briefs the field partner.
borrower.employerNamestringNoEmployer name.
borrower.address.line1stringYes (or full)House / flat / building number and street — where the borrower actually lives. At least 5 characters, must contain a letter, and must not be a placeholder (NA, N/A, -, null, none, nil, unknown …).
borrower.address.fullstringInstead of line1A single-line address. Accepted in place of line1, under the same rule.
borrower.address.line2stringNoSecond address line — floor, wing, area / locality.
borrower.address.landmarkstringNoLandmark that helps the field partner find the house.
borrower.address.citystringYesCity or town. At least 2 characters, must contain a letter, not a placeholder.
borrower.address.statestringYesIndian state or union territory: the full name or the 2-letter code (KA, MH, DL, TN, TS/TG, UP, WB …). Letter case, spacing and punctuation don't matter (Tamilnadu, J & K, U.P., Andhra-Pradesh), and legacy names and common misspellings are recognised (Orissa, Pondicherry, NCT of Delhi, New Delhi, Chattisgarh, Telengana, Uttrakhand, Maharastra, Gujrat, Bengal …). Stored as you sent it.
borrower.address.pincodestring, exactly 6 digitsYesPIN code of the address. Also the reference for the distance check.
borrower.address.latitudestring (recommended) or numberYesWGS84 decimal degrees, at least 4 decimal places (6 recommended). See coordinate rules.
borrower.address.longitudestring (recommended) or numberYesSame rules as latitude.
loan.outstandingAmountnumber (INR), > 0YesTotal currently due. Rounded to whole rupees.
loan.dpdnumber or string range "lo-hi"NoDays past due. A range such as "61-90" is accepted and its upper bound (90) is used. Anything else that is present but unparseable → per-row error. Omitted on a re-push → reset to 0.
loan.dpdBucket——Derived by us from loan.dpd. Do not send it.
loan.loanAmountnumber (INR)NoSanctioned amount.
loan.disbursedAmountnumber (INR)NoDisbursed amount.
loan.emiAmountnumber (INR)NoEMI. Shown to the partner.
loan.lateChargesnumber (INR)NoLate charges / penalty. Omitted on a re-push → reset to 0.
loan.interestRatenumberNoRate of interest (%).
loan.tenurenumberNoLoan tenure.
loan.tenureUnitstringNoUnit for loan.tenure (for example months).
loan.totalRepayablenumber (INR)NoTotal repayable.
loan.totalCollectednumber (INR)NoAmount already collected by you before hand-over.
loan.productNamestringNoProduct / loan type shown to the partner.
loan.nextDueDatedate stringNoAccepted formats below. Parsed in UTC. Present but unparseable → per-row error.
loan.disbursedDatedate stringNoAccepted formats below. Parsed in UTC. Present but unparseable → per-row error.

Any other property — at the top level, inside borrower, borrower.address or loan — is ignored.

Accepted date formats​

loan.nextDueDate and loan.disbursedDate accept any of:

FormatExample
YYYY-MM-DD2026-09-05
ISO 8601 datetime2026-09-05T00:00:00.000Z
DD-MM-YYYY05-09-2026
DD/MM/YYYY05/09/2026
DD-Mon-YY28-Apr-26

All are parsed in UTC — send the calendar date you mean, not a locally-shifted timestamp. A date that is present but matches none of these fails the row (loan.nextDueDate is not a recognised date …).

DPD​

loan.dpd is either a plain number (45) or a "lo-hi" bucket string ("0-30", "31-60", "61-90", "91-180" …); for a range we use the upper bound. We derive loan.dpdBucket from it. Present but unparseable ("n/a", "-", "overdue") → per-row error loan.dpd must be a number or a "lo-hi" range. Omit the field rather than sending a placeholder — but remember that an omitted dpd on a re-push resets the stored value to 0, so send it on every push if you have it. (In a CSV cell or a mapped pull field, a placeholder such as NA or - counts as empty — so it, too, resets DPD to 0 on an update — while any other unreadable value such as overdue fails the row.)

Phone normalisation​

+919876543210, 919876543210, 09876543210 and 9876543210 all become 9876543210. Anything that does not reduce to 10 digits fails the row with borrower.phone does not normalize to a 10-digit Indian mobile.

A complete row​

one Row with every field populated
{
"sourceLoanNumber": "LN-2026-000123",
"borrower": {
"name": "Ravi Sharma",
"phone": "+919876543210",
"email": "[email protected]",
"language": "hindi",
"employerName": "Acme Textiles Pvt Ltd",
"address": {
"line1": "12 MG Road",
"line2": "Near Trinity Metro",
"city": "Bengaluru",
"state": "Karnataka",
"pincode": "560001",
"latitude": "12.975326",
"longitude": "77.606262",
"landmark": "Opp. Garuda Mall"
}
},
"loan": {
"outstandingAmount": 12500,
"dpd": 45,
"loanAmount": 50000,
"disbursedAmount": 48500,
"emiAmount": 2500,
"lateCharges": 350,
"interestRate": 24,
"tenure": 24,
"tenureUnit": "months",
"totalRepayable": 60000,
"totalCollected": 47500,
"productName": "Personal Loan",
"nextDueDate": "2026-09-05",
"disbursedDate": "15-Mar-25"
}
}

Address and coordinates​

Field collection is location-driven. The pin you send is where we send a field partner, and it is used for:

  • Partner matching — the case is offered to partners by distance from the pin, nearest first.
  • The check-in geofence — a partner's visit check-in is measured against the pin.
  • Distance-based partner pay — the partner's earning for the visit is measured to the pin.

A wrong or approximate pin sends a partner to the wrong door and the visit fails. That is why every row must carry a complete postal address (line1 or full, city, state, pincode) and the real coordinates of that address: the actual building — not the centre of a pincode or city, not your branch or office, not a default value.

Coordinate rules​

  • Decimal degrees (WGS84), latitude and longitude in two separate fields, with a dot as the decimal separator — what phone GPS, Google Maps and most address-verification / KYC providers return.
  • A JSON number (12.975326) or a numeric string ("12.975326") is accepted. Send strings — see the warning below.
  • At least 4 decimal places on both values (≈ 11 m); 6 are recommended. Fewer decimals is a rounded, area-level point, not a door.
  • Inside India: latitude 6.0 to 37.5, longitude 67.5 to 97.8.

:::warning Send coordinates as strings We count decimal places on the text exactly as sent only for a string. A JSON number is parsed first, so its trailing zeros are lost on our side even if your request body contains them: 12.970000 sent as a number is read as 12.97 and fails the 4-decimal rule. Send coordinates as strings: "latitude": "12.970000". :::

Each of these is a per-row error. The reason in errors[] starts with the usual Row N (LOAN): prefix, followed by:

Problemreason after the prefix
Both coordinates missing (empty, or a placeholder such as NA, N/A, -, null, none)borrower.address.latitude and borrower.address.longitude are required — send the real coordinates of the borrower's address
Only one sent (the other missing or a placeholder)borrower.address.latitude is missing (longitude was sent) — or the same for longitude
Not a numberborrower.address.latitude is not a number (got "TBD")
Degrees-minutes-seconds (12°58'31"N), or a N/S/E/W letter next to the number (12.97 N)borrower.address.latitude must be decimal degrees (e.g. 12.971599), not degrees-minutes-seconds
Comma as decimal separator (12,975326)borrower.address.latitude uses a comma as the decimal separator — use a dot (12.971599)
0, 0borrower.address.latitude/longitude are 0,0 — send the real coordinates of the address
Latitude and longitude swappedborrower.address.latitude/longitude look swapped (got latitude 77.606262, longitude 12.975326)
Outside Indiaborrower.address.latitude/longitude (51.507351, -0.127758) are outside India
Fewer than 4 decimal placesborrower.address.latitude/longitude need at least 4 decimal places (≈11 m) — got 12.97, 77.6. Send the coordinates of the door, not a rounded or city-level point
Same pin used in different pincodessee one pin per address

The address fields are checked the same way:

Problemreason after the prefix
line1 (and full) missing, shorter than 5 characters, without a letter, or a placeholderborrower.address.line1 is required — house / flat / building and street (at least 5 characters, not a placeholder)
city missing, shorter than 2 characters, without a letter, or a placeholderborrower.address.city is required
state missingborrower.address.state is required
state not a recognised state / UT name or codeborrower.address.state is not a recognised Indian state or union territory (got "Karnatka")

A row reports only its first problem: the basic checks (name, phone, amount, pincode, DPD, dates) first, then line1, city, state and the coordinates, in that order. A fixed row can therefore fail on the next rule — dry-run again after a fix.

One pin per address​

If the same coordinate pair (compared to 5 decimal places) is used for rows in different pincodes within one batch, it is a default, office or branch location rather than a borrower's door, and every row using it is rejected:

Row 12 (LN-2026-000135): borrower.address.latitude/longitude (12.975326, 77.606262) are also used for addresses in other pincodes (682001) — send each borrower's own address coordinates, not a default or office location

The same pin on several rows with the same pincode is fine — family members, or several borrowers in one building.

Far from the pincode (warning)​

If a pin is more than 50 km from our reference point for the row's pincode, the row still imports and comes back in warnings[]:

Row 4 (LN-2026-000127): borrower.address.latitude/longitude are about 63 km from our reference point for pincode 560001 — imported, but please verify they are this borrower's address

This is not a rejection because pincode reference data is noisy, and some genuine addresses sit far from it. Do check the row, though: if the pin is wrong, a partner goes to the wrong place.

What we do with your coordinates​

  • They are stored as the case's pin with precision org_supplied, shown as Your coordinates in the organisation dashboard.
  • They are used immediately for partner matching, the check-in geofence and distance-based pay — the case does not wait for any geocoding.
  • They are never overwritten by our geocoding.
  • One exception: once one of our partners has physically verified the address on a visit (an on-site GPS fix), that verified pin is kept on later re-pushes, unless the address itself changes in your row.
  • A re-push with different coordinates moves the pin and counts as an update (summary.updatedSnapshot). Re-sending the same coordinates changes nothing.

Grace period​

By default the rules on this page are enforced for every organisation. If you need time to add coordinates to your export, Fieldproof operations can put your organisation on a grace period, optionally with an end date — ask [email protected]. During a grace period:

  • A row with both coordinates absent still imports — we place it from the pincode / street address, as before — and comes back in warnings[]. The same applies to a missing line1, city or state, and to a state we do not recognise. A placeholder value (NA, N/A, -, null, none …) counts as absent.
  • Coordinates that are sent are always validated: a half pair (only one of the two) or a malformed value is an error even during grace.
  • A batch committed under the strict rules before your grace period started can be re-sent unchanged: it is processed again under the grace rules, not returned as a replay.
  • The grace period ends automatically at its end date. From then on the same rows are errors, so work through your warnings[] before that.

A case without your coordinates starts at pincode-area precision and is not offered to partners until our geocoding locates the street address. An address we cannot locate waits until it is corrected — you can fix it in the organisation dashboard or in a re-push. A corrected address is geocoded afresh, even if the old one had used up its attempts; sending its coordinates places it immediately.

The batch envelope (push API)​

{
"fullSnapshot": false,
"rows": [ { "sourceLoanNumber": "LN-…", "borrower": { "…": "…" }, "loan": { "…": "…" } } ]
}
  • rows — at least 1 row. Hard cap 20,000 rows per request (413 TOO_MANY_ROWS above that); keep batches ≤ 5,000 for fast responses.
  • fullSnapshot (optional, default false) — asserts "this batch is my complete open book". Any of your active cases absent from the batch is flagged for our operations review (borrower may have paid you directly); you get only the count back in summary.flaggedNeedsReview. A loan whose row is in the batch but was rejected for a data error counts as present and is not flagged. Because of that, a snapshot must be one single request (chunking it would flag everything in the other chunks) — so it is limited to 20,000 rows. Use it for a daily/weekly reconciliation push, never for incremental pushes.
  • Duplicate sourceLoanNumber inside one batch — the first row is kept and every later duplicate is reported as a per-row error (duplicate sourceLoanNumber in file — row N kept, this row skipped). Send each loan once per batch, with its latest state.

Re-push semantics​

Re-sending a row for a loan that has an active case updates that case in place — outstanding, DPD, contact details, address, coordinates. We hold at most one active case per loan number, so a re-push can never create a duplicate.

  • Optional fields you omit on a re-push are preserved from the earlier push — with two exceptions: loan.dpd and loan.lateCharges are reset to 0 when omitted, so always resend them.
  • Nothing can be cleared to null via a re-push.
  • New coordinates move the case's pin, except over a partner's verified pin for an unchanged address — see what we do with your coordinates.
  • If the loan's previous case is already closed (collected or closed_unrecovered), a re-push opens a fresh case with a new caseId, and its event sequence restarts at 1 — see push API and updating your LMS.

Per-row errors​

Validation failures come back per row, never as a whole-batch failure:

"errors": [
{ "index": 17, "sourceLoanNumber": "LN-2026-000140", "reason": "Row 17 (LN-2026-000140): borrower.phone does not normalize to a 10-digit Indian mobile" },
{ "index": 23, "sourceLoanNumber": "LN-2026-000146", "reason": "Row 23 (LN-2026-000146): loan.nextDueDate is not a recognised date (use YYYY-MM-DD, DD-MM-YYYY or DD-Mon-YY; got \"31/13/2026\")" },
{ "index": 31, "sourceLoanNumber": "LN-2026-000154", "reason": "Row 31 (LN-2026-000154): borrower.address.latitude/longitude look swapped (got latitude 77.606262, longitude 12.975326)" }
]

errors[] is capped at 50 entries; summary.errored is always the true count. Only the envelope (missing rows, malformed JSON, too many rows) fails the whole request — see errors.

Per-row warnings​

Rows that were imported but need your attention come back in warnings[], in the same shape as errors[]:

"warnings": [
{ "index": 4, "sourceLoanNumber": "LN-2026-000127", "reason": "Row 4 (LN-2026-000127): borrower.address.latitude/longitude are about 63 km from our reference point for pincode 560001 — imported, but please verify they are this borrower's address" },
{ "index": 9, "sourceLoanNumber": "LN-2026-000132", "reason": "Row 9 (LN-2026-000132): borrower.address.latitude/longitude missing — the case was placed from the pincode / address instead; coordinates become required when your grace period ends" }
]
  • A row listed in warnings[] did import (on a dry-run: would import). It is counted in created, updatedSnapshot or unchanged like any other row — don't treat it as a failure. Check the data and re-push the corrected row.
  • warnings[] is capped at 50 entries. One row can carry more than one warning (for example a missing city and missing coordinates during a grace period); summary.warned is the true number of rows with at least one warning.
  • What can appear:
    • always — a pin far from its pincode;
    • only during a grace period — borrower.address.line1 is missing — it becomes required when your coordinate grace period ends (the same for city and state), borrower.address.state "…" is not a recognised Indian state — it will be rejected when your grace period ends, and the missing-coordinates warning shown above.

:::tip Dry-run first POST /api/v1/partner/cases?dryRun=1 runs exactly this validation and returns the same errors[] and warnings[] without writing anything — the fastest way to check a new export against the contract. :::