BillKit/Docs Console
Build & operate

Errors

Every failure is JSON in the same envelope, including a 500. Branch on code, show the message, and log the request_id.

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

The envelope

json
{
"error": {
  "type": "invalid_request_error",
  "code": "parameter_invalid",
  "message": "Metered prices must use interval='month'.",
  "param": "usage_type",
  "request_id": "9f4c2b1ad7e84c0fa1b3e6d590c72b48"
}
}
FieldNotes
typestringBroad class. Always present.
codestringThe stable identifier to branch on. Always present.
messagestringEnglish, written for a developer reading a log. Do not parse it, and do not show it to your end users.
paramstringPresent when one field caused it. Dotted for nested fields, for example metadata.workspace_id.
reasonstringPresent on a handful of cases where code is too coarse to act on. See below.
request_idstringMirrors the X-Request-Id response header.

This shape holds for absolutely everything, including unhandled server errors and framework-level validation failures. Your parser never needs a fallback path for a plain-text body.

Log the request id

Every response carries X-Request-Id, and every error repeats it in the body. It is the join key into BillKit’s own logs. A support conversation that starts with a request id is a short one; one that starts with “a payment failed this morning” is not.

If you send your own X-Request-Id it is echoed back, provided it is 8 to 128 characters of letters, digits, _, ., : or -. Anything else is replaced with a generated id, because the value ends up in logs and an unvalidated one would let a caller forge log lines.

Status codes

StatusWhen
400A parameter is missing, invalid, or the operation is not allowed in the current state.
401Missing, malformed, unknown or revoked API key.
403The key is valid but its scopes do not cover this route.
404No such object. Also returned for objects in the other mode, or belonging to another account.
409A conflict: an idempotency clash, an operation already in flight for this customer, or an object that is no longer in the state the call needs.
422The request body failed schema validation. Unknown fields land here.
429Rate limited. See Rate limits.
500Something failed on our side. Retry with the same Idempotency-Key.
501A feature that exists but is not enabled on this deployment, such as PDF rendering.

404 rather than 403 across accounts. An object that belongs to someone else answers the same as one that does not exist, so the API never confirms that an id is real to someone who cannot see it. Within your own account, a scope problem is a genuine 403.

There is no 402. Card declines do not surface as an error on your API call, because the charge happens at Mollie after the customer leaves your server. A decline arrives as payment.failed or checkout.session.expired over webhooks.

Codes

codetype · status
authentication_requiredauthentication_error · 401. No Authorization header.
invalid_api_keyauthentication_error · 401. Malformed or unknown key.
revoked_api_keyauthentication_error · 401. The key was revoked.
insufficient_scopepermission_error · 403. The key lacks the route’s scope.
resource_missingnot_found_error · 404.
parameter_invalidinvalid_request_error · 400 or 422. Check param.
parameter_missinginvalid_request_error · 400.
idempotency_in_progressconflict_error · 409. Same key still running.
idempotency_key_in_useconflict_error · 409. Same key, different body or route.
operation_pendingconflict_error · 409. A payment operation is already in flight for this customer.
payment_method_in_useconflict_error · 409. Removing this saved method would strand an active subscription.
checkout_session_not_open409. The session was already completed or expired.
rate_limitedrate_limit_error · 429.
configuration_missingapi_error · 500. A setting the operator has to populate is absent.
not_implementedapi_error · 501.
unhandledapi_error · 500. Report it with the request_id.

Reason codes

code answers “what kind of failure”; many distinct situations share one. reason is the i18n-stable supplement for the cases where a user-facing surface has to say something specific. Map reason to your own copy, and fall back to code when you do not recognise it. New reasons can be added at any time, which is safe precisely because unknown ones fall back.

reasonMeaning
vat_not_recognisedVIES rejected the VAT number.
vat_unverifiableVIES could not be reached or the member state did not answer.
no_active_mandateNo usable payment mandate. The customer has to reauthorize.
customer_not_registered_with_providerThe customer has no counterpart at the payment provider yet.
subscription_terminalThe subscription is cancelled and cannot be changed.
period_window_expiredThe paid-through window has passed, so there is nothing to resume.
coupon_invalid / coupon_expired / coupon_exhausted / coupon_not_applicable / coupon_already_appliedWhy a coupon code was refused. These map straight to checkout copy.
customer_has_active_subscriptionA customer delete or a GDPR purge was refused. Cancel the subscription first.
idempotency_key_too_longThe Idempotency-Key header exceeded 255 characters.

Retrying

429 and 5xx are worth retrying. Everything in the 4xx range other than 429 will fail identically however many times you send it.

Use exponential backoff with jitter, and send the same Idempotency-Key on every attempt. A 500 clears the stored key, so the retry does the work; a success or a client error replays the original response. Either way you cannot double-charge by retrying.