BillKit/Docs Console
Core concepts

Subscriptions

A subscription ties a customer to a price and renews on a schedule. It is created by a paid checkout, and it carries two independent state fields.

Updated Sep 13, 2026 · API version 2026-09-13

There is no create endpoint

POST /v1/subscriptions does not exist. A subscription appears when a checkout session’s payment settles: BillKit stores the mandate that payment produced, creates the subscription, and emits subscription.created. See the quickstart for the full call sequence.

The consequence worth planning for is that creation is asynchronous. Your code does not receive a sub_... from any request it makes. It receives one over a webhook, typically within seconds of the customer finishing at Mollie, but not on the redirect back to your success_url.

Two state fields, not one

Most billing APIs collapse “is this customer entitled” and “will this renew” into one status. BillKit keeps them apart, because a cancelled subscription that is still inside its paid period is both cancelled and entitled, and one field cannot say that.

status answers: what is the billing state right now?

statusMeaning
incompleteCreated but the first payment has not settled. Transient.
trialingInside the trial window. The mandate exists; the full price has not been charged yet.
activePaid and current.
past_dueA renewal failed. Retries are running at Mollie. See Recover failed payments.
canceledTerminal. No further charges.

There is no paused status, and that is the point of having two fields. A paused subscription has been paid for up to current_period_end, so it stays active and only renewal_state changes. Asking for status=paused is a 400 naming the filter that does work.

renewal_state answers: what happens at current_period_end?

renewal_stateMeaning
auto_renewIt will renew. The normal state.
cancelingCancel was requested. Access runs to current_period_end, then stops.
pausedPause was requested. Same paid-through window, no renewal.
stoppedNothing further will happen.

For entitlement, read serves_customer. It is a boolean on every subscription, and it answers the only question an application actually asks: should this customer have the product right now?

Gating on status alone is the mistake having two fields is meant to prevent. A paused subscription keeps status: active indefinitely, because nothing ever closes a paused record out, so a check that reads status alone keeps serving a customer who will never be charged again. Denying every pause is the opposite error: that customer has paid through the end of the current period and is entitled to it.

serves_customer combines both fields with the period boundary:

  • While renewal_state is auto_renew, entitlement follows status alone and current_period_end is deliberately ignored. Every healthy subscription spends a short gap past its period end waiting for the renewal to settle, and denying service in that gap would paywall your entire paying book once a cycle.
  • Under any other renewal state no further charge is coming, so entitlement lasts exactly as long as the period already paid for.

past_due entitles on purpose: the customer paid for the period they are in, and cutting them off the hour a card expires converts a payment problem into a cancellation. incomplete never entitles, because nothing has been paid yet.

The four underlying fields stay on the object, so you can still compute this yourself if you need to. Prefer the boolean: it is computed server-side, and it moves when the rule does.

Cancelling

POST /v1/subscriptions/{id}/cancel takes no body and is always at period end. There is no immediate-cancel flag. It sets renewal_state to canceling and cancel_at_period_end to true, tells Mollie to stop the recurring schedule, and emits subscription.updated. status stays active until the period actually elapses, at which point the subscription becomes canceled and subscription.canceled fires.

Calling it twice is safe: an already-cancelling or already-cancelled subscription is returned unchanged.

To reverse a pending cancel while the period is still running, use POST /v1/subscriptions/{id}/reactivate. It restores auto_renew and re-provisions the schedule at Mollie. It refuses once current_period_end has passed, or if the mandate is gone, because at that point there is nothing to restore and the customer has to go through checkout again.

Refunding on cancellation

By default a cancellation gives no money back, and a refund is a separate operation you issue yourself. If you want it to be automatic, set refund_on_cancel on the price:

refund_on_cancelWhat a cancellation refunds
noneNothing. The default, and what every price created before this setting existed does.
fullThe whole of the most recent charge.
proratedThe unused remainder of the period that charge paid for, by elapsed time.

Only the most recent charge is ever in scope, and only while it is inside the price’s refund window (refund_window_initial_days / refund_window_renewal_days). Those columns used to be a permission, read when you asked for a refund; refund_on_cancel is what turns them into a trigger. A charge outside the window refunds nothing, exactly as a manual refund would be refused.

Both settings end access immediately rather than at period end. Refunding time the customer then keeps would be paying them to stay.

