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

# Refund an overpayment

> Return an overpayment credit to the customer and close the loan.

When a repayment exceeds what the loan still owes, the excess stays on the
loan as a credit balance and the loan moves to `Overpaid`. While it is in
that state **no further repayment can be posted to it** — the loan is
settled, and the customer is owed money back.

This endpoint records that you have returned that money. It does not move
money itself: the wallet credit is yours to make. Once recorded, the credit
is cleared and the loan closes.

Two equivalent forms — prefer the externalId form for partner integrations.

<Warning>
  **Credit the wallet first, then call this.** There is no way to un-refund. If
  you record the refund with us and your wallet credit then fails, our ledger
  says the customer was repaid when they were not. The safe order is: move the
  money, then call this endpoint, retrying with the same `idempotencyKey` until
  it succeeds.
</Warning>

## Path parameters

<ParamField path="loan_external_id" type="string" required>
  The loan's externalId. On the `/v1/loans/{loan_id}/refunds` form, this is
  the numeric LMS id instead.
</ParamField>

## Request body

<ParamField body="amount" type="number" required>
  The overpayment you are refunding. It must equal the loan's current
  overpayment exactly; a disagreement is rejected rather than corrected,
  because it means you are working from a figure that has since changed. You
  already hold this number, since you credited the wallet with it before
  calling, and stating it is what ties the two sides to the same figure.
</ParamField>

<ParamField body="idempotencyKey" type="string" required>
  Your own reference for the wallet credit that returned the money (max 100
  characters). If a refund carrying this key has already been recorded on the
  loan, the original transaction is returned unchanged instead of a second
  refund being made. **This is what makes a retry safe**, and it keeps working
  after the loan has closed.
</ParamField>

<ParamField body="transactionDate" type="string">
  Optional value date in ISO `yyyy-MM-dd`, defaulting to today. Unlike a
  repayment there is no `transactionAt`: nothing about a refund depends on the
  exact instant.
</ParamField>

## Examples

```bash theme={null}
# Read the credit, return it in the wallet, then record it here
curl -sf "$BASE/v1/loans/external/loan-ext-12345" \
  -H "Authorization: Bearer $TOKEN" | jq '{status, totalOverpaid}'
# { "status": "Overpaid", "totalOverpaid": 900 }

curl -sf -X POST "$BASE/v1/loans/external/loan-ext-12345/refunds" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "amount": 900, "idempotencyKey": "wallet-refund-abc123" }'
```

## Response

`200 OK` returns the refund transaction plus the loan's position afterwards.
The loan is now closed and carries nothing outstanding.

```json theme={null}
{
  "id": "739",
  "type": "Credit Balance Refund",
  "date": "2026-09-22",
  "amount": 900,
  "overpayment": 900,
  "idempotencyKey": "wallet-refund-abc123",
  "status": "posted",
  "loanStatus": "Closed (obligations met)",
  "totalOutstanding": 0,
  "availableCreditLimit": 0
}
```

<ResponseField name="loanStatus" type="string">
  `Closed (obligations met)` after a successful refund. The overpayment is
  cleared and `totalOverpaid` on the loan returns to `0`.
</ResponseField>

<ResponseField name="amount" type="number">
  The credit that was returned.
</ResponseField>

## Errors

| Code | Sub-code | When |
| - | - | - |
| `failed_precondition` | `loan_not_overpaid` | The loan carries no overpayment. Either it was never overpaid, or the credit has already been refunded. The message includes the loan's current status |
| `invalid_argument` | `validation_error` | `amount` or `idempotencyKey` is missing; `amount` disagrees with the loan's overpayment; `transactionDate` is not ISO `yyyy-MM-dd` or is in the future; `idempotencyKey` is longer than 100 characters |
| `not_found` | `loan_not_found` | No loan with that id or externalId |

Retrying a refund that already succeeded is **not** an error: with the same
`idempotencyKey` you get the original transaction back with `200`, for as long
as the loan exists.

## Avoiding overpayment in the first place

A refund is a recovery path, not a routine one. To stay out of it, read
`totalOutstanding` immediately before each debit and debit exactly that
figure — never a cached amount, never rounded up. See
[Record a repayment](/repayments/repay).
