e-bon
e-bon.ro
API reference

Webhook events

Receive real-time notifications about fiscal events — receipts, commands, devices, and reports — over signed HTTPS callbacks.

Webhook events

Webhooks let your POS, ERP, or back-office system react in real time to fiscal events on e-bon: a receipt was issued, a device went offline, a Z report was generated. e-bon delivers each event as a signed HTTPS POST to a URL you control.

Subscribe to webhook events

Configure endpoints from the Portal:

Open the webhooks page

In the Portal, go to Settings → Webhooks and click Add endpoint.

Enter the URL and pick events

Paste the HTTPS URL that should receive deliveries, then tick the events you want to subscribe to (for example receipt.created, device.offline, report.generated).

Save and copy the signing secret

After saving, e-bon shows the signing secret (whsec_…) once. Copy it to your secret manager — you'll need it to verify signatures.

Send a test delivery

Click Send test to fire a webhook.test event and confirm your endpoint returns 2xx.

You can also manage webhooks programmatically through the Webhooks API.

Inspect the event envelope

Every delivery is a single JSON object with the same outer shape:

{
  "id": "8f3a9d3e-1b8c-4f02-9b2e-1234567890ab",
  "type": "receipt.created",
  "createdAt": "2026-04-23T08:09:55.123Z",
  "orgId": "acme_corp",
  "data": { /* event-specific, see below */ }
}
id
string required
Event UUID. Stable per delivery — also sent in the X-EBon-Delivery-Id header. Use it to deduplicate retries.
type
string required
The event name, for example receipt.created. See event reference.
createdAt
string required
ISO 8601 timestamp captured when the event was dispatched.
orgId
string required
The organization the event belongs to.
data
object required
Event-specific payload. The shape depends on type — see each event below.

Read the HTTP headers

Every delivery is a POST application/json with these headers:

HeaderMeaningExample
Content-TypeAlways application/json.application/json
X-EBon-SignatureHMAC SHA-256 of the raw request body, prefixed with sha256=.sha256=4c8f…3a9d
X-EBon-EventThe event type (mirrors the body).receipt.created
X-EBon-Delivery-IdThe event id (mirrors the body). Use as your idempotency key.8f3a9d3e-1b8c-4f02-9b2e-1234567890ab
X-EBon-TimestampISO 8601 timestamp of this delivery attempt.2026-04-23T08:09:55.300Z

e-bon waits up to 10 seconds for your endpoint to respond and treats any 2xx status as success. Anything else is a failure and is scheduled for retry.

Verify webhook signatures

The signature is the HMAC SHA-256 of the raw request body, hex-encoded and prefixed with sha256=. Always verify it against the raw bytes you received — never re-serialize the parsed JSON, since whitespace and key order would break the comparison.

Express handler
import { createHmac, timingSafeEqual } from 'node:crypto';
import express from 'express';

const app = express();

// Capture the raw body for signature verification.
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;
  }

  // Signature OK — req.body is the envelope.
  res.status(202).end();
});
Always use a constant-time comparison such as Node's crypto.timingSafeEqual, PHP's hash_equals, or Python's hmac.compare_digest. Comparing with === or == leaks timing information that lets an attacker guess the signature byte by byte.

React to events

The full envelope shape repeats for every event. The sections below document what triggers each event, the data payload, and what to do with it.

receipt.created

Fires after a receipt is persisted on e-bon (issued from the API, the cashier app, or a connected POS).

When to use it: sync the fiscal record back to your ERP, push the receipt to a customer-facing channel (email, app), or update analytics dashboards.

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;
}
example
{
  "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"
  }
}

See the Receipts API for the full schema.

command.completed

Fires when a fiscal command finishes successfully on the AMEF — for example a receipt was printed, or an X/Z report was generated.

When to use it: mark the originating order as fiscalized, attach the returned fiscalId to your records, or trigger downstream workflows.

