Invoices
Issue an invoice via POST /v1/agent/invoices. The seller is derived from the organization (X-Organization-Id), the buyer from the customer field or an existing customer_id. Numbering, VAT calculation, PDF, and QR code generation all happen server-side, the same as for manual issuance.
Endpoint
| Method | Path | Scope | Headers |
|---|---|---|---|
| POST | /v1/agent/invoices | invoice:create | X-API-Key, X-Organization-Id |
This endpoint has a per-org rate limit of 30 requests / minute (on top of the key's global limit). The invoice currency defaults to the firm's currency; for another one see currency below.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
type | invoice | proforma | self_invoice | final_invoice | credit_note | debit_note | No | Document type, defaults to invoice. Each type has its own number series. proforma = proforma (advance) invoice — a payment request, not a tax document, so it has no tax point date and cannot be sent over Peppol; self_invoice goes over Peppol under the BIS Self-Billing 3.0 profile; credit_note = credit note, debit_note = debit note, final_invoice = final settlement invoice. |
customer_id | UUID | Conditionally | ID of an existing customer. Either customer_id OR customer is required. |
customer | object | Conditionally | Inline customer (creates a contact). See the table below. |
items | array of objects | Yes | At least 1 item. See the items table below. |
vehicle_id | UUID | No | ID of an existing vehicle (fleet module). |
vehicle_plate | string (≤20) | No | Vehicle license plate: the vehicle is looked up (normalized to uppercase without spaces). |
payment_method | bank_transfer | cash | card | online | offset | No | Defaults to bank_transfer (mapped internally to "bank"). online = online payment service (PayPal, gateway), offset = mutual set-off. Carried into the Peppol UBL as PaymentMeansCode (UNCL4461): 30 / 10 / 48 / 68 / 97. |
currency | ISO 4217 | No | Document currency. Defaults to the firm's currency (EUR for a Slovak firm, CZK for a Czech one). See Foreign currency and exchange rate. |
exchange_rate | string (decimal) | No | Foreign-currency rate: units of currency per one unit of the firm's home currency. Without it the ECB / CNB rate. See Foreign currency and exchange rate. |
due_days | int 0–365 | No | Payment term in days from the issue date. Defaults to 14. Note: if the organization has defaultDueDays configured, that's used instead. Do not combine with due_date. |
due_date | YYYY-MM-DD | No | Due date given directly — alternative to due_days (both at once = 400). Must not precede issue_date. |
note | string (≤2000) | No | Note on the invoice (visible to the customer). |
internal_note | string (≤2000) | No | Internal note (not on the PDF). |
issue_date | YYYY-MM-DD | No | Issue date. Defaults to today. |
delivery_date | YYYY-MM-DD | No | Delivery date. Defaults to issue_date. |
order_number | string (≤100) | No | Order number (orderReference). |
invoice_number | string (≤50) | No | Your own document number (e.g. from your system). Omit it to draw from the organization's numbering sequence. A taken number returns 409. Variable symbol = the digits of the number (last 10). |
delivery_address | object | Yes for K treatments | Deliver-to address (BG-15): name?, street, city, zip, country (2 letters). Required for intra_eu_goods / intra_eu_transfer / intra_eu_triangulation (BR-IC-12 needs the deliver-to country), optional otherwise. |
exemption_reason | string (1–500) | No | VAT exemption reason (BT-120) for 0 % lines — printed on the PDF and carried into the UBL. Ignored when no line has vat_rate 0. For a non-VAT-payer it replaces the default "Supplier is not a VAT payer" text. |
already_paid | boolean | No | Defaults to false. When true, the invoice is issued with status "paid" and full payment as of the issue date. |
attachments | array (≤10) | No | Attachments (PDF/JPG/PNG/CSV/XLSX/ODS, base64). When sent via Peppol they're embedded directly in the document → they reach the recipient even on other systems (e.g. Doklado). |
{ filename, mime_code, base64_content } (optionally description). Allowed types: PDF, JPG, PNG, CSV, XLSX, ODS (exactly the Peppol BIS 3.0 list — DOC/DOCX/XLS/GIF cannot be delivered over the network, convert them to PDF); 10 MB total max (Peppol limit). The type is verified from the content; CSV is accepted only with a filename ending in .csv. An attachment can also be added later to an existing invoice via POST /v1/agent/invoices/{id}/attachments and managed via GET / DELETE on the same path.Customer object
Maps to the contacts table. For an inline customer, a new contact of type customer is created.
| Field | Type | Required | Description |
|---|---|---|---|
name | string 1–300 | Yes | Company name or customer name. |
ico | string (8 digits) | No | IČO: exactly 8 digits. |
dic | string | No | DIČ: 10 digits (SK) or a country code + 8–10 digits (e.g. CZ12345678). Normalized to uppercase without spaces. |
ic_dph | string (SK + 10 digits) | No | VAT ID in the format SK2120035951. |
address | string | object | No | Either a plain string, or an object { street, city, postalCode, country }. For Peppol, a full address object is required. |
email | email (≤254) | No | An empty string is treated as not provided. |
phone | string E.164 (≤16) | No | E.164 format, e.g. +421900123456. Separators and the 00 prefix are normalized (0042... → +42...). An invalid number returns 400 VALIDATION_ERROR. An empty string is treated as not provided. |
address as an object requires non-empty street, city, postalCode, and a 2-letter country. If you send the address as a plain string, the string is stored in the street field and country is set to "SK"; city and postalCode remain empty: insufficient for Peppol (see below). For Peppol sending, always send a full address object.Line item object (items[])
| Field | Type | Required | Description |
|---|---|---|---|
description | string 1–500 | Yes | Item name / description. |
quantity | number > 0 | Yes | Quantity (a numeric string is also accepted). |
unit | enum | No | Defaults to ks. Allowed: ks, hod, l, m, km, súbor, kg, t, g, cm, m2, m3, set, deň, bal, kpl. Mapped to UN/ECE Rec 20 codes in the UBL (km → KMT, kg → KGM, t → TNE, m2 → MTK, deň → DAY …); súbor (file) has no code and goes out as C62. |
unit_price | number ≥ 0 | Yes | Unit price excluding VAT. |
vat_rate | int | Yes | VAT rate in percent. Allowed: 0, 5, 19, 23. |
tax_treatment | enum | No | Tax treatment of a 0 % line: domestic (default), reverse_charge_service, intra_eu_goods, intra_eu_transfer, intra_eu_triangulation, export_goods. Any value other than domestic is allowed only with vat_rate 0 (otherwise 400) and only for a VAT payer. All lines sharing a rate must share the treatment (otherwise 400). It decides the VAT category in the UBL — see the table below. |
VAT categories for 0 % lines (UBL mapping)
A 0 % rate alone is not enough — EN 16931 distinguishes why a line carries no VAT. The category comes from the line's tax_treatment and the document's exemption_reason (BT-120):
| Situation | tax_treatment | exemption_reason | Category (BT-151) | VATEX code |
|---|---|---|---|---|
| Reverse charge | reverse_charge_service | optional | AE | VATEX-EU-AE |
| Intra-EU supply of goods, transfer of own goods, triangulation | intra_eu_goods / intra_eu_transfer / intra_eu_triangulation | optional | K | VATEX-EU-IC |
| Export outside the EU | export_goods | optional | G | VATEX-EU-G |
| Exempt supply (margin schemes, statutory exemptions …) | domestic / absent | yes (reason text) | E | derived from the text (e.g. VATEX-EU-F) |
| Zero rate without a reason | domestic / absent | no | Z | — |
| Supplier is not a VAT payer | (not allowed) | optional, overrides the default | O | VATEX-EU-O |
tax_treatment and without exemption_reason the behaviour is unchanged (category Z). To inspect the resulting UBL before sending, call GET /v1/agent/invoices/{id}/xml — it returns a preview (see below).tax_treatment only, never from the reason text. If exemption_reason by itself reads as reverse charge / export / intra-EU supply (e.g. "§ 69", "reverse charge", "export") and the line has no cross-border treatment, the request returns 400 naming the treatment to set. K treatments (intra_eu_*) additionally require delivery_address with the deliver-to country (BT-80, BR-IC-12). Category O (outside the scope of VAT) cannot share a document with VAT-bearing lines (BR-O-11). On a self_invoice the supplier is the contact — it must have a VAT id for AE/K/G/E.ic_dph nor isVatPayer), no item may have vat_rate > 0; otherwise the request fails with VALIDATION_ERROR ("Not a VAT payer"). The invoice gets the exemption "Supplier is not a VAT payer".Complete example
POST /v1/agent/invoices
X-API-Key: efk_pk_test_...
X-Organization-Id: 0a2c1f3e-…
Idempotency-Key: 7f0c2a4e-order-2026-042
Content-Type: application/json
{
"customer": {
"name": "Customer s.r.o.",
"ico": "87654321",
"dic": "2010101010",
"ic_dph": "SK2010101010",
"address": {
"street": "Obchodná 5",
"city": "Košice",
"postalCode": "04001",
"country": "SK"
},
"email": "fakturacia@odberatel.sk",
"phone": "+421900123456"
},
"items": [
{
"description": "Service work",
"quantity": 2,
"unit": "hod",
"unit_price": 50,
"vat_rate": 23
},
{
"description": "Spare part",
"quantity": 1,
"unit": "ks",
"unit_price": 80,
"vat_rate": 23
}
],
"payment_method": "bank_transfer",
"due_days": 14,
"note": "Thank you for your business.",
"order_number": "OBJ-2026-042",
"invoice_number": "2026-000042"
}Response
HTTP/1.1 201 Created
{
"id": "b1f0d7a2-…",
"invoice_number": "2026001",
"status": "issued",
"customer": { "id": "…", "name": "Customer s.r.o." },
"items": [ … ],
"subtotal": "180.00",
"vat_total": "41.40",
"total": "221.40",
"currency": "EUR",
"exchange_rate": null,
"issue_date": "2026-06-22",
"due_date": "2026-07-06",
"pdf_url": "https://…",
"created_at": "2026-06-22T10:00:00.000Z"
}The id value is used both when sending via Peppol (POST /v1/agent/peppol/send/{id}) and when checking status.
Foreign currency and exchange rate
Without currency the invoice is issued in the firm's currency (EUR for a Slovak firm, CZK for a Czech one). Send another currency in currency (EUR, CZK, USD, GBP, PLN, CHF, HUF, RON, BGN, SEK, DKK, NOK, HRK, RSD, UAH, RUB, TRY, CNY, JPY, CAD, AUD). exchange_rate is a decimal string greater than 0 (up to 7 integer and 8 decimal digits): units of currency per one unit of the firm's home currency. Slovak firm, 1 EUR = 1.0842 USD: "exchange_rate": "1.0842".
{
"customer_id": "550e8400-…",
"currency": "USD",
"exchange_rate": "1.0842",
"items": [ { "description": "Consulting", "quantity": 1, "unit": "ks", "unit_price": 500, "vat_rate": 0 } ]
}Without exchange_rate on a foreign-currency invoice the server fills in the ECB rate (Slovak firm) or the CNB rate (Czech firm) for the tax point (delivery_date, else issue_date). If no rate can be fetched the invoice is still created with exchange_rate: null. The response of POST /v1/agent/invoices and of GET /v1/agent/invoices/{id} carry exchange_rate (string or null) next to currency.
exchange_rate on a home-currency invoice returns 400. A credit note or debit note with original_invoice_id must use the currency of the original invoice, otherwise 400 - the message names the currency to send. The bank account (bank_account_id) must be in the same currency as the invoice (BANK_ACCOUNT_CURRENCY_MISMATCH).Idempotency (retry without duplicates)
POST /v1/agent/invoices supports an optional Idempotency-Key header: the primary protection mechanism against duplicate invoices on automatic retries. The same key with the same body returns the stored response (the invoice isn't created twice and the number series isn't consumed); the same key with a different body, or a concurrent request with the same key, returns 409. Without the header, behavior is unchanged.
POST /v1/agent/expenses. As an extra measure, store the returned id after a successful response.Downloading PDF / XML
You can download an issued invoice as PDF or as UBL XML. Both operations require the scopeinvoice:download and return the file as an attachment (Content-Disposition: attachment).
| Method | Path | Scope | Content-Type |
|---|---|---|---|
| GET | /v1/agent/invoices/{id}/pdf | invoice:download | application/pdf |
| GET | /v1/agent/invoices/{id}/xml | invoice:download | application/xml |
Once the invoice has been sent via Peppol, the XML is the exact UBL handed to the Access Point (kept for statutory e-invoice archiving). Before sending the endpoint returns a preview rendered by the same builder the send job uses — no DB write, no credit charged, no embedded attachments (those are embedded only at send time). The X-Efaktura-Ubl-Source: archive | preview header tells you which one you got. Use the preview to verify the business JSON → UBL mapping (units, VAT categories, parties). The file name is the invoice number ({numberPrefix}{number}.xml).
GET /v1/agent/invoices/b1f0d7a2-…/xml
X-API-Key: efk_pk_test_...
X-Organization-Id: 0a2c1f3e-…
HTTP/1.1 200 OK
Content-Type: application/xml; charset=utf-8
Content-Disposition: attachment; filename="OF2026001.xml"
X-Efaktura-Ubl-Source: preview
<?xml version="1.0" encoding="UTF-8"?>
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2">…</Invoice>Mutual set-off
A set-off (vzájomný zápočet) of an issued invoice against a received document of the same counterparty (e.g. a platform invoices a client for a service and records the client's fee as a received document). One call writes a payment with method offset on the invoice and on the received document and pairs them. Slovak firms only. To settle just one side, use POST /v1/agent/invoices/{id}/payments.
| Method | Path | Scope | Note |
|---|---|---|---|
| POST | /v1/agent/offsets | invoice:update + receipt:update | Supports Idempotency-Key. |
| GET | /v1/agent/offsets | invoice:read | Filters invoice_id, expense_id, page, per_page (≤100). |
| Field | Type | Required | Description |
|---|---|---|---|
invoice_id | UUID | Yes | Issued invoice, type invoice only. |
expense_id | UUID | Yes | Received supplier invoice of the same counterparty. |
amount | string | Yes | Positive amount with at most 2 decimals. Must not exceed the outstanding balance of either document (no rounding tolerance). |
offset_date | YYYY-MM-DD | No | Defaults to today (Europe/Bratislava). Not in the future and not before the issue date of either document. |
reference | string (≤100) | No | Your set-off / settlement number. |
note | string (≤500) | No | Note. |
POST /v1/agent/offsets
X-API-Key: efk_pk_test_...
X-Organization-Id: 0a2c1f3e-…
Idempotency-Key: 3c8e1d52-settlement-2026-10
Content-Type: application/json
{
"invoice_id": "b1f0d7a2-…",
"expense_id": "5b7e91c4-…",
"amount": "110.70",
"offset_date": "2026-10-31",
"reference": "SETTLEMENT-2026-10"
}
HTTP/1.1 201 Created
{
"data": {
"id": "3d9a60f8-…",
"invoice_id": "b1f0d7a2-…",
"expense_id": "5b7e91c4-…",
"amount": "110.70",
"currency": "EUR",
"offset_date": "2026-10-31",
"reference": "SETTLEMENT-2026-10",
"note": null,
"accounting_program": { "status": "pending", "reason": null },
"created_at": "2026-10-31T09:12:00.000Z",
"invoice": { "id": "b1f0d7a2-…", "total": "270.60", "paid_amount": "110.70", "outstanding": "159.90", "status": "partially_paid" },
"expense": { "id": "5b7e91c4-…", "total": "110.70", "paid_amount": "110.70", "outstanding": "0.00", "payment_status": "paid" }
}
}The payments of a set-off cannot be deleted on their own, nor can the documents it settled (409). A missing document returns 404. 400 errors:
| Code | Meaning |
|---|---|
OFFSET_NOT_SUPPORTED | Czech firm - set-off through the API is for Slovak firms only. |
OFFSET_INVOICE_TYPE | The invoice is not type invoice (self-invoices, proformas and credit notes cannot be set off). |
OFFSET_INVOICE_STATUS | The invoice is a draft, awaits approval or is cancelled. |
OFFSET_EXPENSE_TYPE | The document is not an approved received supplier invoice. |
OFFSET_COUNTERPARTY_MISMATCH | Contact, IČO and DIČ of the two documents all differ. For a person without IČO send the received document with the same contact_id as the invoice. |
OFFSET_CURRENCY_MISMATCH | The documents are in different currencies. |
OFFSET_DATE | The date is in the future or before the issue date of either document. |
OFFSET_EXCEEDS_BALANCE | The amount exceeds the outstanding balance of the invoice or the received document. |
accounting_program.status is pending and reason says why; once created it is sent with internal_document_id. For other programs the set-off is not exported automatically.Completeness requirements for Peppol
An invoice issued via this endpoint is fine as a regular document, but for it to be sendable via Peppol, the resulting UBL XML must pass EN 16931 + Peppol BIS Billing 3.0, plus the Slovak FS overlay. The SK overlay applies when the seller's country is SK, and requires:
| Rule (BT) | Field | Source in the API |
|---|---|---|
SK-BT-35 | Seller street | address.street of the organization |
SK-BT-37 | Seller city | address.city of the organization |
SK-BT-38 | Seller postal code | address.postalCode of the organization |
SK-BT-50 | Buyer street | customer.address.street |
SK-BT-52 | Buyer city | customer.address.city |
SK-BT-53 | Buyer postal code | customer.address.postalCode |
SK-BT-30 | schemeID for the seller's registration ID | derived from the organization's IČO/DIČ |
SK-BT-47 | schemeID for the buyer's registration ID | derived from the buyer's IČO/DIČ |
SK-BT-35-REQUIRED). So when creating an organization and when creating a customer, always send a full address (street + city + postalCode + country).Recipient routing in Peppol goes via the buyer's DIČ (sandbox: 9915:<DIČ>, production: 0245:<DIČ>). For correct VAT categorization (BR‑S/BR‑E), both the seller and buyer should have ic_dph. Before sending, you can pre-validate the UBL XML: see Validation.
You send an invoice issued by this endpoint via POST /v1/agent/peppol/send/{id}; right before sending, check it with a dry run via POST /v1/agent/invoices/{id}/preflight (see Recommended flow): no DB writes and no credit charge. The separate /peppol/preflight is only for UBL/CII you assemble yourself. If you assemble the UBL/CII yourself and don't want to go through /agent/invoices, you can validate, reserve credit, and send in a single call via Connector.