Invoices
Read the freight invoices your organization has recorded — GET /v1/invoices and GET /v1/invoices/{invoice_id}, scope invoices:read. TOVR records invoices your ERP issued; it does not issue them, so there is no PDF, no due date and no payment state.
The invoice resource answers one question: which freight invoices has this organization
recorded, for how much, and which transports do they cover? It is the resource the
invoice.recorded event points at.
Both endpoints require the invoices:read scope. Every endpoint needs a
bearer token, and your organization comes from that token.
TOVR records invoices. It does not issue them. Your ERP creates the document and TOVR
records that it exists. That is why the wire object is invoice_record, not invoice.
There is no PDF, no due date, no payment state and no per-line money. Those fields are not
hidden. They do not exist. If you need one, ask the ERP that issued the invoice.
Why invoices:read is its own scope
invoices:read is separate from shipments:read. An invoice record carries an amount, which is
what the organization billed for a haul. An organization can let a factoring integration read
what it invoiced without also giving it the whole shipment book. It can also let a TMS read the
book without the money. Existing credentials do not get this scope automatically. An
organization admin adds it to the client application in Settings → Developer.
The invoice record
{
"object": "invoice_record",
"id": "inv_0b6f4a8e-5c1d-4e2f-9a7b-3c8d2e1f0a9b",
"external_ref": "2026-000917",
"issuer": "Balkan Freight OOD",
"amount": { "amount": "1500.00", "currency": "EUR" },
"issued_at": "2026-09-15T00:00:00.000Z",
"recorded_at": "2026-09-17T08:41:12.305Z",
"note": null,
"covers": [
{
"object": "invoice_coverage",
"shipment_id": "shp_1f2e9c40-7a3b-4d11-8e62-5b0c9d7a2e14",
"chain_item_id": null
}
]
}| Field | Type | Meaning |
|---|---|---|
object | "invoice_record" | Always this value. |
id | string | inv_<uuid>. This is TOVR's id for the record, not the ERP's invoice number. |
external_ref | string | The ERP's own invoice number, copied exactly. It is unique per organization, so reconcile against your own ledger with it. It is free text, so do not parse it. |
issuer | string | null | Who issued the invoice, as the ERP names them. It is null when nobody said. It is free text and never an id, because the issuer is outside TOVR. |
amount | { amount, currency } | The invoice total. amount is a decimal string with two decimal places, and currency is an ISO 4217 code. Always read the two together. |
issued_at | RFC 3339 | null | The date the ERP issued the invoice. Use this for reporting ("what did we invoice in August"). |
recorded_at | RFC 3339 | null | When TOVR learned about the invoice. This is when invoice.recorded fired. |
note | string | null | Free text that the dispatcher typed with the invoice. Often null. |
covers | array | null | The transports the invoice covers. Only the detail endpoint returns it. On a list row it is null, which means "not included here", never "covers nothing". It can grow after invoice.recorded as later transports are invoiced under the same number, and that growth emits no event — re-read it when you act on it. |
issued_at and recorded_at are two separate fields on purpose. An invoice issued on the 31st
and entered on the 3rd belongs to the earlier month, and invoice.recorded fires on the 3rd.
Report on issued_at. Use the difference between the two to measure recording latency.
covers[]
| Field | Type | Meaning |
|---|---|---|
object | "invoice_coverage" | Always this value. |
shipment_id | string | The transport, as shp_<uuid>. Refetch it through GET /v1/shipments/{id}. You need shipments:read for that call. |
chain_item_id | string | null | The freight-chain leg the transport was invoiced as, when the invoice was raised against a chain. It is an opaque uuid with no endpoint of its own. null means the transport was not invoiced as part of a chain. It does not mean "leg zero". |
Covered transports have no amounts, and you must not calculate any. TOVR does not store per-line money. If we split the total across the lines, that split would be our invention. A sum of lines next to a header total would also give one number two sources. If you need money per transport, get it from the ERP.
List invoices
GET /api/v1/invoices
Returns the organization's recorded invoices, newest issued_at first, with a cursor for
the next page.
| Query parameter | Default | Meaning |
|---|---|---|
limit | 25 | 1 – 100. |
cursor | — | The next_cursor from the previous page. |
sort | issued_at | The only accepted value is issued_at. |
external_ref | — | Finds an invoice by the ERP's own number, as an exact match. |
The parameter list is strict. An unknown parameter returns 422, not a page that silently
ignored it.
curl "https://tovr.eu/api/v1/invoices?limit=50" \
-H "Authorization: Bearer <access_token>"{
"object": "list",
"data": [
{
"object": "invoice_record",
"id": "inv_0b6f4a8e-…",
"external_ref": "2026-000917",
"issuer": "Balkan Freight OOD",
"amount": { "amount": "1500.00", "currency": "EUR" },
"issued_at": "2026-09-15T00:00:00.000Z",
"recorded_at": "2026-09-17T08:41:12.305Z",
"note": null,
"covers": null
}
],
"has_more": false,
"next_cursor": null
}Unlike the change feed, this list's next_cursor is null on the
last page. A cursor that is not valid for the sort returns 422 with code: "invalid_cursor".
If you get one, restart from the first page.
Look up an invoice by the ERP's number
curl "https://tovr.eu/api/v1/invoices?external_ref=2026-000917" \
-H "Authorization: Bearer <access_token>"external_ref is unique per organization, so the page contains at most one record. If the
reference does not belong to your organization, you get an empty page, not a 404. A 404
would confirm that another organization has an invoice with that number.
Get an invoice
GET /api/v1/invoices/{invoice_id}
Returns the record and the transports it covers (covers is populated). invoice_id is the
inv_… id, and a bare uuid is also accepted. It is not the ERP's number. To look up by the
ERP's number, use ?external_ref= above.
curl https://tovr.eu/api/v1/invoices/inv_0b6f4a8e-5c1d-4e2f-9a7b-3c8d2e1f0a9b \
-H "Authorization: Bearer <access_token>"An invoice that does not exist and an invoice that belongs to another organization both return
404. Neither returns 403: a 403 would confirm the record exists. On a record whose key is
a guessable ERP sequence number, that would tell a caller how many invoices another organization
has issued.
What is not in this resource
- Market freight. An invoice recorded against a public-market shipment belongs to no organization. It never appears here and it produces no event.
- Corrections as events. If the ERP re-sends an
external_refTOVR already has, TOVR updates the record in place and emits no event. There is noinvoice.updated. If you depend on amounts not changing, re-read the record. Do not rely on the copy you fetched when the event arrived. - Writes. There is no
invoices:write. TOVR records an invoice when a dispatcher marks a transport as invoiced and enters the ERP's reference. It does not accept invoices over the API.
Errors
| Status | When |
|---|---|
401 | Missing or expired token. |
403 | The token does not have invoices:read. |
404 | No such invoice in your organization. |
422 | Unknown query parameter, sort other than issued_at, or an invalid cursor. |
429 / 402 | Rate limit or monthly quota. See API conventions. |
The change log entry for this resource is
2026-09-17-invoice-events.
Shipments
Create a shipment (a nested tree of stops, contents, costs, and identifiers), list your shipments, get one by its master ID, and change its status or visibility.
Change feed
A resumable feed of what changed. Poll GET /v1/events with a cursor, hold open GET /v1/events/stream, or receive the same events as webhooks. Each event carries an id and a type, never a row — you refetch through the normal read.