BillKit/Docs Console
Subscriptions guide

Free trials

Configure a trial on the price, override it per checkout, and handle the events around conversion.

13 Sept 2026 · Beginner · 8 min read

A card is always taken

BillKit has no card-later trial. Every subscription needs a mandate before it can renew, a mandate only comes from a real payment, so a trial starts with a small verification charge rather than nothing.

The amount is trial_verification_cents on the price, 100 cents by default, plus VAT. It exists because Mollie will not reliably create a card mandate from a zero-value charge.

That charge is not refunded automatically. Nothing in BillKit reverses it. Either keep it small enough to be unremarkable, refund it yourself if you want to, or say what it is on the pricing page. Do not write copy promising it will come back.

If the €1 hold is unacceptable for your market, the honest alternative is not a trial. Sell a cheap first period, or gate the product behind a free tier that has no subscription at all.

Configure it on the price

shell
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": 2900,
  "currency": "EUR",
  "interval": "month",
  "trial_days": 14,
  "trial_verification_cents": 100,
  "payment_methods": ["creditcard", "ideal"]
}'

trial_days is between 0 and 90. trial_verification_cents can be 0, though Mollie may then refuse to create the mandate for some card products.

Everything that decides money on a price is fixed at creation, trial_days included. The only field POST /v1/prices/{id} accepts is active, which takes the price off sale or puts it back. So changing a trial length means creating a new price and pointing new checkouts at it. Existing subscriptions keep the terms they started on, which is usually what you want anyway.

Override per checkout

Useful for a sales-negotiated trial, or for a campaign that runs longer than the default.

shell
curl https://api.billkit.eu/v1/checkout/sessions \
-H "Authorization: Bearer $BILLKIT_API_KEY" \
-H "Content-Type: application/json" \
-d '{
  "customer_email": "ada@example.com",
  "price_id": "price_7Hd3Wm9pQx2vRt5Kna",
  "trial_days_override": 28,
  "success_url": "https://example.com/welcome",
  "cancel_url": "https://example.com/pricing"
}'

The override is capped server-side at twice the price’s trial_days, with a floor of 14 days on that basis. So a price with trial_days: 14 allows up to 28, a price with trial_days: 30 allows up to 60, and a price with no trial at all still allows up to 28. Exceeding the cap is a 400 naming the limit. The absolute ceiling is 180 days.

trial_days_override: 0 disables a trial the price would otherwise grant, which is how you sell the same plan without one.

Metered prices reject trials entirely, at both levels.

The lifecycle

MomentWhat happens
Checkout settlesStatus is trialing. trial_start is now, trial_end is now plus the trial length, and current_period_end equals trial_end. subscription.created and subscription.trial_started fire.
3 days before the endBillKit emails the customer: plan, charge date, amount, and the saved payment method. Once only.
trial_endMollie charges the full price against the mandate. On success the subscription becomes active and subscription.trial_ended, subscription.updated and payment.succeeded fire.
If the charge failsStatus becomes past_due and dunning starts. See Recover failed payments.
If the customer cancelled during the trialThe subscription becomes canceled at trial_end with cancellation_reason: "trial_expired_unpaid".

The renewal is scheduled at Mollie with a start date of trial_end, so nothing has to fire on time in BillKit for the conversion to happen.

There is no subscription.trial_will_end event. The three-day warning is an email only. If you want your own in-app nudge, read trial_end off the subscription and schedule it yourself.

Handling conversion

Node.js
export async function handleEvent(event) {
switch (event.type) {
  case "subscription.trial_started":
    // trial_end is Unix seconds.
    await startTrial(event.data.customer_id, {
      subscriptionId: event.data.id,
      endsAt: new Date(event.data.trial_end * 1000),
    });
    break;

  case "subscription.trial_ended":
    // The trial is over. Check status: converted, or not.
    if (event.data.status === "active") {
      await markConverted(event.data.customer_id);
    }
    break;

  case "subscription.canceled":
    await revokeAccess(event.data.customer_id);
    break;
}
}

Gate entitlement on serves_customer, exactly as you would without a trial. Do not write separate trial logic; trialing is a fully entitled state and the boolean already says so. See Two state fields, not one for why reading status by hand is the thing to avoid.

You cannot extend a running trial

There is no endpoint for it, and no field on any update route that accepts one. The trial length is fixed the moment the checkout session is created.

If you need a customer to have longer, the workable answer is to cancel the subscription, then send them a new checkout session with a larger trial_days_override. That produces a second verification charge, so it is worth making the first trial the right length rather than planning to extend.

Testing it

Attach the customer to a test clock before checkout, then advance the clock past trial_end. The trial reaper runs during the advance, so the conversion events land before the call returns.

What a clock cannot do is make Mollie charge. The renewal is on Mollie’s real-time schedule, so a clock-driven test exercises BillKit’s trial expiry, not the money.