e-bon
e-bon.ro
API reference

Errors reference

Canonical catalogue of every error e-bon can return — HTTP-level codes from the API and FiscalError codes from the device — with per-code recovery steps.

Errors reference

This page is the canonical index of every error the e-bon platform can surface to an integrator. Use it as the single source of truth when you are implementing error handling in your POS, your back-office, or your own tooling on top of @e-bon/sdk.

There are two distinct error families, and they live at different layers:

  • HTTP errors are wire-level failures returned directly by the e-bon REST API. You see them on the response itself — a non-2xx status with a JSON body — for transport, authentication, authorization, validation and rate-limiting failures. They never involve the printer.
  • FiscalError codes are device-side failures. The HTTP request that submitted the command may have succeeded with a 202 Accepted; the failure happened later, on the AMEF or in the fiscal driver. You see fiscal codes inside the command result returned by GET /api/v1/commands/{id} (in the error field), inside command.failed event payloads, and — when you use @e-bon/sdk — as a typed FiscalError exception.

Both families share one rule: branch on the code string, not on the human message. Messages may be reworded; codes are stable.

Error envelope

Every HTTP error response — whether from validation, auth, business logic or an unhandled crash — follows the same wrapped JSON envelope:

{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Request validation failed",
    "details": [
      { "path": "items[0].vatRate", "message": "vatRate must be one of: 0, 9, 11, 21" }
    ]
  }
}
  • error.code — a stable string identifier from the catalogue below. Always present.
  • error.message — a human-readable message in English. Suitable for logging; do not parse.
  • error.details — optional. Present on VALIDATION_ERROR (Zod field errors, as { path, message }[]) and on a handful of other codes that need a structured payload.

A typical 400 VALIDATION_ERROR response in full:

HTTP/1.1 400 Bad Request
Content-Type: application/json

{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Request validation failed",
    "details": [
      { "path": "items.0.vatRate", "message": "vatRate must be one of: 0, 9, 11, 21" },
      { "path": "items.0.unitPrice", "message": "Number must be greater than 0" }
    ]
  }
}
The 429 RATE_LIMIT_EXCEEDED response from the global rate limiter is currently emitted as a flat object — { "code": "RATE_LIMIT_EXCEEDED", "message": "...", "status": 429 } — instead of the wrapped envelope. Match on the code field; treat both shapes as equivalent until the API is normalised.

HTTP error codes

Twelve stable codes. Every non-2xx response from https://api.e-bon.ro carries one of them in error.code (or in code for the 429 outlier above).

UNAUTHORIZED

HTTP 401. The request reached the API but no valid credential could be resolved — missing x-api-key / Authorization header, malformed key, unknown key, or the key has been revoked / marked inactive.

{
  "error": {
    "code": "UNAUTHORIZED",
    "message": "Missing or invalid API key"
  }
}

What to do

  • Confirm the request carries either x-api-key: ebon_live_... or Authorization: Bearer ebon_live_....
  • Check the key has not been revoked or disabled in the Portal (Settings → API keys).
  • Verify you are hitting the right environment — production keys do not work against staging and vice-versa.
  • Rotate the key if you suspect it has leaked, then update your secret store.

TOKEN_EXPIRED

HTTP 401. The JWT access token presented to a Portal-style endpoint has expired. This only applies to JWT auth, not to API keys (which do not expire).

{
  "error": {
    "code": "TOKEN_EXPIRED",
    "message": "Access token expired"
  }
}

What to do

  • Call POST /api/v1/auth/refresh with the refresh token to obtain a fresh access token.
  • Retry the original request with the new Authorization: Bearer <accessToken> header.
  • If the refresh endpoint also returns 401, force the user to log in again.
  • For non-interactive integrations, prefer an API key — it does not expire.

FORBIDDEN

HTTP 403. Authentication succeeded, but the principal does not have the scope or role needed for this endpoint or this resource.

{
  "error": {
    "code": "FORBIDDEN",
    "message": "Insufficient permissions for this operation"
  }
}

What to do

  • Check the API key's configured scopes against the Authentication reference.
  • For JWT auth, confirm the user's organization role (Owner / Admin / Member) covers the action.
  • Re-issue the API key with the correct scopes if it was created with too narrow a set.
  • Make sure the resource (device, receipt, command) actually belongs to the organization the credential is scoped to.

VALIDATION_ERROR

HTTP 400. The request body or query string failed Zod schema validation. details is an array of { path, message } entries, one per failed field.

{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Request validation failed",
    "details": [
      { "path": "items.0.vatRate", "message": "vatRate must be one of: 0, 9, 11, 21" }
    ]
  }
}

