Skip to main content
POST
Create Loans (Batch)

Overview

Create or update (upsert) many loans in a single request. Use this for portfolio boarding and for daily bulk syncs from your loan management system. The endpoint processes loans asynchronously — it returns a job ID immediately (HTTP 202 Accepted) and works the batch in the background. Poll GET /loans/batch/{jobId} for progress and for every row that failed. Each row has exactly the same semantics as Create Loan — same fields, same validation, same upsert behavior, same schedule creation. Nothing about a loan behaves differently because it arrived in a batch.
Customers must exist before their loans are pushed. Board customers first via POST /customers/batch, then send this request. A row whose customerId is unknown fails individually and does not affect the rest of the batch.

Batch Size

There is no fixed limit on rows per request. Large batches are split server-side into slices of 25 rows that are processed in parallel, so a boarding file does not have to be chunked by hand. That said, the whole request is buffered and validated before anything is queued, so prefer requests in the low thousands over one enormous submission — it keeps validation feedback fast and makes a network failure cheaper to retry. Every (customerId, id) pair must be unique within a single request. Rows are worked in parallel slices, so two rows naming the same loan would race — which one wins depends on worker timing, and if they disagree on amountDue the loser is reported as a per-row failure for a loan you did intend to send. We reject the request up front instead. This is a different case from sending the same loan again in a later request, which updates it rather than duplicating it — see Idempotency.

Request Body Format

An array of loan records wrapped in a loans field:

Individual Loan Schema

Each object in the loans array follows the same schema as the single loan endpoint, including all required fields (id, customerId, amountDue, dueDate, servicingStatus, totalBalance, originationDate) and every validation rule — non-negative balances, chargeoff field pairing, settlement percentage pairing, and servicingStatus: ACTIVE on create.

Response Format

Response Fields

  • jobId — Unique identifier for the batch job. Use it with GET /loans/batch/{jobId}
  • totalLoans — Number of loans queued for processing
  • message — How many parallel slices the job was split into, and how to poll it

Checking Job Status

Use GET /loans/batch/{jobId} to check progress and read every row that failed:

Status Values

  • pending — Queued, not yet started
  • processing — Slices are being worked
  • completed — Finished. Individual rows may still have failed — check failures
  • failed — Every row failed, or the job hit a fatal error. See errorMessage
status: completed does not mean every loan succeeded — it means the job finished. Always reconcile against failed and failures rather than treating completed as success.

Failures

failures lists every row that could not be written — never truncated, however many there are. Successful rows are not enumerated; they are the set you submitted minus these. index is the position in your array, not within a server-side slice, so you can walk failures straight against your source file. Common failure messages:
  • 404: Customer with ID {customerId} not found — board the customer first via POST /customers or POST /customers/batch
  • 400: amountDue is immutable... — the row changed amountDue on a loan that already exists
  • 400: Invalid type '...'. Valid values: [...] — an unrecognised enum value, checked when the row is written rather than during request validation
  • 400: Customer timezone is required for schedule creation... — the customer has no valid IANA timezone, so no collections schedule can be built
  • 500: Internal error processing this loan — a fault on our side. The row was not written; resubmit it

Polling

Slices publish their failures when they finish, so failures fills in in bursts and is partial while status is processing. Poll until status is completed or failed before treating the list as the final record. The array is always sorted by index regardless of which slice finished first. progressPercentage is (processed + failed) / totalLoans * 100, so it reaches 100 even when some rows failed. Status errors: 400 if jobId isn’t a valid UUID, 404 if no loan batch job with that ID exists for the authenticated company.

Partial Success Handling

  • Individual failures don’t stop processing — a bad row is recorded and skipped; the remaining loans are still processed, including the rest of its own slice
  • Transaction isolation — each loan is committed in its own transaction, so one failure can never roll back a loan that already succeeded
  • Nothing is truncated — every failed row is reported, however many there are
Note the split between the two kinds of error. Anything the request schema can catch — a negative balance, an unpaired chargeoffDate, a malformed portfolioId, an empty loans array, a duplicate (customerId, id) — is rejected as 422 up front and nothing is queued. Everything checked while the row is written — an unknown customerId, a mismatched immutable amountDue, a customer with no valid timezone — is reported per row in failures. Enum-valued fields fall in that second group, which is easy to guess wrong. type, applicationStatus, servicingStatus, and the no*Reason fields are plain strings on the wire, so an unrecognised value passes request validation and is only rejected when that row is written — a per-row 400, not an up-front 422. This matches Create Loan, which returns 400 for the same mistake.

Idempotency

Batch loan creation is idempotent on (customerId, id) across requests. Every row upserts, so resubmitting a batch after a timeout or a partial failure updates the loans that already landed rather than duplicating them. No schedule or scheduled payment is created a second time. Repeating a pair within one request is the separate case rejected up front — see Batch Size. Retrying a batch verbatim is therefore always safe, and resubmitting the whole file is a reasonable way to recover from a partial failure. Note that amountDue is immutable after creation, so an unchanged resubmission is fine, but changing amountDue on a resubmitted row fails that row.

What the Endpoint Does

  1. Validates the whole request — every row against the loan schema, plus the duplicate check. Any failure rejects the request with 422 and queues nothing
  2. Queues the job — stores the submitted payload, splits it into slices of 25 rows, and returns HTTP 202 with a job ID
  3. Processes slices in parallel — for each loan, in its own transaction:
    • Resolves the customer by customerId
    • Creates the loan, or updates it if an active loan with that id already exists
    • Creates the collections schedule for the resolved taskType (new loans only)
    • Creates the opening scheduled payment (new loans only)
    • Records the row on the job if it failed

When to Use Batch vs Single

Use the batch endpoint when:
  • Boarding an initial portfolio
  • Running a daily or weekly bulk sync from your loan management system
  • You have more than 5–10 loans to push
Use the single endpoint when:
  • Pushing a loan in real time as it boards
  • You need the created loan echoed back synchronously — the batch response contains only a job ID, not loan bodies
  • Testing or debugging one loan

Error Handling

The endpoint returns 202 Accepted whenever the request is well-formed. Request-level failures: Per-row failures are not HTTP errors. They surface in the failures array of the status response with the status code the equivalent single call would have returned — 404 for an unknown customerId, 400 for a changed immutable amountDue, an unrecognised enum value, a missing customer timezone, or a taskType that isn’t valid for your company.

Authentication

Authentication via X-API-Key header:
  • API key is company-scoped
  • Must be obtained from Finosu
  • Same authentication as the single loan endpoint
  • A jobId is only visible to the company that created it

Example Usage

This endpoint is typically used for:
  • Portfolio boarding — push a placement’s loans as they transfer
  • Daily reconciliation — resend open accounts so balances and statuses stay in sync
  • Bulk LMS sync — mirror loan state from your loan management system

Authorizations

X-API-Key
string
header
required

Body

application/json

Batch of loan records

loans
object[]
required

Array of loan records to create. Every (customerId, id) pair must be unique within the request.

Minimum array length: 1

Response

Batch job accepted for async processing. Use GET /loans/batch/{jobId} to check status.

jobId
string<uuid>

Job ID. Use with GET /loans/batch/{jobId}

Example:

"550e8400-e29b-41d4-a716-446655440000"

totalLoans
integer

Number of loans queued for processing

Example:

400

message
string

How many parallel slices the job was split into, and how to poll it

Example:

"Batch job created with 16 parallel batches. Use GET /loans/batch/550e8400-e29b-41d4-a716-446655440000 to check status."