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

# Lending

> Turn loan events into balanced entries.

The Lending service turns loan events into entries on the Lending module's accounts. PanoQore holds no loan terms. Schedules, interest calculation, allocation and credit decisions stay in your product, which tells the ledger what happened.

A loan is two accounts, known by your `loanRef`: principal at `AST:LON:<CCY>:<ref>` and accrued interest at `AST:INT:<CCY>:<ref>`. What is owed is their balances. `loanRef` is letters, digits, `-` and `_`, up to 40 characters, and carries no personal data.

## Before you start

A person must switch on the `lending` module, which opens the accounts below. Until then, opening a loan gets `MODULE_OFF`.

| Account | Used for |
| - | - |
| `LIA:DCL:<CCY>` | Disbursement clearing |
| `AST:ECL:<CCY>:S1`, `S2`, `S3` | Loss allowance by stage |
| `INC:INT:<CCY>`, `INC:PEN:<CCY>` | Interest and penalty income |
| `INC:REC:<CCY>` | Recoveries |
| `EXP:ECL:<CCY>` | Impairment charge |
| `EXP:WOF:<CCY>` | Write-offs |

Your key needs `entries:write` to record events.

## Open a loan

```bash theme={null}
curl -X POST "$PANOQORE_URL/v1/loans" \
  -H "Authorization: Bearer $PANOQORE_API_KEY" \
  -H "Idempotency-Key: open-LN-1042" \
  -H "Content-Type: application/json" \
  -d '{"loanRef":"LN-1042","currency":"NGN"}'
```

```json theme={null}
{
  "loanRef": "LN-1042",
  "currency": "NGN",
  "status": "CLEARED",
  "principalAccountId": "acc_01M6F1DRCZF1EAZT1QJ3JYGXT2",
  "interestAccountId": "acc_01M6F1DRD0E2NTMA3PEDYJRGXV",
  "principalOutstanding": "0",
  "interestOutstanding": "0",
  "totalOutstanding": "0",
  "openedAt": "2026-11-02T21:07:42.111546Z"
}
```

Principal and accrued interest can't go below zero. Taking more off than is owed is refused, including when repayments race, and including entries posted straight to these accounts.

`status` is `OPEN` while something is owed and `CLEARED` when nothing is. `CLEARED` covers a loan not yet drawn, one repaid, and one written off, so a new loan and a written-off loan read the same. Tell them apart from your own records or the entries.

Accounts you opened yourself under those codes count as a loan too.

## Record events

Each event is one call to `POST /v1/loan-events`. With `entries:write`, it posts at once.

```bash theme={null}
curl -X POST "$PANOQORE_URL/v1/loan-events" \
  -H "Authorization: Bearer $PANOQORE_API_KEY" \
  -H "Idempotency-Key: LN-1042:disburse" \
  -H "Content-Type: application/json" \
  -d '{"kind":"DISBURSE","currency":"NGN","loanRef":"LN-1042","amount":"10000000","accountId":"'"$WALLET"'","externalRef":"LN-1042:disburse"}'
```

The answer holds either `entry`, when it posted, or `approval`, when it waits for a person. The entry's `kind` is the event's with `LOAN_` in front, such as `LOAN_DISBURSE`.

| `kind` | Debit | Credit | Fields |
| - | - | - | - |
| `DISBURSE` | Principal | `accountId`, or disbursement clearing if empty | `loanRef`, `amount` |
| `ACCRUE_INTEREST` | Accrued interest | Interest income | `loanRef`, `amount` |
| `CHARGE_PENALTY` | Accrued interest | Penalty income | `loanRef`, `amount` |
| `REPAY` | `accountId` | Principal and accrued interest | `loanRef`, `accountId`, `amount`, `interestAmount` |
| `RECOVER` | `accountId` | Recoveries | `loanRef`, `accountId`, `amount` |
| `PROVISION` | Impairment charge | Allowance at `stage` | `amount`, `stage` |
| `RELEASE_PROVISION` | Allowance at `stage` | Impairment charge | `amount`, `stage` |
| `MOVE_STAGE` | Allowance at `stage` | Allowance at `toStage` | `amount`, `stage`, `toStage` |
| `WRITE_OFF` | Allowance, then write-offs | Principal and accrued interest | `loanRef`, `amount`, `stage`, `reason` |

`stage` is 1, 2 or 3. For `REPAY`, `amount` comes off principal and `interestAmount` off accrued interest, as your product allocated it, and a repayment above what is owed gets `INVALID_AMOUNT`.

`PROVISION`, `RELEASE_PROVISION` and `MOVE_STAGE` act on the portfolio, so they take no `loanRef`.

## Write-offs

A write-off always goes to a person, even from a key with `entries:write`. Everything owed comes off, and `amount` is what the allowance at `stage` absorbs. The rest goes to write-offs. `reason` needs at least 10 characters.

```bash theme={null}
curl -X POST "$PANOQORE_URL/v1/loan-events" \
  -H "Authorization: Bearer $PANOQORE_API_KEY" \
  -H "Idempotency-Key: LN-1042:writeoff" \
  -H "Content-Type: application/json" \
  -d '{"kind":"WRITE_OFF","currency":"NGN","loanRef":"LN-1042","amount":"300000","stage":2,"reason":"Customer unreachable for 180 days","externalRef":"LN-1042:writeoff"}'
```

```json theme={null}
{ "approval": { "approvalId": "adj_01M6F1EE30FN5A2JWAPR6T4A0D" } }
```

An approver decides it in the console, and the entry then posts as `LOAN_WRITE_OFF`. If the loan's balance moved in the meantime, the request ends `FAILED` and must be proposed again. A key cannot approve its own request, or anyone's.

What a person records through the console for any event also goes to a second person.

## Read the book

```bash theme={null}
curl "$PANOQORE_URL/v1/loans/NGN/LN-1042" -H "Authorization: Bearer $PANOQORE_API_KEY"
curl "$PANOQORE_URL/v1/loans?currency=NGN" -H "Authorization: Bearer $PANOQORE_API_KEY"
```

`GET /v1/loan-book` takes no parameters and answers with one line per currency.

```json theme={null}
{
  "lines": [
    { "currency": "NGN", "loans": "1", "openLoans": "1", "principal": "8000000", "interest": "70000", "allowanceStage1": "200000", "allowanceStage2": "300000", "allowanceStage3": "0" }
  ]
}
```

The allowance is shown as a positive number in minor units.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.