Webhooks

Webhooks deliver events (invoices, payments, inventory, Peppol documents) to your HTTPS endpoint. Every delivery is HMAC-signed and retried with exponential backoff until you acknowledge it with HTTP 2xx. A company with its own API key registers endpoints through the API (first section); partners use the portal (the sections after it). Signature, headers and retries are the same for both.

Webhooks for companies (own API key)

Register an endpoint with an API key holding settings:update. Available from the Pro plan. The response carries the signing secret only once. Up to 20 endpoints per company; the URL must be https://. The same endpoints show in eDoklad under Settings → Webhooks. Make and n8n instant triggers create and delete their endpoint themselves.

curl -X POST https://api.efaktura.sk/v1/agent/webhooks \
  -H "X-API-Key: efk_live_…" \
  -H "X-Organization-Id: <uuid>" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://erp.example.com/efaktura","events":["peppol.document.received","invoice.paid"]}'
EventWhen
invoice.createdA document was created — in the app, via API, import, recurring invoice or e-shop.
invoice.updatedStatus, number, total or due date changed.
invoice.deletedA document was deleted.
invoice.sentThe invoice status is sent; invoice.viewed = the customer opened the link.
invoice.paidThe invoice is paid (bank or manual).
invoice.overdueThe invoice is overdue.
payment.receivedA payment was matched to an invoice.
reminder.sentA payment reminder went out.
peppol.document.*sent / delivered / failed / received — as for partners below.
stock.*level_changed / document_confirmed / low — inventory.

Invoice events carry invoiceId, invoiceNumber, type, status, previousStatus, total (string "149.99"), currency, issueDate, dueDate, paidDate, customerName and pdfUrl in data. Delivery history: GET /v1/agent/webhooks/{id}/deliveries; redeliver: POST …/deliveries/{deliveryId}/redeliver; test event: POST /v1/agent/webhooks/{id}/test. Deliveries are kept for 90 days. All endpoints are in the API reference (Webhooks section).

Partners: registering an endpoint

You register an endpoint in the portal under Webhooks, or via POST /v1/partner/webhooks — a portal endpoint authenticated by your portal login session (cookie), not by X-API-Key; an API key gets 401 UNAUTHORIZED there. On creation you get the signing secret only once. Store it. A partner can have up to 20 endpoints; the URL must be https://. By default an endpoint receives events for all of the partner's organizations; pass an optional orgId on creation to restrict it to a single organization.

Partners: event types

EventWhen
peppol.document.sentThe document was accepted by the network (AS4 receipt).
peppol.document.deliveredThe document was delivered to the recipient (MLS/delivery receipt).
peppol.document.failedSending failed (final).
peppol.document.receivedAn inbound document arrived for you over Peppol.
peppol.document.fs_report_acceptedThe Slovak tax authority (Finančná správa) accepted the report of a sent or received document.
peppol.document.fs_report_rejectedThe tax authority rejected the report; the reasons are in data.issues.
participant.activatedThe organization is registered in the SMP and receives documents (after the FS mandate is confirmed or after POST /v1/agent/peppol/enroll).
participant.failedParticipant registration failed (invalid FS token, SMP error, collision with another company, deactivated organization).
participant.deactivatedThe organization was de-registered from the SMP (POST /v1/agent/organizations/{id}/peppol/deactivate).
usage.limit_exceededThe organization exceeded its included monthly transaction volume.

participant.* payloads carry data.participantId (0245:DIČ, 9915:DIČ in the sandbox), data.scheme, data.mode (live / test), data.source (partner_claim / api_enroll / api_deactivate) and a timestamp activatedAt / failedAt / deactivatedAt; participant.failed additionally carries data.code and data.message.

The payload of peppol.document.fs_report_* events carries data.direction (sent / received), data.invoiceId or data.receivedDocumentId, data.originalDocumentId, data.state, data.issues and data.decidedAt. The current report status is also in the fs_report field of GET /v1/agent/peppol/status/{invoiceId} and GET /v1/agent/peppol/received/{id}.

The event peppol.document.status_changed is in the catalog but is not emitted yet. Transport status changes are covered by the sent / delivered / failed events above.

Payload format

