BillKit/DocsConsole
Get started

Authentication

One credential type reaches the API: a secret key sent as a bearer token. The key also decides whether you are in live or test mode.

Updated Sep 24, 2026·OpenAPI v1.1.0

Secret keys

shell
curl https://api.billkit.eu/v1/customers \
-H "Authorization: Bearer $BILLKIT_API_KEY"

There are two key forms. bk_live_... operates on live data and moves real money. bk_test_... operates on test data. There is no publishable key and no separate restricted-key prefix: you narrow a key with scopes, described below, and it keeps its bk_live_ or bk_test_ prefix. The prefix decides the mode.

Status and codeWhen
401 authentication_requiredNo Authorization header, or an empty bearer token.
401 invalid_api_keyThe header does not start with Bearer , the token is not a bk_live_ or bk_test_ key, or no such key exists.
401 revoked_api_keyThe key has been revoked.
403 insufficient_scopeThe key is valid but lacks the scope the route needs.
402 tenant_suspendedThe key is valid but your BillKit account is suspended. Sign in to the console to settle it.

All of them use the standard error envelope.

A secret key is a full credential for your billing data. Keep it server-side. Nothing in a browser or a mobile binary should ever hold one.

Creating and revoking keys

Manage keys in the console under Developers → API keys, or over the API. The plaintext key is returned in the secret field once, on the create call. After that only prefix (the first 16 characters) is readable, because BillKit stores a hash rather than the key. A new key is always in the same mode as the key that created it, so a test key can only create test keys.

shell
curl https://api.billkit.eu/v1/api_keys \
-H "Authorization: Bearer $BILLKIT_API_KEY" \
-H "Content-Type: application/json" \
-d '{"label": "reporting pipeline", "scopes": ["customers:read", "subscriptions:read", "invoices:read"]}'

Keys do not expire. To rotate one, create the replacement, deploy it, then revoke the old one with POST /v1/api_keys/{api_key_id}/revoke, where api_key_id is the key’s id (ak_…, not the key itself). Both keys work during the overlap. Revocation takes effect immediately and cannot be undone. Revoking an already revoked key returns it unchanged, and a key may revoke itself.

GET /v1/api_keys lists the keys in the calling key’s mode, revoked ones included. Check last_used_at before revoking a key you are unsure about. It is updated at most once a minute.

Scopes

Pass scopes and the key is refused on any route outside them with 403 insufficient_scope. The same applies to expand: an expansion needs the read scope of the resource it embeds. Every route needs exactly one scope, except GET /v1/ping, which any valid key can call. "*" grants every scope except provider_credential:write, which a key must list by name.

Omit scopes and the new key gets the scopes of the key that created it. A key created in the console, or by a ["*"] key, has full access. A key created by a narrower key gets that narrower set. A key can never grant a scope it does not hold, so a key with only api_keys:write cannot create a ["*"] key. Asking for a scope the calling key lacks is a 400 parameter_invalid, and so is a scope name that does not exist, so a typo fails when you create the key rather than on every call afterwards.

ResourceScopes
Customerscustomers:read, customers:write
Productsproducts:read, products:write
Pricesprices:read, prices:write
Subscriptionssubscriptions:read, subscriptions:write, subscriptions:cancel. POST …/preview_update needs only subscriptions:read. Billing portal sessions use subscriptions:write.
Usage recordsusage_records:read, usage_records:write
Checkoutcheckout_sessions:read, checkout_sessions:write. These also cover one-off payments (/v1/checkout/one_shot).
Paymentspayments:read
Refundsrefunds:read, refunds:write
Disputesdisputes:read (read only: disputes come from Mollie)
Invoicesinvoices:read, invoices:write, credit_notes:read
Couponscoupons:read, coupons:write
Tax ratestax_rates:read, tax_rates:write
Eventsevents:read
Webhook endpointswebhook_endpoints:read, webhook_endpoints:write
API keysapi_keys:read, api_keys:write
Audit logsaudit_logs:read
Tenanttenant:read, tenant:write. Covers capabilities, portal branding and billing profile.
Mollie credentialprovider_credential:write (POST /v1/tenant/provider_credential). Not carried by "*"; see below.
Test clockstest_clocks:read, test_clocks:write
Data exportdata:export (GET /v1/tenant/export)

subscriptions:cancel is separate from subscriptions:write so that a support tool can pause and resume subscriptions without being able to cancel them. data:export is separate from tenant:read because the export contains every customer’s personal data and payment history, so a general read-only key should not carry it.

provider_credential:write goes further: not even a "*" key carries it. The Mollie credential decides which Mollie account every future payment is paid into, so a leaked full-access key must not be able to swap it for the holder’s own. A key has it only when it is listed by name, and only the console, or a key that already holds it, can grant it; a "*" key asking for it is refused with 400 parameter_invalid. The console itself can always replace the credential.

Credentials that are not API keys

Two other credentials appear in BillKit. Both are short-lived, both cover a single object, and neither works on the routes an API key calls.

CredentialWhat it is for
client_secretReturned by POST /v1/checkout/sessions when ui_mode is embedded. Authorises the Checkout Element’s calls for that one session, and nothing else. Safe to hand to the browser. See Embed Checkout.
bk_portal_…A customer portal session token, created by POST /v1/billing_portal/sessions for one subscription_id. It covers that subscription only and expires after 30 minutes idle or 2 hours in total. The response carries it once, as token and inside url: redirect the customer to url, or pass deliver_email: true to have BillKit email the link. POST /v1/billing_portal/sessions/{session_id}/revoke ends it early.

Pre-authentication throttling

Before a key is looked up, each client address gets a budget of attempts per route: a burst of 20, refilling at 60 per minute. Guessing at keys therefore runs out quickly, and a request with a valid key gives its attempt back, so normal traffic never reaches this limit. When it is exhausted the answer is 429 rate_limited with a Retry-After header. See Rate limits.

Environment variables

The server SDKs (Node, Python, PHP and Laravel) read BILLKIT_API_KEY when you do not pass a key explicitly. The billkit CLI reads it too. Its order is the --api-key flag, then BILLKIT_API_KEY, then the profile saved by billkit login, whose live keys are kept in the operating system’s keychain where one is available. The browser packages never take a secret key.