Errors
The LedgerBee API uses conventional HTTP status codes and returns every error
as a JSON envelope. The status code tells you whether retrying can help; the
machine-readable code tells you exactly what went wrong.
The error envelope
Every error body carries at least statusCode, timestamp, path, and
message. Most errors also include code and details:
Code
| Field | Always present | Description |
|---|---|---|
statusCode | Yes | The HTTP status, repeated in the body. |
timestamp | Yes | ISO 8601 time the error was produced. |
path | Yes | The request path, relative to the /api mount (e.g. /v1/customers). |
message | Yes | Human-readable summary. For logging only — its wording may change. |
code | No | Stable machine-readable identifier (e.g. UNAUTHORIZED, LICENSE.REQUIRED). Branch on this, never on message. |
details | No | Context for the specific code — failing fields, required scopes, the missing license key. |
Validation errors (400)
Request bodies are validated strictly: unknown fields are rejected, not
ignored. A failed validation returns code: "SHARED_VALIDATION_FAILED" with
one details.validationErrors entry per failing field:
property— dot-separated path to the field, including array indices (e.g.lines.0.quantity)constraints— map of failed rule name to a human-readable explanation
Status codes
| Status | When |
|---|---|
400 Bad Request | Validation failed (above), or the request violates a business rule — the code says which. |
401 Unauthorized | Missing, invalid, expired, or IP-restricted API key / OAuth token. The body has code: "UNAUTHORIZED" and message: "Invalid API key". See Authentication. |
403 Forbidden | The credential is valid but lacks a required scope (code: "INSUFFICIENT_PERMISSIONS", with details.requiredScopes), or your company lacks a required license (code: "LICENSE.REQUIRED", with details.licenseKey). See Authentication for scopes and Licenses for modules. |
404 Not Found | No such route, or the referenced resource doesn't exist in your company. |
413 Payload Too Large | The request body exceeds the size limit — message states the received size and the limit. |
429 Too Many Requests | You hit a rate limit. Back off and retry — see Rate limits. |
500 Internal Server Error | Something failed on our side. message is a generic Internal Server Error — retry with backoff and contact support if it persists. |
A 401 or 403 on the OAuth path carries a standard WWW-Authenticate challenge
header. Every challenge carries resource_metadata, the URL of the
protected-resource document naming which authorization server covers that
endpoint. On a company-scoped MCP endpoint it names that company's own
resource, which is what binds the resulting grant to the right company.
| The request | The challenge says |
|---|---|
| carried no credential | no error code — you have not authenticated yet, and resource_metadata is where to start |
| carried a token that is invalid, expired or revoked | error="invalid_token" — refresh or re-authorize |
| carried a valid token without a scope the call needs | error="insufficient_scope", with scope listing the full set to ask for |
A request authenticated with an API key receives no challenge: the challenge describes the OAuth flow, which an API key does not take part in.
Request IDs
Every response, success or error, carries an X-Request-ID header with a
UUID unique to that request. The same ID is stamped on our server-side traces.
Include it in support requests so we can locate the exact request in our logs.
Code
Error codes are stable
code values such as SHARED_VALIDATION_FAILED, UNAUTHORIZED, and
LICENSE.REQUIRED are stable identifiers, safe to branch on. They are never
renamed or repurposed; message text may be reworded at any time. New codes
may be added over time, so treat any unrecognized code as a generic failure
for its status code.
A refusal about a billing group or one of its members answers a
SUBSCRIPTION_BILLING_GROUP_* code, described on
Billing groups. A refusal about an
add-on answers a SUBSCRIPTION_ADDON_* code; the ones a public request can
receive are described on Add-ons. Both families
name subscriptions by their stable id (details.subscriptionId,
details.addonSubscriptionId), and SUBSCRIPTION_BILLING_GROUP_MEMBER_LIMIT
reports details.slotsUsed, details.slotsRequested and details.maxSlots.
details.billingGroupId appears only on the /v1/billing-groups endpoints, and
SUBSCRIPTION_BILLING_GROUP_NOT_FOUND always answers 404.
When the subscription a request is about is itself an add-on, the refusal is an
add-on code even where the same request on a billing-group member answers a
SUBSCRIPTION_BILLING_GROUP_* one. An add-on bills on its parent's invoice and
billing dates, so it cannot be moved, re-timed or re-configured on its own. Each
of these answers 400, and details.subscriptionId names the subscription the
request was about — the add-on, or for a cancellation refused because of its
add-ons, the parent:
code | When | Other details | What to do |
|---|---|---|---|
SUBSCRIPTION_ADDON_FOLLOWS_PARENT_GRID | A change to the add-on's cadence, billing direction or send offset, a pause or resume, a merge, a billing-anchor reset, or a rebuild of an add-on that has not billed yet. | kind (the change requested) on a timeline change, field where one field was refused; a merge or a rebuild carries neither | Cancel the add-on and add it again with the new setup. |
SUBSCRIPTION_ADDON_DETAILS_FOLLOW_PARENT | An update to a billing detail of the add-on: payment terms, department, contact, send offset, payment providers, bank details, extra invoice recipients, automatic card billing or payment-method mode. | field | Change the detail on the parent subscription. The add-on follows it. |
SUBSCRIPTION_ADDON_IMMEDIATE_PRORATION_NOT_ALLOWED | A change to the add-on asked to invoice its proration immediately. | kind | Leave the proration behaviour unset. The change is settled on the parent's next invoice. |
SUBSCRIPTION_ADDON_PREPAID_COMMITMENT_NOT_SUPPORTED | A change would give the add-on a prepaid usage commitment, in the request or on the plan it is replaced with. | kind, planId when the plan carries it | Use a plan without a commitment for the add-on. |
SUBSCRIPTION_ADDON_REFUND_LAST_INVOICE_NOT_SUPPORTED | A cancellation of an add-on, or of a subscription that has add-ons and bills on its own invoice, asked to credit the whole last invoice. That invoice carries the add-ons' lines beside the parent's. A member of a billing group answers SUBSCRIPTION_BILLING_GROUP_REFUND_LAST_INVOICE_NOT_SUPPORTED instead, add-ons or not. | kind | Cancel with a prorated refund or with no refund. |
SUBSCRIPTION_ADDON_PARENT_MISMATCH | The subscription added as an add-on differs from its parent in customer, currency, billing direction, revenue deferral or Leverandørservice collection. The first difference is reported. | field (customerId, currency, billingDirection, deferRevenue or collectViaLeverandorservice), value (the add-on's), parentValue (the parent's) | Align the field on the add-on, or add a subscription that already matches. |