Refund a payment
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.
Summary
Section titled “Summary”- Get a key with the
refundsscope, or an owner or admin seat. - Refund:
POST /checkout/organizations/{orgId}/payments/{paymentId}/refund. - Handle
checkout_payment.partially_refundedandcheckout_payment.refunded.
-
Get permission to refund
Section titled “Get permission to refund”Refunding is a signing decision, like choosing where a payout lands, so a
writekey 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 gets403 INSUFFICIENT_SCOPEand nothing moves. -
Refund the payment
Section titled “Refund the payment”From your server
Section titled “From your server”POST /checkout/organizations/{orgId}/payments/{paymentId}/refundx-api-key: avvio_live_…Idempotency-Key: 5c1a6c7e-2f0d-4a3b-9c1e-7d2b8f4a6e10Content-Type: application/json{ "amount": "12.50", "reason": "requested_by_customer", "note": "Session moved to October" }paymentIdis the payment’sidonGET …/links/{linkId}/payments, andpaymentIdon everycheckout_payment.*event.Field Required Meaning amountno Decimal string in the payment’s currency, at most its decimal places. Omit it to refund everything left reasonno requested_,by_ customer duplicate,fraudulentorother. Shown in the dashboard, echoed on the eventnoteno 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 underrefunds, 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 itconst 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 leftconst rest = await avvio.refundCheckoutPayment(paymentId, { reason: 'other' });avvio-payments checkout refund pay_… --amount 12.50 --reason duplicateavvio-payments checkout refund pay_… --fullThe CLI requires
--fullfor a full refund, so a forgotten--amountnever 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: truein the SDK). After a502 REFUND_OUTCOME_UNKNOWNthe refund sits asstatus: "pending"while we re-read it; the event follows if money moved.From the dashboard
Section titled “From the dashboard”- Open Checkout, then the link. Its Payments table shows each payment’s status and refunds.
- In the row’s Actions menu, choose Refund….
- 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.
- 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.
-
Handle the refund events
Section titled “Handle the refund events”Event When Payment statuscheckout_payment.partially_ refunded Part of the payment went back still paidcheckout_payment.refunded Refunded in full, in one go or as the last piece refundedSubscribe to both by name; an empty
eventslist does not include checkout events. Both fill two fields thepaidevent 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.
When a refund is refused
Section titled “When a refund is refused”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.
Disputes and chargebacks
Section titled “Disputes and chargebacks”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.
Check the outcome
Section titled “Check the outcome”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?