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.
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.
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"
}'{
"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:
| status | Meaning |
|---|---|
| pending | Accepted by the provider, not settled. The normal response to your create call. |
| succeeded | The 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. |
| failed | The 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 interval | First charge | Renewal |
|---|---|---|
| Monthly | 7 days | 3 days |
| Yearly | 30 days | 3 days |
| Metered cycle | n/a | 3 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:
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
| Refund | Effect on the subscription |
|---|---|
| Full, on the charge for the current period | Cancelled 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 charge | Nothing. That charge paid for a period that is already over, so refunding it returns money without taking anything away. |
| Partial | Nothing. It keeps running and renews as normal. |
| Full, on a metered cycle charge | Nothing. That corrects a bill rather than ending the service. |
| Full, on a plan-change charge | Nothing. 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 trial | Cancelled 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 price | Nothing. 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
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.
BillKit