Sending invoices
The end-to-end recipe for getting an invoice from your system, through eFakturuj, onto the Peppol network, and into the Slovak Tax Authority's inbox — and then knowing when it arrived. If you read one guide, read this one.
1. Look up or create the customer
Invoices embed a snapshot of the buyer (name, VAT ID, address, Peppol
ID) so your customer directory is optional — you can ship the buyer
block inline on every POST /invoices. Most integrators still use the
directory because it (a) deduplicates buyer details across invoices,
(b) lets you cache the discovered Peppol participant id, and (c) lets
the dashboard show "X invoices from this customer".
Look up first, create only if missing:
# Search by VAT id, IČO, or name fragment
curl -G https://api.efakturuj.sk/api/v1/customers/search \
-H 'X-Api-Key: efk_live_...' \
--data-urlencode 'q=Acme'
# Returns up to 10 slim matches; pick the right id, or POST a new one.
curl -X POST https://api.efakturuj.sk/api/v1/customers \
-H 'X-Api-Key: efk_live_...' \
-H 'Content-Type: application/json' \
-d '{"name": "Acme s.r.o.", "vat_id": "SK1234567890", "ico": "12345678"}'
A 409 Conflict on POST /customers means a customer with that VAT
id already exists in your organisation — search and reuse.
2. Create a draft invoice
POST /invoices accepts the full document in one call. Key fields and
their Slovak / Peppol semantics:
| Field | Notes |
|---|---|
invoice_number | Optional. Omit to let eFakturuj reserve the next number from your workspace's pattern (preview via GET /invoices/numbering/next). |
invoice_type | UN/CEFACT 1001 code. 380 = commercial invoice (default), 381 = credit note, 325 = proforma. |
currency_code | ISO 4217. EUR for domestic Slovak invoices. Must match the IBAN currency. |
payment_means_code | UN/CEFACT 4461. 30 = SEPA credit transfer (default), 42 = bank account, 48 = card. |
variable_symbol | Slovak banking convention — the numeric reference printed on bank statements. Conventionally the invoice number stripped to digits. |
lines[] | At least one line. Peppol BIS Billing 3.0 imposes a soft cap of 200 lines per document — large invoices may be rejected by downstream Access Points. |
lines[].vat_rate | Slovak rates as of 2025: 0, 5 (food / medicines), 19 (intermediate), 23 (standard). |
lines[].vat_category_code | UN/CEFACT 5305. S (standard) is the default. A 0% rate must be paired with a non-S category — Z (zero rated), E (exempt), AE (reverse charge), or K (intra-community supply). The validator rejects vat_rate=0 + vat_category_code=S. |
curl -X POST https://api.efakturuj.sk/api/v1/invoices \
-H 'X-Api-Key: efk_live_...' \
-H 'Content-Type: application/json' \
-d '{
"issue_date": "2026-05-06",
"due_date": "2026-06-05",
"currency_code": "EUR",
"supplier": {
"name": "Vendere s.r.o.",
"vat_id": "SK2020000001",
"ico": "47000001",
"street": "Hlavná 12",
"city": "Bratislava",
"postal_code": "811 01",
"country_code": "SK",
"peppol_id": "0210:47000001"
},
"supplier_iban": "SK0900000000123456789012",
"buyer": {
"name": "Acme s.r.o.",
"vat_id": "SK1234567890",
"ico": "12345678",
"street": "Račianska 88",
"city": "Bratislava",
"postal_code": "831 02",
"country_code": "SK",
"peppol_id": "0210:12345678"
},
"variable_symbol": "2026001",
"payment_means_code": "30",
"lines": [
{
"line_number": 1,
"item_name": "Consulting — May 2026",
"quantity": "10",
"unit_code": "HUR",
"unit_price": "100.00",
"vat_rate": "23.00",
"vat_category_code": "S"
}
]
}'
The response is the freshly-created InvoiceResponse (status 201) —
the server has assigned a UUID, computed total_net / total_vat /
total_gross, the per-rate vat_breakdown, and the Slovak-specific
zzz_correction_amount (the document-level UBL AllowanceCharge
with reason code ZZZ that reconciles Slovakia's
reverse-from-VAT-inclusive total with Peppol's forward-from-net
calculation):
{
"id": "8f5a1c20-1f6e-4f6c-9f5e-2f5a1c201f6e",
"invoice_number": "2026-001",
"status": "draft",
"total_net": "1000.00",
"total_vat": "230.00",
"total_gross": "1230.00",
"zzz_correction_amount": "0.00",
"lines": [{ "id": "...", "line_number": 1, "line_gross_amount": "1230.00" }]
}
Dart equivalent:
final res = await http.post(
Uri.parse('https://api.efakturuj.sk/api/v1/invoices'),
headers: {
'X-Api-Key': apiKey,
'Content-Type': 'application/json',
},
body: jsonEncode(invoice),
);
if (res.statusCode != 201) throw ApiError.from(res);
final created = jsonDecode(res.body) as Map<String, dynamic>;
3. Validate the UBL (recommended during integration)
POST /invoices/{id}/validate runs the full pipeline against a stored
invoice — Python business-rule checks plus, when the schema files
are installed, the UBL XSD, the Peppol BIS Billing 3.0 Schematron
(both the EN 16931 base and the Peppol overlay), and the Slovak v1.3
Schematron. If everything passes and the invoice is still in DRAFT, it
is auto-promoted to validated.
curl -X POST https://api.efakturuj.sk/api/v1/invoices/$ID/validate \
-H 'X-Api-Key: efk_live_...'
{
"valid": true,
"errors": [],
"warnings": [],
"xml_validation": {
"xsd_valid": true,
"peppol_valid": true,
"slovak_valid": true,
"errors": [],
"warnings": []
},
"schemas_available": {
"xsd_invoice": true,
"schematron_peppol": true,
"schematron_slovak": true
},
"status": "validated"
}
Always validate during development; POST /invoices/{id}/send will
re-run the business-rule check anyway and refuse to queue an invalid
invoice (returning 422 with the same error list). In steady state you
can skip the standalone validate call.
4. Send via Peppol
POST /invoices/{id}/send returns 202 Accepted — delivery is
asynchronous. The route flips the invoice to queued, writes a
sent_peppol audit log entry with PENDING result, and dispatches
two Celery tasks onto the critical queue:
invoice.send_peppol— generates UBL XML, stores it in MinIO, sends it via AS4 to the buyer's Access Point. On success transitions tosent_peppoland stampspeppol_message_id.invoice.send_fs_copy— submits the same UBL to the Slovak Tax Authority (Finančná správa) C5 corner. On success transitions tosent_fsand stampsfs_submission_id.
curl -X POST https://api.efakturuj.sk/api/v1/invoices/$ID/send \
-H 'X-Api-Key: efk_live_...'
{
"invoice_id": "8f5a1c20-1f6e-4f6c-9f5e-2f5a1c201f6e",
"status": "queued",
"message": "Invoice queued for delivery"
}
Only invoices in draft, validated, or approved status may be
sent — others return 409 with error: "invalid_status". A failing
business-rule check returns 422 with the offending rules.
5. Track delivery
Both options are supported; webhooks are recommended for production.
Webhook (preferred)
Subscribe once to invoice.delivered, invoice.rejected, and
invoice.fs_acknowledged (see the Webhooks
guide). The events fire as soon as the Celery tasks finish. Your
handler receives the invoice_id; fetch the full invoice via
GET /invoices/{id} if you need the updated status / message id.
Polling
Poll GET /invoices/{id} and watch status. The full lifecycle
enum (from app/models/invoice.py) is:
draft → validated → approved → queued → sent_peppol → delivered
↘ sent_fs → fs_acknowledged
↘ failed | rejected | cancelled | voided
Polling cadence: 30 seconds is plenty — most deliveries settle inside
3 seconds, AS4 retries can take a few minutes. Stop polling once
status reaches delivered, fs_acknowledged, failed, or
rejected.
Common pitfalls
vat_rateandvat_category_codemust be consistent. A 0% rate withvat_category_code=Sis rejected — pair0withZ,E,AE, orKdepending on why VAT is zero.- Three Slovak business identifiers, three different fields.
vat_id(IČ DPH) is the SK-prefixed VAT number (SK+ 10 digits),dicis the 10-digit tax ID,icois the 8-digit business register number. They go into different columns; mixing them up is the most common Schematron failure. - Missing buyer Peppol ID. You can create an invoice for a buyer
without one, but the send call will fail at the AS4 layer because
there is nothing to look up in the SMP. Either supply
buyer.peppol_iddirectly or run a participant lookup first (GET /lookup/participant/{vat_id}). - Currency mismatch.
currency_codemust match the IBAN currency. A EUR invoice with a USD account triggers a Slovak Schematron failure during validation. - More than 200 lines. Peppol BIS Billing 3.0 caps documents at 200 lines. Receivers may accept more; many do not. Split larger invoices.
See also
- Webhooks — preferred path for tracking delivery.
- Errors — handling 422 / 402 / 429 / 502 responses.
- Receiving invoices — the inbound counterpart.
- API reference → Invoices — endpoint schemas.
- Peppol BIS Billing 3.0 specification — the canonical UBL profile we generate against.
- UBL 2.1 schema — underlying OASIS standard.
- Slovak Finančná správa e-invoicing portal — Slovak v1.3 rules + 2027 mandate documentation.