Skip to content

Refund a booking

POST /v1/bookings/{id}/refunds refunds all or part of a booking’s succeeded payment back to the guest’s original payment method.

Omit amountMinorUnits to refund whatever remains on the payment:

Terminal window
curl -X POST https://api.<domain>/v1/bookings/<RESERVATION_ID>/refunds \
-H "Authorization: Bearer sk_test_..." \
-H "Content-Type: application/json" \
-d '{}'
{
"id": "...",
"reservationId": "<RESERVATION_ID>",
"amountMinorUnits": "4500000",
"currency": "IDR",
"state": "succeeded",
"stripeRefundId": "re_..."
}

Pass amountMinorUnits as a string of minor units, same convention as every other amount on this API — see Quotes on why it’s a string, not a number:

Terminal window
curl -X POST https://api.<domain>/v1/bookings/<RESERVATION_ID>/refunds \
-H "Authorization: Bearer sk_test_..." \
-H "Content-Type: application/json" \
-d '{ "amountMinorUnits": "1500000" }'

You can refund the same booking more than once — for example, one night at a time — as long as the running total of successful refunds never exceeds what was actually paid. Ask for more than what’s left and you get 409 refund_exceeds_payment. Its detail text names the most you can still refund, for example “max 3000000”, so you can retry with a corrected value.

  • A booking with a succeeded payment. A booking that never took a payment, or whose payment failed, returns 409 no_succeeded_payment.
  • The tenant needs a connected Stripe account. If they don’t, 409 stripe_not_configured.
  • Your sk_ key needs the payment:refund:create scope. Ask your operator contact if a refund call comes back 404 and you’re sure the booking ID is right — a scope-less key and a wrong tenant look identical from the outside, by design (see Errors’s 404 policy).
StatuscodeMeaning
404booking_not_foundWrong ID, wrong tenant’s key, or a pk_ key (refunds never reach pk_)
409no_succeeded_paymentThis booking has nothing paid to refund
409stripe_not_configuredThe tenant has no Stripe Connect account yet
409refund_exceeds_paymentYou asked for more than what’s left — detail names the maximum; retry with a smaller amount
409stripe_refund_failedStripe refused the refund; the response’s detail carries Stripe’s own reason
409nothing_to_refundNothing is left to refund on this payment — a repeat call after a prior full refund

A refund.created webhook is sent for every refund. This includes the refund we make on our own when a guest pays after their hold expired. That one has "reason": "payment_after_hold_expired".

GET /v1/payments (sk_ with payments:read, or an operator session) lists the tenant’s payment intents, each with its own refundedMinorUnits — the running total refunded on that payment so far. Check it before a partial refund if you’re not sure how much is left, rather than guessing and hitting refund_exceeds_payment.

  • Errors — the full RFC 9457 error shape and the 404 policy.
  • Holds and confirmation — how a booking gets to a succeeded payment in the first place.