What to do

  • Iterate error.details and surface each path / message pair to the operator or developer.
  • Fix the offending field locally; never blindly retry — the request will fail the same way.
  • Cross-check the relevant resource page (Receipts, Commands, Devices) for the exact accepted shape.
  • For Idempotency-Key validation failures, regenerate a key matching [a-zA-Z0-9_-]{1,128}.

BAD_REQUEST

HTTP 400. The request was malformed in a way that is not specifically a schema violation — invalid JSON, unsupported Content-Type, oversized body, or an endpoint-level precondition that the schema cannot express.

{
  "error": {
    "code": "BAD_REQUEST",
    "message": "Request body is not valid JSON"
  }
}

What to do

  • Confirm the body is valid UTF-8 JSON and that Content-Type: application/json is sent on every POST / PATCH / PUT.
  • Check the payload size against the per-endpoint limits documented on the resource pages.
  • Read the error.message carefully — it usually names the specific precondition that failed.
  • Do not retry without changing the request; this code is non-transient.

NOT_FOUND

HTTP 404. The addressed resource does not exist for the credential's organization. Either the ID is wrong, the resource was deleted, or it belongs to a different organization than the one the credential is scoped to.

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

What to do

  • Confirm the ID is well-formed and matches the prefix expected for the resource (dev_, cmd_, rec_, rep_, key_).
  • List the parent collection (GET /api/v1/devices, GET /api/v1/commands) to verify the resource still exists.
  • Make sure you are not querying with a credential scoped to a different organization.
  • If the resource was deleted, recreate it or remove the stale reference from your local cache.

CONFLICT

HTTP 409. The operation is incompatible with the resource's current state — for example cancelling a command that is already completed or failed, or updating a webhook that has been disabled.

{
  "error": {
    "code": "CONFLICT",
    "message": "Command is already completed and cannot be cancelled"
  }
}

What to do

  • Re-fetch the resource (GET /api/v1/commands/{id}, GET /api/v1/devices/{id}) and inspect status before retrying.
  • Decide whether the conflict is acceptable (the operation already happened) or whether you need to take a different action.
  • Do not blindly retry — you will get the same 409. The state needs to change first.
  • For idempotent operations, prefer the Idempotency-Key flow so a network blip does not surface as a CONFLICT.

UNPROCESSABLE_ENTITY

HTTP 422. The request was syntactically valid and passed schema validation, but the business state of the system rejects it — for example trying to print a refund larger than the original receipt.

{
  "error": {
    "code": "UNPROCESSABLE_ENTITY",
    "message": "Refund amount exceeds the original receipt total"
  }
}

