> ## Documentation Index
> Fetch the complete documentation index at: https://docs.finosu.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Create Loan

> Create or update (upsert) a loan pushed from an external partner. The loan is keyed on (customer, externalReference). Returns 201 on create, 200 on update.

## Overview

Create or update (upsert) a loan pushed from an external partner. The loan is keyed on the combination of `customerId` and `id` (your external loan reference). If an **active** loan with the same external reference already exists for the customer, it is updated in place; otherwise a new loan is created.

This endpoint is designed for **collections intake** -- all loans must be created with `servicingStatus: ACTIVE`.

## Upsert Behavior

`POST /loans` is an **upsert**. If you push a loan that already exists (matched on `id` + `customerId` among active loans), we update the existing record. Soft-deleted or deactivated (NOT\_SERVICING) loans are not matched -- a new active loan is created instead.

* **New loan**: Returns `201 Created`
* **Existing loan updated**: Returns `200 OK`
* **Concurrent duplicate**: Returns `409 Conflict`

## Required Fields

| Field             | Type   | Description                                                                                                        |
| ----------------- | ------ | ------------------------------------------------------------------------------------------------------------------ |
| `id`              | string | Your stable loan reference ID. Must be unique per customer among active loans.                                     |
| `customerId`      | string | Must match the `id` used when the customer was pushed via `POST /customers`.                                       |
| `amountDue`       | number | Origination amount due. **Must be positive. Immutable after creation** -- cannot be changed on subsequent upserts. |
| `dueDate`         | string | Next scheduled payment date (`YYYY-MM-DD`).                                                                        |
| `servicingStatus` | string | Must be `ACTIVE` on create. On upsert, can be set to `NOT_SERVICING` to stop servicing.                            |
| `totalBalance`    | number | Current outstanding balance. Must be non-negative.                                                                 |
| `originationDate` | string | Date the loan was originated (`YYYY-MM-DD`). Required for cadence policy.                                          |

## Validation Rules

### Balance fields (non-negative)

All monetary fields must be `>= 0` when provided: `totalBalance`, `originalFundedAmount`, `originalTotalOwed`, `totalPayoffAmount`, `payoffAmount`, `balanceAtTransfer`, `preTransferPayments`, `chargeoffAmount`.

`amountDue` must be strictly **positive** (`> 0`).

### Rate fields

`interestRate` and `apr` must be non-negative. Values are stored as decimals where `1.0` = 100%. High-interest loans can have values above 1.0 (e.g. `6.49` = 649% APR).

### Chargeoff pairing

`chargeoffDate` and `chargeoffAmount` must **both be set or both omitted**. Setting only one returns a validation error.

### Settlement percentages pairing

`paymentPlanMinPercentage` and `paymentPlanMaxPercentage` must **both be set or both omitted**. When set, both must be between `0` and `1`, and `min <= max`. When both are omitted (null), the loan is **pay-in-full only** (no settlement discount).

For **XRiver** loans, when both percentages are omitted at creation and `originalFundedAmount`, `preTransferPayments`, and `totalBalance` are all provided, the settlement band is computed server-side: if `originalFundedAmount - preTransferPayments` exceeds 65% of `totalBalance`, the loan is pay-in-full only; otherwise it gets a 65%–90% settlement band. Sending the percentage pair explicitly overrides this. The band is set once at creation — later updates never recompute it.

### Portfolio ID

`portfolioId` must be a valid UUID when provided. If omitted, defaults to the configured portfolio for your company.

### Servicing status

`servicingStatus` must be `ACTIVE` on loan creation. To stop servicing on an existing loan, you can either upsert with `servicingStatus: NOT_SERVICING` (which also updates any other loan fields in the same call) or use the dedicated `POST /loans/{id}/cancel-servicing` endpoint. Both cancel all scheduled payments and communications.

### Schedule creation

A collections schedule is automatically created on new loans based on the specified `taskType`. The customer must have a valid IANA timezone set. `taskType` defaults to `Collections` if not specified.

## Request Body

