Idempotență
Idempotență
Când un terminal POS trimite o comandă fiscală și rețeaua cade în mijlocul cererii, lucrul corect de făcut este o reluare. Fără protecție, acea reluare tipărește un al doilea bon. Antetul Idempotency-Key garantează că reluările ajung cel mult o dată pe dispozitiv și că orice încercare repetată primește același răspuns ca prima.
Prima cerere cu o cheie dată se execută și răspunsul este memorat. Orice cerere ulterioară cu aceeași cheie, din aceeași organizație, în următoarele 24 de ore, primește înapoi răspunsul original — comanda nu mai rulează a doua oară.
Trimite antetul
Adaugă Idempotency-Key la orice POST care îl suportă:
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": { "...": "..." } }
Valoarea antetului trebuie să respecte aceste reguli:
| Proprietate | Valoare |
|---|---|
| Lungime | între 1 și 128 de caractere |
| Caractere acceptate | ^[a-zA-Z0-9_-]+$ — doar litere, cifre, sublinieri și liniuțe |
| Numele antetului | Idempotency-Key (case-insensitive, conform HTTP) |
| Opțional | Da. Omite antetul pentru a dezactiva memorarea pentru acea cerere. |
Dacă antetul este prezent dar nu respectă formatul, API-ul răspunde cu 400 VALIDATION_ERROR și cererea nu mai ajunge să se execute. Vezi VALIDATION_ERROR pentru forma răspunsului.
Endpoint-uri suportate
| Metodă | Cale | Descriere |
|---|---|---|
POST | /api/v1/commands | Pune o comandă fiscală în coadă pentru un AMEF. Vezi Comenzi. |
POST | /api/v1/receipts | Stochează un bon tipărit. Vezi Bonuri. |
Trimiterea antetului pe alte endpoint-uri este inofensivă — API-ul îl ignoră.
Cum funcționează reluarea
| Situație | Comportament |
|---|---|
| Antetul lipsește sau este șir gol | Cererea rulează normal, iar răspunsul nu este memorat. |
| Antetul prezent, fără răspuns memorat | Cererea rulează, iar răspunsul este memorat 24 de ore. |
| Antetul prezent, răspuns memorat în ultimele 24 de ore | Se returnează imediat status și body din cache. Comanda nu rulează din nou. |
| Antetul prezent, răspunsul memorat este mai vechi de 24h | Intrarea expirată este eliminată și cererea rulează din nou, reîmprospătând cache-ul. |
După 24 de ore, aceeași cheie devine din nou disponibilă. Este util dacă vrei să refolosești o cheie deterministă între zile, dar reține: după ce fereastra se închide, API-ul nu mai știe dacă operațiunea s-a întâmplat deja.
La ce trebuie să fii atent
Idempotency-Key. Dacă trimiți aceeași cheie cu un payload diferit, primești înapoi răspunsul original, iar noul payload este ignorat în tăcere.De fiecare dată când payload-ul de business se schimbă, generează o cheie nouă. Regulă de aur: o cheie pentru o singură încercare logică a unei singure operațiuni logice. Dacă se schimbă produsele, dispozitivul, prețul sau clientul, este o operațiune diferită și are nevoie de o cheie diferită.400 VALIDATION_ERROR, orice reluare în următoarele 24 de ore va primi același 400 — chiar dacă între timp ai corectat payload-ul. Pentru a ieși dintr-o eroare memorată, trimite o cheie nouă.Generează chei bune
Alege șablonul care se potrivește strategiei tale de reluare:
- Câte un UUIDv4 per încercare. Generează un UUID nou chiar înainte să trimiți cererea și refolosește-l doar pentru reluările transparente ale aceleiași încercări (timeout-uri,
ECONNRESET, 5xx). Când utilizatorul apasă din nou Tipărește — o nouă încercare logică — generează un UUID nou. - Determinist per eveniment de business. Construiește cheia din propriile tale identificatoare, de exemplu
order_<orderId>_attempt_<n>saushift_<shiftId>_close. Această formă este ușor de căutat în logurile tale.
O combinație frecventă este order_<id>_attempt_<n>: incrementează <n> de fiecare dată când utilizatorul reia explicit (deci fiecare reluare explicită retipărește), în timp ce reluările transparente de rețea refolosesc același <n> (și astfel reiau răspunsul memorat).
Ce să nu faci:
- Nu refolosi o cheie între operațiuni distincte.
order_12345refolosit atât pentru o comandăprint_receiptcât și pentru o comandă ulterioarăcancel_receiptva face ca a doua să returneze răspunsul primei. - Nu folosi doar un timestamp de ceas. Două cereri în aceeași milisecundă vor coliziona, iar pentru deduplicare între procese este inutil.
- Nu te baza pe idempotență pentru deduplicare între organizații. Cheile sunt scoped per organizație; aceeași cheie sub două organizații diferite produce două intrări de cache fără legătură.
Rețete de integrare
curl: prima apelare și o reluare
KEY="order_12345_attempt_1"
# Prima apelare — comanda rulează, răspunsul este memorat 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", ... } }
# Întrerupere de rețea → reluare sigură cu ACEEAȘI cheie.
# Comanda NU rulează din nou; corpul memorat se returnează identic.
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", ... } } — același corp ca mai sus
Node: helper de reluare cu 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()}`; // o cheie per încercare logică
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, // ACEEAȘI cheie pe fiecare reluare transparentă
},
body: JSON.stringify(body),
});
if (res.ok) return await res.json();
// Erorile memorate se vor relua 24h — reia doar la eșecuri de transport tranzitorii.
if (res.status >= 500) throw new Error(`tranzitoriu ${res.status}`);
return await res.json(); // 4xx — propagă apelantului, nu relua cu aceeași cheie
} catch (err) {
lastErr = err;
await new Promise((r) => setTimeout(r, 250 * 2 ** i));
}
}
throw lastErr;
}
Când utilizatorul reia explicit (apasă din nou Tipărește după ce a văzut un eșec), apelează din nou sendCommandWithRetry() — randomUUID() produce o cheie nouă pentru noua încercare logică, deci dispozitivul este rugat să tipărească din nou și nu se reia răspunsul memorat anterior.
Dacă preferi chei deterministe, înlocuiește randomUUID() cu ceva de forma `order_${orderId}_attempt_${attemptNumber}` și incrementează attemptNumber doar la reluările pornite de utilizator.
Vezi de asemenea
- Comenzi — endpoint-ul
POST /api/v1/commandscare respectăIdempotency-Key. - Bonuri — endpoint-ul
POST /api/v1/receiptscare respectăIdempotency-Key. VALIDATION_ERROR— răspunsul când antetul are un format invalid.- Prezentare generală API — URL de bază, plicul de eroare, limite de rată și idempotență pe scurt.
Endpoint-uri de stare, identitate și meta
Referință pentru sondele publice de stare, endpoint-ul autentificat de introspecție a identității, fișierul robots și suprafața OpenAPI (Swagger UI + spec brut) expuse de API-ul e-bon.
WebSocket de evenimente
Referință de protocol pentru WebSocket-ul de evenimente în timp real al e-bon — URL de conectare, autentificare prin JWT și prin cheie API, tipurile de evenimente, filtrarea după permisiuni, forma cadrului, semnalul de viață, codurile de închidere și strategia de reluare a conexiunii.