BillKit/DocsConsole
Core concepts

Refunds

Give money back in full or in part. Refunds settle asynchronously, they are bounded by a window you control, and a full refund of the charge that paid for the current period ends the subscription.

Updated Oct 14, 2026·OpenAPI v1.0.0

Issuing one

Name exactly one target. payment_id refunds a subscription charge, one_shot_payment_id refunds a one-off sale, and subscription_id is shorthand for that subscription’s most recent charge. Omit amount_cents and the whole remaining balance goes back.

shell
curl https://api.billkit.eu/v1/refunds \
-H "Authorization: Bearer $BILLKIT_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: refund-order-4711" \
-d '{
  "payment_id": "pay_Zt6Wq1yBn4Xm9RcKvo",
  "amount_cents": 500,
  "reason": "goodwill"
}'
Response · 200 OK
{
"id": "re_Qm8Vc2nK5xT9rPbLzw",
"object": "refund",
"created": 1789344600,
"livemode": false,
"payment_id": "pay_Zt6Wq1yBn4Xm9RcKvo",
"one_shot_payment_id": null,
"subscription_id": "sub_Pv9Kc3nX8mQ2rTyLbd",
"amount_cents": 500,
"currency": "EUR",
"reason": "goodwill",
"status": "pending"
}

reason is optional, up to 500 characters. It is passed to Mollie as the refund description, which can appear on the customer’s card or bank statement, so write it for them to read.

Always send an Idempotency-Key. This is a money-moving call where a lost response and a naive retry means refunding twice. See Idempotency.

Without a key, two identical requests (same charge, same amount) reach Mollie as the same refund, and the second is refused with 409 refund_duplicate rather than paid out twice. Resending it fails the same way every time. If you really mean two refunds of the same amount against one charge, give each its own key.

If Mollie itself refuses the refund, the call returns 402 payment_refused (type: payment_error, reason: payment_refused) with Mollie’s reason in the message. No money moved, and the same request will be refused again: a common cause is a chargeback BillKit has not been told about yet, which has already taken the balance.

If Mollie does not answer at all, BillKit sends the same request again and asks Mollie whether the refund was created before replying: if it was, you get the refund as usual. If BillKit still cannot tell, the call returns 502 saying so, and the amount stays reserved as a pending refund until BillKit confirms the outcome with Mollie, within minutes. Retry with the same Idempotency-Key to finish that refund rather than start another one.

A 200 is not money returned

The refund usually comes back pending and settles later, at the provider, over minutes to days depending on the rail. Occasionally Mollie settles it on the spot and the response already says succeeded. Three statuses, and they are BillKit’s own three rather than Mollie’s wider set:

statusMeaning
pendingAccepted by the provider, not settled. The normal response to your create call.
succeededThe money is on its way back to the customer. A credit note is issued if the charge was invoiced, and the customer gets a confirmation email.
failedThe provider could not return it. Nothing was refunded, and the amount is refundable again.

So handle refund.succeeded and refund.failed, and do not treat refund.created as the end of the story. If you are migrating from an API where refunds are synchronous, this is the difference that catches people. BillKit re-checks pending refunds with Mollie in the background, so a missed notification does not leave one stuck. Only Mollie’s answer moves a refund to succeeded or failed: one still pending after 14 days stays pending, keeps holding its amount against the charge, and is flagged to BillKit’s operators to chase with Mollie. Marking it failed without Mollie’s word would free the amount to be refunded a second time if the first refund then paid out.

A failed refund does not undo a cancellation it caused. See What a refund does to the subscription.

The refund window

A charge is refundable through the API for a bounded period after it settled, measured from the charge itself rather than from when the subscription started. Set no window on the price and it inherits these defaults:

Plan intervalFirst chargeRenewal
Monthly7 days3 days
Yearly30 days3 days
Metered cyclen/a3 days

Each charge is judged by the price it was made on, not the one the subscription is on today. A renewal on the old plan, refunded after an upgrade, keeps the old plan’s renewal window. A plan change charges for a whole new period on the plan the subscription moves to, so its charge counts as a first charge on that plan: it takes the first-charge window, and the refund_window_initial_days override, of the new price.

Override them per price with refund_window_initial_days and refund_window_renewal_days. 0 disables refunds for that charge type, a positive number is that many days, and the ceiling is 365. Both fields are editable on an existing price, so a policy change reaches the customers already on it:

shell
curl https://api.billkit.eu/v1/prices/price_7Hd3Wm9pQx2vRt5Kna \
-H "Authorization: Bearer $BILLKIT_API_KEY" \
-H "Content-Type: application/json" \
-d '{"refund_window_initial_days": 30, "refund_window_renewal_days": 14}'

A charge past its window is a 409 state_conflict, however much of it is unrefunded, as is a payment that was never paid. One-off payments carry their own flat 30-day default with a refund_window_days override per payment.

