eFakturuj / API Docs
eFakturuj Guides

Cookbook: send & receive with webhooks

This is a start-to-finish recipe for the eFakturuj test environment: create an API key, register webhooks, then create → validate → send an invoice from one company to another over Peppol — and watch the webhooks fire on both the sender and the receiver.

Everything here runs against the sandbox. No document reaches the real Peppol network, and nothing is billed.

SettingValue
Base URLhttps://api.sandbox.efakturuj.sk/api/v1
KeysUse sandbox-created efk_… API keys
MoneyDecimal strings ("85.00"), never floats
DatesISO 8601 YYYY-MM-DD

See also: Authentication · Webhooks · Sending invoices · Receiving invoices.

What you'll build

        register webhook            register webhook
        (invoice.delivered)         (invoice.received)
              |                            |
        +-----+-----+                +-----+-----+
        |  SENDER   | -- send -----> | RECEIVER  |
        |  company  |   over Peppol  |  company  |
        +-----------+                +-----------+
   POST /companies/{company_id}/invoices -> validate -> send
                                         inbound invoice appears
                                         webhook: invoice.received

You need two companies in your test workspace — one acts as the sender, one as the receiver. Both must have a peppol_id so they are routable.

Step 0 — Authenticate

Two credentials are in play:

  • Session token (JWT) — from signing in to the dashboard. Required to mint API keys (needs the admin role) and to list your companies.
  • API key (efk_…) — created in Step 1, used for all invoice calls, sent as the X-API-Key header.

Throughout, $JWT is your session bearer token and $KEY is the API key.

Step 1 — Create an API key

POST /api-keys (requires the admin role — this is an authenticated dashboard / JWT action, not something an API key can do). The plaintext key is returned once — store it now; only its prefix is retrievable later.

curl -sS -X POST https://api.sandbox.efakturuj.sk/api/v1/api-keys \
  -H "Authorization: Bearer $JWT" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Test send cookbook",
    "scopes": ["invoices:read", "invoices:write", "invoices:send"]
  }'
{
  "id": "…",
  "name": "Test send cookbook",
  "key_prefix": "efk_a1b2c3d4",
  "scopes": ["invoices:read", "invoices:write", "invoices:send"],
  "is_active": true,
  "api_key": "efk_a1b2c3d4e5f6…"
}

The api_key field is shown only once — copy it now. Valid scopes: invoices:read, invoices:write, invoices:send, validate:only.

export KEY="efk_a1b2c3d4e5f6…"

A company-scoped key is bound to the company you were acting as when you created it. Create one for the sender company (the invoice calls in Step 4 run as the sender).

Step 2 — Register webhooks (both sides)

POST /connect/webhooks. The signing secret is returned once. Register one endpoint on the sender company and one on the receiver company so you see both directions. (Alternatively register a single workspace-scoped webhook via POST /workspaces/{workspace_id}/webhooks — it fires for every company in the workspace.)

curl -sS -X POST https://api.sandbox.efakturuj.sk/api/v1/connect/webhooks \
  -H "X-API-Key: $KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://your-endpoint.example.com/hooks",
    "events": "invoice.delivered,invoice.fs_acknowledged,invoice.rejected"
  }'
{
  "id": "…",
  "url": "https://your-endpoint.example.com/hooks",
  "events": "invoice.delivered,invoice.fs_acknowledged,invoice.rejected",
  "is_active": true,
  "secret": "whsec_…"
}

Repeat while acting as the receiver company, subscribing to the inbound event:

{ "url": "https://your-endpoint.example.com/hooks", "events": "invoice.received" }

Event types:

EventFires forWhen
invoice.deliveredsenderthe invoice is delivered to the recipient's access point
invoice.fs_acknowledgedsenderthe tax-authority (FS) acknowledgement is recorded
invoice.rejectedsenderdelivery or validation was rejected
invoice.receivedreceiveran inbound invoice was received for the company

Verifying the signature

Each delivery carries these headers:

HeaderMeaning
efk-signatureRecommended v2 signature: t=<unix>,v1=<hex HMAC>
X-Webhook-SignatureLegacy HMAC-SHA256 signature retained for one deprecation cycle
X-Webhook-Eventthe event type (e.g. invoice.received)
X-Webhook-Deliveryunique id for this delivery attempt

