Request tracing & logging
Request tracing & logging
This page is for everyone who calls the e-bon HTTP API — POS partners, accounting integrations, third-party dashboards. It explains the one correlation header you need (X-Request-Id), what every error response looks like, and how to file a bug report we can act on quickly.
X-Request-Id header. If you send one, we echo it back. If you don't, we mint a UUID v4. Quote that ID in every bug report and we'll find your call in our logs in seconds.Send a request with a trace ID
The X-Request-Id header travels with every request and every response.
| Direction | Behaviour |
|---|---|
Request X-Request-Id (case-insensitive) | Optional. If you send a non-empty value, we use it verbatim — no validation, no length cap, no character filtering. |
Response X-Request-Id | Always present. We echo your value back if you sent one, otherwise we return the UUID v4 we generated for you. |
A few things to fix in your mental model up front:
- The header is always echoed. There is no opt-out — even health-check responses carry it.
- Headers are case-insensitive on the wire; treat the response header as case-insensitive when reading it.
- We do not validate the inbound value. Pick something sensible: stick to ASCII, keep it under ~256 chars, and prefer UUIDs or your own correlation IDs (e.g. a database PK from your queue table). Do not put PII in the header — it ends up in our logs.
- If you omit the header, we mint a fresh UUID v4. Collisions are not a concern.
X-Request-Id, which is how support traces your call back to the exact line in our system.Understand why this header matters
Three concrete reasons to care:
- Bug reports. When you tell us "we got a 500 from
POST /api/v1/receiptslast Tuesday around 14:30 UTC", we can find that request — in principle — but it takes minutes per call. When you tell us theX-Request-Id, we find it in seconds. See the bug-report checklist below. - Correlating client and server logs. Log the
X-Request-Idfrom the response on every non-2xx outcome (and ideally on every call, sampled). When a partner dashboard emits a Sentry event with the request ID attached, your engineers and ours are looking at the same line. - Tracing retries. If you re-use the same
X-Request-Idon a retry, every attempt shows up in our logs against the same correlation key. This is observability only — re-using the ID does not trigger any deduplication. If you need safe retries with replay, that is the job ofIdempotency-Key, a separate header (see the comparison table and the Idempotency reference).
Read the error envelope
Every non-rate-limit error response uses the same JSON shape:
{
"error": {
"code": "STRING_CODE",
"message": "Human-readable explanation",
"details": "optional, type varies by code"
}
}
Three branches produce that envelope:
| Branch | Trigger | Status | Body |
|---|---|---|---|
| Validation | Your request body or query failed schema validation | 400 | code: VALIDATION_ERROR, message: 'Request validation failed', details: [{path, message}, …] |
| Domain error | A typed application error was raised (e.g. not-found, conflict, forbidden) | mapped per code | code: err.code, message: err.message, details if set |
| Unhandled | Anything else (uncaught exception) | 500 | code: INTERNAL_ERROR, message: 'An internal error occurred' |
The redaction on the unhandled branch matters: in production the message is always 'An internal error occurred'. The original exception message stays in our server logs, keyed by your X-Request-Id. That is exactly the scenario where you need the header in your bug report.
RATE_LIMIT_EXCEEDED body is a flat {code, message, status} object at the top level, not wrapped in {error: {…}}. See Rate limits. Match on the top-level code for 429s.Skip health-check noise in your logs
The four health endpoints — /health, /ready, /healthz, /livez — do not appear in our access log. They still execute, they still return X-Request-Id, they just don't produce a log line (otherwise Kubernetes liveness/readiness probes would drown the log). See Health & meta for their contracts.
Copy a client recipe
Three short reference clients showing the header round-trip and the bug-report capture pattern. None are production-ready (no retries, no metrics) — they exist to get you started.
import { randomUUID } from 'node:crypto';
async function callWithTrace(url, options = {}) {
const requestId = randomUUID();
const headers = { ...(options.headers ?? {}), 'X-Request-Id': requestId };
const res = await fetch(url, { ...options, headers });
const echoed = res.headers.get('x-request-id');
if (!res.ok) {
const body = await res.json().catch(() => null);
console.error('[ebon] request failed', {
method: options.method ?? 'GET',
url,
status: res.status,
sentRequestId: requestId,
receivedRequestId: echoed,
errorCode: body?.error?.code ?? body?.code,
});
}
return res;
}
import logging, uuid, requests
log = logging.getLogger('ebon')
def call_with_trace(method, url, **kwargs):
request_id = str(uuid.uuid4())
headers = {**kwargs.pop('headers', {}), 'X-Request-Id': request_id}
r = requests.request(method, url, headers=headers, **kwargs)
if r.status_code >= 400:
body = r.json() if r.headers.get('content-type', '').startswith('application/json') else None
log.error(
'ebon call failed method=%s url=%s status=%s sent_id=%s recv_id=%s code=%s',
method, url, r.status_code, request_id,
r.headers.get('X-Request-Id'),
(body or {}).get('error', {}).get('code') or (body or {}).get('code'),
)
return r
curl -sS -D - -o /tmp/body.json -w '\nHTTP %{http_code}\n' \
-H "Authorization: Bearer $EBON_KEY" \
-H "X-Request-Id: $(uuidgen)" \
-X POST -d @body.json \
https://api.e-bon.ro/api/v1/receipts \
| grep -i '^x-request-id:'
# Then, on a failure:
cat /tmp/body.json | jq '.error // .'
File a useful bug report
When something is broken in production, send us this checklist. Half the items are obvious; the request ID is the one that turns a 30-minute investigation into a 30-second one.
HTTP method and URL
POST /api/v1/receipts, GET /api/v1/devices/dev_abc/commands?status=pending. Include the query string; exclude any secrets in the path.
Status code and response body
The exact error.code, error.message and (if present) error.details from the JSON envelope. For 429s, the flat {code, message, status} body. Quote it verbatim — do not paraphrase.
The X-Request-Id from the response (required)
The single most useful field. Without it we are searching by timestamp + method + status across every replica's logs. With it, one grep.
Approximate timestamp in UTC
To the minute is enough. Helps when the request ID is missing or when you are reporting a burst of failures.
Redacted request body
The shape, not the secrets. Strip Authorization, x-api-key, JWT bearers, customer PII. We do not capture request bodies in our access log, so we cannot reconstruct yours.
Your client's view
What you sent (method/URL/headers, redacted), what you expected, what you got. If you are retrying with the same Idempotency-Key, say so — Idempotency-Key cache replays look identical to the first call on the wire.
For broader debugging context, the Troubleshooting guide covers integration-level failure modes (auth issues, webhook delivery, device pairing) that often manifest as confusing API errors.
Idempotency-Key vs X-Request-Id
The two headers are independent and complementary. Using both on a retry is correct.
| Aspect | Idempotency-Key | X-Request-Id |
|---|---|---|
| Purpose | Safe-retry semantics — the second call returns the cached response of the first | Observability — correlate client logs with server logs |
| Server behaviour | First request runs; subsequent identical-key requests replay the cached body | Every request runs normally; the value is logged and echoed |
| Scope | POST /api/v1/commands and POST /api/v1/receipts only | Every HTTP request, every endpoint |
| Default if omitted | No caching; the request runs as a fresh POST | Server mints a UUID v4 |
| Retention | 24h per {orgId}_{key} (see Idempotency) | Never persisted by the API — lives only in the request log line |
| Validation | Regex ^[a-zA-Z0-9_-]+$, 1–128 chars; 400 VALIDATION_ERROR on mismatch | None — accepted verbatim |
| Deduplicates calls? | Yes — re-running with the same key returns the cached result | No — for tracing only |
| When to re-use | On retry of a non-idempotent POST you want to be safe to retry | On retry of any call when you want the retry traceable to the original ID |
The combined recipe for a retry of a fiscal receipt POST: keep the same Idempotency-Key (so the second call replays), keep the same X-Request-Id (so both attempts thread together in our logs), and let the API do the rest.
Continue exploring
- API overview — the front door to the API surface.
- API errors — the full
codecatalogue and their HTTP status mappings. - Idempotency — the safe-retry contract; pairs with
X-Request-Idon every retry. - Rate limits — the 429 envelope is FLAT, not the wrapper documented above.
- Health & meta — the four silent paths (
/health,/ready,/healthz,/livez). - Troubleshooting — common integration failure modes.
Rate limits
How the e-bon API throttles requests, what the RateLimit headers mean, what a 429 response looks like, and how to back off correctly from your POS integration.
Receipts
REST endpoints for storing, listing and retrieving fiscal receipts after they have been printed on the AMEF — request and response schemas, curl examples and per-endpoint error codes.