The refund is best effort. A cancellation is recorded whatever happens to the money, so if the refund cannot be created the subscription still cancels and you can refund by hand. Watch refund.created and refund.succeeded rather than assuming.

If you would rather give money back yourself, that is a refund, a separate operation. Refunding an initial or renewal charge in full (counting anything already refunded) cancels the subscription immediately, because a full refund is the “undo this” gesture. A partial refund leaves it running, and refunding a metered usage charge never cancels anything.

Pausing

POST /v1/subscriptions/{id}/pause stops renewals but keeps the record and the paid-through window. It moves renewal_state to paused and leaves status at active, because the customer is still entitled to the period they already paid for. POST /v1/subscriptions/{id}/resume puts it back to auto_renew, subject to the same two conditions as reactivate: the period must not have expired and the mandate must still exist. Pausing an already-paused subscription returns it unchanged.

Pause and resume are refused on metered subscriptions, where they have no defined meaning: usage would keep accruing with nothing to bill it against.

Changing plan

Two calls. Preview first, then commit.

shell
curl https://api.billkit.eu/v1/subscriptions/sub_Pv9Kc3nX8mQ2rTyLbd/preview_update \
-H "Authorization: Bearer $BILLKIT_API_KEY" \
-H "Content-Type: application/json" \
-d '{"target_price_id": "price_Nt8Ql3wZ7bR2vMxKye"}'
Response · 200 OK
{
"target_price_id": "price_Nt8Ql3wZ7bR2vMxKye",
"target_price_cents": 9900,
"currency": "EUR",
"credit_cents": 1740,
"charge_cents": 9835,
"charge_net_cents": 8128,
"charge_tax_cents": 1707,
"refused": false,
"refusal_reason": null,
"proration_at": 1790000000,
"new_period_end": 1792592000
}

The preview writes nothing. credit_cents is the unused remainder of the current period, by elapsed seconds. charge_cents is what the customer pays now, including VAT, and the new period starts immediately rather than continuing the old one.

POST /v1/subscriptions/{id}/update with the same target_price_id commits it. The quote is recomputed server-side, so a stale preview cannot lock in an old number. The charge goes to the stored mandate and the plan actually switches when that payment settles, which means the subscription’s price_id does not change in the response to your update call. Watch for subscription.updated.

A preview can come back refused. When the credit leaves less than 100 cents to charge, the renewal is close enough that switching now would bill twice for the same window. Wait for the renewal and change plan after it.

Three hard limits on plan changes: the currency must match, the target must differ from the current price, and neither side may be metered. Metered and licensed prices renew on completely different machinery, so switching between them means cancelling and starting a new checkout.

The subscription must also be in auto_renew. A cancelling or paused subscription is refused.

Recovering a broken payment method

POST /v1/subscriptions/{id}/reauthorize_payment_method with a return_url returns a Mollie checkout URL for a small verification charge. When it settles, the new mandate replaces the old one, a past_due subscription returns to active, the dunning counters reset, and subscription.payment_method_updated fires.

This verification charge is not refunded. Do not tell customers it will be.

The customer portal exposes the same flow, so most integrations link to the portal rather than building their own page.

Reading subscriptions

GET /v1/subscriptions supports customer_id, status and renewal_state. The two state filters each take a comma-separated list, and an unknown value is a 400 naming the ones that work rather than a silently empty page.

shell
curl "https://api.billkit.eu/v1/subscriptions?status=active,past_due&limit=50" \
-H "Authorization: Bearer $BILLKIT_API_KEY"

Filter on renewal_state when the question is about the period boundary rather than about payments. renewal_state=paused is the paused cohort and renewal_state=canceling is the set that will lapse at the end of the period. status=paused is rejected, because there is no such status.

The dunning fields on the object are dunning_attempts_notified (notices BillKit sent), dunning_last_notified_at, and next_payment_attempt — when the provider is expected to try again, or null when it will not, which is the same shape and meaning as Stripe’s invoice.next_payment_attempt.

There is deliberately no retry ceiling. Mollie retries “up to 5 times, once a day, depending on the failure reason”, and several reasons cancel the subscription on the first failure, so any fixed denominator would be wrong for a large share of real failures. Branch on next_payment_attempt being null instead. See the dunning guide.