Verify efk-signature against the raw body bytes. The signed string is {timestamp}.{raw_body} and the HMAC key is the plaintext whsec_ secret you saved when creating or rotating the webhook:

import hashlib, hmac, time

def valid(raw_body: bytes, efk_signature: str, whsec_secret: str) -> bool:
    parts = dict(item.split("=", 1) for item in efk_signature.split(","))
    ts = int(parts["t"])
    if abs(int(time.time()) - ts) > 300:
        return False
    expected = hmac.new(
        whsec_secret.encode(),
        f"{ts}.".encode() + raw_body,
        hashlib.sha256,
    ).hexdigest()
    return hmac.compare_digest(expected, parts["v1"])

Smoke-test the endpoint

Fire a test delivery without an invoice, then inspect attempts:

curl -sS -X POST https://api.sandbox.efakturuj.sk/api/v1/connect/webhooks/$WEBHOOK_ID/test \
  -H "X-API-Key: $KEY"

curl -sS https://api.sandbox.efakturuj.sk/api/v1/connect/webhooks/$WEBHOOK_ID/deliveries \
  -H "X-API-Key: $KEY"

Tip: during development, point the webhook url at an ngrok tunnel (ngrok http 8899) to a tiny local server. You'll see every delivery — headers and body — in ngrok's inspector at http://localhost:4040, and can replay them.

Step 3 — Pick the sender and receiver

List the companies you can act on ("select from the list"). Each item carries the organisation id you use as {company_id} in Step 4.

curl -sS https://api.sandbox.efakturuj.sk/api/v1/workspaces/$WORKSPACE_ID/companies \
  -H "Authorization: Bearer $JWT"
{
  "data": [
    { "id": "…sender-org-id…",   "name": "Modrý Vrch s.r.o.",    "role": "admin" },
    { "id": "…receiver-org-id…", "name": "Riečna Dielňa s.r.o.", "role": "admin" }
  ]
}
export SENDER="…sender-org-id…"
export RECEIVER="…receiver-org-id…"

Both companies need a peppol_id. If a company doesn't have one yet, set it on its profile before sending; an unroutable recipient can't receive the document.

Step 4 — Create, confirm, validate, send

All four calls run as the sender, authenticated with the API key (X-API-Key).

4a. Create (draft)

POST /companies/{SENDER}/invoices. The buyer is the receiver, and buyer.peppol_id is what routes the document. supplier_iban is required for a credit-transfer invoice (payment means 30).

curl -sS -X POST https://api.sandbox.efakturuj.sk/api/v1/companies/$SENDER/invoices \
  -H "X-API-Key: $KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "invoice_number": "TEST-0001",
    "invoice_type": "380",
    "issue_date": "2026-07-02",
    "due_date": "2026-08-01",
    "currency_code": "EUR",
    "supplier_iban": "SK3112000000198742637541",
    "supplier": {
      "name": "Modrý Vrch s.r.o.", "vat_id": "SK2007233001", "ico": "90723301",
      "street": "Hlavná 12", "city": "Bratislava", "postal_code": "81101",
      "country_code": "SK", "peppol_id": "9925:sk2007233001"
    },
    "buyer": {
      "name": "Riečna Dielňa s.r.o.", "vat_id": "SK2007233002", "ico": "90723302",
      "street": "Štúrova 5", "city": "Košice", "postal_code": "04001",
      "country_code": "SK", "peppol_id": "9925:sk2007233002"
    },
    "lines": [
      {
        "line_number": 1, "item_name": "Konzultačné služby",
        "quantity": "10", "unit_code": "HUR", "unit_price": "85.00",
        "vat_rate": "23.00", "vat_category_code": "S"
      }
    ],
    "payment_means_code": "30",
    "variable_symbol": "20260001"
  }'

Response includes the new invoice id with "status": "draft".

export INV="…invoice-id…"

4b. Confirm (review the draft)

curl -sS https://api.sandbox.efakturuj.sk/api/v1/companies/$SENDER/invoices/$INV \
  -H "X-API-Key: $KEY"

