BillKit/Docs Console
Get started

Quickstart

Create a plan, put a customer through checkout, and end up with a real subscription object. Test mode, no money moved.

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

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:

shell
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.

export BILLKIT_API_KEY=bk_test_...

Check it works. /v1/ping echoes back which tenant and mode the key resolved to.

shell
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.

shell
curl https://api.billkit.eu/v1/products \
-H "Authorization: Bearer $BILLKIT_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "Pro plan"}'
Response · 200 OK
{
"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.

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",
  "payment_methods": ["creditcard", "ideal"]
}'
Response · 200 OK
{
"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.

shell
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"
}'
Response · 200 OK
{
"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.

shell
curl "https://api.billkit.eu/v1/subscriptions?customer_id=cus_Ja7Ye2vMq9xN4TdWpr" \
-H "Authorization: Bearer $BILLKIT_API_KEY"
Response · 200 OK
{
"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.

shell
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"]
}'
Response · 200 OK
{
"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:

billkit listen --forward-to http://localhost:3000/webhooks/billkit

Read Webhooks next for the signature scheme and the retry behaviour, or the CLI guide for the full local loop.