e-bon
e-bon.ro
Reference

Error codes

Reference for every error code returned by the e-bon API — HTTP status, when you receive it, and how to handle it from your integration.

Error codes

When the e-bon API rejects a request it returns a non-2xx HTTP status and a JSON body with a stable error.code. This page lists every code you can receive, what it means, and how to recover.

Use it when you need to:

  • Look up an unfamiliar error.code returned by the API.
  • Decide whether a failed request is safe to retry.
  • Build user-facing error handling in your POS, mobile app, or back-office integration.

Understand the error envelope

Every error response uses the same JSON shape:

{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Request validation failed",
    "details": [
      { "path": "body.amount", "message": "Expected number, received string" }
    ]
  }
}
  • code — a stable identifier from the list below. Branch your client logic on this value, not on the human message.
  • message — a short English description, intended for logs and support tickets. Translate or rewrite it before showing it to end users.
  • details — optional. Always present for VALIDATION_ERROR as an array of { path, message } pairs. May appear on BAD_REQUEST, CONFLICT, and UNPROCESSABLE_ENTITY with route-specific data.
The 429 rate-limit response is the one exception: its body is { "code", "message", "status" } at the top level instead of being wrapped in { "error": { ... } }. See Retry rate-limited requests.

Handle authentication errors

UNAUTHORIZED — 401

You receive this when the request has no valid credential — the Authorization: Bearer <token> header or the x-api-key header is missing, malformed, has an invalid signature, or the token has expired.

{
  "error": {
    "code": "UNAUTHORIZED",
    "message": "Missing or invalid Authorization header. Expected: Bearer <token>"
  }
}

How to handle it:

  • For JWT clients, refresh your access token via POST /api/v1/auth/refresh and retry the request once.
  • For API-key clients, check that the key still exists and is active in the Portal API keys page. Rotate the key if it has been disabled.
  • After a successful refresh or rotation, the original request is safe to retry.

FORBIDDEN — 403

The credential is valid but it does not have permission for the requested action — the JWT user lacks the required role, or the API key is missing the required scope.

{
  "error": {
    "code": "FORBIDDEN",
    "message": "Insufficient role. Required: admin or owner"
  }
}

How to handle it:

  • Do not retry with the same credential — it will keep failing.
  • For JWT clients, sign in as a user with the required role.
  • For API keys, mint a new key with the required scopes from the Portal. See Authentication API for the full scope list.

Fix validation errors

VALIDATION_ERROR — 400

The request body, query string, or headers did not match the schema for the endpoint. The details array always lists each invalid field and why it was rejected.

{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Request validation failed",
    "details": [
      { "path": "body.items.0.quantity", "message": "Expected number, received string" },
      { "path": "body.paymentType", "message": "Invalid enum value." }
    ]
  }
}

How to handle it:

  • Walk details[*].path to locate each invalid field in your request payload.
  • Display the field-level messages next to the corresponding form inputs in your UI.
  • After fixing every field, the request is safe to retry.

A common variant: an invalid Idempotency-Key header (must be 1–128 characters of letters, digits, -, and _) returns VALIDATION_ERROR with the offending value in details. See Idempotency and retries.

BAD_REQUEST — 400

The payload is structurally valid but breaks a semantic rule that schema validation cannot express — for example, two mutually exclusive fields supplied together.

{
  "error": {
    "code": "BAD_REQUEST",
    "message": "Cannot specify both 'startTime' and 'cursor'."
  }
}

How to handle it: Read message, adjust the request, and retry.

Resolve resource errors

NOT_FOUND — 404

The resource ID in the request does not exist (or is not visible to the current credential).

{
  "error": {
    "code": "NOT_FOUND",
    "message": "Device dev_abc123 not found."
  }
}

How to handle it: Verify the resource ID. Do not retry with the same ID — list the parent collection to discover a valid one.

CONFLICT — 409

The request collides with the current state of the resource. Common causes:

  • An idempotency-key replay with a different request body.
  • A device already claimed by another organization.
  • A unique field (email, slug, code) already in use.
{
  "error": {
    "code": "CONFLICT",
    "message": "Device dev_abc123 is already claimed by another organization."
  }
}

How to handle it: Read message to identify the specific conflict. Retrying without resolving the underlying state will fail again — release the resource, change the duplicate field, or generate a fresh idempotency key as appropriate.

UNPROCESSABLE_ENTITY — 422

The payload parses correctly but breaks a business rule — for example, receipt line totals that do not add up to the declared total, or a refund amount larger than the original transaction.

{
  "error": {
    "code": "UNPROCESSABLE_ENTITY",
    "message": "Receipt total does not match the sum of line items.",
    "details": { "expected": 119.0, "received": 120.0 }
  }
}

How to handle it: Use message and details to fix the business-rule violation, then resubmit.

Retry rate-limited requests

RATE_LIMIT_EXCEEDED — 429

You exceeded the per-key (or per-IP) request budget. The response includes a Retry-After header in seconds.

HTTP/1.1 429 Too Many Requests
Retry-After: 47
Content-Type: application/json

{
  "code": "RATE_LIMIT_EXCEEDED",
  "message": "Too many requests, please try again later.",
  "status": 429
}

How to handle it:

  • Wait for the number of seconds in Retry-After before retrying.
  • Add jitter when multiple clients share a key, to avoid synchronised bursts.
  • For sustained throughput above your current limit, contact support to raise the per-key budget rather than retrying through it.
The 429 response body is not wrapped in { "error": { ... } }. Read code from the top level for this status only.

Handle plan limits

TIER_LIMIT_EXCEEDED — 403

A subscription-plan quota is exhausted — typically the monthly receipt cap on the current plan.

{
  "error": {
    "code": "TIER_LIMIT_EXCEEDED",
    "message": "Monthly receipt quota exceeded for the Starter plan."
  }
}

How to handle it:

  • Surface a clear upgrade prompt to the account owner.
  • Direct them to Billing and subscriptions to change plan.
  • Quotas reset at the start of each billing period — operations resume automatically once the window rolls over.

Recover from server errors

INTERNAL_ERROR — 500

Something went wrong on the e-bon side that is not classified as a more specific error.

{
  "error": {
    "code": "INTERNAL_ERROR",
    "message": "An internal error occurred"
  }
}

How to handle it:

  • Capture the X-Request-Id response header — it identifies the failed request in our logs.
  • Retry with exponential backoff and jitter (do not retry tightly).
  • If the error persists, open a support ticket and include the X-Request-Id value. See Request tracing and logging.

SERVICE_UNAVAILABLE — 503

A downstream dependency is temporarily unavailable. The most common case is a fiscal device that is offline or has no controller assigned. Cloud-storage outages (for example logo uploads) also surface here.

{
  "error": {
    "code": "SERVICE_UNAVAILABLE",
    "message": "Device is offline or has no controller assigned"
  }
}

How to handle it:

  • Check the device connection status in the Portal or via the Devices API.
  • Retry with exponential backoff and jitter — 503 responses do not include a Retry-After header.
  • Inform the operator if the device stays offline so they can power-cycle or reconnect it.

Map HTTP status to error code

Use this table when writing proxy, WAF, or observability rules. Branch on error.code (not status) when two codes share a status.

HTTP statusCodes that use it
400VALIDATION_ERROR, BAD_REQUEST
401UNAUTHORIZED
403FORBIDDEN, TIER_LIMIT_EXCEEDED
404NOT_FOUND
409CONFLICT
422UNPROCESSABLE_ENTITY
429RATE_LIMIT_EXCEEDED
500INTERNAL_ERROR
503SERVICE_UNAVAILABLE

Where to next