data: {
  id: string;          // command id
  deviceId: string;
  type: CommandType;   // 'print_receipt' | 'x_report' | 'z_report' | …
  result: CommandResult;
  orgId: string;
}
example
{
  "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 and CommandResult are documented on the Commands API.

command.failed

Fires when a fiscal command is rejected by the AMEF or the device handler — paper out, printer offline, validation error, and so on.

When to use it: alert your operations team, retry the command after fixing the underlying issue (retryable: true), or surface the error to the cashier.

data: {
  id: string;          // command id
  deviceId: string;
  type: CommandType;
  error: string;       // human-readable message
  errorCode: ErrorCode;
  retryable: boolean;
  orgId: string;
}
example
{
  "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"
  }
}

The full list of errorCode values is on the API overview.

command.timeout

Fires when a queued command does not get a reply from its device within the configured window.

When to use it: treat it as a soft failure — the device may have lost connectivity. Check device.online / device.offline events for context before retrying.

data: {
  id: string;
  deviceId: string;
  type: CommandType;
  error: string;
  errorCode: ErrorCode;
  retryable: boolean;
}
example
{
  "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

Fires when a fiscal device reconnects.

When to use it: flush queued commands, clear "device offline" warnings on your dashboard, or notify the operator.

example
{
  "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

Fires when a device disconnects.

When to use it: alert the operator, pause automatic command dispatch, or fall back to a secondary device.

example
{
  "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

Fires alongside command.completed whenever the completed command was an X or Z report.

When to use it: archive the report, post end-of-day totals to your accounting system, or trigger a daily reconciliation job.

data: {
  commandId: string;   // note: commandId, not id
  deviceId: string;
  type: CommandType;   // 'x_report' | 'z_report'
  result: CommandResult;
  orgId: string;
}
example
{
  "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"
  }
}

See the Reports API for the full report record.

webhook.test

Sent only when you click Send test in the Portal or call the test-delivery endpoint. The payload is fixed:

example
{
  "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." }
}

Use it to confirm your endpoint is reachable and your signature verification works end-to-end.

Handle retries

When your endpoint returns a non-2xx status, times out, or is unreachable, e-bon retries the delivery on an exponential backoff schedule.

AttemptDelay before retry
1 → 21 minute
2 → 35 minutes
3 → 430 minutes
4 → 52 hours
5 → 612 hours

After the 5th failed attempt, the delivery is marked failed and no longer retried.

If a webhook accumulates 20 consecutive failed deliveries, e-bon automatically disables the subscription. Re-enable it from Settings → Webhooks in the Portal once you've fixed the endpoint.

You can browse delivery history (status, HTTP code, response excerpt, attempt count) under Settings → Webhooks → Deliveries or via GET /api/v1/org/webhooks/{id}/deliveries. Up to the first 500 characters of your response body are stored on each delivery record.

Follow best practices

  • Respond fast. Acknowledge the request with a 2xx status as soon as you've validated the signature; queue the heavy work (DB writes, downstream API calls) on a background worker. Anything past the 10-second timeout is treated as a failure.
  • Be idempotent. Use X-EBon-Delivery-Id as your deduplication key — a retried delivery has the same id. A simple INSERT … ON CONFLICT DO NOTHING keeps your handler safe.
  • Verify on the raw body. Never recompute the HMAC over a re-serialized JSON object; whitespace and key order matter. Use a constant-time comparison.
  • Store the secret in a secret manager. The raw whsec_… value is shown only at create time and on rotation. Keep it in env vars or a vault, never in source control.
  • Rotate when in doubt. Use Settings → Webhooks → Rotate secret (or POST /api/v1/org/webhooks/{id}/rotate-secret) to mint a fresh secret if the current one might have leaked.

Continue exploring

  • Webhooks API — create, update, rotate secrets, send test deliveries, browse delivery history.
  • API overview › error envelope — the errorCode values that show up in command.failed and command.timeout.
  • SDK › events — server-sent events alternative when you need a push channel without a public HTTPS endpoint.