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.
Secret keys
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 code | When |
|---|---|
401 authentication_required | No Authorization header, or an empty bearer token. |
401 invalid_api_key | The 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_key | The key has been revoked. |
403 insufficient_scope | The key is valid but lacks the scope the route needs. |
402 tenant_suspended | The 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.
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.
| Resource | Scopes |
|---|---|
| Customers | customers:read, customers:write |
| Products | products:read, products:write |
| Prices | prices:read, prices:write |
| Subscriptions | subscriptions:read, subscriptions:write, subscriptions:cancel. POST …/preview_update needs only subscriptions:read. Billing portal sessions use subscriptions:write. |
| Usage records | usage_records:read, usage_records:write |
| Checkout | checkout_sessions:read, checkout_sessions:write. These also cover one-off payments (/v1/checkout/one_shot). |
| Payments | payments:read |
| Refunds | refunds:read, refunds:write |
| Disputes | disputes:read (read only: disputes come from Mollie) |
| Invoices | invoices:read, invoices:write, credit_notes:read |
| Coupons | coupons:read, coupons:write |
| Tax rates | tax_rates:read, tax_rates:write |
| Events | events:read |
| Webhook endpoints | webhook_endpoints:read, webhook_endpoints:write |
| API keys | api_keys:read, api_keys:write |
| Audit logs | audit_logs:read |
| Tenant | tenant:read, tenant:write. Covers capabilities, portal branding and billing profile. |
| Mollie credential | provider_credential:write (POST /v1/tenant/provider_credential). Not carried by "*"; see below. |
| Test clocks | test_clocks:read, test_clocks:write |
| Data export | data: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.
| Credential | What it is for |
|---|---|
| client_secret | Returned 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.
BillKit