eFakturuj / API Docs
eFakturuj Guides

Receiving invoices

eFakturuj operates as a Peppol Access Point and Service Provider, so inbound documents addressed to your Slovak participant id land in the backend without any plumbing on your side. This guide covers what happens between the network and the database, and how an integrator consumes the result.

The path

When a remote Access Point sends an AS4 message to your Slovak participant id (0210:<IČO> or 0245:PSK<member>), the flow is:

  1. AS4 transport — the local Oxalis-NG instance terminates the message, verifies the Peppol PKI signature, and writes the SBDH-wrapped UBL to disk.
  2. Dispatch — the peppol.dispatch_inbound Celery task (running on a tight beat schedule) walks the inbound directory and persists each new SBDH InstanceIdentifier into the peppol_messages table.
  3. Validationpeppol.handle_received_business_doc extracts the inner UBL and runs it through UBL 2.1 XSD + Peppol BIS Billing 3.0 Schematron + Slovak v1.3 Schematron.
  4. Reply legs — the handler builds and ships a Peppol MLS ApplicationResponse back to the original sender (response code AP if validation passed, RE with the failed assertions attached otherwise), and — when the document was valid — files an SK TDD with the Slovak Tax Authority C5 corner inside the architecture's 15-minute SLA.
  5. Persistence — the inbound HTTP receiver (POST /webhooks/peppol/receive, called by Oxalis itself, never by your code) creates an invoices row with source='peppol', status='delivered', the parsed lines, and an audit log entry. The original SBDH-wrapped UBL is kept on disk by Oxalis under the inbound directory (<inbound>/<receiver>/<sender>/<msgid>.doc.xml), referenced from peppol_messages.payload_path; the canonical copy used by GET /invoices/{id}/ubl is regenerated from the database on demand. Outbound invoices, by contrast, persist their UBL to MinIO via store_ubl_xml in the send task.

You don't call any of this — your hooks are at step 5 and after.

Subscribing to inbound events

Webhook subscription is the same surface as outbound delivery (see the Webhooks guide):

curl -X POST https://api.efakturuj.sk/api/v1/webhooks \
  -H 'X-Api-Key: efk_live_...' \
  -H 'Content-Type: application/json' \
  -d '{
    "url": "https://hooks.example.sk/inbound",
    "events": "invoice.delivered"
  }'

Heads up. The currently emitted WebhookEventType values are invoice.delivered, invoice.rejected, and invoice.fs_acknowledged — there is no dedicated invoice.received event yet. Inbound Peppol invoices arrive in the database with source='peppol' and status='delivered', so subscribe to invoice.delivered and branch on source after fetching the invoice. A dedicated inbound event is on the roadmap; track the Changelog for the cutover.

If the wiring you need today doesn't exist, you can fall back to a poll loop on GET /invoices?status=delivered&from_date=YYYY-MM-DDsource is returned in every row, so you can filter to inbound documents client-side.

What the integrator does

When the webhook fires (or your poll picks up a new row):

  1. Fetch the parsed JSONGET /api/v1/invoices/{id} returns the normalized header, supplier / buyer parties, line items, and totals as InvoiceResponse. Most accounting integrations only need this.
  2. Or fetch the raw UBL XMLGET /api/v1/invoices/{id}/ubl returns the original application/xml payload as a download (filename is the invoice number). Use this when you need a field we don't surface in the JSON projection, when you're storing the document for audit, or when you're forwarding it to another system that wants the canonical UBL.
  3. Extract what your accounting system needs — supplier VAT id, line totals, due date, the supplier's IBAN if you want to set up the payment.
  4. Optionally write backPOST /invoices/{id}/payments to mark it paid, the approval-workflow routes if your team needs review.

Webhook payload

When the inbound event ships, the envelope is the same shape as every other webhook (see Webhooks → Event payload):

{
  "id": "9c1f...",
  "event": "invoice.delivered",
  "created_at": "2026-05-06T10:00:01.234567+00:00",
  "data": {
    "invoice_id": "8f5a1c20-1f6e-4f6c-9f5e-2f5a1c201f6e"
  }
}

The body intentionally contains only the invoice_id — fetch the invoice to get the full document. This keeps the payload small enough to fit a 5-attempt retry budget without ballooning your webhook log.

Error scenarios

  • Subscriber returns 5xx. eFakturuj retries up to 5 times with exponential backoff (5^attempt seconds — see the Webhooks retry policy). After the fifth failure the event is dropped from the queue.
  • Subscriber permanently down. Once the retry budget is exhausted the event is gone. Recover by paginating GET /invoices?source=peppol&from_date=YYYY-MM-DD (any status) on a schedule — the database row was created at step 5, independent of webhook delivery.
  • Invalid inbound document. The handler still creates the database row, but ships a negative MLS (response code RE) back to the sender with the failed Schematron assertions. No TDD is filed and no webhook fires for the rejection today; surface the issue from the inbound document directly.

Test the inbound flow

The Sandbox guide (here) covers the per-org sandbox flag for outbound testing. Inbound testing is harder because it requires a real Peppol message — pair the sandbox with a Peppol test SMP to exercise the full path end-to-end. The OpenPeppol testbed publishes participant ids you can target during certification; that's the path the eFakturuj team uses to qualify each release.

See also