To refund outside the window, issue the refund from your Mollie dashboard. BillKit records it against the charge the next time Mollie notifies it about that payment, or at the backstop’s next re-read of the payment when that notification is lost (at most weekly, less often on large accounts), with the reason “Adopted from the payment provider”, and counts it against the refundable balance. It affects the subscription exactly as a refund made through the API would, by the rules in What a refund does to the subscription.

Not every charge is refundable. Only the charges that bought something are: the initial payment, a renewal, a plan change, and a metered cycle. The checkout charge of a trial or a metered price is the initial payment and is refundable like one, though only the trial’s ends the subscription when refunded in full. A mandate verification taken on its own (updating or adding a payment method, activating an imported subscription) is not, because nothing was sold. To see whether a subscription can be refunded right now without working any of this out yourself, ask for expand=refund_eligibility.

Partial refunds

A charge can carry several refunds, and they may sum to at most what was charged. Pending refunds count towards that sum; failed ones do not. An amount_cents above what is left is a 400 parameter_invalid naming the remaining balance, and a refund against a charge with nothing left is a 409 charge_already_refunded. Concurrent requests against the same charge are handled one at a time, so two of them together cannot exceed it either.

amount_refunded_cents and amount_refundable_cents on the payment are the running totals. amount_disputed_cents is money an open chargeback has already pulled back, and it is subtracted from what is still yours to give. A charge that is disputed in full refuses any refund with a 409 charge_disputed, because the bank has already returned the money. amount_refundable_cents does not know about the refund window, so a positive balance can still be refused.

What a refund does to the subscription

RefundEffect on the subscription
Full, on the charge for the current periodCancelled immediately, not at period end. This is the initial or renewal charge named by the subscription’s last_payment_id, or an earlier charge whose value a plan change credited or carried into the current period. A charge whose period is already over refunds without ending anything. A full refund is the "undo this" gesture, and a full refund of a period the customer then keeps would be paying them to stay.
Full, on an older initial or renewal chargeNothing. That charge paid for a period that is already over, so refunding it returns money without taking anything away.
PartialNothing. It keeps running and renews as normal.
Full, on a metered cycle chargeNothing. That corrects a bill rather than ending the service.
Full, on a plan-change chargeNothing. The charge paid only the price difference; the credit from the old plan paid the rest of the period, which the customer keeps.
Full, on the checkout charge of a trialCancelled immediately, like a full refund of any first charge, because that charge started the subscription. The trial ends on the spot: nothing is charged at trial_end and no subscription.trial_will_end follows. Once the first paid period has started, the checkout charge is an older charge and ends nothing.
Full, on the checkout charge of a metered priceNothing. Ending the subscription mid-period would leave the usage recorded since the period started unbilled, so the subscription keeps running and bills that usage at the period close.

“Full” is cumulative: the partial refund that brings the total up to the whole charge is the one that cancels. Only refunds Mollie has accepted count towards that total. The cancellation happens when Mollie accepts the refund, normally inside your create call, with cancellation_reason refund_issued and subscription.canceled, and it stands even if the refund later fails. Refund again once it has failed; do not expect the subscription to come back. A refund that would cancel the subscription is refused with 409 operation_pending while a plan change for the customer is still settling.

A renewal already at Mollie when the subscription is cancelled this way is cancelled at Mollie if it still can be, as a SEPA debit can before it is submitted. If it cannot, and it is then paid, BillKit refunds it in full automatically, because it paid for a period the customer no longer has. The same goes for a plan-change charge that settles after the subscription has ended. Those refunds are exempt from the refund window.

Cancellation on its own is always at period end, so a full refund is one of only two ways to end a subscription early.

The other is refund_on_cancel on the price, which makes cancellation issue a full or prorated refund of the most recent charge and end access immediately. See Refunding on cancellation.

Credit notes and VAT

A settled refund against an invoiced charge issues a credit note automatically, and credit_note.created fires. A partial refund credits its share of the VAT rather than all of it or none, so the credit notes against one sale add up to exactly the tax that sale charged.

Nothing is issued while a refund is pending, and nothing is issued if it fails, because a document that reverses a sale should not exist before the money has actually gone back.

Reading refunds back

shell
curl "https://api.billkit.eu/v1/refunds?limit=20" \
-H "Authorization: Bearer $BILLKIT_API_KEY"

Newest first, covering subscription and one-off refunds alike, with GET /v1/refunds/{id} for one. A refund is listed, pending, from the moment BillKit reserves its amount, which is slightly before Mollie answers the create call. One that Mollie refuses is removed again, and its create call returns 402 payment_refused. There are no filters here, so to find what has been refunded against a particular charge read amount_refunded_cents on the payment rather than paging this list.

Refunds are issued accounting records and are kept indefinitely, like the invoices and credit notes beside them.