e-bon
e-bon.ro
API reference

Idempotency

Use the Idempotency-Key header to make POS retries safe — replay cached responses for 24 hours and avoid double-printed receipts.

Idempotency

When a POS terminal sends a fiscal command and the network drops mid-request, the safe thing to do is retry. Without protection, that retry prints a second receipt. The Idempotency-Key header guarantees retries land at most once on the device, and that every repeat attempt sees the same response as the first.

The first request with a given key runs and the response is stored. Any later request with the same key from the same organization, within 24 hours, gets the original response back — the command does not run again.

Send the header

Add Idempotency-Key to any POST that supports it:

POST /api/v1/commands HTTP/1.1
Host: api.e-bon.ro
Authorization: Bearer ebon_live_<orgId>_<32-hex>
Content-Type: application/json
Idempotency-Key: order-12345-attempt-1

{ "deviceId": "dev_abc123", "type": "print_receipt", "payload": { "...": "..." } }

The header value must match these rules:

PropertyValue
Length1 to 128 characters
Character set^[a-zA-Z0-9_-]+$ — letters, digits, underscores and dashes only
Header nameIdempotency-Key (case-insensitive, per HTTP)
OptionalYes. Omit the header to disable caching for that request.

If the header is present but malformed, the API responds with 400 VALIDATION_ERROR and the request never runs. See VALIDATION_ERROR for the response shape.

Supported endpoints

MethodPathDescription
POST/api/v1/commandsQueue a fiscal command against an AMEF. See Commands.
POST/api/v1/receiptsStore a printed receipt. See Receipts.

Sending the header on other endpoints is harmless — the API ignores it.

How replay works

ScenarioBehavior
Header absent or emptyThe request runs normally and the response is not cached.
Header present, no cached responseThe request runs and its response is cached for 24 hours.
Header present, cached response within 24 hoursThe cached status and body are returned immediately. The command does not run again.
Header present, cached response older than 24 hoursThe expired entry is discarded and the request runs again, refreshing the cache.

After 24 hours the same key becomes available again. That's useful if you want to reuse a deterministic key across days, but remember: after the window closes, the API no longer remembers whether the operation already happened.

Things to watch out for

The request body is not checked. The cache is keyed on Idempotency-Key alone. If you send the same key with a different payload, you get the original response back and the new payload is silently discarded.Whenever the business payload changes, generate a new key. Rule of thumb: one key per logical attempt at one logical operation. If the items, device, price or customer change, that's a different operation and needs a different key.
Two parallel requests with the same key may both run. The header protects sequential retries (the network-blip case it's designed for). It does not serialize concurrent sends. If your client fires the same key twice in parallel, both can hit the device. Serialize retries on the client side.
Errors are cached too. If the first request returns 400 VALIDATION_ERROR, every retry within the next 24 hours gets that same 400 — even if you fix the payload. To recover from a cached error, send a new key.

Generate good keys

Pick whichever pattern fits your retry strategy:

  • One UUIDv4 per attempt. Generate a fresh UUID right before sending and reuse it only for transparent retries of that same attempt (timeouts, ECONNRESET, 5xx). When the user clicks Print again — a new logical attempt — generate a new UUID.
  • Deterministic per business event. Build the key from your own identifiers, e.g. order_<orderId>_attempt_<n> or shift_<shiftId>_close. This shape is easy to grep in your own logs.

A common combination is order_<id>_attempt_<n>: bump <n> every time the user explicitly retries (so each explicit retry re-prints), while transparent network retries reuse the same <n> (so they replay the cached response).

What not to do:

  • Don't reuse a key across distinct operations. order_12345 reused for both a print_receipt and a follow-up cancel_receipt will make the second one return the first one's response.
  • Don't use a wall-clock timestamp alone. Two requests in the same millisecond will collide and it's useless for cross-process deduplication.
  • Don't rely on idempotency to dedupe across organizations. Keys are scoped per organization; the same key under two different orgs gives two unrelated cache entries.

Integration recipes

curl: first call and a retry

KEY="order_12345_attempt_1"

# First call — the command runs, response is cached for 24h.
curl -X POST https://api.e-bon.ro/api/v1/commands \
  -H "Authorization: Bearer ebon_live_<orgId>_<32-hex>" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $KEY" \
  -d '{
    "deviceId": "dev_abc123",
    "type": "print_receipt",
    "payload": { "items": [ { "name": "Espresso", "unitPrice": 8.5, "quantity": 1, "vatRate": 9 } ] }
  }'
# → 202 Accepted, { "command": { "id": "cmd_001", "status": "pending", ... } }

# Network blip → safe retry with the SAME key.
# The command does NOT run again; the cached body is returned verbatim.
curl -X POST https://api.e-bon.ro/api/v1/commands \
  -H "Authorization: Bearer ebon_live_<orgId>_<32-hex>" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $KEY" \
  -d '{ "deviceId": "dev_abc123", "type": "print_receipt", "payload": { "...": "..." } }'
# → 202 Accepted, { "command": { "id": "cmd_001", "status": "pending", ... } } — same body as above

Node: retry helper with fetch

import { randomUUID } from 'node:crypto';

const API = 'https://api.e-bon.ro';
const TOKEN = process.env.EBON_API_KEY!; // ebon_live_<orgId>_<32-hex>

async function sendCommandWithRetry(body: unknown, maxAttempts = 3): Promise<unknown> {
  const key = `cmd_${randomUUID()}`; // one key per logical attempt
  let lastErr: unknown;

  for (let i = 0; i < maxAttempts; i++) {
    try {
      const res = await fetch(`${API}/api/v1/commands`, {
        method: 'POST',
        headers: {
          Authorization: `Bearer ${TOKEN}`,
          'Content-Type': 'application/json',
          'Idempotency-Key': key, // SAME key on every transparent retry
        },
        body: JSON.stringify(body),
      });

      if (res.ok) return await res.json();

      // Cached errors will replay for 24h — only retry on transient transport failures.
      if (res.status >= 500) throw new Error(`transient ${res.status}`);
      return await res.json(); // 4xx — surface to caller, do not retry with same key
    } catch (err) {
      lastErr = err;
      await new Promise((r) => setTimeout(r, 250 * 2 ** i));
    }
  }

  throw lastErr;
}

When the user explicitly retries (clicks Print again after seeing a failure), call sendCommandWithRetry() again — randomUUID() produces a fresh key for the new logical attempt, so the device is asked to print again instead of replaying the previous cached response.

If you prefer deterministic keys, swap randomUUID() for something like `order_${orderId}_attempt_${attemptNumber}` and bump attemptNumber only on user-driven retries.

See also

  • Commands — the POST /api/v1/commands endpoint that honors Idempotency-Key.
  • Receipts — the POST /api/v1/receipts endpoint that honors Idempotency-Key.
  • VALIDATION_ERROR — the response when the header is malformed.
  • API overview — base URL, error envelope, rate limits and idempotency at a glance.