TOVRReference

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
    }
  ]
}
FieldTypeMeaning
object"invoice_record"Always this value.
idstringinv_<uuid>. This is TOVR's id for the record, not the ERP's invoice number.
external_refstringThe 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.
issuerstring | nullWho 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_atRFC 3339 | nullThe date the ERP issued the invoice. Use this for reporting ("what did we invoice in August").
recorded_atRFC 3339 | nullWhen TOVR learned about the invoice. This is when invoice.recorded fired.
notestring | nullFree text that the dispatcher typed with the invoice. Often null.
coversarray | nullThe 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[]

FieldTypeMeaning
object"invoice_coverage"Always this value.
shipment_idstringThe transport, as shp_<uuid>. Refetch it through GET /v1/shipments/{id}. You need shipments:read for that call.
chain_item_idstring | nullThe 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 parameterDefaultMeaning
limit251 – 100.
cursor—The next_cursor from the previous page.
sortissued_atThe 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_ref TOVR already has, TOVR updates the record in place and emits no event. There is no invoice.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

StatusWhen
401Missing or expired token.
403The token does not have invoices:read.
404No such invoice in your organization.
422Unknown query parameter, sort other than issued_at, or an invalid cursor.
429 / 402Rate limit or monthly quota. See API conventions.

The change log entry for this resource is 2026-09-17-invoice-events.

On this page