---
updatedAt: 2026-09-30T17:50:34.235Z
---

Fetch the complete documentation index at: https://docs.avvio.xyz/llms.txt. Use this file to discover all available pages before exploring further. Append .md to any documentation page URL to get its markdown version.

# Refund a payment

`POST https://api.avvio.xyz/business/api/v1/checkout/organizations/{orgId}/payments/{paymentId}/refund`

Refund a card payment, whole or in part.

**Moves money** out of your balance and back to the buyer's card. Omit
`amount` to refund everything still refundable; pass a decimal string
in the payment's currency to refund part of it. `paymentId` is the
payment's `id` from `GET /links/{linkId}/payments` (the acquirer's
`externalId` is accepted too).

**The key needs the `refunds` scope.** It is a consent an owner or
admin grants when the key is issued (`scopes: ["write", "refunds"]`);
a key without it, including one issued before scopes existed, gets
`403 INSUFFICIENT_SCOPE` and nothing moves.

`Idempotency-Key` is required, and the acquirer's refund endpoint has
no idempotency of its own, so the key is what makes a retry safe: reuse
it. A byte-identical refund of the same payment under a *different*
key within fifteen minutes is refused with `DUPLICATE_REQUEST_DETECTED`;
a genuine second refund of the same amount sends `X-Allow-Duplicate:
true`. One refund is in flight per payment at a time: a second one
while the first is still being confirmed is `409 REFUND_IN_PROGRESS`;
read the payment and try again once `refundedBase` has moved.

**When the outcome is unknown** (the card processor did not answer
definitively) the answer is `500 PAYOUT_OUTCOME_UNKNOWN`. The refund
may have gone through. **Do not resend it, and never under a new
`Idempotency-Key`**: read the payment first. `refundedBase` moves, and
`checkout_payment.refunded` or `.partially_refunded` follows, if it
did. A replay of the same key answers `409 PAYOUT_OUTCOME_UNKNOWN`
until we resolve it. (The shared message speaks of a payout and
`GET /orders`; for a refund, read the payment instead.)

A full refund publishes `checkout_payment.refunded`; a partial one
publishes `checkout_payment.partially_refunded` (opt-in, like the rest
of the family). Both carry the refund just made in `refund`. Every
payment read lists its refunds under `refunds`.

## Parameters

- `orgId` (path, required) — The opaque organization id issued to you, normally CUID-shaped (for example `cmsx…`). It is not an `org_`-prefixed alias. Pass it unchanged in every organization-scoped path.
- `paymentId` (path, required) — The payment's `id`, or the acquirer's `externalId`.
- `Idempotency-Key` (header, required) — A unique value per logical operation, 1-255 chars of `A-Z a-z 0-9 _ . : -`. Reuse it to retry. Same key with the same body replays the stored response; same key with a *different* body is a `409`, because answering with the first call's result would hand you a receipt for a payout you did not request. A `4xx` releases the key, so you can fix the body and reuse it. **Reuse it; do not generate one per attempt.** A key minted per attempt defeats replay entirely: every retry looks like a new request, so every retry pays. We also watch for an identical body arriving under a *different* key within 15 minutes and refuse it with `DUPLICATE_REQUEST_DETECTED`. Records are kept for 7 days. That is a retention window only: there is no path where an expired key is re-executed.
- `X-Allow-Duplicate` (header) — Set to `true` to send a request that is byte-identical to one you sent within the last 15 minutes under a different key. `true` is the only accepted value. It switches off the guard that catches a retry arriving under a fresh key, so send it only when you mean to pay twice.

## Example

```bash
IDEMPOTENCY_KEY=$(uuidgen)  # new key per call; reuse it only to retry this exact request
curl -s -X POST "$AVVIO_BASE_URL/checkout/organizations/$AVVIO_ORG_ID/payments/$paymentId/refund" \
  -H "x-api-key: $AVVIO_API_KEY" \
  -H "Idempotency-Key: $IDEMPOTENCY_KEY" \
  -H "content-type: application/json" \
  -d '{
        "amount": "12.50",
        "reason": "requested_by_customer",
        "note": "Session moved to next week"
      }'
```

## Responses

- `200` — The payment after the refund, with the refund that was just made.
- `400` — `VALIDATION_ERROR` (a malformed amount, more decimal places than the currency has, an unknown reason, a note over 255 characters), `REFUND_EXCEEDS_REMAINING` (more than `amountBase - refundedBase`), `REFUND_NOT_ALLOWED` (the payment is not `paid`: already refunded in full, charged back, failed or still pending), `REFUND_DISPUTED` (a dispute is open; it decides where the money goes), `PROVIDER_REJECTED` (the card processor refused, with its words), or `IDEMPOTENCY_KEY_REQUIRED` / `IDEMPOTENCY_KEY_INVALID`. **Nothing moved** on any of them.
- `401` — The key was refused. Nothing ran. - `UNAUTHORIZED`: missing, invalid or revoked, or a key on a route that does not accept one. - `KEY_EXPIRED`: the key passed the expiry it was issued with. Issue a new one; an expired key cannot be rotated. - `KEY_IP_NOT_ALLOWED`: the key is pinned to source addresses and this request came from another.
- `403` — `INSUFFICIENT_SCOPE` (the key is read-only or lacks the `refunds` scope), `FORBIDDEN` (a key for a different organization), `ACCOUNT_BLOCKED`, or `LIVE_KEY_ORG_NOT_APPROVED`. Refunds are not held back by business verification.
- `404` — No such link or product in this organization, or the id belongs to another one.
- `409` — `REFUND_IN_PROGRESS` (another refund on this payment is still being confirmed at the acquirer; nothing further was sent), `CONFLICT` (in the sandbox, another refund landed a moment earlier; read the payment again), or an idempotency conflict: `IDEMPOTENCY_KEY_CONFLICT`, `IDEMPOTENCY_KEY_REQUEST_IN_PROGRESS`, `DUPLICATE_REQUEST_DETECTED`, or `PAYOUT_OUTCOME_UNKNOWN` on a replay of a key whose first call answered `500` (the refund may have gone through; read the payment, do not resend).
- `429` — Too many requests. The default ceiling is **100 requests per minute per API credential** on a 60-second window. High-volume payout and reconciliation routes declare a 600/minute override, and batch submission a 30/minute ceiling. A separate 2,000/minute per-source-IP abuse ceiling always applies. Obey `Retry-After`; it is in seconds and is authoritative. A 429 means the request was refused before the handler ran. Retry reads normally; retry an idempotent mutation with its same `Idempotency-Key`.
- `500` — `PAYOUT_OUTCOME_UNKNOWN`: the call failed after the money may have moved, and we cannot yet say whether it did. **Do not retry with a new `Idempotency-Key`**; that is how a payment goes out twice. Look the outcome up first (the operation says where), or replay the same key, which answers `409 PAYOUT_OUTCOME_UNKNOWN` until we resolve it. Send support the `requestId`. Every other `500` is `INTERNAL`: nothing was recorded under the key, and retrying with the same key is safe.

Machine contract: [partner-checkout.openapi.yaml](/partner-checkout.openapi.yaml), operation `refundCheckoutPayment`.
