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 inwarnings[]. - Optional fields that are present must parse. An unparseable
loan.dpd,loan.nextDueDateorloan.disbursedDateis a per-row error (visible on a dry-run) — it does not silently become0or 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
| Field | Type | Required | Notes |
|---|---|---|---|
sourceLoanNumber | string, ≤ 64 chars | Yes | Your 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.name | string | Yes | Full name. |
borrower.phone | string | Yes | Indian mobile. A +91, 91 or leading 0 prefix is stripped; the result must be exactly 10 digits or the row errors. |
borrower.email | string | No | Borrower email. |
borrower.language | string | No | Borrower's preferred language (free text, e.g. hindi). Briefs the field partner. |
borrower.employerName | string | No | Employer name. |
borrower.address.line1 | string | Yes (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.full | string | Instead of line1 | A single-line address. Accepted in place of line1, under the same rule. |
borrower.address.line2 | string | No | Second address line — floor, wing, area / locality. |
borrower.address.landmark | string | No | Landmark that helps the field partner find the house. |
borrower.address.city | string | Yes | City or town. At least 2 characters, must contain a letter, not a placeholder. |
borrower.address.state | string | Yes | Indian 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.pincode | string, exactly 6 digits | Yes | PIN code of the address. Also the reference for the distance check. |
borrower.address.latitude | string (recommended) or number | Yes | WGS84 decimal degrees, at least 4 decimal places (6 recommended). See coordinate rules. |
borrower.address.longitude | string (recommended) or number | Yes | Same rules as latitude. |
loan.outstandingAmount | number (INR), > 0 | Yes | Total currently due. Rounded to whole rupees. |
loan.dpd | number or string range "lo-hi" | No | Days 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.loanAmount | number (INR) | No | Sanctioned amount. |
loan.disbursedAmount | number (INR) | No | Disbursed amount. |
loan.emiAmount | number (INR) | No | EMI. Shown to the partner. |
loan.lateCharges | number (INR) | No | Late charges / penalty. Omitted on a re-push → reset to 0. |
loan.interestRate | number | No | Rate of interest (%). |
loan.tenure | number | No | Loan tenure. |
loan.tenureUnit | string | No | Unit for loan.tenure (for example months). |
loan.totalRepayable | number (INR) | No | Total repayable. |
loan.totalCollected | number (INR) | No | Amount already collected by you before hand-over. |
loan.productName | string | No | Product / loan type shown to the partner. |
loan.nextDueDate | date string | No | Accepted formats below. Parsed in UTC. Present but unparseable → per-row error. |
loan.disbursedDate | date string | No | Accepted 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:
| Format | Example |
|---|---|
YYYY-MM-DD | 2026-09-05 |
| ISO 8601 datetime | 2026-09-05T00:00:00.000Z |
DD-MM-YYYY | 05-09-2026 |
DD/MM/YYYY | 05/09/2026 |
DD-Mon-YY | 28-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
{
"sourceLoanNumber": "LN-2026-000123",
"borrower": {
"name": "Ravi Sharma",
"phone": "+919876543210",
"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:
| Problem | reason 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 number | borrower.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, 0 | borrower.address.latitude/longitude are 0,0 — send the real coordinates of the address |
| Latitude and longitude swapped | borrower.address.latitude/longitude look swapped (got latitude 77.606262, longitude 12.975326) |
| Outside India | borrower.address.latitude/longitude (51.507351, -0.127758) are outside India |
| Fewer than 4 decimal places | borrower.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 pincodes | see one pin per address |
The address fields are checked the same way:
| Problem | reason after the prefix |
|---|---|
line1 (and full) missing, shorter than 5 characters, without a letter, or a placeholder | borrower.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 placeholder | borrower.address.city is required |
state missing | borrower.address.state is required |
state not a recognised state / UT name or code | borrower.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 missingline1,cityorstate, and to astatewe 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_ROWSabove that); keep batches ≤ 5,000 for fast responses.fullSnapshot(optional, defaultfalse) — 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 insummary.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
sourceLoanNumberinside 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.dpdandloan.lateChargesare reset to 0 when omitted, so always resend them. - Nothing can be cleared to
nullvia 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 (
collectedorclosed_unrecovered), a re-push opens a fresh case with a newcaseId, and its eventsequencerestarts 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 increated,updatedSnapshotorunchangedlike 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 missingcityand missing coordinates during a grace period);summary.warnedis 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 forcityandstate),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.
:::