Quickstart
Create a plan, put a customer through checkout, and end up with a real subscription object. Test mode, no money moved.
Every call below is a real endpoint. Run them in order against a test key and you will finish
with a sub_... in your account.
Before you start
Sign up for a BillKit account in the console (the Console link at the top of this page). A
new account starts in test mode, and nothing you do here moves money. You also need a Mollie
account, because BillKit creates the payments on it. Mollie gives every account a test API key
(test_...) under Developers → API keys in its dashboard.
1. Get a test key
Create one in the console under Developers → API keys. Test keys start with bk_test_, live
keys with bk_live_. The key decides the mode, so there is nothing else to switch.
Check it works. /v1/ping echoes back which tenant and mode the key resolved to.
curl https://api.billkit.eu/v1/ping \
-H "Authorization: Bearer $BILLKIT_API_KEY"2. Connect Mollie
BillKit needs a Mollie credential for the mode you charge in. Without a test credential, step 5
fails with 400 parameter_invalid. Either click Connect with Mollie in the console under
Settings → Mollie, which authorises test and live in one step, or store your Mollie test key
over the API. That call needs a key with the provider_credential:write scope, which a full-access
key does not carry: create one in the console under Developers → API keys and use it for this
call only (see Scopes).
curl https://api.billkit.eu/v1/tenant/provider_credential \
-H "Authorization: Bearer $BILLKIT_API_KEY" \
-H "Content-Type: application/json" \
-d '{"provider": "mollie", "api_key": "test_..."}'The credential is stored for the mode of the key you call with, so a bk_test_ key stores a
test credential. A Mollie live key (live_...) sent with a test key is refused with a 400.
BillKit never returns the credential on any route.
A Mollie organisation can have several website profiles. If yours has more than one, BillKit asks you to choose the one to take payments through right after you authorise, in the same Settings → Mollie screen, and no payment can be created until you do. With a single profile it is chosen for you. You can switch profiles there later: payments already made are unaffected, and new payments, Mollie Components and the methods offered at checkout use the new profile.
Then check what you can charge:
curl https://api.billkit.eu/v1/tenant/capabilities \
-H "Authorization: Bearer $BILLKIT_API_KEY"mollie_connected is true once a credential is stored in either mode. enabled_methods and
enabled_currencies are per mode: they list what the Mollie profile behind the calling key’s
mode allows, and they can differ between test and live. BillKit reads them from Mollie in the
background, so for the first few minutes after connecting provider_capabilities_refreshed_at is
null and both lists are empty. That means “not read yet”, not “nothing enabled”.
3. Create a product
A product is the thing you sell. It carries the name your customer sees on the Mollie payment page and on the invoice.
curl https://api.billkit.eu/v1/products \
-H "Authorization: Bearer $BILLKIT_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "Pro plan"}'{
"id": "prod_kQ2v8Zr4nT1yLpXaBc",
"object": "product",
"created": 1789344000,
"livemode": false,
"name": "Pro plan",
"description": null,
"prices": null,
"stats": null,
"marketing_features": null,
"metadata": {},
"active": true,
"allow_promotion_codes": false,
"default_price_id": null,
"default_price": null,
"archived_at": null
}4. Create a price
The price is what gets charged, and how often. amount_cents is an integer in the smallest unit,
so 2900 is €29.00. payment_methods is the price’s own list of methods offered at checkout. Leave
it out and the price follows your account, offering every method you have on in Mollie and have
not switched off in the console under Settings → Mollie. A list
must contain at least one method that can take a first payment and create a mandate:
creditcard, ideal, eps, applepay or paypal. A list with only directdebit is
refused with a 400, because SEPA Direct Debit can collect renewals but cannot be the first
payment.
A subscription renews on the mandate its first payment created, which is not always the method the
customer picked. ideal and eps create a SEPA mandate, so those subscriptions renew on
directdebit. applepay creates a card mandate and renews on creditcard. creditcard and
paypal renew on themselves.
EPS is Austria-only and Mollie’s minimum for it is €1.00. If a price offers EPS, BillKit raises any
smaller first charge (a 100% discount, say) to €1.00 for every buyer on that price. Google Pay
needs no entry: its method is creditcard, and once you activate Google Pay for your profile in
the Mollie dashboard, Mollie shows it on its own checkout page to buyers on a supported device.
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",
"payment_methods": ["creditcard", "ideal"]
}'{
"id": "price_7Hd3Wm9pQx2vRt5Kna",
"object": "price",
"created": 1789344060,
"livemode": false,
"product_id": "prod_kQ2v8Zr4nT1yLpXaBc",
"product_name": "Pro plan",
"amount_cents": 2900,
"unit_amount_decimal": "2900",
"currency": "EUR",
"interval": "month",
"active": true,
"metadata": {},
"trial_days": 0,
"trial_verification_cents": 100,
"payment_methods": ["creditcard", "ideal"],
"payment_methods_from_account": false,
"refund_on_cancel": "none",
"refund_window_initial_days": null,
"refund_window_renewal_days": null,
"tax_behavior": "unspecified",
"usage_type": "licensed",
"billing_scheme": "per_unit",
"tiers_mode": null,
"tiers": null
}5. Start a checkout session
This is where the subscription comes from. Pass exactly one of customer_id (an existing
cus_...) or customer_email. With customer_email, BillKit creates a customer for the
checkout, and only once the request has passed validation: a refused request creates no customer
and sends no customer.created. It never deduplicates by email, so sending the same address twice
gives you two customers. The Idempotency-Key makes a retried request safe: send a new key for each new
checkout, because reusing one with a different body is a 409.
curl https://api.billkit.eu/v1/checkout/sessions \
-H "Authorization: Bearer $BILLKIT_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: signup-8f21c4" \
-d '{
"customer_email": "ada@example.com",
"customer_name": "Ada Lovelace",
"price_id": "price_7Hd3Wm9pQx2vRt5Kna",
"success_url": "https://example.com/welcome",
"cancel_url": "https://example.com/pricing"
}'{
"id": "cs_Rb4nZ8yQw1eTp6Lksv",
"object": "checkout.session",
"created": 1789344120,
"livemode": false,
"customer_id": "cus_Ja7Ye2vMq9xN4TdWpr",
"price_id": "price_7Hd3Wm9pQx2vRt5Kna",
"success_url": "https://example.com/welcome",
"cancel_url": "https://example.com/pricing",
"status": "open",
"ui_mode": "hosted",
"url": "https://www.mollie.com/checkout/select-method/...",
"client_secret": null,
"customer_email": null,
"customer_country_code": null,
"subscription_id": null,
"payment_id": null,
"expires_at": null,
"trial_days_override": null,
"coupon_id": null,
"payment_method": null,
"metadata": {}
}Send the customer to url. That is Mollie’s hosted page: they pick a method and pay there, then
land on your success_url. In test mode Mollie shows a status picker instead of a real payment
form, so choose paid to complete the flow. Where a card form does appear, use one of the
numbers on Mollie’s Testing reference, or in the
console under Developers → Test cards.
Do not provision on success_url. The redirect only means the customer’s browser came back.
The payment can still fail afterwards, and a customer who closes the tab never redirects at all. Provision on
the checkout.session.completed webhook.
If you would rather keep the customer on your own page, set ui_mode: "embedded" and you get a
client_secret for the Checkout Element instead of a url. See
Embed Checkout in your own page.
6. Read the subscription
Once Mollie reports the payment as paid, BillKit creates the subscription, marks the checkout
session complete, writes a paid invoice for the first charge, and emits
checkout.session.completed, subscription.created, payment.succeeded, invoice.created and
invoice.paid.
curl "https://api.billkit.eu/v1/subscriptions?customer_id=cus_Ja7Ye2vMq9xN4TdWpr" \
-H "Authorization: Bearer $BILLKIT_API_KEY"{
"object": "list",
"has_more": false,
"data": [
{
"id": "sub_Pv9Kc3nX8mQ2rTyLbd",
"object": "subscription",
"created": 1789344300,
"livemode": false,
"customer_id": "cus_Ja7Ye2vMq9xN4TdWpr",
"price_id": "price_7Hd3Wm9pQx2vRt5Kna",
"status": "active",
"renewal_state": "auto_renew",
"serves_customer": true,
"current_period_start": 1789344300,
"current_period_end": 1791936300,
"cancel_at_period_end": false,
"canceled_at": null,
"cancellation_reason": null,
"discount": null,
"payment_method": {
"type": "creditcard",
"brand": "Visa",
"last4": "4242",
"exp_month": 12,
"exp_year": 2030
},
"customer": null,
"price": null,
"mrr_cents": null,
"refund_eligibility": null,
"next_charge_amount_cents": 2900,
"last_payment_id": "pay_Zt6Wq1yBn4Xm9RcKvo",
"last_payment_at": 1789344300,
"payment_count": 1,
"trial_start": null,
"trial_end": null,
"dunning_attempts_notified": 0,
"dunning_last_notified_at": null,
"next_payment_attempt": null
}
]
}7. Receive the events
Renewals, failures, trial ends and cancellations all happen without a request from you, so webhooks are the only complete picture of subscription state. Register an endpoint and keep the secret it returns, because it is shown exactly once.
curl https://api.billkit.eu/v1/webhook_endpoints \
-H "Authorization: Bearer $BILLKIT_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com/webhooks/billkit",
"enabled_events": ["checkout.session.completed", "subscription.created", "invoice.paid", "subscription.past_due"]
}'{
"id": "we_Lm2Qb7xR4nV9pTcKys",
"object": "webhook_endpoint",
"created": 1789344400,
"livemode": false,
"url": "https://example.com/webhooks/billkit",
"enabled_events": ["checkout.session.completed", "subscription.created", "invoice.paid", "subscription.past_due"],
"status": "enabled",
"description": null,
"secret_fingerprint": "3f9a1c8e2b74",
"previous_secret_fingerprint": null,
"previous_secret_retired_at": null,
"secret": "bkwhsec_Xq4Rn8vP2mT7yLbW9cKzJd3sHf6gAe1u"
}The endpoint URL must resolve to a public address. A localhost or private IP URL is refused
with a 400 when you create the endpoint, and every delivery checks the resolved address again.
While you are developing, stream events to your machine with the CLI instead:
Read Webhooks next for the signature scheme and the retry behaviour, or the CLI guide for the full local loop.
BillKit