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"]}'| Event | When |
|---|---|
invoice.created | A document was created — in the app, via API, import, recurring invoice or e-shop. |
invoice.updated | Status, number, total or due date changed. |
invoice.deleted | A document was deleted. |
invoice.sent | The invoice status is sent; invoice.viewed = the customer opened the link. |
invoice.paid | The invoice is paid (bank or manual). |
invoice.overdue | The invoice is overdue. |
payment.received | A payment was matched to an invoice. |
reminder.sent | A 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
| Event | When |
|---|---|
peppol.document.sent | The document was accepted by the network (AS4 receipt). |
peppol.document.delivered | The document was delivered to the recipient (MLS/delivery receipt). |
peppol.document.failed | Sending failed (final). |
peppol.document.received | An inbound document arrived for you over Peppol. |
peppol.document.fs_report_accepted | The Slovak tax authority (Finančná správa) accepted the report of a sent or received document. |
peppol.document.fs_report_rejected | The tax authority rejected the report; the reasons are in data.issues. |
participant.activated | The organization is registered in the SMP and receives documents (after the FS mandate is confirmed or after POST /v1/agent/peppol/enroll). |
participant.failed | Participant registration failed (invalid FS token, SMP error, collision with another company, deactivated organization). |
participant.deactivated | The organization was de-registered from the SMP (POST /v1/agent/organizations/{id}/peppol/deactivate). |
usage.limit_exceeded | The 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}.
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…"
}
}| Event | Fields in data (besides orgId) |
|---|---|
sent | invoiceId, invoiceNumber, documentType, mode, state, messageId, transactionId |
delivered | invoiceId, invoiceNumber, documentType, mode, state, messageId, transactionId |
failed | invoiceId, invoiceNumber, documentType, error |
received | senderName, senderParticipantId, documentNumber, documentType, total, currency |
Headers
| Header | Description |
|---|---|
X-Webhook-Signature | The signature in t=<unix>,v1=<hex> format (Stripe-style). |
X-Webhook-Event | The event name. |
X-Webhook-Id | A delivery UUID (idempotency key). |
X-Webhook-Timestamp | The 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))
);
});
}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.
| Attempt | Wait after the previous failure |
|---|---|
| 2 | 5 s |
| 3 | 1 min |
| 4 | 5 min |
| 5 | 30 min |
| 6 | 2 h |
| 7 | 5 h |
| 8 | 10 h |
| 9 | 10 h |
| 10 | 14 h |
| 11 (last) | 24 h |
After the last failed attempt the delivery is marked as failed; you can still replay it from the portal.
| Mechanism | Behavior |
|---|---|
| Retries | Initial attempt + 10 retries over ~2.7 days (table above). |
| Idempotency | Deduplicate by X-Webhook-Id: a replay sends the same ID. |
| Auto-disable | After 15 consecutive failed deliveries the endpoint is deactivated. |
| Secret rotation | The 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_…"| Rule | Behavior |
|---|---|
| Cursor | Pass after=<next_after> from the previous response while has_more is true. |
| Order | Oldest first; id is an increasing number (string). |
| Deduplication | By event_id when it is set; otherwise every occurrence is logged. |
| Retention | 90 days. |
| Scope | All 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.