Build on the same API our own screens are built on
This is the integration reference, written from the integrator's chair. OneApp Finance is designed API-first: a public REST surface, OAuth2, and signed webhooks back both its own borrower and operations screens and yours, with no private backdoors. What follows is the contract you build against, not a "call it live today" pitch. Where /modularity/ covers who renders each step, this page is the wire-level shape: the endpoints, the auth, the events, and the guarantees the Core is built to hold.
The API is the product surface, not an afterthought.
OneApp Finance is software, not a lender. The platform is designed so there is exactly one engine underneath: the same public API drives the white-label screens we ship and any front end you build. Nothing your integration can reach is a private side channel, and nothing our own screens use is hidden from you. That is the point of an API-first design: the contract is the system, so what you build against is what actually runs.
A public REST surface, designed as a contract
An inbound integration submits a complete application for a first- or second-look decision over the public REST API, the same API OneApp Finance's own screens are built on. The shape below is the designed contract: a typed request, an explicit idempotency key, and a 202 that hands the decision back on a signed webhook. Build against this; nothing here claims a live service you can call today.
Submit an application, get a decision
POST the full application, including the consent evidence, and the Core returns a 202 while it works. The decision arrives on the decision.completed webhook, or inline in sync mode. A single decisioned status carries the approve, decline, refer, or counteroffer outcome. Idempotent on your key, so a retry is safe.
// The contract you build against, not "call it live today".
// Base URL is your own instance host (single-tenant).
POST https://{instance-host}/partner-intake/applications
Authorization: Bearer <oauth2-client-credentials-token>
Idempotency-Key: "7c3f1a90-2b6e-4d1c-9a2f-001"
Content-Type: application/json
{
"look_type": "FIRST_LOOK",
"partner_external_ref": "ACME-APP-99812",
"program_id": "prg_01J...",
"borrower": { "...": "..." },
"property": { "...": "..." },
"requested_amount": { "currency": "USD", "amount_cents": 1850000 },
"consent": {
"soft_or_hard": "HARD_PULL",
"copy_deck_version": "acme_consent_v4",
"render_hash_sha256": "sha256:..."
}
}
// 202 Accepted
{
"application_id": "app_01J...",
"status": "submitted",
"decision": null
}
// Decision arrives on decision.completed (or ?mode=sync).
// One decisioned status carries the outcome on decision_outcome.
OAuth2 client credentials, against your own host
Authentication is designed as OAuth2 client credentials: your service exchanges a client ID and secret for a short-lived bearer token, then carries that token on every call. There is no shared platform gateway and no global tenant. The base URL is your own instance host, because each operator runs a single-tenant install. Your token, your host, your data, never pooled across operators.
- Client credentials grant. Machine-to-machine. You hold the secret, rotate it on your schedule, and scope each client to the programs it may act on.
- Your instance is the base URL. https://{instance-host} resolves to your single-tenant deployment. There is no multi-tenant endpoint to address by accident.
- Scoped tokens. A token is designed to carry only the program and capability scopes you granted the client, so an intake client cannot reach a funding action it was never given.
- Sandbox first. The intended path is to build and certify against a sandbox instance with the same contract, then point the same code at production by changing the host and credentials.
How auth resolves
- OAuth2 client credentials, short-lived bearer tokens
- Base URL is your own single-tenant instance host
- Per-client scopes by program and capability
- One contract across sandbox and production
Subscribe once. The Core tells you what happened.
Every state change you care about is designed to arrive as a signed webhook. Deliveries are retried with backoff until your endpoint returns a 2xx, each carries an event ID so a duplicate is safe to ignore, and you verify the signature against your secret before you trust the body. These are the same events the Core is built to emit, to its own screens and to yours.
- decision.completed The approve, decline, refer, or counteroffer outcome is ready on the application.
- application.status_changed The application moved between states: submitted, decisioned, signed, funded, or withdrawn.
- document.completed An e-sign or disclosure packet finished, with the evidence package attached.
- disbursement.released A staged draw cleared after the homeowner confirmed it on their own device.
- dealer.standing_changed A contractor's standing state changed and may gate their new applications.
// The delivery contract you build against, not "call it live today".
// Signed delivery to the URL you register.
// Verify the signature before you trust the body.
POST https://{your-endpoint}
OneApp-Signature: t=1714000000,v1=<hmac-sha256>
OneApp-Event-Id: "evt_01J..." // dedupe; retried until 2xx
Content-Type: application/json
{
"event": "decision.completed",
"application_id": "app_01J...",
"partner_external_ref": "ACME-APP-99812",
"decision_outcome": "APPROVE", // APPROVE | DECLINE | REFER | COUNTEROFFER
"occurred_at": "..."
}
Retries are safe. Some failures are meant to stay failures.
A distributed integration retries. The contract is designed so that retrying is the correct default, and so that the errors which protect a borrower or the ledger do not quietly disappear on the next attempt. Two ideas do the work: idempotency keys, and typed errors that say plainly whether you should try again.
One key, one effect
Every state-changing call carries an Idempotency-Key you generate. Send the same key twice and the Core is designed to return the original result rather than act a second time. A dropped connection, a timeout, a redeploy mid-call: retry with the same key and you get exactly-once behavior, never a double submission and never a double disbursement.
Errors that tell you what to do
Failures are designed to be typed, not a bare status code. Each error carries a stable code and a retryable flag, so your client knows whether to back off and try again or to stop and fix the request. Transient infrastructure errors are retryable; a malformed or unauthorized request is not.
Some errors are non-retryable by design
A class of errors represents a structural invariant, not a glitch. Try to fund without a consent artifact and the Core is built to return a non-retryable, non-overridable error: hammering the endpoint will never change the answer, because the missing input is the point. The fix is the artifact, not the retry.
Every decision pins the exact version that ran
An audit asks what ran, not what runs today. So each decision is designed to bind the exact model version and policy version that produced it, plus a hashed snapshot of the inputs. A replay loads that pinned version and reproduces the original outcome. It never resolves "latest," because "latest" is whatever changed since, and that is exactly what a regulator does not want to see substituted for the record.
- Decisions are immutable. The model and policy version that decided an application are written into the decision record and do not move when you ship a new version.
- Replay is reproducible. Re-run a past decision and the Core loads the pinned version against the frozen inputs, so you get the same answer years later.
- A change is a new version, never an edit. Tweak a cutoff and you publish a new version. Old decisions keep pointing at the version that was in force when they ran.
- API contracts are explicit. The request and response shapes are designed to be versioned deliberately, so an integration you certify keeps working against the contract it was built on.
What pinning buys you
- Each decision binds its exact model and policy version
- A hashed input snapshot, frozen at decision time
- Replay loads that version, never "latest"
- A new cutoff is a new version, not an edit in place
The full reference comes with onboarding.
An honest note: the endpoints, event names, and field shapes on this page are the designed contract, drawn to show you the integration model, not a published spec you can hit right now. The complete reference, the sandbox instance, and your scoped credentials are provisioned during onboarding, against your own single-tenant host. For who renders each step, how to embed by SDK or iframe, and where ownership sits per step, see Modularity →
Common questions
Can I call the API today?
Is this the same API your own screens use?
If I retry a call, will I double-submit or double-fund?
See the contract running on your own instance
Walk the intake call, the signed events, and version-pinned replay against a sandbox, then map your build with our team.