API overview
A small API with strong guarantees
Two resources — consents and data sessions — eight webhook events, one error envelope. Everything below is normative in the OpenAPI reference, which is generated from the same spec that drives the platform.
Authentication & environments
Every request carries Authorization: Bearer <secret key>.
Keys are per tenant and per environment, and are server-side only — they must never reach a
browser, mobile app or borrower device. Two keys can be active at once, so rotation is
zero-downtime: create the new key, deploy, revoke the old.
| Environment | Base URL | Key prefix | livemode |
|---|---|---|---|
| Sandbox | https://api.sandbox.lenderos.in/v1 | los_sk_test_ | false |
| Production | https://api.lenderos.in/v1 | los_sk_live_ | true |
Sandbox and production are fully separate deployments — separate keys, connector
credentials and data plane. A key presented to the wrong environment fails with
ENVIRONMENT_MISMATCH;
keys never cross environments. The sandbox serves mock FI data and needs no AA contract,
so integration starts before commercial paperwork finishes — promotion to production is a
credential change, never a code change. Optional per-tenant hardening includes a source-IP
allowlist and mutual TLS on the single endpoint where financial data crosses.
Consent lifecycle
A consent is created with canonical parameters — purpose, FI types, mode, validity window, data range, fetch frequency, data-life — either inline or via a saved template that expands to exactly the same document. Lender OS validates the terms against Fair Use bounds, selects a rail from your contracted pool, and registers the consent. Routing is decided at creation and is final for that consent; if no rail can express the terms, the call fails with a typed routing error listing which rails were excluded and why.
The borrower approves on their AA’s own journey via a short-lived URL you mint at
redirect time. The redirect back to your return_url
is advisory — it drives UX and funnel metrics, never state. Authoritative transitions come
only from verified rail notifications, authoritative polls, and the poll-confirmed expiry clock.
- PENDING
- Created and registered with the selected rail; awaiting the borrower.
- ACTIVE
- Borrower approved and the rail confirmed. Data sessions may be opened.
- PAUSED
- Suspended by the borrower at the AA. Resumes as ACTIVE; fetches are refused meanwhile.
- REJECTED
- Terminal. Borrower declined, or the journey/rail failed — with a machine-readable reason.
- REVOKED
- Terminal. Withdrawn by the borrower at the AA, or by you via the API.
- EXPIRED
- Terminal. The consent’s validity window ended, confirmed against the rail.
- FAILED
- Terminal. Registration with the rail failed before the borrower ever saw a journey.
Data sessions
A data session is one acquisition attempt under an active consent. The requested window and the consent’s fetch frequency are enforced locally, before the rail sees the request — an out-of-bounds fetch fails immediately with a typed error instead of a downstream rejection. Scheduling stays yours: the orchestrator enforces that you may fetch, and you decide when.
On rails that push the first data drop automatically after activation, the session appears
with origin: VENDOR_INITIATED
so the fetch is visible, metered and evidenced like any other — and a matching request from
you reuses it rather than paying for a duplicate fetch.
- REQUESTED
- Fetch validated against the consent’s terms and dispatched to the rail.
- READY / FETCHED / DECRYPTED
- The rail-side pipeline. On push-delivery rails these are recorded with coincident timestamps and fast_forwarded: true.
- DELIVERED
- Payload retrievable via the pull API. session.data_ready fires with the data URL.
- PURGED
- Data-life elapsed; data destroyed and attested. The API returns the attestation, not a 404.
- FAILED
- Terminal, only when every account failed. Any successful account makes it a delivery instead.
Financial information crosses exactly one endpoint:
GET /data-sessions/{id}/data.
It returns a normalized envelope wrapping the raw, untransformed ReBIT FI JSON per account —
Lender OS normalizes the wrapper, never the data, so your parsers read ReBIT schemas rather
than proprietary inventions. Partial results are results: accounts that failed carry typed
failure reasons alongside the accounts that delivered.
Webhooks & signatures
Every lifecycle transition is delivered as an HTTPS POST to your endpoint. Events carry
references and the full resource — never financial
information, which keeps borrower data off retry queues and out of your webhook logs.
Delivery is at-least-once with a jittered 24-hour backoff schedule; deduplicate on the event
id and order by the gapless per-entity sequence.
# LenderOS-Signature header on every webhook delivery
LenderOS-Signature: t=1752311642,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
# Verify: HMAC-SHA256 over "<t>.<raw body bytes>"
expected = HMAC_SHA256(endpoint_secret, t + "." + raw_body)
# 1. constant-time compare against each v1 value (several during rotation)
# 2. reject if |now - t| > 300 seconds (replay protection)
# 3. deduplicate on the event id, order by per-entity sequence - consent.activated
- Borrower approved and the rail confirmed; the consent is usable. Also signals a resume from PAUSED.
- consent.rejected
- Terminal decline or journey failure, with reasons and serviceability.
- consent.revoked
- Revoked by the borrower at the AA, or by you. Flag the credit file.
- consent.paused
- Borrower paused data acquisition at the AA. Not terminal.
- consent.expired
- The consent’s validity window ended (rail-confirmed).
- session.data_ready
- FI is retrievable at the referenced data URL. Completeness may be PARTIAL.
- session.data_delivered
- You pulled every available account — the custody-handoff record.
- session.failed
- Every account in the session terminally failed, with a typed reason.
Custody: who decrypts, who holds, who purges
AA payloads are end-to-end encrypted from the FIP. Every consent and session reports
custody.decryption_performed_by:
LENDEROS
The key exchange and decryption happened inside your tenant’s isolated key boundary at Lender OS. Keys are per tenant; no cross-tenant material exists.
RAIL
The contracted rail delivered already-decrypted data — a property of the rail your consent was routed to, reported as-is.
Retention is governed by the consent’s data-life:
every session carries a purge_due_at,
purge is executed and attested rather than merely scheduled, and after purge the data
endpoint returns the attestation — evidence outlives the data. Each fetch also records a
payload hash and, on your first successful retrieval, a custody-handoff event.
One error envelope, no upstream leakage
Every non-2xx response is the same envelope: a broad type you can switch on,
a stable machine code,
a retryable flag,
a request_id for support,
and structured details.
Upstream AA error codes are mapped into this taxonomy inside each connector and
never surface — the only upstream handle you ever see is an
opaque incident reference that Lender OS support can resolve.
{
"error": {
"type": "consent_enforcement_error",
"code": "FETCH_FREQUENCY_EXCEEDED",
"message": "This consent permits 2 fetches per MONTH; 2 have been used.",
"retryable": true,
"request_id": "req_01J8ZJ",
"details": {
"frequency": { "unit": "MONTH", "value": 2 },
"used_in_window": 2,
"window_resets_at": "2026-08-01T00:00:00Z"
}
}
}
Mutating calls are idempotent: Idempotency-Key
is required on consent and data-session creation, replays return the original response, and
conflicting reuse is a typed 409. Versioning is additive within /v1;
clients must tolerate unknown enum members and response fields, and breaking changes ship
as a new major version with at least 12 months of overlap.
Ready to go deeper?
The full interactive reference — every endpoint, schema, webhook payload and error code — is rendered live from the OpenAPI spec.