Skip to content

Give a checkout buyer their card payment back, in full or in part, from the dashboard or your server.

A paid card payment can be refunded at any time until nothing is left, unless the buyer is disputing it. The money goes back to the card, usually within 5 to 10 business days, and the original processing fee is not returned. Bank and stablecoin payments cannot be reversed at the source; send the payer a payout instead.

  1. Get a key with the refunds scope, or an owner or admin seat.
  2. Refund: POST /checkout/organizations/{orgId}/payments/{paymentId}/refund.
  3. Handle checkout_payment.partially_refunded and checkout_payment.refunded.
  1. Refunding is a signing decision, like choosing where a payout lands, so a write key cannot refund by default. In the dashboard only an owner or admin sees the refund action; operators and viewers see the payment without it.

    For the API, an owner or admin creates a key under Developers, chooses Transact and ticks Refund checkout payments. The key gets scopes: ["write", "refunds"], and only an owner or admin can issue or rotate it. A key without the scope gets 403 INSUFFICIENT_SCOPE and nothing moves.

  2. POST /checkout/organizations/{orgId}/payments/{paymentId}/refund
    x-api-key: avvio_live_…
    Idempotency-Key: 5c1a6c7e-2f0d-4a3b-9c1e-7d2b8f4a6e10
    Content-Type: application/json
    { "amount": "12.50", "reason": "requested_by_customer", "note": "Session moved to October" }

    paymentId is the payment’s id on GET …/links/{linkId}/payments, and paymentId on every checkout_payment.* event.

    Field Required Meaning
    amount no Decimal string in the payment’s currency, at most its decimal places. Omit it to refund everything left
    reason no requested_by_customer, duplicate, fraudulent or other. Shown in the dashboard, echoed on the event
    note no Up to 255 characters, for your team. Echoed on the event, never shown to the buyer

    The response is the payment after the refund, plus refund, the piece this call made:

    {
    "id": "cmf9x1b2c0003q8b7h6j8k0lm",
    "status": "paid", // "refunded" once nothing is left
    "amountBase": "15000",
    "refundedBase": "1250", // cumulative, in minor units
    "currency": "USD",
    "decimals": 2,
    "refunds": [
    {
    "id": "cmfa0…",
    "paymentId": "cmf9x1b2c0003q8b7h6j8k0lm",
    "amountBase": "1250",
    "reason": "requested_by_customer",
    "source": "api", // api | dashboard | acquirer
    "status": "succeeded", // pending until the processor confirms it
    "actor": "API key Booking server", // "API key <name>"; a person's name, or their email if no name is set; or null
    "createdAt": "2026-09-21T10:14:02.000Z"
    }
    ],
    "refund": { "id": "cmfa0…", "amountBase": "1250", "…": "…" }
    }

    What is left is amountBase - refundedBase. Every payment read lists all its refunds under refunds, whoever made them. The SDK and CLI make the same call:

    const { PayoutsClient } = require('@avvio/payments');
    const avvio = new PayoutsClient(); // AVVIO_API_KEY, AVVIO_ORG_ID from the environment
    // Part of it
    const partial = await avvio.refundCheckoutPayment(paymentId, {
    amount: '12.50',
    reason: 'requested_by_customer',
    note: 'Session moved to October',
    idempotencyKey: order.refundKey, // yours, stored with the order
    });
    // Everything that is left
    const rest = await avvio.refundCheckoutPayment(paymentId, { reason: 'other' });
    avvio-payments checkout refund pay_… --amount 12.50 --reason duplicate
    avvio-payments checkout refund pay_… --full

    The CLI requires --full for a full refund, so a forgotten --amount never refunds everything.

    One refund runs at a time per payment. For a genuine second refund of the same amount within 15 minutes, send X-Allow-Duplicate: true (allowDuplicate: true in the SDK). After a 502 REFUND_OUTCOME_UNKNOWN the refund sits as status: "pending" while we re-read it; the event follows if money moved.

    1. Open Checkout, then the link. Its Payments table shows each payment’s status and refunds.
    2. In the row’s Actions menu, choose Refund….
    3. Choose Full refund (Refund what’s left after a partial one) or Part of it and an amount. Pick a reason and, optionally, a note the buyer never sees.
    4. Check the summary and press Refund.

    The row lists each refund with its date, reason and who made it (the key’s name, for an API refund). When the action is unavailable, the menu says why: disputed, charged back, already refunded in full, or not paid.

  3. Event When Payment status
    checkout_payment.partially_refunded Part of the payment went back still paid
    checkout_payment.refunded Refunded in full, in one go or as the last piece refunded

    Subscribe to both by name; an empty events list does not include checkout events. Both fill two fields the paid event leaves empty:

    {
    "type": "checkout_payment.partially_refunded",
    "data": {
    "paymentId": "cmf9x1b2c0003q8b7h6j8k0lm",
    "clientReferenceId": "order_1042",
    "status": "paid",
    "amount": "150.00",
    "refunded": "12.50", // cumulative, decimal string
    "refund": { // the piece this event is about
    "id": "cmfa0…",
    "amount": "12.50",
    "reason": "requested_by_customer",
    "note": "Session moved to October",
    "source": "api", // api | dashboard | acquirer
    "at": "2026-09-21T10:14:02.000Z"
    }
    // …the rest of WebhookCheckoutPayment
    }
    }
    switch (event.type) {
    case 'checkout_payment.partially_refunded':
    // The order stays fulfilled. Record the piece and the running total.
    await orders.noteRefund(event.data.clientReferenceId, event.data.refund, event.data.refunded);
    break;
    case 'checkout_payment.refunded':
    // Nothing is left. Reverse the fulfilment.
    await orders.cancel(event.data.clientReferenceId, 'refunded');
    break;
    }

    Refunds made in the card processor’s own tools arrive the same way within minutes, with source: "acquirer" and no actor.

Every refusal leaves the money where it was, except REFUND_OUTCOME_UNKNOWN, which may have moved it. Errors lists the refund types (REFUND_NOT_ALLOWED, REFUND_DISPUTED, REFUND_EXCEEDS_REMAINING, REFUND_IN_PROGRESS, REFUND_OUTCOME_UNKNOWN) with what to do. A malformed amount, reason or note is a 400 VALIDATION_ERROR, and a same-amount repeat under a new key is a 409 DUPLICATE_REQUEST_DETECTED.

A buyer can dispute a card payment weeks after paying. While the dispute is open, disputeSubstatus is set, no event fires, and refunds are refused, since a refund plus a lost dispute would pay the buyer twice. A lost dispute takes the money back and sends checkout_payment.reversed; a reversed payment cannot be refunded. A won dispute returns the payment to paid.

Read the payment from GET /links/{linkId}/payments: status and refundedBase say what has gone back. In the sandbox a total ending .03 is refunded in full 45 seconds after payment, and .06 is half refunded. You can also refund a sandbox payment yourself, or call POST /checkout/…/payments/{paymentId}/simulate with {"action":"refund"}.

Was this page helpful?