# Fieldproof Partner API — complete documentation Generated from https://developers.fluxusforge.in at build time. 30 pages, in reading order. Fieldproof is a Fluxus Forge product. Fieldproof never holds borrower funds; cash is never accepted. --- # Welcome _How the Fieldproof Collection Network works and what integrating takes._ Source: https://developers.fluxusforge.in/getting-started/welcome # Fieldproof Collection Network — Developer Documentation Fieldproof runs a nationwide field-collection network: verified, trained collection partners who visit your overdue borrowers, collect payment on **your own payment rails**, and feed every update back into your system in near-real-time. Integrating means two data flows: ```mermaid sequenceDiagram participant You as Your LMS participant QK as Fieldproof participant FP as Field partner participant B as Borrower You->>QK: 1. Push overdue cases (signed API) QK->>FP: 2. Case assigned / accepted FP->>B: 3. Field visit B-->>You: 4. Pays on YOUR rails (QR / your gateway link) QK-->>You: 5. Signed webhooks: visit, payment, closure ``` 1. **Cases in** — you push your overdue loans to our API (or we pull from yours, or ops imports a CSV). 2. **Our field partners** run the entire visit / collection workflow — nothing for you to build there. 3. **Money** goes **directly to you** — via your static QR / bank details or payment links minted by your own gateway. Fieldproof never holds your borrowers' funds, and cash is not accepted on your cases. 4. **Updates out** — every meaningful change (case received, assigned, visit outcome, payment, closure) is delivered to your webhook endpoint as a signed, sequenced event, with a poll API as fallback. ## What integration takes | Effort level | You build | Typical time | |---|---|---| | **API (recommended)** | One outbound API client + one webhook receiver | 1–2 engineer-days | | **Pull** | One read endpoint on your side | ~1 engineer-day | | **CSV** | Nothing — email us a file | 0 | Start with the [integration paths](/getting-started/integration-paths) chooser, then follow the [quickstart](/getting-started/quickstart) — most teams see their first `case.received` webhook within an hour of receiving sandbox credentials. > **WORKING REFERENCE IMPLEMENTATION** — Everything in these docs is implemented in a small runnable **reference > organisation** (mock LMS + dashboard) you can download and run against the > sandbox — see [Reference implementation](/testing/reference-implementation). --- # Integration paths _Choose how cases reach us and how money reaches you._ Source: https://developers.fluxusforge.in/getting-started/integration-paths # Choose your integration path Two decisions define your integration. Both are set per-organisation at onboarding and can be changed later. ## Decision 1 — How do your cases reach us? | | **Push API** (recommended) | **Pull API** | **CSV** | |---|---|---|---| | How it works | You `POST` batches of overdue loans to our API | We fetch from a read endpoint **you** expose, every 30 min | You send a file; our ops imports it | | Freshness | Real-time | ≤30 min | Manual | | You build | Outbound HTTPS client with [request signing](/authentication/signing-requests) | One `GET` endpoint ([contract](/sending-cases/pull-api)) | Nothing | | Best for | Teams with engineering capacity | Systems that can't make outbound calls | Getting started before any build | ## Decision 2 — How does money reach you? **Always directly.** Fieldproof never holds or routes your borrowers' funds, and **cash is not accepted** on your cases — our partner app enforces this. You choose the rail: | | **Mode A: your static QR / bank** | **Mode B: your gateway's links** | |---|---|---| | Borrower pays via | Your fixed UPI QR / bank transfer, shown in the partner app | A payment link minted by **your** gateway, sent to the borrower | | Confirmation | Partner uploads proof → our ops verifies → webhook | **You** call [`payments/confirm`](/payments/your-gateway) when your gateway captures | | You build | Nothing | A mint-link endpoint + one confirm call | | Best for | Fastest start; no gateway required | Fully automatic, instant confirmation | ## The requirement matrix
#### You build / provide - Org registration details ([what exactly](/getting-started/onboarding)) - **Push**: an API client that signs requests - **Pull**: one cursor-paged read endpoint - A webhook receiver (HTTPS, verifies signatures, dedupes) — or use the [poll API](/receiving-updates/poll-api) instead - **Mode A**: your QR image / UPI ID / bank details - **Mode B**: a mint-link endpoint + capture confirmations
#### We provide - Sandbox + production credentials and this documentation - The entire field operation: partner network, visits, RBI-compliant conduct - Case management, deduplication, geocoding, assignment - Signed webhooks with retries, a poll API, delivery log + self-serve replay - Proof verification (Mode A) and payment ledger either way - The [reference implementation](/testing/reference-implementation) of every contract you need to build
Whatever you choose, the [data contract](/core-concepts/data-contract) and the [event stream](/receiving-updates/event-reference) are identical — switching paths later doesn't change your parser. --- # Onboarding & registration _Everything you provide to register, and everything you receive back._ Source: https://developers.fluxusforge.in/getting-started/onboarding # Onboarding & registration Registration is a short exchange with the Fieldproof integration team. Here is the complete list — gather these before your kickoff call and onboarding typically completes the same day. ## What you provide ### 1. Organisation identity | Item | Notes | |---|---| | **Organisation code** | Short uppercase identifier (e.g. `LENDER_X`). Immutable — it tags every case, payment, and event of yours. | | Display name & short name | Shown to borrowers in the partner app and on receipts. | | Logo (PNG/SVG) + brand colour | Borrower-facing screens re-skin per lender. | | Registered address & support phone | Printed on receipts. | ### 2. Compliance | Item | Notes | |---|---| | **RBI regulatory disclosure text** | The fair-practices line printed on every borrower receipt, e.g. *"Payment collected by Quikkred Financial Services on behalf of <you> for loan {{loan}}."* Have your legal team approve the wording. | | Receipt footer note (optional) | Refund/dispute contact wording. | > **ENTITY NAME IS LEGALLY BOUND TO YOUR APPROVAL** — The disclosure names the **collecting legal entity**, not the product brand. > Fieldproof is the product; the collecting entity on your receipts is whatever > your legal team approved at onboarding. If that entity ever changes, every > live lender must re-approve the wording before the change ships — it cannot be > updated unilaterally. ### 3. Technical | Item | Notes | |---|---| | Integration mode | Push / pull / CSV — see [integration paths](/getting-started/integration-paths). | | **Webhook endpoint** | One HTTPS URL. Plus which [event types](/receiving-updates/event-reference) you want (default: money + terminal events). | | Payment mode + artefacts | **Mode A**: static QR image, UPI ID, bank details. **Mode B**: your mint-link API spec + auth header. | | *(Pull mode)* your read API | URL, auth header we should send, desired schedule. | | *(Optional)* IP allowlist | Restrict your API keys to your egress IPs. | | Technical contact | Email + phone — receives delivery-failure alerts and coordinates key rotation. | ## What you receive | Item | Notes | |---|---| | **Sandbox credentials** | `apiKeyId` (e.g. `pk_yourorg_test_…`) + HMAC secret — delivered over a secure channel, shown once. Work only against the sandbox host. | | **Production credentials** | Issued after the [go-live checklist](/testing/go-live-checklist) passes. Live keys work only against production. | | Base URLs | Sandbox: `https://alpha-gig.quikkred.in` (test keys) · Production: `https://gig.quikkred.in` (live keys, after go-live). | | This documentation + [OpenAPI spec](/api-reference) | The spec is also served by the API itself at `GET /api/v1/partner/openapi.json`. | | The [reference implementation](/testing/reference-implementation) | Runnable example of every contract. | > **CREDENTIALS ARE SHOWN ONCE** — The HMAC secret cannot be re-displayed — store it in your secrets manager on > receipt. If lost, request a rotation: a new secret is issued while the old one > keeps working until you confirm cutover ([key security](/authentication/key-security)). ## The onboarding sequence ```mermaid flowchart TD A[You send registration details] --> B[We register your org + config] B --> C[Sandbox credentials issued] C --> D[You integrate & test against sandbox] D --> E[Go-live checklist together] E --> F[Production credentials issued] F --> G[First production push] ``` --- # Environments & credentials _Sandbox vs production, key environments, and how credentials behave._ Source: https://developers.fluxusforge.in/getting-started/environments # Environments & credentials | | **Sandbox** | **Production** | |---|---|---| | Purpose | Build + test your integration end-to-end | Live borrower data | | Accepts | **Test keys only** (`pk_…_test_…`) | **Live keys only** (`pk_…_live_…`) | | Data | Test cases, simulated partners | Real cases, real field partners | | Signature debugger | [`/debug/echo-signature`](/authentication/debugging-signatures) available | Not available (by design) | | Base URL | `https://alpha-gig.quikkred.in` | `https://gig.quikkred.in` | The environment split is **enforced by the servers, not by convention** — a test key presented to production is rejected with `401`, and vice versa. There is no way to accidentally point a test integration at live data. ## Credential model Your credential has two parts: - **`apiKeyId`** — public identifier, sent as the Bearer token. Identifies your organisation; safe to log. - **`hmacSecret`** — never transmitted. Proves possession by [signing every request](/authentication/signing-requests) and verifies the webhooks we send you. Store only in your secrets manager. Both sandbox and production get their own independent pair. ## Operational properties | Property | Behaviour | |---|---| | Rotation | Zero-downtime: a new secret is issued while the old stays valid until you confirm cutover. | | Revocation | Effective within 60 seconds. | | Brute-force lockout | Repeated invalid signatures soft-lock the key for 15 minutes (`429 AUTH_LOCKED`) and alert both sides. | | Rate limits | Default 60 requests/min and 10 case-batches/hour per organisation — see [errors & rate limits](/resources/errors). | | IP allowlist | Optional — your keys can be restricted to your egress IPs. | --- # Quickstart: first case in 15 minutes _Health check → push a case → receive your first webhook._ Source: https://developers.fluxusforge.in/getting-started/quickstart # Quickstart You need your **sandbox credentials** ([onboarding](/getting-started/onboarding)); the sandbox base URL is `https://alpha-gig.quikkred.in`. Three steps: verify auth → push one case → receive the webhook. ## 1. Verify your credentials Every request is HMAC-signed ([full spec](/authentication/signing-requests)). Minimal signed `GET`: ```js title="health-check.js (Node 18+)" const crypto = require("crypto"); const BASE = "https://alpha-gig.quikkred.in"; // sandbox (test keys); production: https://gig.quikkred.in const KEY = process.env.QK_KEY_ID; // pk_yourorg_test_... const SECRET = process.env.QK_SECRET; async function call(method, path, bodyObj) { const body = bodyObj ? JSON.stringify(bodyObj) : ""; const ts = Date.now().toString(); const canonical = `${method}\n${path.split("?")[0]}\n${ts}\n${body}`; const sig = crypto.createHmac("sha256", SECRET).update(canonical).digest("hex"); const res = await fetch(BASE + path, { method, headers: { authorization: `Bearer ${KEY}`, "content-type": "application/json", "x-quikkred-timestamp": ts, "x-quikkred-signature": sig, }, ...(bodyObj ? { body } : {}), }); return { status: res.status, json: await res.json() }; } call("GET", "/api/v1/partner/health").then(console.log); // → { status: 200, json: { ok: true, lenderCode: "YOURORG", env: "test", ... } } ``` A `401 SIGNATURE_INVALID`? Use the [signature debugger](/authentication/debugging-signatures) — it shows the exact canonical string the server computed so you can diff yours. ## 2. Push a case (dry-run first) ```js const batch = { rows: [{ sourceLoanNumber: "LN-2026-0001", // your stable loan ID borrower: { name: "Test Borrower", phone: "9876543210", // Indian mobile address: { line1: "12 MG Road", city: "Bengaluru", pincode: "560001" }, }, loan: { outstandingAmount: 12500, dpd: 45, emiAmount: 2500 }, }], }; // Validate without writing: await call("POST", "/api/v1/partner/cases?dryRun=1", batch); // → summary: { created: 1, errored: 0 }, errors: [] // Commit: await call("POST", "/api/v1/partner/cases", batch); ``` Re-sending the identical batch is always a safe no-op, and re-sending a row for an existing loan **updates** it — duplicates are impossible by design ([reliability](/core-concepts/reliability)). ## 3. Receive your first webhook Within seconds of the commit, your registered endpoint receives a signed `case.received` event: ```json { "eventId": "evt_9f2c81a4d0b1e6a2c3d4", "type": "case.received", "occurredAt": "2026-07-23T09:31:00.000Z", "version": 1, "lenderCode": "YOURORG", "sourceLoanNumber": "LN-2026-0001", "sequence": 1, "payload": { "sourceLoanNumber": "LN-2026-0001", "caseId": "CC-2026-XXXXXXX", "status": "pending" } } ``` Your receiver must verify the signature, respond `2xx` fast, and dedupe on `eventId` — the [webhooks page](/receiving-updates/webhooks) has the complete receiver contract and copy-paste receivers in Node and Python. No endpoint yet? The same events are available by polling: ```js await call("GET", "/api/v1/partner/cases/updates?limit=50"); ``` ## Where to go next - [Updating your LMS from our events](/receiving-updates/updating-your-lms) — turn webhooks into loan records - [Data contract](/core-concepts/data-contract) — every field you can send - [Webhooks](/receiving-updates/webhooks) — build a correct receiver - [Payments](/payments/how-money-flows) — pick how money reaches you - [Go-live checklist](/testing/go-live-checklist) — what we verify together --- # Case lifecycle & statuses _The external status model and visit outcomes you will see._ Source: https://developers.fluxusforge.in/core-concepts/case-lifecycle # Case lifecycle & statuses Your system sees a deliberately **simple four-state model** — internal operational sub-states are mapped so we can evolve them without breaking you. ```mermaid flowchart TD S([your push]) -->|case.received| P[pending] P -->|case.assigned| IP[in_progress] IP -->|"payment.collected (full)
then case.closed"| C[collected] IP -->|"case.closed
(failed / recalled)"| CU[closed_unrecovered] P -->|recall| CU ``` Visits and **partial** payments don't 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. | External status | Meaning | |---|---| | `pending` | Accepted, awaiting a field partner | | `in_progress` | Partner assigned / working / payment or verification in flight | | `collected` | Fully collected — terminal | | `closed_unrecovered` | Terminal without full recovery (incl. your recall, write-off) | ## Visit outcomes Delivered in `visit.completed` events: | Outcome | Meaning | |---|---| | `collected` / `partial` | Money collected (full / part) | | `ptp` | Promise-to-pay with a committed date (`ptpDate`) | | `delivered_acknowledged` | Payment notice delivered and acknowledged | | `refused` / `not_available` | Borrower refused / not found this visit | | `wrong_address` / `shifted` | Address problem — **your cue to push a corrected address** (just re-send the row) | ## Reading state correctly - The webhook stream is the *narrative*; `GET /cases/{loan}` is the *authoritative snapshot* — when in doubt (e.g. a sequence gap), fetch it. - Payment events carry **absolute values** (`totalCollected`, `outstandingAfter`) — never accumulate deltas yourself. --- # Data contract _Every field a case row can carry — required, recommended, formats._ Source: https://developers.fluxusforge.in/core-concepts/data-contract # Data contract One row = one overdue loan. The same shape is used by the push API, the pull API, and (as columns) the CSV path. Unknown fields are ignored, never fatal. ## Required | Field | Type | Rules | |---|---|---| | `sourceLoanNumber` | string ≤64 | **Your** stable loan ID — the key for every later lookup, update, webhook, and payment. Unique within your book. | | `borrower.name` | string | Full name. | | `borrower.phone` | string | Indian mobile; `+91` / `91` / leading-`0` accepted, normalised to 10 digits. | | `borrower.address.pincode` | string | 6-digit PIN. Drives partner matching — accuracy directly affects collection speed. | | `loan.outstandingAmount` | number | Total currently due (INR), > 0. | ## Strongly recommended | Field | Why it matters | |---|---| | `loan.dpd` (number or `"61-90"` range) | Sets priority, partner incentive, and visit type. | | `loan.emiAmount`, `loan.loanAmount`, `loan.disbursedAmount` | Shown to the partner; better borrower conversations. | | `loan.nextDueDate`, `loan.disbursedDate` | Dates: `YYYY-MM-DD`, `DD-MM-YYYY`, `28-Apr-26` all accepted. | | `loan.lateCharges`, `loan.interestRate`, `loan.tenure`, `loan.totalRepayable`, `loan.productName` | Dispute handling at the door. | | `borrower.address.line1/line2/city/state/landmark` | Better geocoding → faster visits. | | `borrower.email`, `borrower.language`, `borrower.employerName` | Payment-link delivery & partner briefing. | ## The batch envelope (push API) ```json { "fullSnapshot": false, "rows": [ { "sourceLoanNumber": "LN-…", "borrower": { … }, "loan": { … } } ] } ``` - `fullSnapshot: true` asserts "this is my complete outstanding book" — any of your active cases **absent** from the batch get flagged for review (borrower may have paid you directly). Use it for daily/weekly reconciliation pushes; never for incremental ones. - Re-sending a row for an existing loan **updates** it (outstanding, contact, address). One active case per loan is enforced by us — duplicates are structurally impossible. Validation failures come back per-row with the index, field, and reason — see [errors](/resources/errors). --- # Idempotency & reliability _The guarantees — no lost updates, no double-applies, recoverable order._ Source: https://developers.fluxusforge.in/core-concepts/reliability # Idempotency & reliability Three promises, and what backs each one: ## 1. Nothing is ever lost Every business event is written to a durable event store **in the same database transaction** as the case change — an update cannot exist without its event. Webhook delivery retries on a ladder (`0s → 30s → 2m → 10m → 1h → 6h → 24h`); after that the event parks **replayable for 30 days** (self-serve). The [poll API](/receiving-updates/poll-api) is a second, independent path to the same events. ## 2. Nothing is applied twice | Boundary | Mechanism | |---|---| | Your batch pushes | Content-hash dedupe: an identical committed batch is a guaranteed no-op. Optionally send an `Idempotency-Key` header — a replay returns the original response verbatim. | | Your payment confirms (Mode B) | Unique on `(loan, reference)` — a replay gets `409 DUPLICATE_CONFIRMATION` with the original booking. | | Our webhooks | At-least-once delivery — **you** dedupe on `eventId` (retries and replays are normal, not errors). | ## 3. Order is recoverable, not guaranteed Retries make in-order delivery impossible, so every event carries a per-case monotonic **`sequence`**. Receiver rule: - apply only if `sequence` > the last you applied for that loan; - on a gap, fetch `GET /cases/{loan}` for the authoritative snapshot; - payloads carry **absolute values** (`totalCollected`, `outstandingAfter`) — a missed intermediate event can never corrupt your books. > **SEEN IN PRACTICE** — Under concurrent delivery a `case.closed` (seq 4) can genuinely arrive before > the `payment.collected` (seq 2–3) that caused it. A sequence-aware receiver > handles this without special cases — the terminal event's absolute totals are > already correct. --- # Signing requests (HMAC) _The two-part credential and the canonical string every request signs._ Source: https://developers.fluxusforge.in/authentication/signing-requests # Signing requests Every call to the Partner API carries a **Bearer key ID** (identifies you) and an **HMAC-SHA256 signature** (proves you hold the secret — which is never transmitted). The same scheme, in reverse, signs the webhooks we send you. ## Headers ``` Authorization: Bearer Content-Type: application/json X-Fieldproof-Timestamp: X-Fieldproof-Signature: ``` ## The canonical string ``` signature = hex( HMAC-SHA256( METHOD + "\n" + PATH + "\n" + TIMESTAMP + "\n" + RAW_BODY, hmacSecret ) ) ``` | Component | Rule | Common mistake | |---|---|---| | `METHOD` | Uppercase (`POST`) | lowercase | | `PATH` | URL path only, **no query string** (`/api/v1/partner/cases`) | including `?dryRun=1` | | `TIMESTAMP` | Exact value of the `X-Fieldproof-Timestamp` header, unix **milliseconds** | seconds instead of ms | | `RAW_BODY` | The **exact bytes** you transmit; empty string for GET | re-serialising JSON after signing (key order/whitespace changes the bytes) | The timestamp must be within **±5 minutes** of server time (replay protection). ## Implementations ```js title="Node.js" const crypto = require("crypto"); function sign(method, path, rawBody, secret) { const ts = Date.now().toString(); const canonical = `${method}\n${path}\n${ts}\n${rawBody}`; const sig = crypto.createHmac("sha256", secret).update(canonical).digest("hex"); return { ts, sig }; } const body = JSON.stringify({ rows: [/* … */] }); const { ts, sig } = sign("POST", "/api/v1/partner/cases", body, process.env.QK_SECRET); // send `body` EXACTLY as signed — do not stringify again ``` ```python title="Python" def sign(method: str, path: str, raw_body: str, secret: str): ts = str(int(time.time() * 1000)) canonical = f"{method}\n{path}\n{ts}\n{raw_body}" sig = hmac.new(secret.encode(), canonical.encode(), hashlib.sha256).hexdigest() return ts, sig body = json.dumps(payload, separators=(",", ":")) # serialise ONCE, sign, send as-is ts, sig = sign("POST", "/api/v1/partner/cases", body, SECRET) ``` ```php title="PHP" function qk_sign(string $method, string $path, string $rawBody, string $secret): array { $ts = (string) round(microtime(true) * 1000); $canonical = "$method\n$path\n$ts\n$rawBody"; return [$ts, hash_hmac('sha256', $canonical, $secret)]; } ``` ```bash title="cURL (bash)" TS=$(date +%s%3N) BODY='{"rows":[...]}' SIG=$(printf 'POST\n/api/v1/partner/cases\n%s\n%s' "$TS" "$BODY" \ | openssl dgst -sha256 -hmac "$QK_SECRET" -r | cut -d' ' -f1) curl -X POST "$QK_BASE/api/v1/partner/cases" \ -H "Authorization: Bearer $QK_KEY_ID" -H "Content-Type: application/json" \ -H "X-Fieldproof-Timestamp: $TS" -H "X-Fieldproof-Signature: $SIG" \ -d "$BODY" ``` ## Verifying OUR webhooks Identical scheme, with **your callback path** as `PATH` and the raw request body you received as `RAW_BODY`. Always compare with a constant-time function. See [webhooks](/receiving-updates/webhooks) for full receiver code. ## Test it immediately `GET /api/v1/partner/health` is the auth smoke test — a correct signature returns your organisation code: ```json { "ok": true, "lenderCode": "YOURORG", "env": "test", "serverTime": "…" } ``` Failing? The [signature debugger](/authentication/debugging-signatures) shows you the server-side canonical string to diff against yours. --- # Debugging signatures _The sandbox echo endpoint that shows you exactly what the server computed._ Source: https://developers.fluxusforge.in/authentication/debugging-signatures # Debugging signatures Signature mismatches are the #1 integration stall — and impossible to debug blind, because a correct server never reveals what it expected. The sandbox fixes that with a dedicated endpoint that **accepts an invalid signature on purpose** and echoes back the server-side computation. ## `POST /debug/echo-signature` (sandbox only) Send any request with your valid **test** `apiKeyId` and any signature attempt: ```json title="Response" { "success": true, "canonicalString": "POST\n/api/v1/partner/debug/echo-signature\n1753260000000\n{\"probe\":true}", "canonicalParts": { "method": "POST", "path": "/api/v1/partner/debug/echo-signature", "timestamp": "1753260000000", "bodyBytes": 14, "bodySha256": "9c9d…" }, "expectedSignature": "3f2a…", "receivedSignature": "deadbeef…", "signatureMatches": false, "timestampWithinSkew": true, "note": "Sign hex(HMAC-SHA256(canonicalString, hmacSecret)) over the EXACT raw body bytes — do not re-serialize JSON after signing." } ``` ## How to use it 1. Point your failing client at `/api/v1/partner/debug/echo-signature`. 2. Compare `canonicalParts` with what your code signed: - `path` differs → you included the query string, or a proxy rewrote the path. - `timestamp` differs → you signed a different value than you sent. - `bodySha256` differs → your transmitted bytes ≠ signed bytes (re-serialisation, encoding, or a middleware mutating the body). 3. When `expectedSignature` matches what your code produces, point back at the real endpoints — done. Live keys get `403 SANDBOX_ONLY` — this endpoint never exists in production. --- # Key lifecycle & security _Rotation, revocation, lockout, and how to store credentials._ Source: https://developers.fluxusforge.in/authentication/key-security # Key lifecycle & security ## Storage rules - The **HMAC secret** goes in your secrets manager only — never in code, config repos, logs, or client-side apps. It is shown exactly once at issuance. - The `apiKeyId` is public-safe (it identifies, the signature proves). - Server-side only: signing must never happen in a browser or mobile app you ship to end users. ## Rotation (zero-downtime) Request a rotation any time (scheduled hygiene or suspected exposure): 1. We issue a **new secret**; the old one keeps working. 2. You deploy the new secret at your pace and confirm. 3. We retire the old secret. Total downtime: zero. During the grace window both secrets sign valid requests, and our webhooks are signed with the **new** (active) secret. ## Revocation If a key is compromised, revocation takes effect within **60 seconds** of the request. Your integration halts until a replacement is issued — prefer rotation unless the secret is definitely exposed. ## Automatic protections on our side | Protection | Behaviour | |---|---| | Signature-failure lockout | ≥10 invalid signatures in 5 min → key soft-locked 15 min (`429 AUTH_LOCKED`), both sides alerted. A leaked key ID without the secret is useless *and* noisy. | | Environment locking | Test keys rejected in production and vice versa — misconfiguration fails loudly, immediately. | | Replay window | Requests older/newer than 5 minutes are rejected. | | IP allowlist (optional) | Keys restricted to your registered egress IPs. | | Audit trail | Every request is logged (metadata only, 90 days) — usable for your own incident forensics on request. | --- # Push API (recommended) _POST your overdue book — dry-run, commit, update, snapshot, recall._ Source: https://developers.fluxusforge.in/sending-cases/push-api # Push API
#### You build - A signed HTTPS client ([signing](/authentication/signing-requests)) - A job that pushes new/changed loans (real-time or batched)
#### We provide - Validation with per-row errors, dedupe, geocoding, assignment - `case.received` webhooks + the full [event stream](/receiving-updates/event-reference) - Batch idempotency + `Idempotency-Key` replay safety
## Endpoints | Method | Path | Purpose | |---|---|---| | `POST` | `/api/v1/partner/cases` | Push a batch of [rows](/core-concepts/data-contract) — hard cap 20,000; keep batches ≤5,000 for fast responses. `?dryRun=1` validates without writing. | | `GET` | `/api/v1/partner/cases/{sourceLoanNumber}` | Authoritative live snapshot of one case. | | `POST` | `/api/v1/partner/cases/{sourceLoanNumber}/recall` | Withdraw a case (borrower paid you directly / restructured). Idempotent. | Full schemas in the [API reference](/api-reference). ## Recommended rhythm 1. **New overdue loans** — push as they cross your DPD threshold (or hourly). 2. **Changes** — re-push the row when outstanding/contact/address changes; it updates in place. If the loan's previous case is already **settled/closed**, the re-push opens a **fresh case** instead (the closed case keeps its history) — use this when a borrower re-defaults after an earlier cycle. 3. **Daily reconciliation** — one `fullSnapshot: true` push of your complete active book; anything we hold that's missing from it is flagged both sides. 4. **Recalls** — call recall the moment a borrower settles with you directly, so no partner makes a wasted (and borrower-annoying) visit. ## Response shape ```json { "success": true, "dryRun": false, "batchId": "…", "summary": { "totalRowsInCsv": 200, "validRows": 198, "created": 150, "updatedSnapshot": 40, "unchanged": 8, "errored": 2 }, "errors": [ { "index": 17, "sourceLoanNumber": "LN-…", "reason": "borrower.phone: invalid_mobile" } ] } ``` Rows fail **individually** — one bad phone number never blocks the batch. --- # Pull API (we fetch from you) _The one endpoint you expose if you can't make outbound calls._ Source: https://developers.fluxusforge.in/sending-cases/pull-api # Pull API If your system can't call ours, expose **one read endpoint** and we fetch on a schedule (every 30 minutes by default).
#### You build - One cursor-paged `GET` endpoint (below) - An auth header you tell us at onboarding
#### We provide - The scheduled fetcher with durable cursors and retries - Everything downstream identical to push (dedupe, webhooks, …)
## The contract you implement ``` GET ?cursor=&limit= Authorization: → 200 { "rows": [ /* case rows — same shape as the push API */ ], "nextCursor": "" | null, "hasMore": true | false } ``` Rules that make it robust: - Return rows changed **since the cursor you last issued**. We persist `nextCursor` only after a page imports successfully — a failed fetch retries from the same point, so cursor reads must be repeatable. - Re-serving an identical page is always safe (content-hash dedupe on our side). - Your own field names are fine — a column mapping can be configured at onboarding; otherwise use the canonical [data contract](/core-concepts/data-contract). Push is still recommended where possible: it's real-time, pull is polled. --- # CSV (no-code fallback) _Start collecting before writing any code._ Source: https://developers.fluxusforge.in/sending-cases/csv # CSV import Zero engineering: send your overdue book as a CSV and our operations team imports it. Ideal for a pilot while your API integration is being built — everything downstream (field work, webhooks if configured, payments) works identically. ## File format One row per loan, headers matching the [data contract](/core-concepts/data-contract) field names (a custom column mapping for your existing export format can be configured at onboarding — send us a sample file): ```csv sourceLoanNumber,borrower.name,borrower.phone,borrower.address.pincode,borrower.address.line1,borrower.address.city,loan.outstandingAmount,loan.dpd,loan.emiAmount,loan.nextDueDate LN-2026-0001,Ravi Sharma,9876543210,560001,12 MG Road,Bengaluru,12500,45,2500,2026-08-05 ``` - Re-sending a file is safe — identical files are no-ops, changed rows update. - Errors come back as a per-row report before anything is committed. - Cadence and delivery channel (email / portal) are agreed at onboarding. When your API build is ready, switching to [push](/sending-cases/push-api) changes nothing else — same data, same events, same payments. --- # Webhooks _The receiver contract — five rules and copy-paste implementations._ Source: https://developers.fluxusforge.in/receiving-updates/webhooks # Webhooks Every meaningful change on your cases is POSTed to your registered HTTPS endpoint as a signed event, with automatic retries. This page is the complete receiver contract. ## The delivery ``` POST Content-Type: application/json X-Fieldproof-Event: payment.collected X-Fieldproof-Delivery: evt_9f2c… ← dedupe key X-Fieldproof-Timestamp: 1753260000000 X-Fieldproof-Signature: hex(HMAC-SHA256("POST\n\n\n", hmacSecret)) ``` Body envelope: see the [event reference](/receiving-updates/event-reference). ## The five receiver rules 1. **Verify the signature** over the exact raw bytes before trusting anything — same scheme as [request signing](/authentication/signing-requests), with *your callback path* as `PATH`. Constant-time compare. 2. **Respond `2xx` fast** (<10s). Queue heavy processing. Non-2xx or timeout → retry ladder `0s → 30s → 2m → 10m → 1h → 6h → 24h`, then parked (replayable 30 days) and your technical contact alerted. 3. **Dedupe on `eventId`** — delivery is at-least-once; retries and replays are normal. 4. **Order by `sequence`, not arrival** — apply only if greater than the last applied for that loan; on a gap, `GET /cases/{loan}` for truth. 5. **Use absolute values** from payloads (`totalCollected`, `outstandingAfter`) — never accumulate deltas. ## If your endpoint goes down After ~20 consecutive failures we suspend delivery (attempts aren't wasted), alert your technical contact, and probe every 15 minutes. On recovery the backlog drains automatically in per-case sequence order. Nothing is lost. ## Reference receiver (Node/Express) ```js const express = require("express"); const crypto = require("crypto"); const app = express(); const SECRET = process.env.QK_SECRET; const PATH = "/webhooks/quikkred"; app.use(PATH, express.raw({ type: "application/json", limit: "1mb" })); // RAW body! function verify(req) { const ts = req.header("x-quikkred-timestamp"); const sig = req.header("x-quikkred-signature"); if (!ts || !sig) return false; if (Math.abs(Date.now() - Number(ts)) > 5 * 60 * 1000) return false; const expected = crypto.createHmac("sha256", SECRET) .update(`POST\n${PATH}\n${ts}\n${req.body.toString("utf8")}`).digest("hex"); return sig.length === expected.length && crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected)); } app.post(PATH, async (req, res) => { if (!verify(req)) return res.status(401).end(); const event = JSON.parse(req.body.toString("utf8")); const isNew = await db.insertIfAbsent("events", event.eventId, event); // rule 3 if (!isNew) return res.json({ duplicate: true }); res.json({ received: true }); // rule 2 — ack now queue.push(async () => { // rules 4 & 5 const last = await db.lastSequence(event.sourceLoanNumber); if (event.sequence <= last) return; if (event.sequence > last + 1) return refetchCase(event.sourceLoanNumber); await applyEvent(event); await db.setLastSequence(event.sourceLoanNumber, event.sequence); }); }); ``` ```python title="Python/Flask (verification core)" from flask import Flask, request app = Flask(__name__) SECRET = b"..."; PATH = "/webhooks/quikkred" @app.post(PATH) def hook(): ts = request.headers.get("X-Fieldproof-Timestamp", "") sig = request.headers.get("X-Fieldproof-Signature", "") if abs(time.time() * 1000 - float(ts or 0)) > 300_000: return "", 401 canonical = f"POST\n{PATH}\n{ts}\n".encode() + request.get_data() expected = hmac.new(SECRET, canonical, hashlib.sha256).hexdigest() if not hmac.compare_digest(sig, expected): return "", 401 # dedupe on eventId, ack fast, apply by sequence — as in the Node example return {"received": True} ``` Test any deployment instantly with `POST /api/v1/partner/webhooks/test` — a signed `ping` fires at your endpoint and the delivery result returns synchronously. Next: [Updating your LMS from our events](/receiving-updates/updating-your-lms) — the transactional handler that turns these deliveries into correct loan records. --- # Event reference _Every event type with a real sample payload._ Source: https://developers.fluxusforge.in/receiving-updates/event-reference # Event reference Common envelope for every event (webhook body and poll-API item are identical): ```json { "eventId": "evt_01a2b3c4d5e6f7a8b9c0", "type": "payment.collected", "occurredAt": "2026-07-23T09:31:00.000Z", "version": 1, "lenderCode": "YOURORG", "sourceLoanNumber": "LN-2026-0001", "sequence": 3, "payload": { } } ``` Default subscription: `case.received`, `payment.collected`, `case.closed`. Opt into the rest at onboarding (or later). ## `case.received` Your pushed case is live in the network. ```json { "sourceLoanNumber": "LN-2026-0001", "caseId": "CC-2026-2388ED42", "status": "pending" } ``` ## `case.assigned` A field partner took the case. ```json { "sourceLoanNumber": "LN-2026-0001", "caseId": "CC-2026-2388ED42", "assignedAt": "2026-07-23T06:10:11.000Z" } ``` ## `visit.completed` A visit was recorded, whatever the outcome ([outcome list](/core-concepts/case-lifecycle)). ```json { "sourceLoanNumber": "LN-2026-0001", "caseId": "CC-2026-2388ED42", "outcome": "ptp", "visitAt": "2026-07-23T08:45:02.000Z", "ptpDate": "2026-07-26", "notes": "Will pay after salary credit" } ``` ## `payment.collected` Money verifiably received on your rails. **Absolute values.** ```json { "sourceLoanNumber": "LN-2026-0001", "caseId": "CC-2026-2388ED42", "amount": 3000, "mode": "upi", "reference": "UTR2026072312345", "totalCollected": 3000, "outstandingAfter": 9500, "collectedAt": "2026-07-23T09:30:12.000Z" } ``` Timing: Mode A fires **after our ops verifies the proof**; Mode B fires when **you** confirm the capture. Money is never announced before it is booked. ## `verification.completed` Only if you use the address-verification product. ```json { "sourceLoanNumber": "LN-2026-0001", "result": "verified", "verifiedAt": "…" } ``` ## `case.closed` Terminal — fully collected, recalled, or closed unrecovered. ```json { "sourceLoanNumber": "LN-2026-0001", "caseId": "CC-2026-2388ED42", "status": "collected", "reason": "full_settlement", "totalCollected": 12500 } ``` `reason` values include `full_settlement`, `recalled`, or a terminal visit outcome. ## `ping` Sent by the webhook self-test — verify, dedupe, return 2xx like any event. --- Ready to consume these? [Updating your LMS from our events](/receiving-updates/updating-your-lms) has the complete, transactional apply-handler with SQL. --- # Poll API (fallback) _The same event stream, pulled by cursor._ Source: https://developers.fluxusforge.in/receiving-updates/poll-api # Poll API The same events as webhooks, pulled on your schedule. Use it if you can't receive webhooks, and as your **recovery lane** after an outage on your side. ``` GET /api/v1/partner/cases/updates?limit=100 GET /api/v1/partner/cases/updates?cursor= GET /api/v1/partner/cases/updates?since=2026-07-23T00:00:00Z (first call only) ``` ```json title="Response" { "success": true, "events": [ /* same envelope as webhooks, oldest first */ ], "nextCursor": "665fa1b2c3d4e5f6a7b8c9d0", "hasMore": false } ``` - Persist `nextCursor` after processing a page; resume from it next call. - Events appear here regardless of webhook delivery status — the two lanes are independent projections of the same durable store. - Apply the same `eventId` dedupe + `sequence` rules as the [webhook receiver](/receiving-updates/webhooks) — you may see events on both lanes. --- # Delivery log & replay _Self-serve visibility into every webhook we sent you — and re-sending them._ Source: https://developers.fluxusforge.in/receiving-updates/delivery-log # Delivery log & replay No support ticket needed to answer "did you send it?" — your delivery history is queryable, and any event is re-sendable, via the API itself. ## Inspect deliveries ``` GET /api/v1/partner/webhooks/deliveries?limit=50 GET /api/v1/partner/webhooks/deliveries?status=exhausted&since=2026-07-20T00:00:00Z ``` ```json { "deliveries": [{ "eventId": "evt_…", "type": "payment.collected", "sourceLoanNumber": "LN-…", "sequence": 3, "status": "delivered", // pending | in_flight | delivered | exhausted "attempts": 1, "deliveredAt": "…", "lastStatusCode": 200, "lastError": null }] } ``` `exhausted` = the full retry ladder failed (~31 hours); the event stays replayable for 30 days. ## Replay ``` POST /api/v1/partner/webhooks/deliveries/{eventId}/replay ``` Queues an immediate re-delivery — works on `delivered` events too (lost it after processing? replay it). Your receiver's `eventId` dedupe makes replays harmless by design. ## Test your endpoint ``` POST /api/v1/partner/webhooks/test ``` Fires a signed `ping` and returns the result synchronously — `{ delivered, statusCode, latencyMs, error }`. Run it on every deploy of your receiver. --- # Updating your LMS from our events _Copy-paste patterns for applying payment and closure events to your loan records._ Source: https://developers.fluxusforge.in/receiving-updates/updating-your-lms # Updating your LMS from our events This is the page most integrations actually ship from: **exactly how to write our events into your loan tables** — correctly, idempotently, and crash-safe. It applies identically whether events arrive by [webhook](/receiving-updates/webhooks) or the [poll API](/receiving-updates/poll-api). ## The three tables you need You almost certainly have the first already: ```sql -- 1. Your existing loans table — add two columns for sync state: ALTER TABLE loans ADD COLUMN qk_total_collected DECIMAL(14,2) NOT NULL DEFAULT 0; ALTER TABLE loans ADD COLUMN qk_last_sequence INT NOT NULL DEFAULT 0; -- 2. Event dedupe store (at-least-once delivery → replays are normal): CREATE TABLE qk_events ( event_id VARCHAR(64) PRIMARY KEY, -- our eventId received_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); -- 3. Collections ledger (one row per payment we report): CREATE TABLE qk_collections ( reference VARCHAR(128), -- UTR / gateway txn id (may be NULL for some proofs) event_id VARCHAR(64) NOT NULL, loan_number VARCHAR(64) NOT NULL, amount DECIMAL(14,2) NOT NULL, mode VARCHAR(20), collected_at TIMESTAMP, PRIMARY KEY (event_id) ); ``` ## The handler (complete, transactional) The four rules encoded once — dedupe, sequence-guard, absolute values, single transaction: ```js title="apply-event.js (Node — any SQL client; adapt freely)" async function applyFieldproofEvent(event) { const loanNo = event.sourceLoanNumber; await db.transaction(async (tx) => { // Rule 1 — dedupe: INSERT the eventId first; a duplicate aborts cleanly. const inserted = await tx.query( "INSERT INTO qk_events (event_id) VALUES (?) ON CONFLICT DO NOTHING", [event.eventId], ); if (inserted.rowCount === 0) return; // replay → already applied // Rule 2 — sequence guard: never apply older state over newer. const { qk_last_sequence } = await tx.queryOne( "SELECT qk_last_sequence FROM loans WHERE loan_number = ? FOR UPDATE", [loanNo], ); if (event.sequence <= qk_last_sequence) return; // stale → skip (dedupe row stays) switch (event.type) { case "payment.collected": { const p = event.payload; // Rule 3 — ABSOLUTE values: SET totals from the payload, never += . await tx.query( `UPDATE loans SET outstanding_amount = ?, -- p.outstandingAfter qk_total_collected = ?, -- p.totalCollected qk_last_sequence = ? WHERE loan_number = ?`, [p.outstandingAfter, p.totalCollected, event.sequence, loanNo], ); // Ledger row for your finance team (amount here IS incremental). await tx.query( `INSERT INTO qk_collections (event_id, reference, loan_number, amount, mode, collected_at) VALUES (?, ?, ?, ?, ?, ?)`, [event.eventId, p.reference, loanNo, p.amount, p.mode, p.collectedAt], ); break; } case "case.closed": { const p = event.payload; await tx.query( `UPDATE loans SET collection_status = ?, -- 'collected' | 'closed_unrecovered' qk_total_collected = ?, outstanding_amount = CASE WHEN ? = 'collected' THEN 0 ELSE outstanding_amount END, qk_last_sequence = ? WHERE loan_number = ?`, [p.status, p.totalCollected, p.status, event.sequence, loanNo], ); break; } case "case.received": case "case.assigned": case "visit.completed": // Status-only — store what you find useful (e.g. p.ptpDate on // visit.completed for your own follow-up scheduling). await tx.query( "UPDATE loans SET qk_last_sequence = ? WHERE loan_number = ?", [event.sequence, loanNo], ); break; } }); } ``` ## Rule 4 — the gap re-fetch If `event.sequence > qk_last_sequence + 1`, you missed something (out-of-order delivery, or an outage). You can still apply the event — absolute values keep your totals correct — but reconcile the loan afterwards from the authoritative snapshot: ```js if (event.sequence > lastSeq + 1) { const { case: c } = await quikkred("GET", `/api/v1/partner/cases/${loanNo}`); await db.query( `UPDATE loans SET outstanding_amount = ?, qk_total_collected = ?, qk_last_sequence = ? WHERE loan_number = ?`, [c.outstandingAmount, c.totalCollected, event.sequence, loanNo], ); } ``` ## Worked example — one loan, full lifecycle A ₹12,500 loan, collected in two payments. Events as your endpoint receives them (note: 3 and 4 can arrive in either order — the handler above is correct both ways): | # | seq | type | payload (relevant) | Your `loans` row after applying | |---|---|---|---|---| | 1 | 1 | `case.received` | `status: "pending"` | unchanged (seq=1) | | 2 | 2 | `payment.collected` | `amount: 5000, totalCollected: 5000, outstandingAfter: 7500` | outstanding **7500**, collected **5000** | | 3 | 3 | `payment.collected` | `amount: 7500, totalCollected: 12500, outstandingAfter: 0` | outstanding **0**, collected **12500** | | 4 | 4 | `case.closed` | `status: "collected", totalCollected: 12500` | status **collected** | If #3 is lost entirely, #4 still lands your row on collected/12500/0 — that is why the payloads carry absolutes. ## When is a payment "real"? A `payment.collected` is only ever emitted for money **verifiably in your account**: - **[Mode A](/payments/static-qr)** — after our operations team verifies the partner's payment proof. `reference` carries the UTR where extracted; `amount` is the operator-confirmed figure (it may correct the partner's claim — trust the event). - **[Mode B](/payments/your-gateway)** — the event mirrors **your own** `payments/confirm` call, so `reference` is your transaction ID. Your books and ours agree by construction; treat the event as the ack that we booked it (partner earning credited, case advanced). So: post the event to your loan ledger as the booking of record, and use your bank/gateway statement as the audit pair. ## Testing this handler The [reference implementation](/testing/reference-implementation)'s loan book is driven by exactly this logic — run it beside your build and both should show identical state for every case. Then use [delivery replay](/receiving-updates/delivery-log) to fire a duplicate at your endpoint (must no-op) and the sandbox lifecycle to prove the out-of-order case. --- # How money flows (never through us) _The core guarantee and the two payment modes compared._ Source: https://developers.fluxusforge.in/payments/how-money-flows # How money flows **Every rupee is collected directly on YOUR payment rails.** Fieldproof runs the field operation and keeps the authoritative collection record — but never holds, routes, or touches your borrowers' money. Consequently **cash is not accepted** on your cases: the partner app removes the option entirely and the API rejects it. ```mermaid flowchart LR B[Borrower] -- pays directly --> Y[(Your account)] FP[Field partner] -- facilitates --> B QK[Fieldproof] -- proof verification / your confirmation --> L[Ledger record in our CRM] L -- payment.collected webhook --> You[Your LMS] ``` ## The two modes | | **Mode A — your static QR / bank** (default) | **Mode B — your gateway's links** | |---|---|---| | Money lands in | Your bank, directly | Your gateway/bank, directly | | Borrower pays via | Your fixed QR / UPI ID / bank transfer, shown in the partner app | A link minted by **your** gateway, delivered by WhatsApp + email | | Confirmation | Partner uploads proof → our ops verifies (OCR + duplicate-UTR checks) → webhook | **You** call `payments/confirm` on capture → instant webhook | | Record in our CRM | Ledger row + proof image + verifying operator | Ledger row + your transaction reference | | You build | Nothing | A mint-link endpoint + one confirm call | | Confirmation latency | Ops verification (typically same day) | Instant | Details: [Mode A](/payments/static-qr) · [Mode B](/payments/your-gateway) · [Reconciliation](/payments/reconciliation) **The invariant either way**: a `payment.collected` event is only ever emitted for money that is verifiably in **your** account — proof-verified (A) or confirmed by you (B). We never announce money before it is booked. --- # Mode A: your static QR / bank _Zero-build payments — proof-verified collections into your account._ Source: https://developers.fluxusforge.in/payments/static-qr # Mode A — your static QR / bank details
#### You provide (once, at onboarding) - Your fixed UPI QR image and/or UPI ID - Bank account details (name, account, IFSC) as a transfer fallback #### You build - Nothing
#### We provide - The QR/bank block rendered in the partner app at the borrower's door - Proof capture (screenshot), OCR extraction, duplicate-UTR detection - Human verification before anything is booked - The ledger record + `payment.collected` webhook after verification
## The flow 1. Partner shows the borrower **your** QR / bank details in the app. 2. Borrower pays — money lands in your account immediately. 3. Partner submits the collection with a payment-proof screenshot; the case holds in a *pending verification* state. **No webhook yet.** 4. Our operations team verifies the proof (OCR-extracted UTR/amount as advisory, duplicate-UTR checks across all cases). 5. On verification: the ledger row is written and `payment.collected` fires — with the **operator-confirmed amount** (which may correct the partner's claim). A full clearance also fires `case.closed`. ## Reconciling on your side Match `payment.collected` events (amount + `reference` = UTR where extracted) against your bank credits. Because verification is human-gated, treat the webhook as the bookkeeping truth and your bank statement as the audit pair — discrepancies are a support escalation, not a normal case. --- # Mode B: your gateway's links _The mint-link endpoint you build and the confirm call you make._ Source: https://developers.fluxusforge.in/payments/your-gateway # Mode B — payment links from your own gateway Fully automatic: our partner requests a link, **your gateway** mints it, the borrower pays you directly, you confirm the capture, the case updates instantly.
#### You build 1. A **mint-link endpoint** we call (below) 2. A **confirm call** to us when your gateway captures
#### We provide - Link delivery to the borrower (WhatsApp + email) - Case/ledger updates + partner earning on your confirmation - Idempotency on your confirmations (safe retries) - `payment.collected` / `case.closed` webhooks
## 1. The mint-link endpoint you expose ``` POST ← registered at onboarding, with your auth header { "sourceLoanNumber": "LN-2026-0001", "caseId": "CC-2026-2388ED42", "visitId": "665f1a2b3c4d5e6f7a8b9c0d", "amount": 5000, "currency": "INR", "borrowerName": "Ravi Sharma", "borrowerPhone": "9876543210", "description": "Loan repayment for LN-2026-0001" } → 200 { "success": true, "link": { "linkId": "your-link-id", "url": "https://pay.you.com/…" } } ``` Respond within **10 seconds**; non-2xx or timeout surfaces as "link unavailable" to the partner, who falls back to your QR/bank details. ## 2. The confirmation you send When your gateway captures the payment: ``` POST /api/v1/partner/cases/LN-2026-0001/payments/confirm { "amount": 5000, "reference": "your-txn-id", ← idempotency key "paidAt": "2026-07-23T09:30:00Z", "mode": "upi", "linkId": "your-link-id" ← optional, ties it to the exact visit } → 200 { "success": true, "caseId": "CC-…", "outstandingAfter": 7500, "status": "in_progress" } ``` - Idempotent on `(loan, reference)` — a retry returns `409 DUPLICATE_CONFIRMATION` with the original booking. Retry freely. - A full-clearing confirmation returns `"status": "collected"`, closes the case, credits the field partner, and fires `case.closed`. - Confirm **promptly** — the partner at the door is watching the case update, and their earning is created from your confirmation. - After your confirm, we emit `payment.collected` (mirroring your reference) back to you — apply it with the standard handler in [Updating your LMS](/receiving-updates/updating-your-lms) so your books and ours stay provably identical. --- # Reconciliation _Three independent ways to prove the books agree._ Source: https://developers.fluxusforge.in/payments/reconciliation # Reconciliation Three layers, from real-time to batch: ## 1. Event-level (continuous) Every `payment.collected` carries the amount, mode, reference (UTR / your transaction ID), and the **absolute** `totalCollected` / `outstandingAfter`. Match these against your bank/gateway credits as they arrive. ## 2. Case-level (on demand) `GET /api/v1/partner/cases/{sourceLoanNumber}` is the authoritative snapshot — status, total collected, visit and payment history. Use it to resolve any single-loan question instantly. ## 3. Book-level (daily/weekly) Push your complete active book with `fullSnapshot: true` ([push API](/sending-cases/push-api)). Any case we hold active that is missing from your file is flagged for review on both sides — catching direct settlements that skipped a [recall](/sending-cases/push-api). A periodic payment report (CSV) can additionally be arranged at onboarding for finance teams that prefer file-based reconciliation. --- # Sandbox testing _Prove every leg of your integration before go-live._ Source: https://developers.fluxusforge.in/testing/sandbox # Sandbox testing The sandbox is a full copy of the production surface that accepts **test keys only** ([environments](/getting-started/environments)). Everything you build is provable there: | What to prove | How | |---|---| | Auth & signing | `GET /health` returns your org code; failures → [echo-signature](/authentication/debugging-signatures) | | Case ingestion | `POST /cases?dryRun=1` → zero row errors → commit → cases visible | | Webhook receiver | `POST /webhooks/test` fires a signed ping and returns the result synchronously | | Dedupe | Replay a delivered event ([delivery log](/receiving-updates/delivery-log)) — your receiver must report it as a duplicate, not re-apply | | Sequence handling | Push + confirm quickly — events can arrive out of order; verify your books stay correct | | Payments (Mode B) | Your mint endpoint gets called; your `payments/confirm` closes the case; `409` on a repeated reference | | Recovery | Poll `GET /cases/updates` and reconcile it against what your webhooks stored | The fastest way to see the whole lifecycle is the [reference implementation](/testing/reference-implementation) — run it with your sandbox keys and watch every panel light up. --- # Reference implementation _A runnable mock organisation implementing every contract in these docs._ Source: https://developers.fluxusforge.in/testing/reference-implementation # Reference implementation A small, self-contained Node application that plays a complete organisation — **both sides of every contract in these docs** — with a dashboard UI. Ask your integration contact for the download; run it in two minutes: ```bash npm install PARTNER_API_BASE=https:// \ LENDER_CODE=YOURORG \ API_KEY_ID=pk_yourorg_test_xxx \ HMAC_SECRET= \ npm start # → http://localhost:4747 ``` ## What it demonstrates (copy the code freely) | Contract | Where in the code | |---|---| | Request signing + every API call | the `sign()` / `quikkred()` client | | A **correct webhook receiver** — signature verify over raw bytes, `eventId` dedupe, per-loan sequence ordering with gap detection, absolute-value application | `POST /webhooks/quikkred` | | The [pull API](/sending-cases/pull-api) contract | `GET /overdue-cases` | | The [Mode B mint-link](/payments/your-gateway) contract + confirm call | `POST /payment-links` + the pay page | The dashboard shows a mock loan book that updates itself **purely from webhook payloads** — which is exactly the behaviour your production integration should have. Its isolated JSON store never touches your systems. For webhooks to reach it from the sandbox, expose it with any HTTPS tunnel (`npx localtunnel --port 4747`, ngrok, VS Code dev tunnels) and register that URL as your callback. --- # Go-live checklist _What we verify together before production credentials are issued._ Source: https://developers.fluxusforge.in/testing/go-live-checklist # Go-live checklist Walked jointly with your integration contact once sandbox testing is done. Production credentials are issued when every box is checked. ## Technical - [ ] `GET /health` returns your org code with sandbox credentials - [ ] A real batch pushed with `?dryRun=1` — **zero** row errors - [ ] Webhook endpoint: verifies signatures, responds 2xx < 10s - [ ] `eventId` dedupe proven — we send a deliberate replay, your system reports duplicate without re-applying - [ ] Sequence handling proven (out-of-order tolerant, gap → re-fetch) - [ ] **Mode A**: QR/bank details render correctly on a sample receipt · **Mode B**: mint endpoint tested + a confirm round-trip completed - [ ] Recall tested (you can withdraw a loan cleanly) ## Operational - [ ] RBI disclosure wording approved by your legal team on a sample receipt - [ ] Technical contact confirmed (receives delivery-failure alerts) - [ ] Secrets stored in your secrets manager; rotation procedure understood - [ ] First production push scheduled — start with a small batch, ops on standby both sides, then your full book with `fullSnapshot: true` ## After go-live (first 24h) - Watch your [delivery log](/receiving-updates/delivery-log) and your own webhook processing for anomalies; our side alerts automatically on delivery failures. --- # Errors & rate limits _Every error code, what causes it, and what to do._ Source: https://developers.fluxusforge.in/resources/errors # Errors & rate limits ## Error envelope ```json { "success": false, "error": { "code": "VALIDATION_FAILED", "message": "human-readable explanation", "rowErrors": [ { "index": 3, "sourceLoanNumber": "LN-…", "field": "borrower.phone", "reason": "invalid_mobile" } ] } } ``` Every response carries an `X-Request-Id` header — include it in support queries and we can trace the exact request. ## Codes | HTTP | Code | Cause | Your action | |---|---|---|---| | 401 | `UNAUTHORIZED` | Unknown/revoked key, wrong environment, missing headers, IP not allowed | Check key + environment; see [environments](/getting-started/environments) | | 401 | `SIGNATURE_INVALID` | Signature mismatch | [Debug it](/authentication/debugging-signatures) | | 401 | `TIMESTAMP_SKEW` | Timestamp outside ±5 min | Sync clocks (NTP); sign at send time | | 403 | `SANDBOX_ONLY` | Sandbox-only endpoint with a live key | Use test credentials | | 403 | `PAYMENT_MODE_MISMATCH` | `payments/confirm` while not on Mode B | Confirm your configured payment mode | | 404 | `CASE_NOT_FOUND` | Unknown loan for your org | Check `sourceLoanNumber`; was it pushed/committed? | | 409 | `DUPLICATE_CONFIRMATION` | Payment reference already booked | Treat as success — the original booking is returned | | 413 | `TOO_MANY_ROWS` | Batch above the row cap (20,000) | Split the batch | | 422 | `VALIDATION_FAILED` | Envelope or per-row validation failure | Fix rows in `rowErrors`; others were unaffected | | 429 | `RATE_LIMITED` | Per-org limits exceeded | Honour `Retry-After` | | 429 | `AUTH_LOCKED` | Signature-failure lockout (15 min) | Stop, [debug the signature](/authentication/debugging-signatures), wait | ## Rate limits (per organisation, adjustable) | Limit | Default | |---|---| | Requests | 60 / minute | | Case batches | 10 / hour | | Batch size | 20,000 rows (keep ≤5,000 for fast responses) | --- # Security & compliance commitments _What we guarantee about your data — the answers for your security review._ Source: https://developers.fluxusforge.in/resources/security # Security & compliance commitments Written for your security questionnaire. ## Data protection - **Tenant isolation is structural**: every read and write is scoped to your organisation at the query layer and covered by an automated cross-tenant test matrix. Another organisation's key cannot see your data — probes return 404, revealing nothing. - Your API secret is stored only AES-256-GCM-encrypted; the raw value exists nowhere after issuance. Database encryption at rest; encrypted backups. - Borrower phone/email are masked in all logs; request and webhook **bodies are never logged**. API audit logs (metadata only) retain 90 days. - Webhooks contain only data about **your own** loans and borrowers. ## Credential safety - Environment-locked keys (test rejected on production, and vice versa). - Signature-failure lockout + alerting; revocation effective ≤ 60s; zero-downtime rotation on request. - Optional hardening: IP allowlisting of your keys; our static egress IPs for your firewall. ## Money - We never hold or route borrower funds — collections land directly on your rails ([how money flows](/payments/how-money-flows)). Cash is not accepted. - A payment event is only emitted for verifiably booked money. ## Field conduct - Collections are performed under RBI fair-practices guidelines: trained and verified partners, disclosure of the lender-of-record on receipts (your registered disclosure text), regulated visit hours and frequency. ## Durability - Events are written transactionally with the case change; deliveries retry ~31 hours then remain replayable 30 days; the poll API is an independent second lane. See [reliability](/core-concepts/reliability). --- # Versioning policy _What "v1 is stable" means, precisely._ Source: https://developers.fluxusforge.in/resources/versioning # Versioning policy The API surface lives under `/api/v1/`. **v1 is stable**: ## We may (non-breaking — your parser must tolerate) - **Add** fields to responses and webhook payloads - **Add** new event types (subscribe-only — never sent unless you opt in, beyond the documented default set) - **Add** new endpoints and optional request fields ## We will never (in v1) - Remove or rename an existing field - Change a field's type or semantics - Change the signing scheme, envelope shape, or error contract Breaking changes ship as `/api/v2/…` with **at least 6 months** of parallel v1 support and direct notice to your technical contact. The machine-readable spec at [`/api-reference`](/api-reference) (also served by the API itself) is regenerated from the running service — it cannot drift from behaviour. --- # FAQ & support _Common questions, and how to reach us._ Source: https://developers.fluxusforge.in/resources/faq # FAQ **Can we integrate without building anything?** Yes — [CSV](/sending-cases/csv) in, and read-only tracking via reports. Webhooks/API can come later without redoing anything. **We can't receive webhooks (corporate network). Are we stuck?** No — the [poll API](/receiving-updates/poll-api) delivers the identical event stream by cursor. Many orgs start polling and add webhooks later. **Do you collect cash?** No. Your borrowers pay on your rails only ([how money flows](/payments/how-money-flows)); the partner app removes the cash option on your cases entirely. **What if a borrower pays us directly while a case is active?** Call [recall](/sending-cases/push-api) — the case closes cleanly and any assigned partner is stood down immediately. Your daily `fullSnapshot: true` push is the safety net for missed recalls. **A webhook says `payment.collected` but the amount differs from the partner's claim.** The webhook carries the **operator-verified** amount (Mode A) or **your** confirmed amount (Mode B) — it is the booking truth by construction. **We missed some webhooks during an outage.** Nothing is lost: exhausted deliveries stay [replayable for 30 days](/receiving-updates/delivery-log), and the poll API replays the full stream by cursor. **Can the same loan be sent twice?** Safely, yes — re-sending updates the existing case; duplicates are structurally impossible ([reliability](/core-concepts/reliability)). **How fresh is `GET /cases/{loan}`?** Read-your-writes: it always reflects every event already emitted to you. ## Support - **Integration engineering**: your named integration contact (assigned at onboarding) — include the `X-Request-Id` of any failing call. - **Delivery-failure alerts** go to your registered technical contact automatically. - **Security disclosures**: security@quikkred.in. ---