eFakturuj / API Docs
eFakturuj Guides

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:

FieldNotes
invoice_numberOptional. Omit to let eFakturuj reserve the next number from your workspace's pattern (preview via GET /invoices/numbering/next).
invoice_typeUN/CEFACT 1001 code. 380 = commercial invoice (default), 381 = credit note, 325 = proforma.
currency_codeISO 4217. EUR for domestic Slovak invoices. Must match the IBAN currency.
payment_means_codeUN/CEFACT 4461. 30 = SEPA credit transfer (default), 42 = bank account, 48 = card.
variable_symbolSlovak 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_rateSlovak rates as of 2025: 0, 5 (food / medicines), 19 (intermediate), 23 (standard).
lines[].vat_category_codeUN/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>;

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:

  1. invoice.send_peppol — generates UBL XML, stores it in MinIO, sends it via AS4 to the buyer's Access Point. On success transitions to sent_peppol and stamps peppol_message_id.
  2. invoice.send_fs_copy — submits the same UBL to the Slovak Tax Authority (Finančná správa) C5 corner. On success transitions to sent_fs and stamps fs_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_rate and vat_category_code must be consistent. A 0% rate with vat_category_code=S is rejected — pair 0 with Z, E, AE, or K depending 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), dic is the 10-digit tax ID, ico is 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_id directly or run a participant lookup first (GET /lookup/participant/{vat_id}).
  • Currency mismatch. currency_code must 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