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:
- AS4 transport — the local Oxalis-NG instance terminates the message, verifies the Peppol PKI signature, and writes the SBDH-wrapped UBL to disk.
- Dispatch — the
peppol.dispatch_inboundCelery task (running on a tight beat schedule) walks the inbound directory and persists each new SBDH InstanceIdentifier into thepeppol_messagestable. - Validation —
peppol.handle_received_business_docextracts the inner UBL and runs it through UBL 2.1 XSD + Peppol BIS Billing 3.0 Schematron + Slovak v1.3 Schematron. - Reply legs — the handler builds and ships a Peppol MLS
ApplicationResponseback to the original sender (response codeAPif validation passed,REwith 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. - Persistence — the inbound HTTP receiver
(
POST /webhooks/peppol/receive, called by Oxalis itself, never by your code) creates aninvoicesrow withsource='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 frompeppol_messages.payload_path; the canonical copy used byGET /invoices/{id}/ublis regenerated from the database on demand. Outbound invoices, by contrast, persist their UBL to MinIO viastore_ubl_xmlin 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
WebhookEventTypevalues areinvoice.delivered,invoice.rejected, andinvoice.fs_acknowledged— there is no dedicatedinvoice.receivedevent yet. Inbound Peppol invoices arrive in the database withsource='peppol'andstatus='delivered', so subscribe toinvoice.deliveredand branch onsourceafter 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-DD — source 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):
- Fetch the parsed JSON —
GET /api/v1/invoices/{id}returns the normalized header, supplier / buyer parties, line items, and totals asInvoiceResponse. Most accounting integrations only need this. - Or fetch the raw UBL XML —
GET /api/v1/invoices/{id}/ublreturns the originalapplication/xmlpayload 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. - 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.
- Optionally write back —
POST /invoices/{id}/paymentsto 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^attemptseconds — 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
- Sending invoices — the outbound counterpart.
- Webhooks — subscription, signature verification, retry policy.
- Sandbox — per-organisation test mode.
- API reference → Invoices —
GET /invoices,GET /invoices/{id},GET /invoices/{id}/ublschemas.