Overview
Expenses can be stored in two shapes:- Flat — a single amount plus one VAT rate. Fine for a utility bill, fuel
ticket, or any receipt with one subtotal. This is the original shape; you
send
amount+vatand the server stores it directly. - Itemized — an ordered array of
items, each with its own name, quantity, unit price, and VAT rate. This is how you model restaurant bills, supermarket receipts, or any multi-row supplier invoice where different rates apply to different goods.
Expense object. The items array
is always present on retrieve — for flat expenses the server synthesizes a
single line so API consumers can render a uniform table.
When to use itemized lines
Senditems whenever any of these are true:
- The receipt has two or more VAT rates (common in Romanian restaurants: 11% food + 21% drinks, or supermarkets that mix 5% / 9% / 19% baskets).
- You need per-line categories or per-line product associations for accounting or analytics.
- The customer wants the line detail preserved for SAF-T D406 or future audit exports.
Request shape
Create with items
items is supplied:
amount,vat, andvat_breakdownon the request body are ignored — the server derives all three from the lines.- The expense is persisted with real line rows (
firm_expense_line). - Header
vatis set to the dominant rate — the rate with the largest net across all lines. Ties resolve to first occurrence. vat_breakdownis populated automatically when two or more distinct rates are present;nullfor single-rate lines.
Rounding rule — matches printed receipts
Line aggregates are rounded to 2 decimals before they are summed into the header. This mirrors the standard Romanian invoice convention used byFirmBill and ensures the header equals the sum of the visible per-line
totals bit-exact.
For the example above:
| Line | Net (qty × unit_price) | VAT | Gross |
|---|---|---|---|
| Meniul zilei | 3 × 28.74 = 86.22 | round(86.22 × 0.11, 2) = 9.48 | 95.70 |
| Caserolă | 6 × 1.24 = 7.44 | round(7.44 × 0.21, 2) = 1.56 | 9.00 |
| Header | 93.66 | 11.04 | 104.70 |
quantity × unit_price values yourself — the API intentionally exposes
only the 2-decimal authoritative values.
Response shape
Retrieve always returns anitems array — at minimum one entry.
GET /expenses/{id}
Multi-rate without lines
If you don’t need per-line data but your receipt still mixes VAT rates, sendvat_breakdown alongside amount / vat on the flat path:
items entry from the header totals.
Update semantics
PUT /expenses/{id} with items replaces all line rows for that
expense. Send the full desired set, not a diff. Lines you omit are deleted.
Omitting items from the update body leaves the existing lines alone and
updates only the header fields you did pass (description, date, etc.).
Switching an expense between flat and itemized:
- Flat → itemized: send
items; header is derived from them, legacyamount/vaton the body are ignored. - Itemized → flat: send
amount+vatand an emptyitems: []to explicitly clear the lines. Otherwise the existing lines persist.
Field reference
Human-readable line name. Shows up in the app’s line table.
Positive number. Fractional quantities (e.g.
0.5 kg) are allowed.Net unit price (excluding VAT). The server always treats
quantity × unit_price as the line net; there is no per-line
with_vat flag.One of
0, 5, 9, 11, 19, 21.Optional longer description rendered under the line name.
Optional UN/ECE unit code (e.g.
H87 for pieces, KGM for kilograms).
Preserved round-trip but not validated.Optional per-line category override. Falls back to the expense-level
category_id when omitted.Optional link to a product in the firm’s catalog.
e-Factura inbound → lines
When an expense is reconciled from an inbound e-Factura (FirmSupplierInvoice),
the backend reuses the <cac:InvoiceLine> rows from the UBL XML to build the
canonical line shape. You never need to POST these manually for reconciled
bills — the expense already carries firm_expense_line rows matching the
printed invoice, and vat_breakdown is computed from them.
If you subsequently PUT items[] on such an expense, the e-Factura-derived
lines are replaced (standard replace semantics apply).