```json theme={null}
{
  "id": "LOAN-12345",
  "customerId": "CUST-001",
  "type": "CASH_ADVANCE",
  "applicationStatus": "FUNDED",
  "originalFundedAmount": 500.00,
  "originalTotalOwed": 650.00,
  "interestRate": 0.2999,
  "apr": 0.3550,
  "originationDate": "2026-04-01",
  "startDate": "2026-04-01",
  "totalBalance": 425.00,
  "payoffAmount": 425.00,
  "amountDue": 125.00,
  "dueDate": "2026-05-01",
  "servicingStatus": "ACTIVE",
  "autopayEnabled": true,
  "isVisibleInBorrowerPortal": true,
  "portfolioId": "3607837d-2daf-4772-90db-638a2fa008df",
  "paymentPlanMinPercentage": 0.60,
  "paymentPlanMaxPercentage": 0.80,
  "taskType": "Collections"
}
```

## Error Responses

| Status Code | Description                                                                                                          |
| ----------- | -------------------------------------------------------------------------------------------------------------------- |
| 400         | Invalid enum value, `servicingStatus` not `ACTIVE` on create, mismatched `amountDue` on upsert, invalid timezone     |
| 404         | Customer with the given `customerId` not found                                                                       |
| 409         | Concurrent duplicate -- another request created the same loan                                                        |
| 422         | Validation error (negative balance/rate, unpaired chargeoff/settlement fields, invalid UUID, missing required field) |
| 500         | Internal server error                                                                                                |


## OpenAPI

````yaml POST /loans
openapi: 3.1.0
info:
  title: Finosu API
  description: Finosu API
  license:
    name: MIT
  version: 1.0.0
servers: []
security:
  - apiKeyAuth: []
paths:
  /loans:
    post:
      description: >-
        Create or update (upsert) a loan pushed from an external partner. The
        loan is keyed on (customer, externalReference). Returns 201 on create,
        200 on update.
      requestBody:
        description: Loan data
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/NewLoan'
        required: true
      responses:
        '200':
          description: Existing loan updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Loan'
        '201':
          description: Loan created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Loan'
        '400':
          description: >-
            Invalid enum value, servicingStatus not ACTIVE on create, mismatched
            amountDue on upsert, invalid timezone
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: Customer not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '409':
          description: Concurrent duplicate — another request created the same loan
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '422':
          description: >-
            Validation error (negative balance, rate > 1, unpaired
            chargeoff/settlement fields, invalid UUID, missing required field)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '500':
          description: Internal server error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