4c. Validate (XSD + Peppol BIS 3.0 + Slovak rules)

curl -sS -X POST https://api.sandbox.efakturuj.sk/api/v1/companies/$SENDER/invoices/$INV/validate \
  -H "X-API-Key: $KEY"
{ "valid": true, "errors": [], "xml_validation": { "xsd_valid": true, "peppol_valid": true } }

Fix any valid: false before sending — see Troubleshooting.

4d. Send over Peppol

curl -sS -X POST https://api.sandbox.efakturuj.sk/api/v1/companies/$SENDER/invoices/$INV/send \
  -H "X-API-Key: $KEY"
{ "invoice_id": "…", "status": "queued", "message": "Invoice queued for delivery" }

Delivery is asynchronous. Poll the invoice until it reaches a terminal state:

curl -sS https://api.sandbox.efakturuj.sk/api/v1/companies/$SENDER/invoices/$INV \
  -H "X-API-Key: $KEY" | grep -o '"status":"[^"]*"'

Step 5 — Watch both sides

Receiver side (reliable in the test environment) — within a few seconds the receiver automatically gets an inbound invoice (this is the receive bridge, not a manual step) and its invoice.received webhook fires:

{
  "id": "evt_…",
  "event": "invoice.received",
  "created_at": "2026-07-02T…Z",
  "data": {
    "invoice_id": "…receiver-side-invoice-id…",
    "source": "peppol",
    "invoice_number": "TEST-0001",
    "supplier_vat_id": "SK…"
  }
}

Confirm the inbound document on the receiver:

curl -sS "https://api.sandbox.efakturuj.sk/api/v1/companies/$RECEIVER/invoices?page=1&page_size=20" \
  -H "X-API-Key: $RECEIVER_KEY"

It shows "source": "peppol" and supplier_name = your sender.

Sender side — the sender's invoice.delivered / invoice.fs_acknowledged webhooks fire only once a delivery-status callback confirms the AS4 hop and moves the sender's invoice to delivered. In the current test loop the sender's own copy may sit at sent_peppol until that callback arrives, so invoice.received on the receiver is the signal to rely on for confirming the document moved.

Inspect delivery attempts for any webhook (status, HTTP code, retries):

curl -sS https://api.sandbox.efakturuj.sk/api/v1/connect/webhooks/$WEBHOOK_ID/deliveries \
  -H "X-API-Key: $KEY"

A non-2xx endpoint is retried with exponential backoff (up to 5 attempts); each attempt is one row.

Troubleshooting

SymptomCauseFix
valid:falseBR-61credit transfer (code 30) needs a payment accountadd supplier_iban (Slovak SK + 22 digits)
warning SK-BR-04variable_symbol contains non-digitsuse digits only, e.g. "20260001"
send succeeds, receiver sees nothingrecipient not routableensure the buyer's peppol_id is set and the company exists in the test directory
sender stays sent_peppol, no invoice.delivereddelivery-status callback not yet received in the test looprely on the receiver's invoice.received; the sender flips to delivered once delivery is confirmed
409 invoice_number_conflictnumber already used for the org/tax-yearpick a new invoice_number
webhook signature mismatchbody changed, wrong secret, or stale timestampverify efk-signature with the plaintext whsec_ secret over {timestamp}.{raw_body} (Step 2)
403 on /api-keysnot an adminmint keys with an admin session token

Quick reference

ActionCall
Create API keyPOST /api-keys (JWT, admin)
Register webhook (company)POST /connect/webhooks
Register webhook (workspace)POST /workspaces/{workspace_id}/webhooks
Test a webhookPOST /connect/webhooks/{webhook_id}/test
Webhook deliveriesGET /connect/webhooks/{webhook_id}/deliveries
List companiesGET /workspaces/{workspace_id}/companies
Create invoicePOST /companies/{company_id}/invoices
ValidatePOST /companies/{company_id}/invoices/{invoice_id}/validate
SendPOST /companies/{company_id}/invoices/{invoice_id}/send
Read / pollGET /companies/{company_id}/invoices/{invoice_id}
List (see inbound)GET /companies/{company_id}/invoices