What to do

  • Read error.message for the specific business rule that failed.
  • Re-fetch the related resources (the original receipt, the device's current state) and adjust the request.
  • Do not retry without changing the payload.
  • If the rule does not match what you expect, check the resource reference page for the latest constraints.

RATE_LIMIT_EXCEEDED

HTTP 429. You exceeded a rate limit window. The response includes a Retry-After header (integer seconds) that tells you exactly how long to wait. Note that this code currently arrives as a flat object, not as the wrapped envelope.

HTTP/1.1 429 Too Many Requests
Retry-After: 47
RateLimit-Limit: 150
RateLimit-Remaining: 0
RateLimit-Reset: 47
Content-Type: application/json

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

What to do

  • Sleep for the value in Retry-After (seconds) before the next attempt — never sooner.
  • Implement exponential backoff on top, capped at a sensible maximum, in case 429s repeat.
  • Spread bursty traffic across the rate-limit window instead of firing it in one shot.
  • If you regularly hit limits, raise it with the e-bon team or batch operations through fewer requests; see Rate limits.

TIER_LIMIT_EXCEEDED

HTTP 403. The organization's subscription plan does not allow this action — for example trying to create more than two devices on the Free plan, or creating an API key on the Free plan at all.

{
  "error": {
    "code": "TIER_LIMIT_EXCEEDED",
    "message": "Free plan is limited to 2 devices"
  }
}

What to do

  • Check the tier enforcement table for the limits applied to your plan.
  • Upgrade the organization's plan in the Portal billing page to lift the limit.
  • Remove unused devices or keys before creating new ones, if you do not want to upgrade.
  • This code is permanent for the current plan; do not retry without changing state.

INTERNAL_ERROR

HTTP 500. An unhandled server-side error. The request did not produce the intended outcome and the server has logged the failure with a request ID for investigation.

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

What to do

  • For idempotent operations (and all POSTs carrying an Idempotency-Key), retry once after a short backoff.
  • Capture the request ID from your own logs (and from the response headers if present) so support can correlate.
  • Do not assume the operation did or did not happen — re-fetch the resource to find out.
  • If the error persists, file a support request with the request ID, command ID and reproduction steps.

SERVICE_UNAVAILABLE

HTTP 503. The server is temporarily refusing requests — usually a deploy in progress, a dependency outage, or a forced maintenance window.

{
  "error": {
    "code": "SERVICE_UNAVAILABLE",
    "message": "Service temporarily unavailable, please try again shortly"
  }
}

What to do

  • Retry with exponential backoff — start at 1s and double up to a sensible cap.
  • Reuse the same Idempotency-Key so duplicates are absorbed by the cache once the service is back.
  • Check the e-bon status page or your support channel before raising an alert.
  • Do not promote this to an alert on the first occurrence; transient unavailability is expected during deploys.

Fiscal / device error codes (FiscalError)

Twenty-five codes grouped into six families by the leading digit. They surface inside command results (error.code on GET /api/v1/commands/{id}), inside command.failed event payloads, and as the code field on the typed FiscalError exception thrown by @e-bon/sdk.

The shape of FiscalError in the SDK:

class FiscalError extends Error {
  code: ErrorCode       // one of the codes below
  message: string
  retryable: boolean    // precomputed via isRetryable(code)
  commandId?: string
  deviceId?: string
}

Only five codes are retryable: E101 ConnectionTimeout, E102 ConnectionLost, E500 TimeoutCommand, E501 TimeoutResponse, E502 TimeoutConnection. Everything else is non-transient and needs an action — on the request payload, on the printer, or from a service technician.

E1xx Connection errors

Failures while opening or sustaining a transport-layer connection between the E-BON mobile app and the AMEF printer. Always check the configured transport endpoint (TCP host and port, Bluetooth pairing, USB cable, serial port) before anything else.

E100 ConnectionRefused

  • Retryable: no.
  • Meaning: the printer actively refused the TCP / Bluetooth / USB / serial connection.
  • Recovery: check the transport — verify the TCP host and port, the Bluetooth pairing, the USB cable, or the serial port — and confirm the printer is powered on and not already in use by another controller.

E101 ConnectionTimeout

  • Retryable: yes.
  • Meaning: the connection attempt timed out before the printer answered.
  • Recovery: retry with exponential backoff (the SDK's retryable flag is already true for this code); if it keeps failing, check the transport — TCP host reachability, Bluetooth pairing, USB cable, serial port — for a dropped link.

E102 ConnectionLost

  • Retryable: yes.
  • Meaning: an established connection dropped mid-operation.
  • Recovery: retry with the same Idempotency-Key; if the loss repeats, check the transport (TCP, Bluetooth, USB, serial port) for an unstable physical link.

E103 ConnectionNotFound

  • Retryable: no.
  • Meaning: the configured transport endpoint cannot be reached at all.
  • Recovery: check the transport configuration on the device — TCP host and port, Bluetooth pairing, USB device path, serial port name — and confirm the printer is on the same network or bus.

E2xx Protocol errors

Failures at the protocol layer — the printer answered, but the bytes do not line up with the configured protocol. Check that the protocol selected for the AMEF in the Portal matches the printer's firmware and model.

E200 ProtocolMismatch

  • Retryable: no.
  • Meaning: the printer answered, but its protocol does not match the one configured for the device.
  • Recovery: check the protocol/firmware match between the configured AMEF and the actual printer; reconfigure the device with the correct protocol in the Portal.

E201 ProtocolUnsupported

  • Retryable: no.
  • Meaning: the command is not supported by this printer's protocol.
  • Recovery: check the protocol/firmware match between the configured AMEF and the printer; pick a command variant the printer's protocol supports, or update the printer's firmware.

E202 ProtocolChecksumError

  • Retryable: no.
  • Meaning: a frame was received but failed checksum validation.
  • Recovery: check the protocol/firmware match between the configured AMEF and the printer; ensure no other software is talking to the printer on the same transport at the same time.

E203 ProtocolFramingError

  • Retryable: no.
  • Meaning: a malformed frame arrived at the protocol layer.
  • Recovery: check the protocol/firmware match between the configured AMEF and the printer; verify the cabling or radio link is stable enough to deliver complete frames.

E3xx Fiscal errors

Failures coming from the fiscal logic of the printer — paper, daily limit, open receipts, fiscal memory. These need an operator on-site or a service intervention, not another network round-trip.

E300 FiscalMemoryFull

  • Retryable: no.
  • Meaning: the printer's fiscal memory (Memorie Fiscală) is full.
  • Recovery: contact service for fiscal-memory full — this requires authorised service intervention; the device cannot keep printing fiscal receipts until it is replaced.

E301 FiscalReceiptOpen

  • Retryable: no.
  • Meaning: a receipt is already open on the printer.
  • Recovery: ask the operator to close the open receipt (finalise or void it) on the printer, then resubmit the command.

E302 FiscalNoReceiptOpen

  • Retryable: no.
  • Meaning: the command requires an open receipt but none is open.
  • Recovery: open a receipt first (the SDK / API exposes the dedicated command), then run the line-level command again.

E303 FiscalDailyLimitReached

  • Retryable: no.
  • Meaning: the printer reached its per-day fiscal limit and cannot print more receipts today.
  • Recovery: ask the operator to run a Z report (close-of-day) on the printer, then retry — the daily counters reset.

E304 FiscalHardwareError

  • Retryable: no.
  • Meaning: a printer hardware error — paper, head, cover, drawer or similar.
  • Recovery: ask the operator to replace the paper, close the cover, or clear the jam; if the fault persists, contact service.

E4xx Validation errors

The driver rejected the command's payload before sending anything to the printer. These are deterministic — the same payload will fail the same way until you change it.

E400 ValidationInvalidPayload

  • Retryable: no.
  • Meaning: the command payload was rejected by the driver.
  • Recovery: fix the request payload — re-read the command's reference and adjust the body, then resubmit.

E401 ValidationMissingField

  • Retryable: no.
  • Meaning: a required payload field is missing.
  • Recovery: fix the request payload by adding the required field; check the command reference for the full required-field list.

E402 ValidationInvalidAmount

  • Retryable: no.
  • Meaning: an amount field is outside the legal range.
  • Recovery: fix the request payload — clamp amounts to the documented range and re-check the units (lei, bani, integer vs. decimal).

E403 ValidationInvalidVatRate

  • Retryable: no.
  • Meaning: the VAT rate is not one of the four legal Romanian rates (0, 9, 11, 21).
  • Recovery: fix the request payload — pick one of 0, 9, 11, 21; reject any other value at your POS layer before submitting.

E5xx Timeout errors

The command was dispatched but did not complete within the expected window. All E5xx codes are retryable.

E500 TimeoutCommand

  • Retryable: yes.
  • Meaning: the command did not complete within the per-command timeout.
  • Recovery: retry with exponential backoff; respect the same Idempotency-Key so a successful first attempt cached on the server is not re-executed.

E501 TimeoutResponse

  • Retryable: yes.
  • Meaning: the printer started the command but did not return a response in time.
  • Recovery: retry with exponential backoff; respect the same Idempotency-Key and consider checking the printed paper to confirm whether the command actually executed.

E502 TimeoutConnection

  • Retryable: yes.
  • Meaning: a connection-level timeout occurred while talking to the printer.
  • Recovery: retry with exponential backoff; respect the same Idempotency-Key and, if the timeout repeats, also verify the transport (TCP, Bluetooth, USB, serial).

E9xx Unknown errors

Unclassified or unexpected driver-layer failures. They escape the categories above and need investigation.

E900 Unknown

  • Retryable: no.
  • Meaning: an unclassified driver error.
  • Recovery: file a support request with the request ID and the failed command ID so the e-bon team can investigate.

E901 InternalError

  • Retryable: no.
  • Meaning: an unexpected internal error in the driver layer.
  • Recovery: file a support request with the request ID and the failed command ID; capture the device model and firmware version if you have them.

E902 UnexpectedResponse

  • Retryable: no.
  • Meaning: the printer returned a response the driver could not interpret.
  • Recovery: file a support request with the request ID and the failed command ID; include the AMEF model so the driver can be taught the new response shape.

Universal SDK pattern

Every catch site in user code can use the same shape: catch both error families, retry only when err.retryable is true (for fiscal failures) or Retry-After is set (for HTTP rate limits), reuse the same Idempotency-Key on the retry.

import { EBonApiError, FiscalError, isRetryable } from '@e-bon/sdk';

try {
  await client.commands.send(body);
} catch (err) {
  if (err instanceof FiscalError && err.retryable) {
    // E101, E102, E500, E501, E502 — retry with the same Idempotency-Key.
    return await client.commands.send(body);
  }

  if (err instanceof EBonApiError && err.status === 429 && err.retryAfter) {
    await sleep(err.retryAfter * 1000);
    return await client.commands.send(body);
  }

  throw err;
}
isRetryable(code) is re-exported from @e-bon/sdk if you receive a code through an out-of-band channel (a webhook, a database row) and need the same answer without constructing a FiscalError.

See also

  • API overview — the at-a-glance summary tables for both error families plus rate limits, idempotency and tier enforcement.
  • SDK errors — the FiscalError and EBonApiError class shapes, re-exports, and the recommended retry pattern in full.
  • Troubleshooting — symptom-driven diagnostics for the most common integration problems.