BillKit/DocsConsole
Checkout

Create a Checkout session

Starts a subscription by sending the customer through hosted or embedded Checkout. The session carries the price, the trial and the URLs to return to, and the subscription is created once the first payment succeeds.

POST/v1/checkout/sessions
Operation create_checkout_session·Scope checkout_sessions:write·OpenAPI v1.1.0

Parameters

ParameterDescription
cancel_urlstring (uri)requiredWhere the buyer is sent if they leave the payment without completing it. Must be an absolute URL. At most 2083 characters.
price_idstringrequiredId of the recurring price the subscription is created on.
success_urlstring (uri)requiredWhere the buyer is sent after completing the payment. Must be an absolute URL. At most 2083 characters.
countrystring | nulloptionalThe buyer's ISO 3166-1 alpha-2 country, if known. Stored on the customer only when they have no country yet, so VAT applies to the first charge. Never overwrites an existing country. At most 2 characters.
coupon_codestring | nulloptionalCode of a coupon to apply. Must be active and eligible for this price. A redemption is counted only when the first payment settles. At most 64 characters.
customer_emailstring (email) | nulloptionalEmail for a new customer, created with the session. A new customer is always created; BillKit never matches an existing one by email. Exactly one of customer_id and customer_email. Must be a valid email address.
customer_idstring | nulloptionalId of an existing customer to check out. Exactly one of customer_id and customer_email.
customer_namestring | nulloptionalName for the customer created from customer_email. Refused alongside customer_id. At most 255 characters.
metadataobject | nulloptionalKey-value pairs stored on the session and echoed on it and on the checkout.session.completed event. BillKit does not interpret them. At most 50 keys.
payment_methodenum | nulloptionalThe one payment method to offer. Must be in the price's payment_methods, and must be able to start a mandate, so directdebit is refused. When omitted, the methods the price allows and the account can accept are offered. Must be omitted when ui_mode is embedded. One of creditcard, directdebit, ideal, eps, applepay, paypal.
trial_days_overrideinteger | nulloptionalTrial length in days for this checkout, replacing the price's trial_days. 0 removes the price's trial. At most twice the price's trial_days, or 28 when that is under 14. Refused for a metered price. Between 0 and 180.
ui_modeenumoptionalhosted creates the payment now and returns a url to redirect the buyer to. embedded returns a client_secret for the embedded checkout element, where the buyer picks the payment method. One of hosted, embedded. Defaults to "hosted".

Returns

A Checkout session with status: “open”. In hosted mode, redirect the customer to url with a 303. In embedded mode url is null and client_secret is set instead: hand it to the Checkout Element. The secret works for 30 minutes and is never returned by a later GET. cancel_url is required either way.

Identify the customer with exactly one of customer_id or customer_email. The email creates a new customer every time and never matches an existing one. Send country when you know where the buyer is. It fills in the customer’s country if they have none, and never overwrites one they already have. VAT on the first charge is worked out from that country, and the hosted flow never asks the buyer for it, so a hosted checkout for a customer with no country charges no VAT.

payment_method is for hosted sessions only and must be on the price’s payment_methods list. directdebit is refused with a 400, because SEPA Direct Debit cannot be the first payment; charge by iDEAL or EPS and the renewals go by direct debit. A hosted session for a customer whose first payment from an earlier checkout is still in progress returns 409 operation_pending until that one resolves.

This is how subscriptions are created. There is no endpoint that creates one directly, because a subscription needs a payment mandate and the mandate is what Checkout collects. The subscription does not exist yet when this call returns; it is created when the first payment succeeds, and checkout.session.completed is your signal to provision the account. If a hosted session’s payment fails or is abandoned at the provider, the session moves to expired and emits checkout.session.expired. A failed payment, not an abandoned one, also emits payment.failed, with no subscription_id. An embedded session stays open after a failed attempt, whether declined on the spot or after 3-D Secure, so the buyer can try another method until it expires.

Send an Idempotency-Key header. This endpoint can move money, and a retried create without a key can produce a second session and a second charge.

Test it

Run it with a bk_test_ key, open the url it returns, and choose paid at Mollie’s test-mode status selector. BillKit has no test card numbers of its own; Mollie owns that half, and lists them on its Testing page. Then inspect the events it produced in the console under Developers → Events.

Errors

