Metered billing, end to end
Charge for consumption. Create a per-unit price, post usage records, and let BillKit close the period and collect.
Metered billing charges for what a customer used rather than a fixed fee. In BillKit it is a property of the price, and it changes who drives the renewal: a licensed subscription is renewed by Mollie on a schedule, while a metered one is closed and charged by BillKit at each period end.
Create a metered price
Set usage_type: "metered". amount_cents stops meaning “the period fee” and starts meaning
“the price of one unit”.
curl https://api.billkit.eu/v1/prices \
-H "Authorization: Bearer $BILLKIT_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"product_id": "prod_kQ2v8Zr4nT1yLpXaBc",
"amount_cents": 2,
"currency": "EUR",
"interval": "month",
"usage_type": "metered",
"payment_methods": ["creditcard"]
}' That is €0.02 per unit, billed monthly. Two constraints are enforced at create time:
intervalmust bemonth. A yearly meter is refused.trial_daysmust be 0. A trial has no meaning when the first period’s charge is already zero until usage arrives.
There is one meter per subscription, not many. BillKit has no named meters, no aggregation choice and no per-event names: usage is a stream of integers against one subscription, summed. If you bill for two different things, sell two subscriptions.
Checkout
Identical to any other price. The customer goes through POST /v1/checkout/sessions as normal,
with one difference in what gets charged: because the period’s cost is not knowable yet, only
trial_verification_cents is taken (100 cents by default, plus VAT). Its only job is to mint the
mandate BillKit will charge later.
That charge shows on the customer’s statement as “product name mandate verification”. It is a real charge and it is not automatically refunded, so either keep it small or tell the customer what it is.
Once the payment settles, the subscription is active with a one-month period, and no recurring
schedule is created at Mollie. That absence is deliberate: Mollie can only charge a fixed amount,
and a fixed amount is exactly the wrong thing for a meter.
Post usage
occurred_at is left off here on purpose, so it defaults to now. Send it only when the work
happened earlier than the call, and never earlier than the subscription itself: a timestamp before
created is refused with a 400.
curl https://api.billkit.eu/v1/subscriptions/sub_Pv9Kc3nX8mQ2rTyLbd/usage_records \
-H "Authorization: Bearer $BILLKIT_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: job-4471-batch-19" \
-d '{
"quantity": 4200,
"metadata": {"job_id": "job_4471"}
}' {
"id": "ur_Bd6Xk9wQ2nR5tMvPzy",
"object": "usage_record",
"created": 1789344012,
"livemode": false,
"subscription_id": "sub_Pv9Kc3nX8mQ2rTyLbd",
"quantity": 4200,
"occurred_at": 1789344000,
"invoice_id": null,
"identifier": null,
"written_off_at": null,
"metadata": {"job_id": "job_4471"}
} | Field | Rules |
|---|---|
| quantityinteger | Required. Between 1 and 1,000,000. Integers only, so pre-round fractional units to your billing granularity. |
| occurred_atinteger | Optional, Unix seconds. Defaults to now. At most 5 minutes in the future, and never before the subscription was created. |
| metadataobject | Optional. Up to 50 keys. |
Records are immutable and always additive. There is no set mode and no way to correct one after
the fact, so post from the code path that actually did the work.
Without an Idempotency-Key, two identical posts are two records. Nothing deduplicates on
quantity and timestamp, because two API calls in the same second is a normal thing for a busy customer to do.
Derive the key from your unit of work: the job id, the request id, the batch number.
Usage is only accepted while the subscription is active or past_due. past_due is on the list
on purpose: a customer whose last invoice failed is still consuming, and dropping that usage would
mean never being able to bill for it. Anything else, including trialing and canceled, is
refused with a 400.
What happens at period close
BillKit sweeps every few minutes. When current_period_end passes, for one subscription:
- Sum the
quantityof every record not yet attached to an invoice. - Open an invoice with a single line item,
quantity × amount_centsnet, and compute VAT on it.invoice.createdfires. - Stamp
invoice_idon exactly those records, and roll the period forward one month. - Charge the invoice total against the stored mandate.
On success the invoice becomes paid and invoice.paid, subscription.updated and
payment.succeeded fire. On failure the invoice stays open, the subscription goes past_due,
and invoice.payment_failed, subscription.past_due and payment.failed fire.
Two cases produce no invoice at all. Zero usage rolls the period and bills nothing. Usage worth less than 100 cents gross also bills nothing, but leaves the records unstamped so they roll into the next period rather than being lost. That floor exists because a €0.04 card charge costs more in fees than it collects.
Late usage is never dropped
The sum is “every record with no invoice yet”, with no lower bound on occurred_at. A record
posted after its period closed lands on the next invoice instead. You bill one cycle late, but
you bill.
When a charge fails
BillKit retries the open invoice every 24 hours, up to 4 attempts, and gives up 120 hours after
the invoice was due. At that point the invoice is marked uncollectible, the subscription is
cancelled, and invoice.marked_uncollectible plus subscription.canceled fire.
This is different from a licensed subscription, where Mollie owns the retry schedule. See Recover failed payments.
Reading usage back
curl "https://api.billkit.eu/v1/subscriptions/sub_Pv9Kc3nX8mQ2rTyLbd/usage_records?invoice_id=pending&limit=100" \
-H "Authorization: Bearer $BILLKIT_API_KEY" invoice_id=pending returns only the records not yet billed, which is the running total for the
current period. Pass a real inv_... instead to see exactly what a given invoice was built from,
which is the query to reach for when a customer disputes a line.
What metered subscriptions cannot do
| Operation | Result |
|---|---|
| pause / resume | 400. Usage would keep accruing with nothing to bill it against. |
| update (plan change) | 400. Proration is arithmetic on a fixed period fee, and a per-unit price has none. Cancel and start a new checkout. |
| Coupons | Refused at checkout. |
| Trials | Refused at price creation and at checkout. |
| Portal plan switching | Metered prices are excluded from the switch list. |
cancel and reactivate both work normally. Cancelling runs one final close at
current_period_end, so the last partial period’s usage is billed before the subscription ends.
Testing it
Attach the customer to a test clock, post some usage, then advance the clock
past current_period_end. The close, the invoice and the charge all run before the advance call
returns, so you can read the invoice immediately afterwards.
BillKit