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
Connect Mollie to your test mode first. BillKit creates the payment on your Mollie account, so step 4 fails without a stored test credential. Connect it in the console under Settings → Mollie, or store a Mollie test key over the API:
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_..."}' GET /v1/tenant/capabilities reports mollie_connected and, once BillKit has read your Mollie
profile, the payment methods and currencies you can actually charge. The answer is per mode:
a bk_test_ key reports what your Mollie test profile enables, a bk_live_ key what your live
one does. They can legitimately differ, so check the mode you are about to charge in.
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. 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,
"archived_at": null
} 3. Create a price
The price is what actually gets charged, and how often. amount_cents is an integer in the
smallest unit, so 2900 is €29.00. payment_methods is the allowlist offered at checkout, and it
must contain at least one method that can create a mandate (creditcard, ideal or
applepay). directdebit alone is not enough: SEPA settles renewals but cannot be the first
payment. Apple Pay is offered through Mollie’s own sheet, so the buyer is redirected there and
back. The mandate it mints is a card mandate, so those subscriptions renew on creditcard.
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"],
"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
} 4. 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 in the same
transaction. It never deduplicates by email, so sending the same address twice gives you two
customers.
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,
"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.
5. Read the subscription
Once Mollie reports the payment as paid, BillKit creates the subscription, marks the checkout
session complete, and emits checkout.session.completed, subscription.created and
payment.succeeded.
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,
"customer": null,
"price": null,
"mrr_cents": null,
"refund_eligibility": null,
"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
}
]
} 6. 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", "invoice.payment_failed"]
}' {
"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", "invoice.payment_failed"],
"status": "enabled",
"description": null,
"secret_fingerprint": "3f9a1c8e2b74",
"previous_secret_fingerprint": null,
"previous_secret_retired_at": null,
"secret": "bkwhsec_Xq4Rn8vP2mT7yLbW9cKzJd3sHf6gAe1u"
} The endpoint URL must be publicly resolvable. BillKit refuses to deliver to private, loopback or
link-local addresses, so http://localhost:3000 will never receive anything. 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