Evenimente webhook
Evenimente webhook
Webhook-urile permit POS-ului, ERP-ului sau back-office-ului tău să reacționeze în timp real la evenimentele fiscale din e-bon: a fost emis un bon fiscal, un dispozitiv s-a deconectat, s-a generat un raport Z. e-bon livrează fiecare eveniment printr-un POST HTTPS semnat către un URL controlat de tine.
Abonează-te la evenimente webhook
Configurează endpoint-urile din Portal:
Deschide pagina de webhook-uri
În Portal, accesează Setări → Webhook-uri și apasă Adaugă endpoint.
Introdu URL-ul și alege evenimentele
Lipește URL-ul HTTPS care va primi livrările, apoi bifează evenimentele la care vrei să te abonezi (de exemplu receipt.created, device.offline, report.generated).
Salvează și copiază secretul de semnare
După salvare, e-bon afișează secretul de semnare (whsec_…) o singură dată. Copiază-l în secret manager-ul tău — îți va trebui pentru a verifica semnăturile.
Trimite o livrare de test
Apasă Trimite test pentru a declanșa un eveniment webhook.test și a confirma că endpoint-ul tău întoarce 2xx.
Poți administra webhook-urile și programatic prin API-ul Webhooks.
Inspectează plicul evenimentului
Fiecare livrare este un singur obiect JSON cu aceeași formă exterioară:
{
"id": "8f3a9d3e-1b8c-4f02-9b2e-1234567890ab",
"type": "receipt.created",
"createdAt": "2026-04-23T08:09:55.123Z",
"orgId": "acme_corp",
"data": { /* specific evenimentului, vezi mai jos */ }
}
X-EBon-Delivery-Id. Folosește-l pentru a deduplica reîncercările.receipt.created. Vezi referința de evenimente.type — vezi fiecare eveniment mai jos.Citește antetele HTTP
Fiecare livrare este un POST application/json cu următoarele antete:
| Antet | Semnificație | Exemplu |
|---|---|---|
Content-Type | Întotdeauna application/json. | application/json |
X-EBon-Signature | HMAC SHA-256 al corpului brut al cererii, prefixat cu sha256=. | sha256=4c8f…3a9d |
X-EBon-Event | Tipul (type) evenimentului (oglindă a corpului). | receipt.created |
X-EBon-Delivery-Id | ID-ul evenimentului (oglindă a corpului). Folosește-l drept cheie de idempotență. | 8f3a9d3e-1b8c-4f02-9b2e-1234567890ab |
X-EBon-Timestamp | Marcaj de timp ISO 8601 al acestei încercări de livrare. | 2026-04-23T08:09:55.300Z |
e-bon așteaptă până la 10 secunde un răspuns de la endpoint-ul tău și tratează orice status 2xx ca succes. Orice altceva este o eroare și se programează pentru reîncercare.
Verifică semnătura webhook-ului
Semnătura este HMAC SHA-256 al corpului brut al cererii, codificată hex și prefixată cu sha256=. Verifică-o întotdeauna pe baza octeților bruți primiți — nu re-serializa niciodată JSON-ul parsat, fiindcă spațiile albe și ordinea cheilor ar strica comparația.
import { createHmac, timingSafeEqual } from 'node:crypto';
import express from 'express';
const app = express();
// Capturează corpul brut pentru verificarea semnăturii.
app.use(express.json({
verify: (req, _res, buf) => {
(req as unknown as { rawBody: Buffer }).rawBody = buf;
},
}));
app.post('/webhooks/e-bon', (req, res) => {
const rawBody = (req as unknown as { rawBody: Buffer }).rawBody;
const sent = req.header('X-EBon-Signature') ?? '';
const expected = `sha256=${createHmac('sha256', process.env.EBON_WEBHOOK_SECRET!)
.update(rawBody)
.digest('hex')}`;
const a = Buffer.from(sent);
const b = Buffer.from(expected);
if (a.length !== b.length || !timingSafeEqual(a, b)) {
res.status(401).end();
return;
}
// Semnătura OK — req.body este plicul.
res.status(202).end();
});
<?php
$rawBody = file_get_contents('php://input');
$sent = $_SERVER['HTTP_X_EBON_SIGNATURE'] ?? '';
$secret = getenv('EBON_WEBHOOK_SECRET');
$expected = 'sha256=' . hash_hmac('sha256', $rawBody, $secret);
if (!hash_equals($expected, $sent)) {
http_response_code(401);
exit;
}
// Semnătura OK — decodifică și procesează.
$event = json_decode($rawBody, true);
http_response_code(202);
printf '%s' "$(cat ./body.json)" \
| openssl dgst -sha256 -hmac "$EBON_WEBHOOK_SECRET" \
| awk '{ print "sha256=" $2 }'
crypto.timingSafeEqual în Node, hash_equals în PHP sau hmac.compare_digest în Python. Compararea cu === sau == lasă să scape informații de timing care permit unui atacator să ghicească semnătura octet cu octet.Reacționează la evenimente
Forma plicului se repetă pentru fiecare eveniment. Secțiunile de mai jos descriu ce declanșează fiecare eveniment, payload-ul data și ce să faci cu el.
receipt.created
Se declanșează după ce un bon fiscal este persistat în e-bon (emis din API, din aplicația de casă sau dintr-un POS conectat).
Când îl folosești: sincronizează înregistrarea fiscală în ERP, trimite bonul către un canal vizibil clientului (email, aplicație) sau actualizează dashboard-urile de raportare.
data: {
id: string;
orgId: string;
deviceId: string;
total: number;
currency: string;
items: Array<{ name: string; price: number; quantity: number; vatRate: number }>;
payments: Array<{ method: string; amount: number }>;
operatorId: string;
fiscalId?: string;
fiscalDate?: string;
customerCif?: string;
qrCode?: string;
source: 'api' | 'pos' | 'app';
createdAt: string;
}
{
"id": "8f3a9d3e-1b8c-4f02-9b2e-1234567890ab",
"type": "receipt.created",
"createdAt": "2026-04-23T08:09:55.123Z",
"orgId": "acme_corp",
"data": {
"id": "rcp_abc123",
"orgId": "acme_corp",
"deviceId": "dev_xyz",
"total": 4250,
"currency": "RON",
"items": [{ "name": "Espresso", "price": 850, "quantity": 5, "vatRate": 19 }],
"payments": [{ "method": "card", "amount": 4250 }],
"operatorId": "op_42",
"fiscalId": "FISC-2026-000123",
"source": "api",
"createdAt": "2026-04-23T08:09:55.000Z"
}
}
Vezi API-ul Bonuri pentru schema completă.
command.completed
Se declanșează când o comandă fiscală se finalizează cu succes pe AMEF — de exemplu un bon a fost tipărit sau s-a generat un raport X/Z.
Când îl folosești: marchează comanda originară ca fiscalizată, atașează fiscalId-ul întors la înregistrările tale sau pornește fluxuri downstream.
data: {
id: string; // id-ul comenzii
deviceId: string;
type: CommandType; // 'print_receipt' | 'x_report' | 'z_report' | …
result: CommandResult;
orgId: string;
}
{
"id": "evt_…",
"type": "command.completed",
"createdAt": "2026-04-23T08:10:01.000Z",
"orgId": "acme_corp",
"data": {
"id": "cmd_abc123",
"deviceId": "dev_xyz",
"type": "print_receipt",
"result": { "receiptId": "rcp_abc123", "fiscalId": "FISC-2026-000123" },
"orgId": "acme_corp"
}
}
CommandType și CommandResult sunt documentate pe API-ul Commands.
command.failed
Se declanșează când o comandă fiscală este respinsă de AMEF sau de handlerul dispozitivului — fără hârtie, imprimantă deconectată, eroare de validare etc.
Când îl folosești: alertează echipa de operațiuni, reîncearcă comanda după ce remediezi cauza (retryable: true) sau afișează eroarea către operator.
data: {
id: string; // id-ul comenzii
deviceId: string;
type: CommandType;
error: string; // mesaj lizibil
errorCode: ErrorCode;
retryable: boolean;
orgId: string;
}
{
"id": "evt_…",
"type": "command.failed",
"createdAt": "2026-04-23T08:10:02.000Z",
"orgId": "acme_corp",
"data": {
"id": "cmd_abc123",
"deviceId": "dev_xyz",
"type": "print_receipt",
"error": "Paper jam",
"errorCode": "DEVICE_ERROR",
"retryable": true,
"orgId": "acme_corp"
}
}
Lista completă de valori errorCode este pe prezentarea API-ului.
command.timeout
Se declanșează când o comandă din coadă nu primește răspuns de la dispozitivul ei în fereastra configurată.
Când îl folosești: tratează-l ca o eroare temporară — dispozitivul ar putea fi deconectat. Verifică evenimentele device.online / device.offline înainte să reîncerci.
data: {
id: string;
deviceId: string;
type: CommandType;
error: string;
errorCode: ErrorCode;
retryable: boolean;
}
{
"id": "evt_…",
"type": "command.timeout",
"createdAt": "2026-04-23T08:15:00.000Z",
"orgId": "acme_corp",
"data": {
"id": "cmd_abc123",
"deviceId": "dev_xyz",
"type": "print_receipt",
"error": "Command timed out waiting for device reply",
"errorCode": "COMMAND_TIMEOUT",
"retryable": true
}
}
device.online
Se declanșează când un dispozitiv fiscal se reconectează.
Când îl folosești: golește comenzile din coadă, șterge avertismentele „dispozitiv deconectat” din dashboard sau notifică operatorul.
{
"id": "evt_…",
"type": "device.online",
"createdAt": "2026-04-23T08:00:00.000Z",
"orgId": "acme_corp",
"data": { "id": "dev_xyz", "status": "online", "orgId": "acme_corp" }
}
device.offline
Se declanșează când un dispozitiv se deconectează.
Când îl folosești: alertează operatorul, suspendă trimiterea automată de comenzi sau bascuează pe un dispozitiv secundar.
{
"id": "evt_…",
"type": "device.offline",
"createdAt": "2026-04-23T08:30:00.000Z",
"orgId": "acme_corp",
"data": { "id": "dev_xyz", "status": "offline", "orgId": "acme_corp" }
}
report.generated
Se declanșează în paralel cu command.completed ori de câte ori comanda finalizată a fost un raport X sau Z.
Când îl folosești: arhivează raportul, trimite totalurile de final de zi în sistemul de contabilitate sau pornește un job zilnic de reconciliere.
data: {
commandId: string; // atenție: commandId, nu id
deviceId: string;
type: CommandType; // 'x_report' | 'z_report'
result: CommandResult;
orgId: string;
}
{
"id": "evt_…",
"type": "report.generated",
"createdAt": "2026-04-23T22:00:00.000Z",
"orgId": "acme_corp",
"data": {
"commandId": "cmd_abc123",
"deviceId": "dev_xyz",
"type": "z_report",
"result": { "reportId": "rpt_…", "totals": { "gross": 125000, "net": 105042 } },
"orgId": "acme_corp"
}
}
Vezi API-ul Rapoarte pentru înregistrarea de raport completă.
webhook.test
Trimis doar atunci când apeși Trimite test în Portal sau apelezi endpoint-ul de livrare de test. Payload-ul este fix:
{
"id": "evt_…",
"type": "webhook.test",
"createdAt": "2026-04-23T08:10:00.000Z",
"orgId": "acme_corp",
"data": { "test": true, "message": "This is a test event from e-bon." }
}
Folosește-l pentru a confirma că endpoint-ul tău este accesibil și că verificarea semnăturii funcționează cap-coadă.
Gestionează reîncercările
Când endpoint-ul tău întoarce un status non-2xx, depășește timeout-ul sau este inaccesibil, e-bon reîncearcă livrarea cu backoff exponențial.
| Încercare | Întârziere până la reîncercare |
|---|---|
| 1 → 2 | 1 minut |
| 2 → 3 | 5 minute |
| 3 → 4 | 30 de minute |
| 4 → 5 | 2 ore |
| 5 → 6 | 12 ore |
După a 5-a încercare eșuată, livrarea este marcată failed și nu mai este reîncercată.
Poți parcurge istoricul livrărilor (status, cod HTTP, extras din răspuns, număr de încercări) din Setări → Webhook-uri → Livrări sau prin GET /api/v1/org/webhooks/{id}/deliveries. Primele 500 de caractere ale răspunsului tău sunt stocate pe fiecare înregistrare de livrare.
Aplică bunele practici
- Răspunde rapid. Confirmă cererea cu un status
2xximediat ce ai validat semnătura; pune munca grea (scrieri în DB, apeluri către API-uri downstream) pe un worker de fundal. Orice depășește timeout-ul de 10 secunde este tratat ca eșec. - Fii idempotent. Folosește
X-EBon-Delivery-Iddrept cheie de deduplicare — o livrare reîncercată are acelașiid. Un simpluINSERT … ON CONFLICT DO NOTHINGîți păstrează handlerul în siguranță. - Verifică pe corpul brut. Nu recalcula niciodată HMAC-ul peste un JSON re-serializat; spațiile albe și ordinea cheilor contează. Folosește o comparație în timp constant.
- Stochează secretul într-un secret manager. Valoarea brută
whsec_…este afișată doar la creare și la rotație. Ține-o în variabile de mediu sau într-un vault, niciodată în repo. - Rotește când ai dubii. Folosește Setări → Webhook-uri → Rotește secretul (sau
POST /api/v1/org/webhooks/{id}/rotate-secret) ca să generezi un secret nou dacă există suspiciunea că cel actual a fost compromis.
Continuă explorarea
- API-ul Webhooks — creează, actualizează, rotește secretele, trimite livrări de test, parcurge istoricul livrărilor.
- Prezentare API › plicul de eroare — valorile
errorCodecare apar încommand.failedșicommand.timeout. - SDK › evenimente — alternativa server-sent events când ai nevoie de un canal push fără un endpoint HTTPS public.
Chei API
Endpoint-uri REST pentru a lista, crea, actualiza și revoca cheile API ale organizației. Secretul brut este returnat o singură dată la creare.
Colecție Postman
Importă colecția oficială Postman pentru e-bon — peste 77 de cereri preconfigurate, organizate în 12 dosare, care acoperă fiecare endpoint REST public al API-ului e-bon.