> ## 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 a paid penalty

> Return a penalty the customer already paid, when it turns out not to have been owed.

An overdue penalty is charged at the loan's due instant and, because the LMS
settles penalties before interest and principal, the next repayment pays it
first. Sometimes that penalty should never have applied: the customer paid on
the wallet before the due instant, but the repayment reached us after it and
without a `transactionAt`, so it settled the penalty instead of removing it.

This endpoint corrects that. It does **not** move money. It returns what was
paid on the penalty to the loan as a credit balance, and the loan moves to
`Overpaid`. You then return that credit to the customer with
[Refund an overpayment](/repayments/refund), exactly as for any other
overpayment.

Two equivalent forms, `/v1/loans/{loan_id}/penalties/refund` and the
externalId form above. Prefer the externalId form for partner integrations.

<Warning>
  **This is a two-step correction.** After this call the customer is owed money
  and the loan shows it as `totalOverpaid`. The correction is complete only
  when you have credited the wallet and recorded it with `POST .../refunds`.
  Read `totalOverpaid` from this response and pass it as that call's `amount`.
</Warning>

## Path parameters

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

## Request body

<ParamField body="idempotencyKey" type="string" required>
  Your own reference for this correction, for example the support ticket or
  the wallet transaction id that proved the payment was on time (max 100
  characters). If a penalty refund carrying this key has already been recorded
  on the loan, the original is returned unchanged instead of a second refund
  being made. **This is what makes a retry safe.**
</ParamField>

<ParamField body="note" type="string">
  Optional free text recorded on the LMS transaction, such as why the penalty
  was not owed (max 500 characters).
</ParamField>

There is no `amount`: the whole paid penalty is returned, and the amount you
state is on the follow-up refund call. There is no `transactionDate` either:
the LMS books a charge refund on the day it is recorded and accepts no other.

## Examples

```bash theme={null}
# 1. Return the paid penalty to the loan
curl -sf -X POST "$BASE/v1/loans/external/loan-ext-12345/penalties/refund" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "idempotencyKey": "cs-ticket-4411", "note": "wallet shows payment before the due instant" }'

# 2. Credit the wallet, then record it (amount = totalOverpaid from step 1)
curl -sf -X POST "$BASE/v1/loans/external/loan-ext-12345/refunds" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "amount": 1000, "idempotencyKey": "wallet-refund-abc123" }'
```

## Response

`200 OK` returns the refund transaction(s) and the loan's position afterwards.

```json theme={null}
{
  "refunded": 1000,
  "transactions": [
    {
      "id": "1786",
      "type": "Charge Refund",
      "date": "2026-09-28",
      "amount": 1000,
      "overpayment": 1000,
      "idempotencyKey": "cs-ticket-4411",
      "status": "posted"
    }
  ],
  "idempotencyKey": "cs-ticket-4411",
  "loanStatus": "Overpaid",
  "totalOutstanding": 0,
  "totalOverpaid": 1000
}
```

<ResponseField name="refunded" type="number">
  The total returned to the loan by this call.
</ResponseField>

<ResponseField name="transactions" type="array">
  One `Charge Refund` transaction per penalty charge refunded. A loan normally
  carries one penalty, so this is normally one entry.
</ResponseField>

<ResponseField name="loanStatus" type="string">
  `Overpaid` after a successful refund on a settled loan.
</ResponseField>

<ResponseField name="totalOverpaid" type="number">
  The credit now owed to the customer. Pass this as `amount` to
  `POST .../refunds`.
</ResponseField>

## Errors

| Code | Sub-code | When |
| - | - | - |
| `failed_precondition` | `no_paid_penalty` | The loan carries no paid penalty: none was charged, it was waived, or it is still outstanding. An outstanding penalty is settled by the repayment, not refunded |
| `failed_precondition` | `penalty_already_refunded` | Every paid penalty on the loan has already been returned to it, under a different key. Retrying with the original key returns that refund instead |
| `invalid_argument` | `validation_error` | `idempotencyKey` is missing or longer than 100 characters; `note` is longer than 500 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`.

## Avoiding the need for this

This endpoint exists for a penalty that was paid because timing evidence
arrived late. Send `transactionAt`, the exact wallet time, on every repayment
including the first call. A payment made before the due instant then removes
its penalty as the repayment posts, and there is nothing to refund. See
[Record a repayment](/repayments/repay).