components:
  schemas:
    NewLoan:
      required:
        - id
        - customerId
        - amountDue
        - dueDate
        - servicingStatus
        - totalBalance
        - originationDate
      type: object
      properties:
        id:
          description: Your stable loan reference ID
          type: string
          example: LOAN-12345
        customerId:
          description: External reference of a previously-pushed customer
          type: string
          example: CUST-001
        type:
          type: string
          enum:
            - CASH_ADVANCE
            - PERSONAL_LOAN
            - INSTALLMENT_LOAN
        applicationStatus:
          type: string
          enum:
            - PENDING
            - APPROVED
            - FUNDED
            - WITHDRAWN
            - REJECTED
        originalFundedAmount:
          type: number
          minimum: 0
          example: 500
        originalTotalOwed:
          type: number
          minimum: 0
          example: 650
        interestRate:
          description: Non-negative decimal where 1.0 = 100% (e.g. 6.49 = 649%)
          type: number
          minimum: 0
          example: 0.2999
        apr:
          description: Non-negative decimal where 1.0 = 100%
          type: number
          minimum: 0
          example: 0.355
        originationDate:
          description: Required. Date the loan was originated.
          type: string
          format: date
          example: '2026-04-01'
        startDate:
          type: string
          format: date
        totalBalance:
          description: Required. Current outstanding balance.
          type: number
          minimum: 0
          example: 425
        payoffAmount:
          type: number
          minimum: 0
        totalPayoffAmount:
          type: number
          minimum: 0
        balanceAtTransfer:
          type: number
          minimum: 0
        preTransferPayments:
          type: number
          minimum: 0
        amountDue:
          description: Origination amount due. Must be positive. Immutable after creation.
          type: number
          exclusiveMinimum: 0
          example: 125
        dueDate:
          description: Next payment date (YYYY-MM-DD)
          type: string
          format: date
          example: '2026-05-01'
        servicingStatus:
          description: Must be ACTIVE on create. Use cancel-servicing endpoint to stop.
          type: string
          enum:
            - ACTIVE
        isSettled:
          type: boolean
        chargeoffDate:
          description: Must be paired with chargeoffAmount
          type: string
          format: date
        chargeoffAmount:
          description: Must be paired with chargeoffDate
          type: number
          minimum: 0
        autopayEnabled:
          type: boolean
          example: true
        noCallReason:
          type: string
          enum:
            - CUSTOMER_OPT_OUT
            - LEGAL_HOLD
            - BANKRUPT
            - INCARCERATED
            - DECEASED
            - MILITARY
            - FRAUD
            - AGENCY_REQUEST
            - COMPLAINT
            - WRONG_NUMBER
            - OTHER
        noTextReason:
          type: string
          enum:
            - CUSTOMER_OPT_OUT
            - LEGAL_HOLD
            - BANKRUPT
            - INCARCERATED
            - DECEASED
            - MILITARY
            - FRAUD
            - AGENCY_REQUEST
            - COMPLAINT
            - WRONG_NUMBER
            - OTHER
        noEmailReason:
          type: string
          enum:
            - CUSTOMER_OPT_OUT
            - LEGAL_HOLD
            - BANKRUPT
            - INCARCERATED
            - DECEASED
            - MILITARY
            - FRAUD
            - AGENCY_REQUEST
            - COMPLAINT
            - WRONG_NUMBER
            - OTHER
        isVisibleInBorrowerPortal:
          type: boolean
          example: true
        daysPastDueAtBoarding:
          type: integer
          minimum: 0
        daysSinceOriginationAtBoarding:
          type: integer
          minimum: 0
        externalLmsMetadata:
          type: object
        portfolioId:
          description: Portfolio UUID for payment processor routing
          type: string
          format: uuid
        paymentPlanMinPercentage:
          description: >-
            Min settlement percentage (0-1). Both min+max required together.
            NULL = pay-in-full.
          type: number
          minimum: 0
          maximum: 1
        paymentPlanMaxPercentage:
          description: >-
            Max settlement percentage (0-1). Both min+max required together.
            NULL = pay-in-full.
          type: number
          minimum: 0
          maximum: 1
        taskType:
          description: >-
            Schedule task type (e.g. Collections). Defaults to Collections for
            JustLoans.
          type: string
          example: Collections
    Loan:
      type: object
      properties:
        id:
          type: string
          description: External loan reference ID
        customerId:
          type: string
          description: External customer reference ID
        type:
          type: string
        applicationStatus:
          type: string
        originalFundedAmount:
          type: number
        originalTotalOwed:
          type: number
        interestRate:
          type: number
        apr:
          type: number
        originationDate:
          type: string
          format: date
        startDate:
          type: string
          format: date
        totalBalance:
          type: number
        payoffAmount:
          type: number
        totalPayoffAmount:
          type: number
        balanceAtTransfer:
          type: number
        preTransferPayments:
          type: number
        amountDue:
          type: number
        dueDate:
          type: string
        servicingStatus:
          type: string
        stopServicingReason:
          type: string
        isSettled:
          type: boolean
        chargeoffDate:
          type: string
        chargeoffAmount:
          type: number
        autopayEnabled:
          type: boolean
        noCallReason:
          type: string
        noTextReason:
          type: string
        noEmailReason:
          type: string
        isVisibleInBorrowerPortal:
          type: boolean
        daysPastDueAtBoarding:
          type: integer
        daysSinceOriginationAtBoarding:
          type: integer
        externalLmsMetadata:
          type: object
        portfolioId:
          type: string
          format: uuid
        paymentPlanMinPercentage:
          type: number
        paymentPlanMaxPercentage:
          type: number
        taskType:
          type: string
    Error:
      required:
        - error
        - message
      type: object
      properties:
        error:
          type: integer
          format: int32
        message:
          type: string
  securitySchemes:
    apiKeyAuth:
      type: apiKey
      in: header
      name: X-API-Key

````