The body is JSON with an event / timestamp (ISO 8601) / data envelope. The data.orgId field is always present and identifies the client organization. Test sends carry data.mode = "test".

{
  "event": "peppol.document.delivered",
  "timestamp": "2026-06-12T19:48:01.000Z",
  "data": {
    "invoiceId": "b1f0…",
    "invoiceNumber": "2026001",
    "documentType": "invoice",
    "mode": "test",
    "state": "DELIVERED",
    "messageId": "…",
    "transactionId": "…",
    "orgId": "0a2c…"
  }
}
EventFields in data (besides orgId)
sentinvoiceId, invoiceNumber, documentType, mode, state, messageId, transactionId
deliveredinvoiceId, invoiceNumber, documentType, mode, state, messageId, transactionId
failedinvoiceId, invoiceNumber, documentType, error
receivedsenderName, senderParticipantId, documentNumber, documentType, total, currency

Headers

HeaderDescription
X-Webhook-SignatureThe signature in t=<unix>,v1=<hex> format (Stripe-style).
X-Webhook-EventThe event name.
X-Webhook-IdA delivery UUID (idempotency key).
X-Webhook-TimestampThe signature's Unix time (seconds).

Verifying the signature

The string <timestamp>.<raw body> is signed with HMAC-SHA256 and your secret; the result is lowercase hex in the v1= segment. During secret rotation the header may carry multiple v1= segments (old + new): a match on either is sufficient. Compare in constant time and reject a timestamp that's too old (replay protection).

import { createHmac, timingSafeEqual } from "node:crypto";

function verify(rawBody, header, secret, toleranceSec = 300) {
  const parts = Object.fromEntries(
    header.split(",").map((kv) => kv.split("="))
  );
  const t = Number(parts.t);
  if (!t || Math.abs(Date.now() / 1000 - t) > toleranceSec) return false;

  const expected = createHmac("sha256", secret)
    .update(`${t}.${rawBody}`)
    .digest("hex");

  // header may carry several v1=… segments during secret rotation
  return header
    .split(",")
    .filter((kv) => kv.startsWith("v1="))
    .some((kv) => {
      const got = kv.slice(3);
      return (
        got.length === expected.length &&
        timingSafeEqual(Buffer.from(got), Buffer.from(expected))
      );
    });
}
Use the raw request body (byte-for-byte), not re-serialized JSON, otherwise the signature won't match.

Delivery and retries

Delivery is durable (a queue + persisted history). Your endpoint should respond with HTTP 2xx within a few seconds; do the processing asynchronously. On failure (non-2xx or timeout), delivery is retried 10× over about 2.7 days: 11 attempts total, so an endpoint that is down over a weekend still receives every event.

AttemptWait after the previous failure
25 s
31 min
45 min
530 min
62 h
75 h
810 h
910 h
1014 h
11 (last)24 h

After the last failed attempt the delivery is marked as failed; you can still replay it from the portal.

MechanismBehavior
RetriesInitial attempt + 10 retries over ~2.7 days (table above).
IdempotencyDeduplicate by X-Webhook-Id: a replay sends the same ID.
Auto-disableAfter 15 consecutive failed deliveries the endpoint is deactivated.
Secret rotationThe old secret verifies for another 24 h (dual v1= signature).

Event feed (pull)

Every event is also written to the event feed, even when you have no endpoint registered. If you cannot receive webhooks (firewall) or an outage outlasted the retries, fetch what you missed with GET /v1/agent/events using a partner key. The payload field is exactly what the webhook would have sent.

curl "https://api.efaktura.sk/v1/agent/events?after=1841&limit=100" \
  -H "X-API-Key: efk_pk_live_…"
RuleBehavior
CursorPass after=<next_after> from the previous response while has_more is true.
OrderOldest first; id is an increasing number (string).
DeduplicationBy event_id when it is set; otherwise every occurrence is logged.
Retention90 days.
ScopeAll your organizations; each event carries org_id.

Testing

In the portal you can send a test event to an endpoint, view the delivery history (payload + response), and replay any past delivery. In the sandbox (test key), a real peppol.document.delivered with data.mode = "test" fires after delivery between registered test participants.