# Changelog 2026-09-14

Statistics now cover any period you ask for, products finally know both of their prices, and e-invoicing statuses speak the platform's language.

<!-- truncate -->

## Statistics, over any period

The statistics endpoints used to think in calendar years and months. They now take a date range and a bucket size, like the dashboard does.

Every endpoint that covers a period accepts the same three parameters:

- `from` and `to` — an inclusive date range, which takes precedence over `year`
- `year` — shorthand for a whole calendar year, defaulting to the current one
- `periodicity` — `Daily`, `Weekly`, `Monthly` (the default), `Quarterly` or `Annually`

Each bucket carries its own `range`, and a `partial` flag set when the requested range clipped it. A March bucket in a range ending on the 10th holds ten days of activity, so do not compare it with a full February at face value.

New endpoints:

- `GET /v1/Statistics/TurnoverComparison` — turnover over a period against the same period one year earlier and against the period of equal length just before it. Pass `expenses=true` for purchases, and `paid=true` to count what was settled rather than what was invoiced.
- `GET /v1/Statistics/CashFlowByPeriodicity` — receipts, expenses and profits, each as settled, outstanding and projected. Every list shares the same buckets, so index N is the same period throughout.

`GET /v1/Statistics/TurnoverEvolution` follows the same model. It takes `from`, `to`, `year` and `periodicity`, and `compare` (0 to 2) returns the same period one and two years back as extra series. It can also break down two new dimensions through `type`:

- `Margin` — what the lines brought in, less what their goods cost
- `Discount` — the gap between the gross amount and what was actually invoiced, line and document discounts included

⚠️ **Breaking change**: `TurnoverEvolution` now returns a single `TurnoverEvolutionResult` object instead of an array of years, and its `years` parameter is replaced by `compare`. The series are in `series`, the requested period first.

⚠️ **Breaking change**: these endpoints are removed, replaced by the ones above:

| Removed | Use instead |
| --- | --- |
| `GET /v1/Statistics/SalesTurnoverStatistics` | `TurnoverComparison` |
| `GET /v1/Statistics/ExpensesTurnoverStatistics` | `TurnoverComparison?expenses=true` |
| `GET /v1/Statistics/MicrobusinessSalesTurnoverStatistics` | `TurnoverComparison?paid=true` |
| `GET /v1/Statistics/MicrobusinessExpensesTurnoverStatistics` | `TurnoverComparison?expenses=true&paid=true` |
| `GET /v1/Statistics/StatsByMonth` | `CashFlowByPeriodicity`, or `TurnoverEvolution` for invoiced sales and expenses |

For a micro-business, `paid` is always applied: it is taxed on what it collects, not on what it invoices. Check `company.isMicroBusiness` in `GET /v1/Settings` before reading the numbers.

## Who owes you, and how late

Four endpoints answer the questions you ask before picking up the phone.

- `GET /v1/Statistics/UnpaidInvoices` — the aged balance of what customers owe you, as totals per age bucket
- `GET /v1/Statistics/UnpaidExpenses` — the same for what you owe suppliers. Subtract one from the other for a net position.
- `GET /v1/Statistics/TopOverdueCustomers` — customers with overdue payments, ranked by amount. `take` returns 1 to 20, 5 by default.
- `GET /v1/Statistics/PaymentDelay` — how long payments actually take over a period, from the document date and from the due date, plus the share of the amount paid late. `side` is `Customers` (the default) or `Suppliers`.

An aged-balance bucket is keyed by `period`: negative is overdue, `0` is due today, positive is upcoming. The steps are 1, 7, 15, 30, 60 and 90 days, so `-7` means more than 90 days late.

`TopOverdueCustomers` returns the age of each customer's oldest unpaid payment and `lastReminderOn`, the date of the last reminder sent to them. `null` means they were never chased, which is not the same as chased and still not paying.

For `PaymentDelay`, read `medianDaysToPayment` and `medianDaysLate` rather than the averages. A handful of very late payers can drag the mean to a month while the typical invoice is paid on time.

## Products, with both prices

A product now stores its sale price both excluding and including tax, and returns both, along with the margin figures derived from them:

- `taxExcludedPrice` and `taxIncludedPrice`
- `margin` — the tax-excluded price less the purchase price, per unit
- `markupRate` — the margin as a percentage of the purchase price (FR « taux de marge »)
- `marginRate` — the margin as a percentage of the sale price (FR « taux de marque »)

`margin`, `markupRate` and `marginRate` are computed on every save. A value you send for them is ignored.

Two fields are deprecated as a result:

| Deprecated | Use instead |
| --- | --- |
| `unitPrice` | `taxExcludedPrice` |
| `isUnitPriceTaxIncluded` | `taxIncludedPrice`, to send a tax-included price |

⚠️ **Behaviour change on read**: `unitPrice` is now always the tax-excluded price, and `isUnitPriceTaxIncluded` is always `false`. `unitPrice` used to hold whichever price the product was entered in, so for a product priced tax-included it now returns a different number. If your code read the flag before using the price, the result is the same. If it assumed the price included tax, read `taxIncludedPrice` instead.

Writing works as before. `unitPrice` on its own is read as tax-excluded, and `isUnitPriceTaxIncluded: true` still means the price you send includes tax.

## Markup is not margin

Document lines had a `marginRate` that divided the margin by the purchase price. That ratio is a markup rate, and products now have a real `marginRate` that divides by the sale price. Both could not share one name.

⚠️ **Breaking change**: on quote, invoice and credit lines, `marginRate` is renamed `markupRate`. The value is unchanged. Only the name is new.

## E-invoicing statuses, as the platform names them

`eInvoicingStatus` on invoices returns the code defined by the e-invoicing platform, not an internal name:

- `api:uploaded`, `api:validated`, `api:sent`, and so on, for the access point's own steps
- `fr:200` to `fr:228` and `fr:501` for the French lifecycle statuses — `fr:200` is *Déposée*, `fr:212` is *Encaissée*, `fr:213` is *Rejetée*
- `ppf:*` for the public platform's flows, and `aoz:*` for statuses added by Altoviz

⚠️ **Breaking change**: if you compared this field to names like `FrSubmitted` or `ApiSent`, switch to the codes. The full list is in the `EInvoicingStatusCode` schema.

One status is new: `aoz:importerror` marks a received invoice that could not be turned into an expense yet, because its Factur-X could not be read or its supplier could not be matched. It is retried automatically, and it is replaced by the next `fr:*` status the platform sends.

## Settings

`GET /v1/Settings` returns three new values:

- `sales.outstandingLimit` — the default maximum outstanding balance for a customer that has no limit of its own
- `sales.invoices.invoiceZeroQuantityLines` — whether invoicing a quote brings along lines with nothing left to invoice, such as a zero quantity or a line already invoiced in full
- `eInvoicing.validateAddressesBeforeSubmit` — whether both parties' e-invoicing addresses are checked before an invoice is submitted

`sales.marginRate` is removed.