StatusCause
400The request was refused (type: invalid_request_error): a required parameter is missing (parameter_missing), one failed validation (parameter_invalid) or is not one the operation takes (parameter_unknown), and param names it.
401No credential, or one that does not authenticate.
402The account is suspended (tenant_suspended), or the payment provider refused a charge or refund the call made (type: payment_error, code: payment_refused).
403Authenticated, but not allowed: missing scope (insufficient_scope), a key of the wrong mode (mode_mismatch), or a refused origin. Needs the checkout_sessions:write scope.
404No such object in this account and mode.
409Conflicts with the object's current state (state_conflict, or a more specific code), or an operation still in flight (operation_pending).
413The request body is larger than the API accepts.
429Rate limited. Retry-After says when to try again.
500Something failed on BillKit's side (type: api_error). Retry with the same Idempotency-Key.
502The payment provider failed or did not answer.
503Temporarily unable to serve. reason says whether the request was attempted.
Request · Node.js
const session = await client.checkoutSessions.create({
  customer_id: "cus_9XKp2vQ1",
  price_id: "price_9XKp2vQ1",
  success_url: "https://acme.example/welcome",
  cancel_url: "https://acme.example/pricing",
  metadata: { workspace: "acme-eu" },
});

// A hosted session always has a url; it is null only on an embedded one.
return Response.redirect(session.url!, 303);
Request · Python
session = client.checkout_sessions.create(
    customer_id="cus_9XKp2vQ1",
    price_id="price_9XKp2vQ1",
    success_url="https://acme.example/welcome",
    cancel_url="https://acme.example/pricing",
    metadata={"workspace": "acme-eu"},
)

return redirect(session["url"], code=303)
Request · PHP
$session = $client->checkoutSessions->create([
    'customer_id' => 'cus_9XKp2vQ1',
    'price_id'    => 'price_9XKp2vQ1',
    'success_url' => 'https://acme.example/welcome',
    'cancel_url'  => 'https://acme.example/pricing',
    'metadata'    => ['workspace' => 'acme-eu'],
]);

header('Location: ' . $session['url'], true, 303);
Request · Laravel
// Checkout implements Responsable, so returning it redirects.
return $request->user()->checkout('price_9XKp2vQ1', [
    'success_url' => route('welcome'),
    'cancel_url'  => route('pricing'),
    'coupon_code' => 'LAUNCH',
]);
Request · React
// Ask your server for an embedded session, then mount the element.
// It authenticates with the session's client_secret; there is no
// publishable key in BillKit.
import { BillKitProvider, CheckoutElement } from "@billkit-eu/react";

<BillKitProvider>
  <CheckoutElement
    clientSecret={clientSecret}
    theme={{ colorPrimary: "#2f6bff", borderRadius: "10px" }}
    onSuccess={({ sessionId }) => router.push(`/welcome?cs=${sessionId}`)}
  />
</BillKitProvider>;
Request · curl
curl -X POST https://api.billkit.eu/v1/checkout/sessions \
  -H "Authorization: Bearer $BILLKIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"cancel_url":"https://acme.example/pricing","price_id":"price_9XKp2vQ1","success_url":"https://acme.example/welcome","customer_id":"cus_9XKp2vQ1"}'
Request body · every field
{
  "cancel_url": "https://acme.example/welcome",
  "country": "string",
  "coupon_code": "string",
  "customer_email": "ada@example.com",
  "customer_id": "cus_9XKp2vQ1",
  "customer_name": "Ada Lovelace",
  "metadata": {
    "key": "value"
  },
  "payment_method": "creditcard",
  "price_id": "price_9XKp2vQ1",
  "success_url": "https://acme.example/welcome",
  "trial_days_override": 0,
  "ui_mode": "hosted"
}
Response · 200 OK
{
  "cancel_url": "https://acme.example/welcome",
  "client_secret": "string",
  "coupon_id": "coup_9XKp2vQ1",
  "created": 1789392000,
  "customer_country_code": "NL",
  "customer_email": "ada@example.com",
  "customer_id": "cus_9XKp2vQ1",
  "expires_at": 1789392000,
  "id": "obj_9XKp2vQ1",
  "livemode": false,
  "metadata": {},
  "object": "checkout.session",
  "payment_id": "pay_9XKp2vQ1",
  "payment_method": "creditcard",
  "price_id": "price_9XKp2vQ1",
  "status": "open",
  "subscription_id": "sub_9XKp2vQ1",
  "success_url": "https://acme.example/welcome",
  "trial_days_override": 0,
  "ui_mode": "hosted",
  "url": "https://acme.example/welcome"
}