---
title: "Changelog"
description: "Every change to the Flowie Exchange API, newest first. Additive, deprecation, and breaking changes are all tracked here."
canonical: "https://docs.get-flowie.com/changelog"
source: "https://docs.get-flowie.com/changelog.html"
---

# Changelog

Changelog

# What's new

Every change to the Flowie Exchange API, newest first. Additive changes land continuously under `/v1/`; deprecations are announced six months in advance and flagged with a `Sunset` response header.

How to read this

New additive — always safe to adopt.  
Changed behavior refined — read carefully.  
Deprec sunset date announced.  
Break only ever in a new major (`/v2/…`).  
Fix bug fix. 

## v3.18.3 — A company with no declared line is still addressable

2026-09-18

Found by running the signing flow end to end rather than reading the spec: the port could be opened and then not signed.

Fix

**`addressScope` defaults to the company’s own SIREN and SIRET** when the annuaire holds no addressing line and the company is not on Peppol. It used to come back empty, which made the mandate miss a decree item — so [Change platform](<portability/index.html>) offered a signature and `POST /v1/portability/requests/{ref}/mandate/signature/document` then refused it with `409 addressScope still missing`. A French company is addressable by its SIREN and SIRET whether or not anyone declared a line for it; both go in, because a port covering the legal unit but not the establishment leaves invoices addressed to the SIRET arriving at the platform you are leaving. A declared line still wins, and a non-French registration gets no invented scope.

## v3.18.2 — Sign the agreement with your own document

2026-09-18

There were two ways to sign, and neither fits a taxpayer whose legal representative is not a Flowie user: an approval check needs an account, and asserting a paper signature keeps nothing at all.

New

**`POST /v1/portability/requests/{ref}/mandate/signature/document` — upload the agreement you signed.** Multipart: the file, who signed it, when. We store the document under your organization and record the signature bound to it, so it replays onto the mandate like any other and `mandateGaps` closes.

New

**`agreementSha256` is the digest of _your_ file, not of our rendering.** They are different documents — you may have signed your own wording, or your advocate’s — and recording ours as the one signed would be a statement we cannot support. `mandateSha256` still binds to the decree content, so amending the mandate afterwards invalidates this signature exactly as it invalidates an in-app one.

New

**The same thing from[Change platform](<portability/index.html>), without an account.** When a request comes back needing a signature, the page asks for the name, the date and the file, and records it. Name, date and file together or not at all: a name with no document is the assertion the platform stopped accepting.

Changed

**A signature cannot be dated in the future** (`400`), a file over 10 MB is refused (`413`), and an agreement that already has a signature cannot receive a second one (`409`) — the evidence chain folds signatures in order, so accepting one would replace the recorded signatory, instant and document with no trace of the first. An act dated after the moment it is recorded is worse than an acknowledged gap.

## v3.18.1 — Signing a mandate reaches the services it depends on

2026-09-18

Shipped hours earlier in [v3.16.23](<#2026-09-18-sign-in-app>), and it would have failed on the first real call.

Fix

**`POST /v1/portability/requests/{ref}/mandate/signature` called approval and the documents service with no credentials.** Both calls opened their own HTTP client, so neither carried the bearer token nor the actor headers the documents service authorises a write by — every attempt would have come back `401`/`403` and surfaced to you as `502 The approval service did not answer`, which reads like an outage rather than a missing credential. They now go through the same authenticated path as every other internal call.

Fix

**The check is scoped to your organization again.** Approval reads `X-Flowie-Organization-Id`; the shared client sends `X-Organization-Id`. A call carrying only one of the two is either unscoped or unauthenticated, so both go out now.

## v3.18.0 — Agent plugin: the tools, the skills and the CLI on one page

2026-09-18

The MCP server and the hosted agent skills existed but were documented apart, and the skills were not documented at all. There is now one page that sets an agent up.

New

**[Agent plugin](<build-with-ai/plugin.html>) — public beta.** One setup covering all three agent surfaces: the MCP server (`/mcp` curated, `/mcp/full`), the 14 hosted agent skills, and the CLI. Copy-paste quickstarts for Claude Code, Cursor, VS Code and Codex, a one-request check that the connection works, and an explicit statement of what beta covers — endpoints, auth and the skill index URL are under the [deprecation policy](<deprecation-policy.md>); individual tool names and skill contents can still change and are announced here.

New

**The agent skills are documented.** Fourteen skills have been served at `/.well-known/agent-skills/index.json` with a per-skill `sha256` digest, and no page named them. They are now listed, with what each one covers and how to load it in a client that does not read the index by itself.

Fix

**A failed MCP mount now stops the process instead of logging a warning.** When a dependency resolve broke `FastApiMCP`, the app started healthy and `/mcp` answered `404` on every environment while the documentation advertised 34 tools. All three mounts — curated, full and discovery — now raise at import, so a rollout catches the failure rather than a customer.

## v3.17.2 — The same correction, on the other four deposit paths

2026-09-18

v3.17.0 stopped a raw-body deposit being stored under a type its bytes contradict. It reached one of the five ways to deposit a file; this reaches the rest.

Fix

**A file sent as a base64`file` attachment is stored under the type its bytes actually are.** That covers JSON mode on [`POST /v1/documents/send`](<reference/index.html#send-document>), `POST /v1/documents/send/batch` and the event path. A PDF declared as `application/json` — the default most HTTP clients send — was persisted as JSON, because the declared type was believed whenever the caller supplied one and the bytes were only consulted when they had not. Nothing that is accepted today starts being refused, and a deposit whose declaration was already right is unaffected.

Fix

**A declaration more precise than the bytes survives, including the ones an allowlist had missed.** Only an unambiguous binary container overrides the declaration, and text is never second-guessed. A container subtype is kept whenever it says more than the container does: a structured syntax suffix that matches the bytes ([RFC 6839](<https://www.rfc-editor.org/rfc/rfc6839>)’s `+zip` / `+gzip` — this is how a signed `application/vnd.etsi.asic-e+zip` e-invoice keeps its identity), and any type in the vendor tree ([RFC 6838](<https://www.rfc-editor.org/rfc/rfc6838>) `application/vnd.…`), which names one specific format and so is never the generic container itself — an `.xlsm`, `.ppsx`, `.odg` or `.apk` is no longer flattened to `application/zip`. A suffix that _contradicts_ the bytes is still a mislabel and is still corrected.

Fix

**A declaration that names no type at all no longer becomes one.** `contentType=pdf` — a file extension where a media type belongs — used to be stored verbatim as `pdf`; it is now read from the bytes as `application/pdf`. A declaration that is only parameters (`; charset=utf-8`, which is what an ERP concatenating an empty base type sends) or only whitespace now falls back to the sniff instead of being persisted as the document’s media type.

## v3.17.1 — 0147 is Shine, and the Confidence column is back

2026-09-18

Fix

**`0147` belongs to _Shine_ , not _PAYFLOWS_.** PAYFLOWS held it by inference: its own SIREN routes on `0147` in the PPF annuaire. That is because it is a Shine client, not because the number is its own. Shine states the number in its terms of use (§4.1), so the row is now `published` with that quote. `GET /v1/portability/platforms` returns `matricule: "0147"` on Shine and `null` on PAYFLOWS. Fifteen rows are `published` and 63 `inferred`. 92 of 165 rows still carry a matricule.

New

**The directory shows a Confidence column again.** It says how each number was established (_confirmed_ , _published_ , _inferred_ or _unknown_), and it sorts. Like the confidence filter, it appears only to a signed-in reader, the same reader who can see the numbers.

## v3.17.0 — `gzip` request bodies, and a deposit stored as what it is

2026-09-18

Both changes are additive: no request that is accepted today starts being refused.

New

**`Content-Encoding: gzip` is decompressed on the way in.** Send a gzip-compressed body (`x-gzip` and lists that reduce to gzip, such as `identity, gzip`, work too) on any endpoint, including the raw-body form of `POST /v1/documents/send`, and the handler reads the payload you meant to send. Useful when the document carries an embedded PDF: an ERP invoice of that shape typically drops by about a third on the wire. A body this service cannot inflate — an encoding it does not implement, a payload that is not really gzip, or one that would expand past its ceiling — is passed through untouched, exactly as before, so nothing that worked stops working.

Fix

**A raw-body deposit is stored under the type its bytes actually are.** Raw-body mode keeps the payload verbatim under the `contentType` and `filename` you pass in the query string, and nothing checked that the two agreed — a compressed or binary payload deposited as `application/xml` was persisted as if it were XML, so nothing downstream could read it. The declared type is now corrected from the first bytes, the same way the stored file extension already was. Only an unambiguous binary container overrides the declaration; text is never second-guessed, and a more specific declaration wins over a broader one (an `.xlsx` stays an `.xlsx`, not a zip).

Changed

**Two consequences of that correction, for clients that read them.** `storedFormat` now answers `gzip` for a gzip payload where it answered `binary`, on every deposit route. And when you send no `Idempotency-Key`, the implicit key is derived from the payload including its content type — so a deposit whose declared type was wrong hashes differently after this release, and a retry that crosses the upgrade mints a new document rather than replaying the old one. Only previously-mislabelled deposits are affected; a correctly declared one keeps the same key.

## v3.16.23 — The designation agreement can be signed in Flowie

2026-09-18

A mandate could be _recorded_ as signed — on paper, or in your own tool — but there was nowhere to actually sign one. That left the act the whole port rests on outside the platform.

New

**[`POST /v1/portability/requests/{ref}/mandate/signature`](<reference/index.html#sign-portability-mandate>) puts the agreement in front of someone.** The text is rendered from the mandate the request already holds — the five items article 242 _nonies_ E bis requires — and an approval check is opened on it for the Flowie users you name. They sign by deciding; what is stored is their decision, its instant and its author, none of which the API supplies. The response carries the text as presented and its digest, so you can show exactly what was signed. A request opened on an identifier alone is refused with `409` rather than rendering the agreement with blanks where the decree wants values.

New

**[`GET /v1/portability/requests/{ref}/mandate/signature`](<reference/index.html#read-portability-mandate-signature>) reads the decision back.** `signed` stays `false` while the check is open, refused or withdrawn. The first read that finds a passed one writes the signature into the hash-linked evidence chain, so the request carries its `signedAt` from then on whether or not this endpoint is called again.

New

**Only a decision on _this_ agreement is a signature.** The check carries the context key `portability:<requestRef>:<mandateSha256>`. An approval vote is scoped to the object it hangs on, so without that key a decision taken on the same document for another reason would read here as a signature. Move the _date d’effet_ or the address scope and the digest moves with it — the earlier decision stops answering for the new agreement, because it is not the agreement that was signed.

## v3.16.22 — The request says what the agreement still needs

2026-09-17

A request opened from [Change platform](<portability/index.html>) showed its four deadlines and nothing else, so it read as complete — while the plan directly above it said, correctly, that a dated and signed designation agreement is required.

New

**`mandateGaps` on [`POST /v1/portability/requests`](<reference/index.html#portability>) and `GET /v1/portability/requests/{ref}`.** Which decree items are still missing — `signatory` when nobody is named, `signature` while the agreement is unsigned. They were only ever inside the first entry of the evidence chain, so a client had to dig for the one fact that decides whether a port can proceed. It sits _beside_ the mandate, never inside it: that object is hashed, and a derived list folded in would change its own digest the moment you signed it.

Fix

**The page no longer says nothing is missing when the agreement is.** “Nothing missing: this company can be filed as it stands” was about the _company’s identifiers_ , four lines above a block asking for a signed agreement that had never been collected. It now reads “this company is fully identified”, and the receipt lists what the agreement still needs in words. _You re-grant_ became _You provide_ in all three languages.

## v3.16.21 — A name in a field is no longer a signature

2026-09-17

The designation agreement is what authorises a port: it is what the outgoing platform may object to as `mandate_invalid`, and what the administration asks for when a port is contested.

Changed

**`signatory` declares who will sign; it no longer signs.** Supplying the name used to stamp `signedAt` with the server’s clock, so the mandate claimed to be signed when nobody had seen a document and nothing was kept. `signedAt` now stays `null` until an act is recorded, and `mandateGaps` reports `signature` until then — a gap the outgoing platform would otherwise raise five business days later. A client that read `signedAt` as proof was reading its own input echoed back.

New

**`signedAt` and `signatureMethod` on [`POST /v1/portability/requests`](<reference/index.html#portability>).** A mandate signed outside Flowie — on paper, or in your own tool — is signed, and these record it: you assert the instant, we keep it verbatim in the hash-linked evidence chain with how it was taken (`paper`, `external`, `approval`). What the platform will not do is invent the instant on your behalf.

New

**A signature is bound to the agreement it signed.** The record carries the SHA-256 of the mandate’s data and of the rendered document, so amending the _date d’effet_ or the address scope afterwards leaves a signature that no longer verifies — the port that was agreed to is not the port being made. The two digests are separate on purpose: rewording the document does not void past signatures.

## v3.16.20 — The deadlines read as dates, in the page’s own language

2026-09-17

The four clocks on [Change platform](<portability/index.html>) are legal deadlines — notification, objection window, _date d’effet_ , twelve-month continuity — and every one of them printed exactly as the API spells it.

Fix

**`2026-09-21` became _21 septembre 2026_ on the French page, _21 settembre 2026_ on the Italian one.** An ISO date on a French page is foreign at best and ambiguous at worst: a reader who takes it for a day-month reads the objection window as March. The month is spelled out in the page’s own language, which no locale can misread, and the ISO value stays in the `<time datetime>` attribute for anything parsing the page. The deadline a company is given is the one thing on that page that must not be guessable.

## v3.16.19 — The company picker searches the real directory

2026-09-16

Typing a real company into [Change platform](<portability/index.html>) found nothing, so there was no company to select and no migration to open.

Fix

**Finding a company by name now asks production, like the three registry reads beside it.** Who routes a company, the platform behind a matricule and the access point outside France were pointed at the real directory on 14/09; the name search was left on the page’s own base, which is staging, where `PPF_ANNUAIRE_ENV=sandbox` points the directory at the PISTE _test_ registry on purpose. A real company is not in it: _UP TO TECH_ (SIREN 912494937), routed by platform `0022` in the real directory, came back as an empty list. Nothing that _writes_ moved — resolving a taxpayer and opening a request stay on the page’s own sandbox tenant.

## v3.16.18 — Four more numbers, and the table sorts and filters again

2026-09-18

New

**Four more matricules confirmed by their own operators.** _GURUSOFT_ `0136` and _SUPER PDP_ `0111` were unknown; _KLEKOON_ `0112` and _FIDUCIAL CLOUD_ `0102` were inferred. Fourteen rows are `confirmed` and 92 of 165 carry a number. Super PDP's answer reached us on 16 September rather than in this round, and its evidence carries that date.

Fix

**The Matricule column sorts again, and the search matches the number.** Gating the column took its sort with it and dropped the number out of the search haystack. Rows with no matricule sort last in either direction, as the gaps should.

New

**Filter by how a number was established.** _Confirmed_ , _published_ , _inferred_ and _unknown_ are chips again, next to the status ones. They appear once the column has filled and go away with it: offering them to a reader who cannot see a single matricule would only empty the table.

## v3.16.17 — A signed-in reader on the wrong environment is told so

2026-09-17

Fix

**The matricule column stayed empty for a signed-in reader whose Environment selector said _Production_.** Signing in on this site always issues a token for one tenant, but the selector rewrites the API base for whichever environment you pick, and the base is only ever filled when missing — never corrected. Pick the other environment and every authenticated call is answered `401 Invalid token`. The lookup now tries the base the session names and, if that is refused, the base the token was issued for; the directory is identical in both, so either answers the same thing.

Changed

**A refused sign-in no longer reads as a missing one.** Both rendered _not public_ , so a reader who was signed in, and whose token had simply been rejected, saw exactly what a signed-out visitor sees. That state now says so in its own words.

## v3.16.16 — Ten operators answered, and the page stopped quoting the others

2026-09-17

We asked every Plateforme Agréée whether the `matriculePlateforme` we held for it was right. Ten replied with the number themselves — and several replied that the number is not ours to publish at all.

New

**Six more matricules carry`matriculeConfidence: "confirmed"`.** _B4VALUE.NET_ , _GROUPE SIGMA_ and _Aruba S.p.A._ were unknown until their operators told us; _Weproc_ , _Paragon_ and _Docoon_ confirmed numbers we had only published or inferred. Ten rows are now `confirmed`, and coverage is 90 of 165. The four levels are `confirmed`, `published`, `inferred` and unknown, in that order of authority.

Changed

**The only matricule a signed-out reader can see on[Plateformes Agréées](<compliance/fr/platforms.html>) is ours.** The table column has been behind a docs sign-in since v3.16.15, but the page still worked through a dozen other operators' numbers to explain its method — including two who had asked us to stop. Every illustration now uses `0064`, which is Flowie's. The arguments are unchanged; they simply stop borrowing other people's numbers.

Fix

**The signed-in column actually fills now.** It read `payload.platforms` where [`GET /v1/portability/platforms`](<reference/index.html>) answers `{data, total, source, snapshotDate}`, so the lookup threw into a silent catch and a signed-in reader saw _not public_ exactly like everyone else. The catch now says something when it fires.

## v3.16.15 — The matricule is for the platforms, not for the open web

2026-09-17

We asked all 157 reachable Plateformes Agréées to check the `matriculePlateforme` we publish for them. Two answered that the DGFiP treats the number as something the platforms exchange between themselves through the PPF annuaire, not as something to publish. It is no longer public.

Changed

**[Plateformes Agréées](<compliance/fr/platforms.html>) shows the matricule only to a signed-in reader.** Anyone else sees _not public_. The number, how it was established and the evidence behind it have also left `assets/pa-directory.json`, the static file that page reads — hiding a table column would have meant nothing while the asset itself was one fetch away. The _Confidence_ and _Evidence_ columns are gone with it: both restated the number in prose. Row anchors are the platform name now, so a link to `#0003` lands on the page rather than on the row.

New

**`matriculeConfidence` can be `confirmed`.** Four operators replied to that round with the number themselves, which beats anything we can read on a website or derive from the annuaire, and `matriculeEvidence` carries when and how they told us. Docnova / MELASOFT was unknown until they answered. Read `confirmed` as a stronger `published`: it does not change what the field means, only how sure we are.

Changed

**[`GET /v1/portability/platforms`](<reference/index.html>) is now the only way to the numbers**, and it authenticates you, as it always did. Nothing about the endpoint changed — it is simply no longer mirrored into a file the open web can read.

## v3.16.14 — A Factur-X is judged against its own profile, not the core one

2026-09-16

A conformant EXTENDED credit note was refused over a rounding cent its profile allows, and the emitter was told the fault was theirs.

Fix

**A deposited CII is no longer pinned to the EN 16931 core schematron.** The validator reads the Factur-X level from BT-24 and routes the document to the ruler that level belongs to — but only when no syntax is announced, and we were announcing `CII`. A document declaring `urn:cen.eu:en16931:2017#conformant#urn:factur-x.eu:1p0:extended` was therefore measured against `BR-S-08`, which allows no rounding difference at all, instead of its own `BR-FXEXT-S-08`, which allows one cent per line, allowance and charge. Credit notes whose VAT breakdown and line total differed by exactly that cent came back `NON_CONFORME BR-S-08`. They are now accepted, as the profile they declare always intended. Genuine failures are unaffected: a line with no item name is still refused on `BR-25`. UBL is unchanged — it carries no level to discover.

## v3.16.13 — A `cii-xml` we cannot read is refused, not recorded empty

2026-09-16

Changed

**`format: "cii-xml"` answers `422` when the payload cannot be read as a CrossIndustryInvoice** — not a CII, no invoice number, no issue date, neither a line nor a stated total, or an encoding other than UTF-8. It used to reach the UBL pipeline, which finds no UBL header in a CII and recorded a document with no buyer and 0.00 totals. Send `format: "raw"` to archive such a payload as-is, or `"json"` to state the document yourself.

Fix

**One CII detector for the deposit and for`send`.** Each carried its own copy of the window / BOM / root-name dance, so a CII written in UTF-16 was recognised by one path and archived raw by the other.

## v3.16.12 — A Factur-X sent through `send` is read, not archived

2026-09-16

A Factur-X is a PDF with the invoice inside it. The deposit route read that invoice; `POST /v1/documents/send` kept the PDF and never opened it, so an outgoing invoice was recorded with no number, no buyer and 0.00 totals, and had nowhere to be delivered.

Changed

**[`POST /v1/documents/send`](<reference/index.html#send-document>) reads a Factur-X PDF or a CII.** With `format=auto` (or no format) and `type` set to an invoice, a credit note or a debit note, a Factur-X PDF/A-3 in `file`, a bare CII file, or a CII in `xml` becomes a structured document: the number, dates, seller, buyer, lines and totals come from the CII, the document type from its BT-3, the CII is kept as the original and the PDF as the readable copy. The response is the structured one — `deliveryStatus` `pending` or `awaiting_registration` and the invoice number — instead of `stored` with a `fileId` and a `storedFormat`. Send `format=raw` to keep archiving a Factur-X without it being read. A file that is not a readable invoice is stored exactly as before: an ordinary PDF, a self-billed document, a CII with no issue date or with neither a line nor a stated total, or a CII not encoded in UTF-8.

Fix

**`format: "cii-xml"` reads the CII.** The value was accepted and then ignored: a CII sent with it went through the UBL pipeline, which found no UBL header and recorded a document with no buyer and 0.00 totals.

## v3.16.11 — The answer says which directory it came from

2026-09-16

Two registers behind the French lookups have a test twin, and a test register that has never heard of a company is not the same thing as a company that does not exist. The payload could not tell you which one you were reading.

New

**`directory` on [`GET /v1/portability/companies`](<reference/index.html#portability>) and [`GET /v1/portability/annuaire/{siren}`](<reference/index.html#portability>)** — `production` for the real PPF annuaire and Peppol Directory, `sandbox` for their test twins. It is what makes `declared: false` readable: in the sandbox a taxpayer who is really routed by a platform comes back with no lignes at all, so the answer is about the test dataset and never about the taxpayer.

Changed

**A sandbox answer is never`exhaustive`.** A company search for a real company returned an empty list that asserted it was the whole answer, because the list genuinely was complete — for a register that was never going to hold it. An empty sandbox answer now carries a `reason` naming the registers that were searched, and a row found there is returned as before but still not called exhaustive. A register that did not answer keeps its own reason: an outage and an empty register are different failures.

New

**Thirty-one more[Plateformes Agréées](<compliance/fr/platforms.html>) are named behind their matricule**, taking the directory from 55 of 165 to 86 — among them SEPTEO `0049`, EURO INFORMATION `0013`, INFOLOGIC `0122`, SERES `0020` and DEXT `0028`. Each number is proven from the operator’s own SIREN routing its own lignes in the annuaire, and where several platforms sit on one matricule the earliest-immatriculée is the operator and the rest are its clients — which is what keeps TUNGSTEN and EY off CEGEDIM’s `0003` and SAGE off AVALARA’s `0035`. Anything we could not prove stays published as unknown, because a wrong matricule routes invoices to the wrong platform.

## v3.16.10 — Three compliance pages catch up with the regulators

2026-09-14

Three country pages were quoting mandate dates the states behind them had already moved. The product was never wrong — the documentation was stale, which on a compliance page is the same thing to a reader planning a rollout.

Fix

**[Spain](<compliance/es.html>) — Veri*Factu is not live, and was not live in July 2025.** The page dated the obligation to 2025-07-01 for corporate income tax payers. _Real Decreto-ley_ 15/2025 of 2 December 2025 (BOE-A-2025-24446) postponed it to **2027-01-01** for corporate income tax payers and **2027-07-01** for all remaining taxpayers. The deadline table, the TL;DR, the background prose and the FAQ all said “already live for corporate”; none of them do now, and the decree and the AEAT note on the extended deadline are linked as primary sources.

Fix

**[Poland](<compliance/pl.html>) — the schema is FA(3), not FA(2).** KSeF 2.0 replaced FA(2) with **FA(3)** on 2026-02-01, for every structured invoice regardless of the source document’s date — so a page still naming FA(2) described a format the platform had stopped accepting seven months earlier. Every mention is corrected, including the `ksef.invoiceType` field description and the `KSEF-21100` error row, and the Ministry of Finance page defining the mandatory scope is linked.

Fix

**[UAE](<compliance/ae.html>) — 1 July 2026 is a voluntary pilot, not the mandate.** The page presented it as Phase 1 going mandatory. Per Ministerial Decisions 243 and 244 of 2025, the pilot opens 2026-07-01 and the first obligation lands **2027-01-01** for annual revenue of **AED 50m or more** , then 2027-07-01 for all other VAT-registered businesses and 2027-10-01 for government entities. The ASP appointment deadline the page carried (2026-07-31) has moved to **2026-10-30**.

New

**The UAE page says that B2C is out of scope.** Article 4 of Ministerial Decision 243 of 2025 excludes business-to-consumer transactions from the mandate until the Minister decides otherwise, along with certain financial and airline services. The page had been silent on it, which reads as “in scope” to anyone sizing the work.

Changed

**“Facts last refreshed” now dates each country, not the build.** The stamp was the build date, so regenerating the site moved all 44 country pages to today whether or not anything had been re-verified. It now comes from the country record, and only a country actually re-checked against its regulator carries the newer date.

## v3.16.9 — The search names the platform you are on

2026-09-14

A switch starts with knowing who you are switching _from_. The result carried a row for it, but it printed a bare four-digit `matriculePlateforme` — and on the public site it printed nothing at all.

New

**[Change platform](<portability/index.html>) names the platform routing the company today**, in the single result and in the bulk table: _CEGEDIM · 0003_ rather than `0003`. The number identifies a platform but names none, so it is joined against the [Plateformes Agréées directory](<compliance/fr/platforms.html>), and the join carries its own evidence — a matricule the operator publishes is marked as such, one we inferred from its own routing is marked _inferred_ , because that is strong evidence and not proof. Outside France there is no registry to read, so the answer comes from the access point certificate instead, and the row names the provider that serves that Peppol participant.

Fix

**That row was empty on the public site.** The page asks staging, and staging points the annuaire at the PISTE _test_ directory on purpose, so it answers “nothing declared” for every real company. Who routes a taxpayer is public registry data, so those three read-only lookups — the annuaire line, the matricule join and the access point — now ask production, whatever environment the rest of the page uses. Nothing that writes moved: resolving a company and opening a request stay on the sandbox.

Fix

**A company outside France was looked up in the French annuaire.** A SIREN was derived from any identifier with nine digits in it, so a Spanish Peppol participant became a nine-digit “SIREN” that no annuaire would ever match, and its access point was never consulted. The country decides which registry can answer, as it already did for which identifiers are displayed.

## v3.16.8 — Changing platform works for a French company on Peppol

2026-09-14

[`POST /v1/portability/resolve`](<reference/index.html#resolve-portability-taxpayer>) and the company search behind it looked a French company up on Peppol with the exact participant filter — the query the network does not answer for French companies, which publish addressing lines (`0225:{siren}_{suffix}`) rather than `0009:{siren}`. A company the directory reports as active with a dozen lines came back “not on the Peppol network”, on the very path where a customer is trying to become reachable.

Fix

**A French company is resolved through its addressing lines.** The same reading the directory endpoints use, so the three surfaces now agree about the same company. An id naming a mailbox the network does not carry still resolves to nobody, and a directory that did not answer is reported as such rather than as an absent company.

Changed

**One company, one suggestion.** A name search returned one row per mailbox — thirteen results reading “SOCIETE AIR FRANCE”, between which nothing lets you choose. French addressing lines are folded into the company they address. Rows are also credited to the register that answered: a company found on the network no longer reports `resolvedFrom: "annuaire"`.

## v3.16.7 — Verify answers about the network, not the company register

2026-09-13

[`POST /v1/directory/verify`](<reference/index.html#verify-recipient>) answered `exists: true, canReceive: true` for every French company listed in the PPF annuaire, whatever its Peppol state. The annuaire lists French legal entities; the network lists mailboxes. The same company came back from `GET /v1/directory/{peppol_id}` with no Peppol line at all — one API, one ID, two opposite answers — and a sender who ran the recommended pre-flight check was told “yes” about a document that could not be delivered.

Changed

**`exists` now means “on the Peppol network”.** It used to be true for any French company the PPF annuaire knew. A client that gates sends on `exists` will stop sending to French recipients that are not on the network — which is the point, but it is a change of meaning on a field that already existed. Document-type matching is now on whole names too: `order` no longer matches a participant that only advertises `order-response`, and an empty `documentType` matches nothing rather than everything.

Fix

**Verification reads the company’s Peppol lines.** `exists` now means “on the network”, and `canReceive` that a line actually advertises the document type asked for. A French company the annuaire knows but Peppol does not now answers `false` to both, and is still named in `recipientName`. An ID that names one addressing line answers for that line alone.

New

**`acceptedBy`** lists the participant IDs that advertise the requested document type — the addresses to send it to. A company ID such as `0009:{siren}` is not one of them, so a “yes” now comes with somewhere to send.

## v3.16.6 — French companies on Peppol are found, with every line

2026-09-13

A French company looked up by SIREN (`GET /v1/directory/0009:{siren}`) came back with no Peppol presence — no document types, no `smpStatus` — unless it was published under that exact participant ID. The network lists French companies under the French addressing scheme `0225`, often only as suffixed lines (`0225:{siren}_{suffix}`): of 22 large French companies checked, 21 were on the network and none under `0009:{siren}`. Callers read that as “not on the network”.

Fix

**The lookup reads every Peppol line of the company.** For a French company ID, `smpStatus` is `active` when any line is on the network, `documentTypes` lists what at least one line receives and `registeredSince` is the earliest line’s. Looking up one line directly (`0225:{siren}_{suffix}`) still describes that line alone, and a line the network does not carry is not reported as reachable. The PPF annuaire enrichment is unchanged.

New

**`participants` on [`GET /v1/directory/{peppol_id}`](<reference/index.html#lookup-peppol>)** lists every line of the company, one row per participant ID, in the shape of a search result. It is empty when the company has no line on the network and when the ID is not a French one.

New

**`includeSubEntities=true` lists those lines for a French SIREN or SIRET** on `GET /v1/directory/search`, which answered with the single `0009:{siren}` row.

## v3.16.5 — The switch says what it involves before you start it

2026-09-13

Opening a portability request printed a reference and five dates. _Objection window ends — 2026-09-18_ means nothing unless you already know the procedure, and nothing on the page said what the click had committed you to.

Changed

**[Change platform](<portability/index.html>) now states what the switch involves, before the button.** The regime the company falls under, whether its routing address changes, what has to be re-granted and who is left holding the archive — all of it already came back from `POST /v1/portability/resolve` in the `requirements` block, and the page was discarding it. Below that, the four steps of the procedure in order, each with a sentence saying what happens at that point. One button opens the request, and the same four steps come back carrying their real dates.

Fix

**The dates are readable on a phone.** They sat in a two-column grid behind a long localised label, so `2026-09-13` wrapped to `2026-` / `09-13` on a narrow screen. Each step keeps its label and its date on one line that wraps as a unit.

Fix

**The “how a switch really goes” cards on that page lost their styling** in the previous release — the new step list reused their class names inside the same stylesheet and overrode them.

## v3.16.4 — The company search leads somewhere

2026-09-13

On [Change platform](<portability/index.html>), resolving a company ended the journey: the page printed the identifiers, the platform routing them and “nothing missing — this company can be filed as it stands”, and then stopped. The request it was describing was one call away the whole time, with nothing on the page to press.

New

**Open a portability request straight from the result.** The resolved company now carries a primary action that calls `POST /v1/portability/requests` with the identifier already on screen, and renders what comes back: the reference, and the four dates that actually govern a port — when the outgoing platform must be notified, when the objection window closes, when the move takes effect, and how long reception continuity runs. It uses the page's own throwaway sandbox key, so nothing reaches a real platform.

## v3.16.3 — Every worked example is one click from running

2026-09-13

The filter recipes got a _Try it_ button last release. The `curl` examples everywhere else — the guides, the sandbox quickstart, the compliance pages — still had to be selected, copied and rebuilt by hand in the Playground.

New

**A _Try it_ on every worked `curl` example that is one request** — 77 of them, across the [guides](<guides/index.html>), the [sandbox](<sandbox/index.html>), the [API Reference](<reference/index.html>) and the compliance pages, in English, French and Italian. One click loads the method, path and body exactly as printed: into the try-it panel beside the reference, or the [Playground](<playground/index.html>) everywhere else. A `{placeholder}` in the example becomes an input you fill before Send, as it already did.

New

**A tabbed example loads the tab you are reading.** The _Search_ and _Action_ tabs next to an endpoint each send their own request now, rather than the block's first one.

Changed

**An example the button cannot send truthfully does not get one.** No button on a shell script rather than a single request, on a call that is not a `/v1` endpoint, on an example carrying a header the Playground link cannot pass (`Idempotency-Key`, `X-Sandbox-Force-Error`), or on one whose value is truncated for reading (`evt_01…`) — sending something other than what you are reading is worse than copying it yourself.

## v3.16.2 — A register that did not answer is not a company that does not exist

2026-09-13

When no company register answered — the annuaire rate-limits a burst with `429 REST search rate limit exceeded` — `GET /v1/portability/companies` returned an empty list flagged `exhaustive: true`. That flag asserts the list is _complete_ , so a lookup nobody was able to perform was reported as a settled fact about the company: the migration form said “no company matches that name”, and a bulk run counted the line as **not found**. The company was there the whole time, and retrying returned it.

Fix

**An outage no longer reads as an absent company.** When the search fails upstream it answers `exhaustive: false` with `match: "unavailable"`, instead of claiming a complete empty list. A register that genuinely answers “nothing here” is unchanged and still `exhaustive: true` — the two cases are now told apart.

New

**`reason` on the company search response.** Set alongside `match: "unavailable"`, it says in plain words that nothing could be looked up and names the way through — retry, or address the company by its SIREN or SIRET. It is `null` on every other answer.

Changed

**The migration form says which lines it could not check.** The company picker shows that the register did not answer rather than closing as though nothing matched, and a bulk run now reports those lines separately from the ones that are genuinely unknown, so a rate-limited batch is no longer indistinguishable from a batch of missing companies. This holds however the line was written: a name goes through the company search, while a SIRET or SIREN goes straight to `POST /v1/portability/resolve`, whose `503` — “the company directory did not answer” — is now read as _could not check_ rather than lumped in with a `404`, which remains a register genuinely resolving nothing. In English, French and Italian.

## v3.16.1 — The migration form's company search is usable again

2026-09-13

The company picker added to [Ask us to migrate you](<guides/portability-europe.html#request>) in the Europe guide brought its own layout with it, and that layout was written for the one place it already ran.

Fix

**The identifier field is an input again, not a 320 pixel tall box.** The picker wraps the field it attaches to, and the wrapper hard-coded the sizing of the [Change platform](<portability/index.html>) page, where the field sits in a horizontal row. In the guide the same field is stacked under its label, so that `320px` applied to the height instead of the width. The wrapper now takes the sizing of the field it replaces, whichever way the page lays it out.

Fix

**Picking a suggestion with` Enter` looks up the company you picked.** The form ran its own lookup on `Enter` as well, so the half-typed name won and the company highlighted in the list was resolved as if you had never chosen it. `Enter` now belongs to the list while one of its rows is highlighted, and to the form otherwise.

Fix

**The picker speaks the page's language.** Its two notes — “not in the list” and “no exact match, so we searched…” — were shown in English on the French and Italian guides.

## v3.16.0 — Who routes this company, in every country

2026-09-13

France answers from a closed registry. Peppol has no registry to consult — and until now the answer outside France was simply missing: `accessPoint` came back `null` for every Peppol participant, and the SMP fallback was configured in no environment and reported our own URL when it did run.

New

**`GET /v1/portability/access-point/{participantId}`** — the international twin of the annuaire lookup. Resolves a Peppol participant through SML (DNS `NAPTR`), its SMP, and the endpoint certificate, and answers with the platform that routes them: `apName` (the legal name, from the certificate's `O=`), `apPeppolId`, `apCountry`, `smpUrl`, `endpointUrl`, `transportProfile`, `documentTypeCount`, `certificateExpiresOn` and `resolvedVia`. A participant nobody routes answers `200` with `error` set — “not registered” is an answer, not a failure. `zone` accepts only the four known SML zones.

New

**[Who routes this company — every country](<compliance/routing.html>)** , in English, French and Italian. A per-country map of 52 jurisdictions with participant counts and identifier schemes _measured_ from the Peppol Directory rather than asserted (8.1 million participants; France alone carries 4.5 million, 93 % of them on the reform's `0225` addressing scheme), plus a live resolver box. The eight countries whose authority publishes a provider registry are called out separately — [France is the one built out in full](<compliance/fr/platforms.html>).

Fix

**SML lookups were pointed at a retired DNS zone.** OpenPeppol moved production to `participant.sml.prod.tech.peppol.org` with NAPTR/SHA-256 on 19 March 2026 and retired `edelivery.tech.ec.europa.eu` with the CNAME/MD5 scheme. Querying the old zone returns `NXDOMAIN` for every participant on the network, which is indistinguishable from “nobody routes them”. Both schemes are now attempted and the answer reports which replied.

Changed

**The Plateformes Agréées directory gained contacts, sorting and a calmer table.** Every row now carries the operator's website, contact address and establishment straight from the DGFiP list; every column sorts, with rows that have no matricule sorting last in both directions; and the evidence quote is clamped to two lines and opens on click instead of setting the row height. The French and Italian pages are fully translated, table and legend included.

## v3.15.4 — Run a filter recipe in one click

2026-09-12

The [filter recipes](<reference/index.html#search-recipes>) on `POST /v1/documents/search` are the queries people actually build — and trying one meant selecting the JSON, copying it, opening the Playground and pasting it back in.

New

**A _Try it_ button under every worked recipe.** One click loads that example — the exact JSON shown, not a reformatted equivalent — into the try-it panel next to the reference, with the method and path already set to `POST /v1/documents/search`; press Send and you have the response. On a narrow screen it opens the slide-up request builder instead. In English, French and Italian.

## v3.15.3 — Pick a language once, and the docs keep it

2026-09-12

Every link on a translated page pointed at the English version of the next page, so reading these docs in French or Italian lasted exactly one click. Picking a language now holds across the whole site.

Fix

**The language survives navigation.** A page opened in French kept its own body translated but linked onward in English — the top menu, the sidebar and every in-page link. Those links are now rewritten to the translated page before you click one, so the menu, the sidebar and the page you land on are all in the language you are reading. It costs no redirect and no flash of English, and a page nobody has translated yet still opens in English and hands you back to your language on the next link.

Fix

**The top menu is fully translated.** Sixteen menu labels and descriptions had no translation and rendered in English inside an otherwise French or Italian menu — the whole [Portability](<portability/index.html>) dropdown, the Guides entry for receiving invoices, and the French UBL generator. A menu entry that is missing a translation now fails the build instead of shipping.

Changed

**Adding a language is one file.** The language set, the switcher in the top bar, the `hreflang` tags, the sitemap alternates and the list of which pages exist in which language are all generated from `docs/i18n.json`. Nothing lists the published languages by hand any more.

## v3.15.2 — The API Reference shows the path, not the host

2026-09-12

Every endpoint header on the [API Reference](<reference/index.html>) opened with the same thirty-two characters of base URL, so the part that actually names the endpoint — `/v1/documents/{document_id}/pdf` — began where the eye had already stopped reading. On a phone the host ate most of the line.

Changed

**The base URL is collapsed to a small`…` chip**, and the path is spelled out in full. Hover or focus the chip and the base expands back in place; the complete URL is the tooltip on the whole line; clicking the chip copies it. The chip is keyboard-reachable and expands on tap, where there is no hover to give.

Changed

**Copying an endpoint line still gives you the absolute URL** — `https://back.flowie.ink/exchange/v1/documents/{document_id}/pdf`, not the path that happens to be on screen — against the environment selected in the Session panel, so a reader on Production never walks away with a staging host. Selecting part of a path by hand still copies exactly that part.

Changed

**The try-it rail’s path field shows its whole value on hover** instead of clipping it at the width of the rail.

## v3.15.1 — One company is one row in the company search

2026-09-12

Searching a French company by name on `GET /v1/portability/companies` returned one row per _addressing line_ rather than one per company. “Boulanger” came back six times — all the same company, SIREN 347384570 — and the picker, seeing two rows named exactly `BOULANGER`, refused with “several companies match that name”.

Fix

**The French addressing suffix is no longer treated as part of the company's identity.** Scheme `0225` carries the SIREN plus a routing suffix naming one mailbox (`347384570_parcage`, `101133882_01_NDF`); that suffix was being folded into the identity key, so every mailbox looked like a different company. A company listed under both its SIREN and its SIRET also stopped counting twice. Only `0225` is special-cased — an underscore in any other scheme is still part of the identifier.

Fix

**`siren` and `siret` are now real.** `siren` used to carry the raw identifier tail — a routing suffix, a VAT country prefix, or a SIRET presented as a SIREN — and `siret` read a field the directory result has never had, so it was always `null`. Both are now derived properly, and stay `null` for non-French companies: a Belgian registration number is not a SIREN, and presenting it as one is worse than an empty field.

## v3.15.0 — Who is `0003`? The PA matricule directory

2026-09-12

The PPF annuaire routes on a four-digit number and never names it; the DGFiP publishes the names and never the numbers. Nothing joined the two, so `GET /v1/portability/annuaire/{siren}` could tell you a taxpayer sits on `0003` and no one could say who that is. It can now.

New

**[Plateformes Agréées — who is behind each matricule](<compliance/fr/platforms.html>)** — all 166 PAs the DGFiP has immatriculated, joined to the `matriculePlateforme` each one routes on, searchable by number, name or domain. **17** are published by the operator itself (the row links the page and quotes the sentence), **38** are proven from the operator’s own lignes d’adressage in the annuaire, and the remaining **111** are shown as unknown rather than guessed — a wrong matricule routes invoices to the wrong platform.

New

**`GET /v1/portability/platforms` carries the matricule** — `matricule`, plus `matriculeConfidence` (`published` or `inferred`), `matriculeEvidence`, `matriculeQuote` and the `siren` an inferred number was proven from. A new `?matricule=0003` parameter resolves one platform by its number, and a bare 3–4 digit `q` is read the same way.

Changed

**`GET /v1/portability/annuaire/{siren}` names the platform it reports.** `currentPaName` and `currentPaConfidence` now sit next to `currentPaMatricule`. Both are `null` for `9998` (PPF default routing) and `9999` (generic code), which are not platforms, and `null` for a matricule whose holder we cannot prove. No existing field changed.

## v3.14.14 — A Markdown twin for every page

2026-09-11

An agent that wants to read this documentation should not have to parse HTML, and an agent that wants the whole API surface should not have to fetch a megabyte of spec. Both are now one guess away.

New

**Append`.md` to any documentation URL.** `/reference` → [`/reference.md`](<reference.md>), `/compliance/it/` → `/compliance/it.md`, `/guides/send-invoice` → `/guides/send-invoice.md` — every page, in all its sections, served as `text/markdown` with a `Link: rel="canonical"` back to the HTML. `Accept: text/markdown` on the HTML URL returns the same bytes. [`llms/manifest.json`](<llms/manifest.json>) lists the whole map.

New

**Every served Markdown document opens with frontmatter** — `title`, `description`, `canonical`, `source`, and on a per-endpoint slice its `method`, `path` and `endpoint_id`. Metadata without scraping it back out of the prose. Each body now leads with its own `<h1>`.

New

**[`llms/endpoints.md`](<llms/endpoints.md>) — all 130 operations in one Markdown file**, generated from the contract: method, path and what each one does. It is what `llms.txt` used to inline.

Changed

**`llms.txt` is a navigation index again.** It had grown to 32 KB, past the size at which ingestion clients start truncating — and what they truncate is whatever sits below the endpoint list. The full list moved to `llms/endpoints.md` and `llms.txt` keeps the map: one line per group with its operation count, the countries and their URL pattern, and every machine-readable surface this site publishes. It also links the URLs that answer `200` instead of the ones that redirect — `/reference/`, not `/reference/index.html`.

New

**Markdown to the AI crawlers that are reading rather than rendering.** GPTBot, ClaudeBot, PerplexityBot, Google-Extended, Applebot-Extended and their peers get the Markdown representation of a page when they ask for the HTML. Search crawlers are deliberately excluded — they index the HTML, and serving them something else would be cloaking — as are the two crawlers `robots.txt` disallows.

Changed

**[`?mode=agent`](<?mode=agent>) now carries the whole integration, not just the map of it**: a four-step quickstart with the exact `curl` for each call, the error shape with the seven you will actually hit and what each one means, the Markdown convention, the coverage (networks, formats, the per-country URL pattern) and the Postman collection.

New

**Seven more[Agent Skills](<.well-known/agent-skills/index.json>), fourteen in all** — document search and its filter language, webhooks (subscribe, verify the HMAC, replay what you missed), recording a payment, generating a compliant French UBL invoice with no key, moving a taxpayer to another platform, turning an `X-Request-Id` into the captured envelope, and connecting to the MCP servers.

Changed

**[`sitemap.xml`](<sitemap.xml>) carries a `<lastmod>` on every entry**, taken from the commit that last changed the page, so a crawler can tell what moved since it last visited. `robots.txt` gained a `Schemamap:` directive, and [`schema-map.xml`](<schema-map.xml>) now lists twelve feeds rather than five.

Fix

**The unmaintained per-page Markdown mirrors are gone.** Every page had two byte-identical copies next to it, `x.md` and `x.html.md`, that no build step regenerated: `/compliance/be.md` was serving content three weeks older than the page it mirrored. Those URLs all still answer — from the single generated copy, which CI fails on if it drifts. The pages for the fifteen countries outside Europe had no Markdown twin at all and now do.

## v3.14.13 — directory search works outside France

2026-09-11

`GET /v1/directory/search` read every identifier as a French one. A `country` that was passed did not change how the query was interpreted, which made the endpoint unusable for non-French parties — and, in one case, made it answer with the wrong company.

Fix

**A`vatNumber` now only ever returns the company it names.** The value was placed on the Peppol Directory `participant` filter unqualified. That filter accepts only `iso6523-actorid-upis::<scheme>:<id>` and drops anything else, so with a `country` still in the query the search silently became “every participant in that country” and the first row came back — a `200`, well-formed, describing somebody else. Measured against the live directory, one such query matched 114,219 rows. `vatNumber` is now a filter in the strict sense: every row, from the Peppol Directory or the French annuaire or its name index, is checked to carry the requested identifier, and one that does not is dropped. A VAT matching nothing returns an empty list.

Changed

**A foreign registration number in`q` is no longer refused as a truncated SIREN.** The 9-or-14-digit rule is the French SIREN/SIRET shape and now applies only when the query is scoped to France. An 8-digit Dutch KvK or a 10-digit Belgian CBE was answered `400` with `country=NL` or `country=BE` sitting in the same query. Inside France the rule still applies, but its `400` detail string changed — it now reads `partial French identifier` and, with no `country` given, points at setting one. Re-read it if you match on that text.

Fix

**A VAT states its own country, and it outranks a`country` that contradicts it.** A VAT with a foreign prefix had its trailing nine digits queried against the French annuaire, which answers almost any well-formed SIREN — so `?vatNumber=BE0123456789`, or `?vatNumber=PT123456789&country=FR`, could be answered with a French company.

Fix

**A French VAT that fails its own check key returns nothing.** `FR` \+ a 2-digit key + a 9-digit SIREN is arithmetic, so a value like `FR99999999999` cannot be anyone’s VAT. It is now refused before the annuaire is asked about the SIREN buried inside it — which is how an impossible VAT used to come back carrying a real, unrelated company.

Changed

**`country` now selects the identifier scheme.** A bare number in `q` with a known `country` is looked up as that country’s national register number (NL → `0106`, BE → `0208`, …) and the row is checked to be the participant that was asked for, falling back to the name index otherwise. No scheme is ever guessed: where we have none for a country, the exact lookup is skipped rather than sent under an invented one. `q` itself stays a search — its results are never filtered by an identity the caller did not assert. Note the directory exposes no cross-scheme identifiers, so a company registered under its national number is not findable by its VAT; that query returns an empty list rather than a near miss.

## v3.14.12 — The supplier's side of a wrong _maille_

2026-09-10

“I sent the invoice to the wrong address — what now?” had an answer only from the buyer's seat. The French refusal page now answers it from the supplier's, where the question is actually asked.

New

**[Wrong _maille_ — the supplier's side](<compliance/fr/refusal-rejection.html#wrong-maille>)** on the refusal & rejection page. The three nested mailles France addresses at (SIREN, SIRET, reception point `{siren}_{siret}[_{suffix}]`), and the two outcomes of getting one wrong: **rejected before delivery** , where the invoice never existed and you re-send the same number with no _avoir_ , and **delivered to the wrong establishment** , where you cannot recall it and regularisation means an _avoir_ plus a _facture rectificative_ under a new number.

New

**Six questions answered in place** — what to do after a pre-delivery rejection, what to do after a misdelivery, whether the invoice number can be re-used, how to find the right reception point ([`GET /v1/portability/annuaire/{siren}`](<reference/index.html#portability>) returns `level`, the maille the deciding declaration sits at; [`POST /v1/directory/verify`](<reference/index.html#verify-recipient>) pre-flights the send), whether the payment deadline restarts, and how to catch a wrong maille before the customer does. In English, French and Italian, with `FAQPage` markup.

Changed

**The fabricated-motif warning now covers the addressing codes too.** `ADR_ERR`, `ROUTAGE_ERR` and `DEST_INC` circulate widely as the motifs for a wrong recipient and appear nowhere in the AFNOR XP Z12-012 referential. Read the real `reasonCode` off `document.failed` or [`GET /v1/documents/{id}`](<reference/index.html#get-document>).

## v3.14.11 — `sort` accepts the string it always documented

2026-09-10

Every description of `POST /v1/documents/search` offered `"sort": "issuedAt:desc"` as an alternative to the map, and the serialiser behind it has always handled that form. The request never got there: `sort` was typed as an object, so a string was refused at validation with `body.sort: Input should be a valid dictionary`.

Fix

**`"sort": "issuedAt:desc"` now sorts.** So does a list mixing both forms — `["issuedAt:desc", {"number": "asc"}]` — which the same serialiser already understood. The map form is unchanged, including the left-to-right precedence of its keys.

Fix

**A negative`cursor` no longer travels upstream.** `"cursor": "-5"` was forwarded as an offset of `-5`, which the search index has no meaning for. It is floored to the first page, like the unparseable cursor already was.

## v3.14.10 — Every e-invoicing format, on one page

2026-09-09

“Which format do you support?” was answered in forty-seven country pages and nowhere in one place. It now has a page.

New

**[E-invoicing formats](<reference/formats.html>)** — the complete map of the formats an invoice travels in, and which of them Flowie produces, accepts and delivers. The four layers (semantic model, syntax, profile/CIUS, network), a sortable catalogue of every format with its official specification, and deep dives on **EN 16931** , **UBL 2.1** , **UN/CEFACT CII** , **Factur-X / ZUGFeRD** , **Peppol BIS Billing 3.0 and PINT** , the national European profiles (XRechnung, FatturaPA, Facturae, KSeF FA(3), ISDOC, OIOUBL, EHF, Finvoice, ebInterface, UBL.BE) and the non-EU clearance schemas (ZATCA, MyInvois, GST INV-01, ETA, UBL-TR).

New

**Every`format` value on [`POST /v1/documents/send`](<reference/index.html#send-document>) is documented side by side** — `json`, `ubl-xml`, `cii-xml`, `auto`, `raw` — with what each one does to your bytes, and why a Factur-X deposit must be announced as CII rather than UBL.

New

**Primary sources, linked.** Each format links its own specification — the European Commission for EN 16931 (the **2026 edition** published in May 2026, which formally withdrew the 2017 one), OASIS, UNECE, OpenPeppol, FNFE-MPE, FeRD, KoSIT, Agenzia delle Entrate, DGFiP, ZATCA and the rest — so a disagreement between this documentation and a national profile can be settled at the source.

## v3.14.9 — Build a `/documents/search` query without writing the JSON

2026-09-09

`POST /v1/documents/search` is the one endpoint whose body is a predicate tree, and the reference offered a single free-text box for it: you had to know the filter syntax before you could try anything.

New

**A query builder on[Advanced search](<reference/index.html#search-documents>).** Pick a field, an operator and a value; add conditions; nest an `$or` inside an `$and` to any depth; set `query`, `sort`, `limit` and `cursor`. The request body is written as you go and loads straight into the try-it rail, so the query you built is the query you send. Every field in [Filterable fields](<reference/index.html#search-fields>) and every operator in [Operators](<reference/index.html#search-operators>) is offered — including `companyId`, which is restricted to `$eq` because it selects a company rather than matching a column.

New

**The generated body reads like the worked examples.** Conditions that can be siblings are emitted as sibling keys, two bounds on one field collapse into a closed range, and an explicit `$and` appears only where it is needed to group an `$or`. Values are typed from the field: an amount is a number, a date gets a date picker, `$in` takes a comma-separated list, `$null` sends `true`, and known values (`INVOICE`, `CREDIT_NOTE`, …) are suggested as you type. The builder is available in English, French and Italian.

## v3.14.8 — A bare value in `filters` really means equals

2026-09-09

`POST /v1/documents/search` has documented a bare value as shorthand for `$eq` since the endpoint shipped. It never worked: the filter was forwarded without an operator and the whole search came back `400 — Invalid field 'filters': Missing operator or token in filter expression`, whatever else the body contained.

Fix

**`{"number": "FA2500189"}` now matches, at any depth.** A bare scalar under a field name is sent as `$eq` and a bare array as `$in` — inside `$and` / `$or` branches too, where the same body failed for the same reason. A match that already names its operator (`{"$gte": …}`) is untouched, so nothing that worked before changes shape.

Fix

**The operator list is the one the search index actually accepts.** `$not` was documented as a boolean combinator in three languages and in the OpenAPI description, and was rejected on arrival — there is no negation combinator, only `$and` and `$or`. The reference now lists the twelve operators that exist (`$eq` `$ne` `$lt` `$lte` `$gt` `$gte` `$contains` `$notContains` `$in` `$notIn` `$null` `$notNull`) instead. Negate a leaf with the `$ne`, `$notIn`, `$notContains` or `$notNull` form of it.

Changed

**An operator this endpoint does not support is refused here, by name.** It used to travel to the search index and come back as an error in _its_ vocabulary, namespaced to a service you do not call. The `400` now says which operator it rejected and lists the fourteen it accepts.

## v3.14.7 — The organization picker can no longer lock itself out

2026-09-09

Picking an organization in the docs did nothing, or was impossible: the picker was not on screen at all, and `X-Flowie-Organization-Id` stayed on a value the caller could not act as.

Fix

**The active organization is no longer sent on`GET /v1/me`.** The docs inject the picked organization into every call, and that included the one call that reports which organizations the credential may name — which is circular. Once a stored pick went stale (an organization from the other environment, a membership that ended), `/v1/me` answered `403` to the docs themselves, so the picker never rendered, and every other request kept going out under an organization the token does not grant. Nothing in the UI could clear it and a reload did not help. The identity call now goes out unscoped, so the picker always has something to draw.

Fix

**A selection the credential cannot act as is dropped, not kept.** The picked organization is one value shared across environments, tokens and sessions, while the organizations it names are not. It is now reconciled against every fresh `/v1/me` — an organization outside what the credential grants falls back to the caller's default — and signing out or clearing the key clears it, so the next credential does not inherit it.

Fix

**The[Playground](<playground/index.html>) and the Session panel are one selection again.** The Playground read the organization the server defaults to instead of the one you picked, so it reset its own switcher on every refresh and pinned that default as a request header, where it outranked the topbar. Both now read and write the same selection, in all three languages, and a pick made in either is reflected in the other and in the cURL, Python and JavaScript snippets.

## v3.14.6 — Search works on a phone

2026-09-08

Documentation-site changes only — no API surface moved.

Fix

**The search button is in the top bar at every screen width.** Below 1000px it was hidden, and the two other ways in — `⌘K` and `/` — both need a keyboard. So on a phone, and on a phone held sideways, the docs had no search at all: 170 reference entries, 300 pages and no way to ask them anything. The magnifier now stays put down to the narrowest phone, as a 40 px tap target.

Changed

**The top menu moves into the hamburger drawer below 1000px, not below 861px.** That is the width the search button needed back, and the menu was not fitting there anyway — the French and Italian menus ran off the edge of the bar by up to 64 px across the whole 861–999 px band. The same sections are in the drawer, in the same order, localized.

Changed

**The search panel itself is laid out for a small screen.** It opens just under the top bar and takes the full height rather than floating in the middle at 70% of it, so results are not left under the on-screen keyboard; each result keeps its title on one line and drops its description underneath instead of squeezing both onto one; and the `ESC` chip is a real button, which is the only way to close the panel on a device with no Escape key.

## v3.14.5 — Refusal: which errors justify a `210`, and when it is still possible

2026-09-08

The French negative-status page now answers the question integrators actually ask — _the invoice is wrong about who I am, can I refuse it?_ — case by case, with a verdict each.

New

**[Can I refuse for that? — the identity& address cases](<compliance/fr/refusal-rejection.html#cases>).** Separates whether you _may_ refuse (always — no platform adjudicates a refusal) from whether you _should_ , which depends on which of three layers carries the error: identity, addressing, or the descriptive postal address. Worked verdicts for a [wrong postal address with a correct SIRET](<compliance/fr/refusal-rejection.html#case-address>) and a [wrong SIRET with a correct SIREN](<compliance/fr/refusal-rejection.html#case-siret>), plus a table covering wrong VAT numbers, delivery addresses, missing legal mentions, changed bank details, disputed amounts and duplicates — each with its reason code.

New

**[Is it still refusable?](<compliance/fr/refusal-rejection.html#timing>) — the lifecycle answer, per state.** A refusal available yesterday may not be available today. Documents what the transition machine actually permits: refusing an [already-approved invoice](<compliance/fr/refusal-rejection.html#case-approved>) needs a dispute first and costs the supplier a credit note; an [already-paid one](<compliance/fr/refusal-rejection.html#case-paid>) cannot be refused at all; a [refusal sent by mistake](<compliance/fr/refusal-rejection.html#case-undo>) cannot be undone; and [partial refusal](<compliance/fr/refusal-rejection.html#case-partial>) does not exist — though partial _approval_ does, and the page now shows the call: `206` is an `approved` carrying a `remainingAmount`, the same discriminator pattern as `208`.

Changed

**The refusal page's case headings are now questions, and it publishes`FAQPage` structured data.** Anchors are unchanged, so existing links still resolve. Answer engines can now cite each case as a question and answer pair rather than inferring the pairing from headings.

## v3.14.4 — `GET /v1/partners/{id}` answers with the id you asked for

2026-09-07

A partner lookup that fell through to the newer partner referential came back `200` with an id that nothing could look up, and most of the record empty. Both are fixed, and the lookup no longer depends on the old referential at all.

Fix

**The`id` in the response is the partner organization id again, not an internal relationship id.** The endpoint tried the legacy partner referential first and, when it did not answer, read the relationship record from the newer one. That record is keyed on the relationship, so its own identifier was returned as the partner's `id` — a value no other call accepts, so a client that read it and looked it back up got a `404`. The lookup now reads the partner record, which is keyed on the counterparty, and the `id` round-trips.

Fix

**`name`, `country`, `vatNumber`, `peppolId` and `role` are populated on that path.** The relationship record carries none of them, so all five came back `null` whenever the fallback ran — with a `200`, so nothing signalled it. French partners also regain the `peppolId` derived from their SIREN, a VAT number computed from it rather than assembled by prefix, and the legal-registry `enrichment` block.

Changed

**The lookup is filtered upstream instead of paged and matched locally.** It used to fetch the first few hundred relationships and search them in memory, so a partner outside that page was reported as not found — which is also when the empty response above appeared. The counterparty is now matched by the referential itself, so the result does not depend on how many partners you have.

## v3.14.3 — The machine-to-machine grant, end to end

2026-09-02

An unattended backend authenticates with OAuth 2.0 client credentials, and the one thing that grant needs — naming the organization on every call — was documented as if the token carried it. It does not.

New

**The client-credentials grant is in the[API Reference](<reference/index.html#m2m>), in all three languages.** The Authentication section listed two kinds of credential, JWT and API key, and said nothing about the grant every ERP connector actually uses. It now carries the token request, the two token endpoints, the `audience` that is not the API base URL, and the two different `403`s — `access_denied` at the token endpoint (an audience the client is not authorized for, not a bad secret) and `Token does not grant access to organization …` on the first API call (a missing organization membership).

New

**The[Postman collection](<postman_collection.json>) gets the token for you.** A new _auth_ folder holds _Get an access token (OAuth 2.0 client credentials)_ : fill in `tokenUrl`, `clientId`, `clientSecret` and `audience`, send it once, and the `access_token` is stored in `{{token}}` for every other request in the collection. Its tests name the likely cause when the token endpoint refuses.

Changed

**Every request in the collection now sends`X-Flowie-Organization-Id: {{organizationId}}`.** The header was there on 86 requests with a literal `<string>` as its value, which no organization matches; the legacy `Organization-Id` alias shipped alongside it, enabled, with the same placeholder. Set the `organizationId` variable once and the collection is usable with a machine token; the alias is kept but off by default.

Fix

**The docs no longer claim a machine token carries its organizations.** [The authentication guide](<auth.md>) and the [developers page](<developers/index.html>) said the organizations a technical account belongs to are “written into the token when it is issued” and that `X-Flowie-Organization-Id` only selects among them. An Auth0 `client_credentials` token has an empty `_permissions` claim and names no organization at all: the header is the only source of the tenant, it is required on every call, and the organization it names is checked against the technical account's real membership. The error text for a call that omits it is quoted as the API returns it.

## v3.14.2 — Pick your language from the top menu

2026-08-28

Documentation-site changes only — no API surface moved.

New

**A language switcher in the top menu.** English, French and Italian now sit behind a globe next to the session pill, on every page, instead of inside the session popover. Picking one keeps you on the same page in that language, and the choice is remembered.

New

**First visit follows your browser's language.** A browser asking for French or Italian lands on the French or Italian page, when that page exists. Choosing a language from the switcher — English included — overrides the detection from then on. Crawlers and the embedded playground are never redirected, so indexed URLs keep serving what they say they serve.

Changed

**The request log at the bottom of the API Reference and Playground starts collapsed.** It used to open by default and cover up to a third of the viewport, including on a first visit with no requests in it. Open it once and it stays open.

Changed

**The first top-menu entry is "Home", not "Docs".** Every page here is docs; the entry points at the documentation home page, and now says so.

## v3.14.1 — Flowie's Plateforme Agréée number is 0064

2026-08-27

Our PA (formerly PDP) registration number was published as `0040` across the documentation, and used as the default sender routing id. It is `0064`.

Fix

**`FLOWIE_PA_ROUTING_ID` now defaults to `0064`.** This is the scheme-0238 addressing identifier Flowie sends as the sender routing id on PPF lifecycle reports when a document carries none of its own, and the matricule it declares as gaining PA on a portability request. Deployments that set the variable explicitly are unaffected; any relying on the default were announcing `0040`.

Fix

**Every page now reads`0064`.** The France compliance pages, the integration and lifecycle guides, the French landing-page badge, the lifecycle status schema and the OpenAPI description carried the wrong number, in all three languages. The test that guards the number was pointed at `docs/compliance/fr.html`, which has been a redirect stub since the page moved to `compliance/fr/index.html` — so it asserted against 543 bytes of redirect and never saw the number at all.

## v3.14.0 — State a line's amount and Flowie stops recomputing it

2026-08-26

A line is worth `quantity × unitPrice` only when nothing else applies to it. Put a discount on it and that multiplication is simply the wrong number — and until now there was no way to say so.

New

**`netAmount` on an invoice line (BT-131).** State it and it is used verbatim: as the line total, as the taxable base its (category, rate) VAT bucket contributes to (BT-116), and as the line amount stored on the transaction document. `quantity` and `unitPrice` still travel as BT-129 / BT-146; they stop deciding the amount. This is the field to reach for when a line carries a discount or a surcharge, or when its price is quoted per batch — 519.1 ML at 12.60 less a 3 % line discount is 6344.44, not the 6540.66 the multiplication gives. Document totals already had the same escape hatch one level up, in `totals`.

Fix

**`totals` now holds for zero.** A stated `vatAmount: 0` — every line exempt, reverse-charged or outside the scope of VAT — was read as "unset" and replaced with the VAT computed from the lines. Only an absent key means absent now. Same for `netAmount` and `grossAmount`.

Fix

**The line amount stored on the transaction document matches the BT-131 in the same document.** It was rounded with Python's `round()`, which rounds halves to even, while every other amount rounds halves away from zero as an accountant does — so `3 × 2.675` was stored as 8.02 and rendered as 8.03.

Fix

**A credit note sent as`format=ubl-xml` with negative totals keeps them.** The header reader only recognised unsigned amounts, so an _avoir_ issued as an Invoice with `InvoiceTypeCode 381` and a `TaxExclusiveAmount` of `-10006.46` matched nothing and the document was filed with totals of 0.

## v3.13.0 — Every business term of EN 16931, in one place, with what France does to it

2026-08-25

The BT ids were everywhere and nowhere: quoted one at a time by a schematron, a _cas d'usage_ , a field description. There was no list.

New

**[Business terms — the whole EN 16931 model](<compliance/fr/business-terms.html>).** All 30 business groups and all 164 business terms (BT-1 to BT-165; BT-4 is unassigned in the published model), each with its UBL 2.1 path, its EN 16931 cardinality, whether France makes it mandatory, conditional or value-restricted — naming the `BR-FR-*` rule that does it — and the `POST /v1/documents/send` field that carries it. Filterable and searchable across ids, both names, the UBL path and the French note, and deep-linkable per term (`…/business-terms.html#BT-121`). English, French and Italian.

New

**`GET /v1/tools/fr/ubl/business-terms` and `…/business-terms/{term_id}` — no API key required.** The same referential as JSON, from the same source, filterable by `group`, `scope`, `fr`, `mapped` and `q`. Each entry carries `fr` \+ `frNote` (what the reform requires and why) and `api` \+ `apiState` — whether a field is one you send, one Flowie derives, one accepted but not yet rendered, or one you have to carry yourself in `format=ubl-xml`. The gaps are published rather than hidden.

## v3.12.2 — The reference menu reads in the order the page scrolls, and the chips work in French

2026-08-25

Two things that made the [API Reference](<reference/index.html>) harder to use than it looks: a menu that disagreed with the page, and a request builder that only answered to clicks in English.

Changed

**The sidebar now lists endpoints in the order you scroll past them.** It didn't: _AFNOR XP Z12-013_ and _PunchOut_ were documented straight after Payments but listed after _UBL generator_ , so the highlighted entry jumped backwards as you scrolled and clicking an entry landed somewhere you weren't expecting. The AFNOR and PunchOut sections moved to where the menu says they are; _Request inspector_ , the company _Resolve_ /  _Search_ lookups and the two lifecycle batch calls, whose sections can't move without splitting an endpoint they share, had their menu entries moved instead. A test now fails the build if the two ever disagree again.

New

**Eleven routes that were scroll-only are now in the menu.** `GET /v1/me`, the six OAuth and handoff endpoints (a new _Agent auth_ group) and four Portability endpoints — `resolve`, open, read and record-a-step on a migration request — were documented in the body but absent from the sidebar, so the only way to reach them was to scroll past them or already know the anchor.

Fix

**Click-to-build-the-request works on the French and Italian pages.** The value chips and the inline field inputs are wired up by matching each parameter list to its heading — and the match only knew the English wording, so on `index.fr.html` nothing on the page was clickable at all and on `index.it.html` every request-body chip was dead. The heading test now reads _Corps de la requête_ and _Corpo della richiesta_ as well as _Request body_.

Fix

**The request builder was dying on first paint.** The [Playground](<playground/index.html>)'s startup called `Prism.highlightElement` behind an `if (window.Prism)` that is already true while Prism's core is still a deferred script — so the very first render threw, and everything registered after it never ran. Among the casualties: the listener the reference rail uses to hand the builder a new request. That is why the rail head could read `POST /v1/documents/{document_id}/lifecycle` while the live cURL still showed `GET /v1/me`, and why clicking a value chip changed nothing.

Fix

**Open an endpoint's link and the rail offers _that_ endpoint.** Landing on `#update-lifecycle`, or clicking it in the menu, left the rail on whichever endpoint happened to cross the screen last during the jump — _Update lifecycle_ in the menu, `GET /v1/documents/{document_id}/structured` in the rail. The rail now reads the endpoint the fragment names, and re-picks from where the page actually came to rest rather than from what moved on the way there. _Try it_ also rebuilds the request even when the rail is already on that endpoint, and a request pinned while the builder is still loading is replayed instead of dropped.

## v3.12.1 — Type a company name instead of hunting for its SIRET, and an advanced mode for the rest

2026-08-24

New

**Company search on the[UBL generator](<compliance/fr/ubl-generator.html>)'s Simple tab.** Type a name — or any identifier you already hold — and pick the company: the legal name, SIRET, VAT number and city fill themselves from the national register and the Peppol Directory. Asking a taxpayer for its SIRET sends it hunting for a number; asking what it is called does not. The street and postal code are _not_ in either register, so those stay typed and the form says so rather than leaving a half-filled address looking complete. The lookup is metered, so it uses the API key the reference pages already store; with no key the form still works and explains why the search is idle.

New

**An _Advanced fields_ panel** on the same tab: currency (`BT-5`), buyer reference (`BT-10`), purchase order (`BT-13`), a part-payment already received (`BT-113`), BIC (`BT-86`), payment terms (`BT-20`), the invoice a credit note corrects (`BT-25`/`BT-26`) and a free note (`BT-22`). Everything the reform lets you state on top — the cadre, the tax point, the VAT reason and the three legal mentions are still worked out for you.

Fix

**The Simple tab answered`HTTP 404`.** It called the production host, where the endpoint has not rolled out yet; it now calls the sandbox host like every other try-it surface in these docs, which is where you can actually run it today.

## v3.11.13 — Change platform, in whichever country the company is in

2026-08-24

Changed

**The[Portability section](<portability/index.html>) no longer reads as a French page.** It was written from the French reform outwards — SIRET, _annuaire_ , _jours ouvrés_ , an 18-field CSV — which is one of the regimes the endpoints already cover, not the shape of all of them. The four steps are now stated as what they are everywhere (identify, agree, tell the outgoing platform, move the routing on a date), and each country's specifics are named as that country's: France's clocks are France's, the twelve-month minimal service is France's, the prescribed inter-platform format is France's. The per-country differences live in [changing platform in Europe](<guides/portability-europe.html>), and `resolve` returns the regime that applies to the company you actually looked up.

Changed

**The field takes a tax ID, a VAT number or a Peppol id, not just a SIRET** , the pasted list takes all of them mixed, and the result card labels a company's identifiers the way its own country does — a Belgian company has no SIREN. `GET /v1/portability/companies` now says plainly where each mode works: search _by name_ needs a public register we are connected to (France today), while the identifier path answers in any country the API covers, so a form should accept both and never require the name.

Fix

**The French and Italian pages speak French and Italian.** They shipped with the English page script, so a French reader got "Looking up…" and "not resolved" in the middle of a French page. Every runtime string now comes from a per-page table.

## v3.11.12 — Change platform: a section of its own, and nobody has to look up their own SIRET

2026-08-23

Portability stops being a chapter of the docs and becomes the thing you actually do: [a page](<portability/index.html>) where you type a company name and get its migration, one company or a whole portfolio.

New

**`GET /v1/portability/companies?q=` — type a name, pick the company.** Asking a taxpayer for its SIRET sends it hunting for a number; asking what it is called does not. A name fragment returns real companies with their SIREN, **re-ranked** before they leave the API — the directory answers in SIREN order, which puts _CARREFOUR GIGNAC_ above the company anyone typing "carrefour" means, so exact names come first, then names starting with the query, shortest first. A query that already is an identifier (SIREN, SIRET, VAT, Peppol id) short-circuits to a direct lookup; `match` says which happened. Under three characters, or a directory that does not answer, returns an empty list with the reason rather than an error — a suggestion is a shortcut, never a dependency.

New

**[Portability is its own section](<portability/index.html>) — one company in thirty seconds, or five thousand.** The fast path is a single field with name autocomplete: pick the company and its taxpayer, its current routing platform and what its move still needs come back resolved. Under it, the same thing at scale — paste a list of names or identifiers straight out of a spreadsheet, every line resolved four at a time, and a table of who each line really is, who routes them today and what is missing, exportable as CSV or as the ready-made `POST /v1/companies/import/batch` payload. No account and no API key: the page mints a throwaway sandbox key. English, French and Italian.

Changed

**The migration form in[Changing platform in Europe](<guides/portability-europe.html#request>) autocompletes company names too**, and picking one runs the resolution by itself — the same picker, shared as `assets/portability-app.js`, keyboard-navigable and announced as a combobox.

## v3.12.0 — A compliant French invoice from your own data, with no case to pick first

2026-08-23

The catalogue answers "what does _this_ business case look like?". This answers the one everybody asks first: "here is my data — make it legal."

New

**`POST /v1/tools/fr/ubl/simple` — no API key required.** Send your parties and your lines; get back compliant EN 16931 UBL 2.1 and the `POST /v1/documents/send` body. The type code (`BT-3`), the _cadre de facturation_ (`BT-23`), the tax point (`BT-8`), the VAT category with the exemption reason EN 16931 demands, the three legal mentions of `BR-FR-05` and the `BAR` regime note are all derived from what you sent — and each one comes back in `inferred` **with the rule that forced it** , so you can check the reasoning rather than trust it. `warnings` flags what is legal but probably wrong: a VAT rate not in force in France, an intra-community supply with no buyer VAT number, one delivered inside France.

New

**A _Simple_ tab on the [UBL generator](<compliance/fr/ubl-generator.html>) page.** Fill in a short form, get the XML, the send request and the totals, and see every inference explained next to them. The 59-case catalogue moves behind an _All business cases_ tab — still one click away, no longer the price of entry.

Fix

**Site search was broken on every documentation page.** An unescaped apostrophe in a French search entry (`'the date d'effet'`) made `assets/search-index.js` a JavaScript syntax error, so the search box silently returned nothing anywhere on the site. Fixed, and a test now parses the file rather than reading it with a regex, which is how it slipped through.

Changed

**Validation errors say which field to fix.** `errors[]` on `/v1/tools/fr/ubl/generate-and-validate` now carries `location` (the XPath the rule fired on) and `businessTerms` (the BT codes it is about) alongside the rule id.

## v3.11.11 — Portability: a non-production deployment can run the real send without writing to a real platform

2026-08-23

New

**Recipient override (`PORTABILITY_RECIPIENT_OVERRIDE`).** Set it on a non-production deployment and every inter-PA message is delivered to that address instead of the counterparty Plateforme Agréée — while the message still resolves the real recipient and records it, so `recipientSource` reads `override:registry:ESKER->contact-pdp@esker.com` and the audit trail is the one production would produce. It exists so staging can exercise the send path end to end: without it, testing the channel with a real platform name emails that platform.

## v3.11.10 — Portability: sent the way every Flowie email is sent, and the port actually executes

2026-08-23

Two ends of the same job: the inter-PA message now leaves through the platform’s own mail path, and agreeing a port can now move the routing on the date you agreed.

New

**`POST /v1/portability/routing` — switch the taxpayer’s routing at the _date d'effet_.** Agreeing a port changes nothing by itself; what decides where an invoice goes is the taxpayer’s e-invoicing address, which is what the directory routes on. As the gaining platform the address is declared with `validFrom` = the date d'effet, so a port agreed in August for 1 October does not start pulling invoices in August. As the losing platform (`role: "LOSING_PA"`) nothing is deleted: emission stops on the date d'effet while reception stays open until `effectiveDate + 12 months` — the minimal service LFI 2026 requires, so flows in flight still resolve (`minimalServiceMonths` to lengthen it). Idempotent on the SIREN; an organization with several connections must name one with `connectionId` rather than have one picked for it.

New

**`POST /v1/companies/import` takes `effectiveDate`.** When you onboard the taxpayer and the port in one move, the date is carried onto the routing address it creates instead of the address starting the moment the call lands. Same field on the batch items.

New

**`GET /v1/portability/annuaire/{siren}` — what the directory says right now.** The PPF _annuaire_ answers with the taxpayer's _lignes d'adressage_ ; this returns the one that decides where invoices go — `currentPaMatricule` (the platform routing it; `9998` is the PPF default, i.e. nobody has been declared and there may be nothing to port), `effectiveFrom`, an `effectiveTo` when a departure is already scheduled, and `isFlowie` once a switch has propagated to us — plus the raw lines as evidence. Read it before a port to know who you are porting away from, and after to know whether it landed; the routing switch also reads it straight back, so its response says `propagated: false` with a date rather than letting you assume the port is live.

Changed

**The message goes out through Flowie’s own mail path** , so it carries the platform’s sender identity and delivery handling rather than a relay only the Exchange API knows about, and a retry cannot produce a second email to a counterparty (the log id is the idempotency key). That path attaches files by reference, so the 18-field CSV travels **inline in the body** , byte-identical to the `csvRow` you get back and to what was hashed. Need it as a real `.csv` file? Pin the direct relay with `PORTABILITY_TRANSPORT=smtp`. The new `transport` field on the response and in the message log says which one carried it, and `smtpMessageId` is now `providerMessageId` — it is no longer always an SMTP id.

Changed

**The guide gains a fifth step** — [switch the routing](<guides/portability.html#execute>) — and the tracking section is corrected: the PPF _annuaire_ is read-only to us, but a port is not stuck outside the directory. It is written into the routing address and propagates from there; the `annuaire` snapshot on each message is what proves the before and checks the after. English, French and Italian.

## v3.11.9 — Portability: the message is sent, and it is on the record

2026-08-23

The inter-PA channel stops being a formatter you print from. A portability message now leaves the platform by email, addressed on its own, and every one of them is kept.

Changed

**`POST /v1/portability/messages` emails the message to the counterparty Plateforme Agréée** instead of handing you a subject line to send yourself. The normalised subject goes in the subject, the 18-field CSV goes in as an attachment, and the body restates the délais the AIFE puts on the recipient (24 h acknowledgement, 5 business days, _silence vaut accord_). Sending stays gated: nothing leaves unless the dispatch kill-switch and an SMTP relay are both configured, and a sandbox key never reaches a real platform — in which case `dispatched` is `false` and `reason` says which of `dispatch_disabled`, `not_configured`, `no_recipient`, `sandbox` or the SMTP error applies. The operation is renamed `send_portability_message`; agents that called it by its old id (`build_portability_message`) need the new name.

New

**You no longer need the other platform's email address.** Name the counterparty (`losingPaName`, or `gainingPaName` when you are the losing platform) and it is resolved against the registry of registered Plateformes Agréées: the dedicated portability inbox the platform published if it has one, its DGFiP _courriel de contact_ otherwise. The response's `recipientSource` records which happened — `registry:<name>`, `explicit`, or `unresolved` — so a message never goes somewhere unexplained. An explicit `to` still wins.

New

**`GET /v1/portability/messages` and `GET /v1/portability/messages/{id}` — the register.** Every message is logged whether or not it was emailed, so a dry environment produces the same audit trail minus the mail and a relay outage leaves you the exact message to re-send. List them newest first, filtered by `requestRef`, `siren`, `state`, `messageType` or `dispatched`; fetch one for the proof bundle — the CSV row exactly as sent with its `csvSha256`, the recipient and how it was resolved, the SMTP message id, and what the PPF _annuaire_ answered for that taxpayer at the moment we sent. The annuaire has no write API for a port, so that snapshot is how a request stays checkable against the directory. Each send also writes a `portability.message.sent` / `portability.message.recorded` entry to the event log.

New

**`GET /v1/portability/platforms` — every Plateforme Agréée, with where to write to it.** Both official DGFiP lists (registered operators, and those still awaiting the interoperability tests) merged with the dedicated portability inboxes platforms circulated among themselves; `contactEmail` is the address a port request would actually be sent to. Filter with `q` and `status`. The payload carries its own `snapshotDate` and `source`, because the list changes every week.

Changed

**The[portability guide](<guides/portability.html>) is updated end to end** — addressing, the send, the message log, the proof bundle and the new address book, in English, French and Italian. What has _not_ changed: there is still no server-side SLA timer, and the wire format (subject grammar, status codes, 18 columns) remains a working model pending the AIFE annex of 15/07.

## v3.11.8 — A platform change you can start with one identifier, and prove afterwards

2026-08-22

New

**`POST /v1/portability/resolve` — one identifier in, the whole taxpayer out.** A SIRET, a SIREN, a VAT number, a Peppol id, a national registration number, a domain, an e-mail or `name:<company>` resolves to the legal name, country, identifiers and the French annuaire addressing line, plus the regime that governs the switch and what must be re-granted in that country. Whatever cannot be resolved comes back in `stillNeeded` rather than being guessed. The [request form](<guides/portability-europe.html#request>) now runs on it: two visible fields (your identifier and your e-mail), everything else resolved, everything overridable.

New

**`POST /v1/portability/requests` and `GET /v1/portability/requests/{ref}` — the request as a first-class resource, with the proof attached.** Opening one produces the designation agreement art. 242 nonies E bis requires (taxpayer, incoming platform, previous platform, effective date, scope of addresses, signatory), numbered and digested; the decree's deadlines computed in _jours ouvrés_ on the French civil calendar — _jours fériés_ excluded, and each step dated by its day in Paris rather than in UTC (D+2 notice, 5-day objection window, 15 days to the annuaire, one year of continuity); and a hash-linked evidence chain whose verification names the entry that was altered. State is never stored — it is re-derived from the chain, so `tacitApproval` flips by itself once the objection window lapses (_le silence vaut accord_).

New

**`POST /v1/portability/requests/{ref}/events` — an objection is recorded, then judged.** The outgoing platform may object only on grounds questioning the taxpayer's intent (`more_recent_agreement`, `identity_mismatch`, `mandate_invalid`). Any other ground — an unexpired contract, unpaid invoices, a notice period — is stored verbatim and flagged `admissible: false`: the request keeps running and carries the mandate digest as the answer to it. See [why they cannot just say no](<guides/portability-europe.html#proof>).

Changed

**The`Portability` tools are now on the curated MCP server** at `/exchange/mcp`, alongside `UBL Generator` — eight tags, not six. A platform change is four calls with no human judgement in between, so an agent asked to move a company can run the whole procedure; the alternative was an agent hand-writing an inter-PA message. See [the tool catalog](<build-with-ai/index.html#tools>) and [the agent walkthrough](<guides/portability-europe.html#agent>).

## v3.11.7 — Changing platform in Europe, and a request form that needs no API key

2026-08-22

New

**[Changing platform in Europe](<guides/portability-europe.html>) — what a switch takes in every European country.** The France guide covers the one regulated hand-over on the continent; this page covers everywhere else, where leaving a provider is three unrelated jobs: a registry edit, an authorisation to re-grant at the tax authority, and an archive to get back. It separates the five layers a switch touches, gives a per-country matrix (does your routing address change, what must you re-grant, who holds the archive), and works through the Peppol migration key, Italy's _codice destinatario_ and _delega_ , Polish KSeF certificates, Romanian SPV users, Hungarian technical users, Greek myDATA channels, Spanish _Crea y Crece_ , Portuguese document series, Croatian AMS and Turkish _özel entegratör_ — plus the EU Data Act right to switch, retention per country, and an exit checklist. What we could not verify is listed as such.

New

**A migration request form that runs entirely in the browser.** [Ask us to migrate you](<guides/portability-europe.html#request>) without an account, an API key, or a single HTTP call: pick a country and the form validates your identifier for that country, shows what that country actually requires (address, authorisation, archive), and composes the request for e-mail or clipboard. Nothing is transmitted from the page. Localized in French and Italian.

## v3.11.6 — The docs stop scrolling sideways, and reference tables use the whole column

2026-08-21

Fix

**Reference tables now fill the content column instead of a 68-character measure.** Every table was capped at the same width as a paragraph, so the widest content on a page sat in the narrowest box — the [lifecycle status → API memo](<compliance/fr/lifecycle.html#cheatsheet>) wrapped each `POST /v1/documents/{id}/lifecycle` body over three lines with 700 px of empty page beside it. Tables now stretch to the column, and each one scrolls inside its own box when its columns genuinely need more room (keyboard-reachable, so the hidden columns are not mouse-only).

Fix

**No doc page scrolls sideways any more, from 320 px up.** A table that cannot shrink below its own columns used to push the entire document — top bar included — past the viewport between 861 and 1050 px; the French and Italian top navs overflowed the same band by up to 30 px; a single long inline `code` token did it to the changelog on a phone; a schema row carrying a code sample did it to every [API reference](<reference/index.html>) page; and the [Playground](<playground/index.html>) request bar, the [request inspector](<playground/requests.html>) key row and the [webhook signature verifier](<reference/webhooks.html#verifier>) each ran off the right edge on a 360 px screen. All of them now wrap, shrink or scroll in place. Documentation only — no API change.

## v3.11.5 — Address a recipient by your own reference

2026-08-21

New

**`"to": "cf:CODE_CLIENT=C-4471"` routes on the code your own ERP already holds for the customer** (aliases `ref:`, `customfield:`). Store it on the partnership as a custom field and it becomes a routing key: an exact match on a value you control, so unlike `name:` it does not depend on spelling, accents or locale, and it survives a change of legal name. See [Addressing by your own reference](<reference/index.html#addressing-by-your-own-reference>).

Changed

**Two deliberate limits, so a routing key can never quietly send a document to the wrong company.** The field name is required — `cf:C-4471` is a `400`, because Flowie will not guess which custom field holds your reference. And no match is an error, never a fallback: an unknown reference is a `404` with nothing created, and a reference carried by more than one partner is a `409` rather than an arbitrary pick.

## v3.11.4 — Invoice amounts round the way an accountant rounds them

2026-08-21

Fix

**Amounts landing exactly on a half-cent no longer round to the nearest EVEN cent.** The totals ran on floats through Python's builtin rounding, which rounds halves to even — `0.125` became `0.12`, not `0.13`. Accounting systems round halves away from zero, so every amount on a half-cent was a coin flip against the customer's ERP. Totals are now computed as exact decimals and rounded half-up throughout. This is the second half of the one-cent gap EPSA reported on 2026-08-17; the first half, the per-category VAT grouping required by `BR-CO-17`, shipped in v3.10.1.

## v3.11.3 — Payment means: the whole UNTDID 4461 code list, and nothing that isn't in it

2026-08-21

Fix

**`payment.means: "ZZZ"` is now sent as the payment means code (`BT-81`), not as free text.** `ZZZ` — "mutually defined", the code for a means you and your customer agreed between yourselves — is the one entry in UNTDID 4461 that is not a number, and it was being filed as the means _description_ (`BT-82`) because the value was sorted on whether it looked numeric. Codes like `"97"` (clearing between partners, the netting a customer and supplier settle against each other) were always carried correctly and are unaffected.

Fix

**A number that is not in the code list is no longer passed off as one.** `"999"`, `"123"` and `"0"` used to reach `BT-81`, where the recipient rejects the invoice under `BR-CL-16` — the very rule the split exists to respect. They are now carried as the means description instead. The list runs 1 to 97 plus `ZZZ`; a padded code (`"07"`) states the code it pads (`7`). A number inside the range that the list happens to leave unused still goes to `BT-81`: you typed a code, and a rejection you can act on beats a silent demotion to prose.

Changed

**The documented examples now name the codes people actually send** — `30` credit transfer, `42` payment to bank account, `48` bank card, `49` direct debit, `58` SEPA credit transfer, `97` clearing between partners, `ZZZ` mutually defined — in [the `payment.means` reference](<reference/index.html#send-document>) and in the OpenAPI schema.

## v3.11.2 — Portability, one step from anywhere in the docs

2026-08-20

Changed

**The[change of Plateforme Agréée](<guides/portability.html>) is now promoted where people look for it — and labelled France-only.** The guide shipped in v3.11.1 was reachable only from the Guides dropdown. It now gets a band on the docs home page, a card in _Where to go next_ , its own [section on the Guides overview](<guides/index.html#portability>), and a callout at the top of [France · PPF](<compliance/fr/index.html>). The nav insight, that section and the search entry all say **France (PPF) only** : outside France there is no _Plateforme Agréée_ to leave. Documentation only — no API change.

## v3.11.1 — Portability guide: change of Plateforme Agréée, end to end

2026-08-20

New

**[Portability (change of PA)](<guides/portability.html>) — a guide covering the whole inter-PA hand-over.** The four endpoints shipped in v3.7.0 now have an end-to-end walkthrough: import the taxpayer from its SIRET (and in bulk, with per-item results), build the normalised inter-PA message, and parse an inbound one. It documents the eight request states with their wire codes (`REQ`, `ACK`, `ACC`, `REJ`, `TAC`, `MIG`, `CMP`, `ERR`), the three legal clocks (24 h acknowledgement, 5 business days, _silence vaut accord_), the 18 CSV columns one by one, and how to follow a migration today by polling `GET /v1/events` for `company.imported` / `company.import.pending` — which are event-log entries, not webhook events. It also states plainly what is still provisional: the subject grammar, status codes and column set are a working model pending the AIFE annex of 15/07. Localized in French and Italian.

## v3.11.0 — French e-invoice UBL generator: a compliant document for every business case

2026-08-19

Every business situation the French reform recognises, as a document you can actually run — and validate.

New

**`GET /v1/tools/fr/ubl/scenarios` and `POST /v1/tools/fr/ubl/generate` — no API key required.** 59 scenarios covering **all 45 numbered _cas d'usage_** of AFNOR XP Z12-014 plus 9 foundations (deposit 386, final-after-deposit with the deposit netted in BT-113, self-billed 389, factored 393, reverse charge, franchise en base, intra-community, export, B2G Chorus Pro). Each one carries the business narrative — what happens, why the reform treats it as its own case, what goes wrong, which BT fields carry it — plus EN 16931 UBL 2.1 XML, the computed totals and the exact `POST /v1/documents/send` body. Both endpoints are pure functions of the request and touch no tenant data, which is why they need no key. See the new [UBL generator](<compliance/fr/ubl-generator.html>) page.

New

**`POST /v1/tools/fr/ubl/generate-and-validate`** (API key required) generates a case and runs it through the official FNFE **XP Z12-012 v1.3.0** schematrons — XSD, the EN 16931 profile rules, the French `BR-FR` rules — plus the complementary CIUS-FR field checks, returning every failing rule flattened. Use it as the reference answer when your own output for the same case is rejected.

Fix

**`POST /v1/documents/validate` with `format: "ubl-xml"` now actually validates.** It was posting `{"content", "format"}` to the validator, which answers that with `400 Provide either 'xml' or 'xml_base64'` — so every well-formed invoice came back `valid: false` with a `validator_error`. The request now uses the service's own contract, and the response reads the v3 shape (findings nest under `official.steps[].messages` and `custom.checks[].messages`; there is no flat `errors` list), so failures list the real rule ids instead of coming back empty. The non-blocking validation on the send path was broken the same way and is fixed with it.

Fix

**Each validation error now names the field to go and fix.** `errors[].field` carried the step name (`EN16931`), which only says which rulebook rejected you; it now carries the business term the rule is about (`BT-9`) or, failing that, the XPath the rule fired on. Deposited **Factur-X (CII)** is also announced to the validator as CII rather than UBL — the wrong schematron was being run, so well-formed Factur-X was logged as broken.

New

**The`UBL Generator` tag is exposed on the curated MCP server** (`/mcp`), so an agent asked to build a French deposit or self-billed invoice can fetch the reference sample instead of guessing at BT-113.

## v3.10.0 — Every BT you send reaches the e-invoice, not just the required ones

2026-08-19

**In plain terms:** what you put on an invoice now reaches the invoice your customer receives — the bank details you want to be paid on, the order number they match against, the wording that justifies charging no VAT. Until now only the handful of fields your e-invoicing template happened to mark mandatory made it through; everything else was accepted and quietly dropped. Nothing about how you call the API changes.

The mechanics: French e-invoicing templates declare almost every field past the EN 16931 core as _optional_ , and the structured `format=json` mapping used to fill only the fields a template marked **required** (plus a short allowlist). A value you sent for any other field was accepted with `200` and then carried by nothing.

Changed

**Anything you state in`document` now reaches its BT-* field whether or not your template marks it required.** The rule is keyed on where a value came from: what you sent is always carried, while values Flowie _derives_ (totals summed from your lines, the `BT-3` type code, `BT-126` line numbering, the `BT-118` breakdown category) still fill required fields only — a default is not a statement. Where both exist, yours wins: `BT-40` comes from `seller.address.country` when you send an address and falls back to the VAT number's prefix when you don't. Previously this had to be fixed one field at a time (`BT-25`, then `BT-119`/`BT-120`/`BT-121`, then `BT-10`/`BT-13`/`BT-16`/`BT-84`). See [Which BT fields are sent](<reference/index.html#bt-mapping>).

New

**Party identity and postal address are mapped in full.** `seller.vatNumber` / `buyer.vatNumber` reach `BT-31` / `BT-48`, and `address` now renders the whole postal-address group — `BG-5` (`BT-35` street, `BT-36` street 2, `BT-37` city, `BT-38` post code, `BT-39` subdivision, `BT-40` country) for the seller and `BG-8` (`BT-50`…`BT-55`) for the buyer, where only the country code was carried before. Parties stated through the role-tagged `document.parties` list are mapped too — they used to lose every party BT.

New

**The payment group (`BG-16`) is mapped.** `payment.means` reaches `BT-81` when it is a UNTDID 4461 code (`"30"`, `"58"`) and `BT-82` when it is free text — free text in `BT-81` fails `BR-CL-16`. `payment.reference` reaches `BT-83`, `payment.bic` reaches `BT-86`, and the account name `BT-85` accompanies the `BT-84` IBAN. Only `BT-84` was carried before; the rest reached the free-text payment-instructions line, which no e-invoice element reads.

New

**Delivery date and line period are no longer dropped.** `delivery.actualDeliveryDate` (also accepted as `deliveryDate` / `date`) reaches `BT-72`, and a line's `period` reaches `BT-134`/`BT-135` (`BG-26`) — both were documented as "accepted but not yet emitted". A line's `vatRate` also reaches `BT-152`, its own semantic field, in addition to driving the VAT breakdown.

Changed

**No request shape changed and nothing new is required.** A BT still only renders where your template declares a field for it, so orgs whose templates declare fewer fields see no difference. If you relied on a value you send _not_ appearing on the e-invoice, stop sending it.

## v3.9.1 — AFNOR flows: search and getFlow answer the same thing

2026-08-19

Fix

**`POST /v1/flows/search` now answers with the FlowInfo you deposited**, exactly like `GET /v1/flows/{flowId}?docType=Metadata`. The two reads disagreed on the same flow: search returned the invoice number as both `name` and `trackingId`, `flowSyntax: "UBL"` and `processingRuleSource: "Computed"`, because it never re-read the deposited FlowInfo. **If you matched flows on those fields, they change value — to the ones you submitted.**

Fix

**The`trackingId` filter finds your deposits.** It was comparing your AFNOR `trackingId` against the transaction document's invoice number, so searching a tracking id you had just deposited returned an empty page. `trackingId`, `flowType` and `flowDirection` are now all matched against the deposited values. Pair them with `updatedAfter`: a query with no window may report an approximate match set past 1000 scanned rows (it is logged, never silently partial).

Fix

**A flow you deposited is reported as an emission.** Depositing on the platform _is_ emitting, so a deposited invoice is now `flowDirection: "Out"` / `flowType: "CustomerInvoice"` (and `CustomerInvoiceLC` for CDAR/FRR) whatever the parties on the resulting document say. It used to be derived from your role on the document alone, which reported `In`/`SupplierInvoice` for invoices you had just sent. Documents you only received are unaffected.

Fix

**A rejection is no longer hidden behind another party's status.** A document carries one lifecycle status per party and they disagree; the acknowledgement used to be whichever one was read first, so a flow rejected at ingress could answer `Ok` on one endpoint and `Error` on the other. Ingress rejection is a property of the flow: any provable rejection now wins on both endpoints, and `details[].reasonMessage` carries that party's own reason (e.g. the failed schematron rule ids) instead of a generic label.

Fix

**`POST /v1/flows/search` reports when a flow last _changed_ , not when it arrived.** `updatedAt` was falling back to the reception timestamp, so a document that arrived last week and changed status today reported last week — and its own `updatedAfter` window then dropped it, hiding the status change from anyone polling. Document list items now carry `updatedAt` too.

Fix

**The`sender`/`receiver` of a document are resolved by role, not by whoever shares the organization id.** When one organization holds several roles on the same document (`sellerId` equals `payerId`), the first party in the list used to fill _both_ slots — so a rejection recorded on the seller was invisible on the document read, while the list read reported it.

New

**`lifecycleStatusReason` on the `sender`/`receiver` of document responses and list items** — why that party is in its current stage.

## v3.9.0 — AFNOR lifecycle flows: no more phantom invoices, correct LC flow types

2026-08-17

Fix

**A lifecycle deposit (`flowSyntax: CDAR`/`FRR`) no longer materialises a transaction document.** Every lifecycle deposit used to create a phantom 0.00 EUR draft invoice in the owning organization's Invoices tab. Lifecycle flows are now archived verbatim: `docType=Original` returns the deposited bytes byte-identical, `Metadata` round-trips the deposited FlowInfo, and `Converted`/`ReadableView` answer a clean 404 (a lifecycle report has neither).

Fix

**Lifecycle flows report their real`flowType`.** A deposited CDAR was typed `CustomerInvoice` (derived from the phantom invoice); it is now `CustomerInvoiceLC`, and `POST /v1/flows/search` filtered on the `…LC` flow types finds it.

Changed

**`POST /v1/flows/search` merges lifecycle flows with invoice flows** in `updatedAt` order, with a backward-compatible pagination cursor — cursors issued before this release keep working mid-pagination.

## v3.8.0 — AFNOR flows: acknowledgements settle, trackingId search works, statusReason exposed

2026-08-17

Fix

**Flow acknowledgements settle on`Ok`/`Error` instead of sticking on `Pending`.** The acknowledgement is now derived from the document status's semantic lifecycle group, so localised status labels (e.g. `Rejetée`) resolve correctly. Platform rejections report `Error` with the itemised reason in `details` (`reasonCode: REJECTED`); a counterparty's business refusal is not an ingestion error and stays `Ok`.

Fix

**`POST /v1/flows/search`: the `trackingId` filter now actually filters.** It used to be forwarded upstream as ignored free text and returned unfiltered results; it is now enforced strictly. A `trackingId` longer than 64 characters (the deposit cap) is rejected with `422` instead of silently matching nothing. `updatedAfter`/`updatedBefore` are also pushed down to the store, so polling "what changed since my last sync?" no longer pages through the organization's whole history.

Fix

**Bare-XML deposits declared`Factur-X` are mapped as the CII or UBL they actually carry** (including UTF-8 BOM-prefixed payloads) instead of producing an empty 0.00 EUR document shell.

New

**`statusReason` on document responses and list items** — why the document reached its current status (e.g. the validation errors behind a rejection), also surfaced in the AFNOR acknowledgement's `details[].reasonMessage`.

## v3.7.0 — PA-to-PA portability: SIRET import, bulk import & inter-PA channel

2026-08-02

New

**`POST /v1/companies/import` — onboard a company for portability migration from just its SIRET.** A taxpayer files a portability request giving only its `siret` (or `siren`); Flowie derives the SIREN, country and Peppol id, resolves the legal name and current PA from the PPF annuaire, then provisions the company on Sovos (via the request's `sovosOrganizationId` or the configured `SOVOS_ORGANIZATION_ID`) or registers it locally as pending, and emits `company.imported` / `company.import.pending`. Supply `sovosOrganizationId` \+ `sovosCompanyId` instead to import an existing Sovos company.

New

**`POST /v1/companies/import/batch`** imports many companies in one call for bulk portability onboarding — concurrent, idempotent, with per-item `imported`/`failed` results in order (a per-item failure never aborts the batch).

New

**PA-to-PA portability channel —`POST /v1/portability/messages` and `POST /v1/portability/messages/parse`.** Build the normalised inter-plateforme message (codified subject + 18-field CSV + status code) a portability request must exchange with the losing/gaining PA, and parse inbound messages back into structured fields. The wire format is a best-guess pending the AIFE 15/07 annex; outbound dispatch is gated by `PORTABILITY_DISPATCH_ENABLED` (default off), so the endpoint builds and returns the message without emailing a counterparty.

## v3.6.6 — New document type: purchase request (requisition)

2026-08-02

New

**`purchase-request` is now a document `type`.** [`POST /v1/documents/send`](<reference/index.html#send-document>) accepts `type: "purchase-request"` for a **purchase requisition** — the buyer's internal request to authorise a purchase, ahead of the order. It maps to the transaction-documents `PURCHASE_REQUEST` document and takes the same `from`/`to` \+ `document` body as the other order-side types. The [Document & invoice types](<reference/document-types.html#orders>) page gains an _Orders, quotes & requisitions_ section covering it alongside `quote`, `purchase-order` and `sales-order` (with a ready example in the tester), and how to chain them via `orderReference`. Localized in French and Italian.

## v3.6.5 — Peppol capabilities are no longer inferred for French companies

2026-08-02

Fix

**`GET /v1/directory/{peppol_id}` stops reporting Peppol capabilities a French company may not have.** When a participant was resolved from the PPF annuaire, `documentTypes` was hardcoded to `invoice`/`credit-note` and `smpStatus` was derived from the INSEE administrative state — so any going concern in France was reported as able to receive Peppol invoices. The Peppol Directory is now queried alongside the annuaire: `documentTypes`, `accessPoint`, `registeredSince` and `smpStatus` are returned only when the company is actually on the network, and left empty otherwise. Identity, VAT and the French addressing plan are unchanged. **If you branched on`smpStatus` for French participants, it can now be `null`.**

Fix

**Peppol participant lookups now reach the directory at all.** The `participant` filter was sent as a bare identifier (`921376265`) where the Peppol Directory requires the fully-qualified form (`iso6523-actorid-upis::0009:921376265`); it answered HTTP 400 and the error was swallowed, so _every_ participant looked unregistered. Registration status and document types are now returned for companies that really are on the network.

## v3.6.4 — French addressing plan on directory lookups

2026-08-02

Fix

**`GET /v1/directory/{peppol_id}` now returns the French addressing plan for any French participant.** The `enrichment.platformeAgree` block — the routing platform an invoice must be addressed to, its addressing level and effective dates — was only attached when the participant was already registered with Flowie, so for an ordinary French company looked up straight from the PPF annuaire it was silently absent. It is now returned on both paths, under the same key. Identity and VAT enrichment are unchanged, and the two annuaire calls are made concurrently so the extra data costs no additional latency.

## v3.6.3 — Download the Postman collection

2026-08-02

New

**The API Reference now has a[Postman collection](<reference/index.html#postman>) section.** One-click download of the ready-made collection (every endpoint, pre-filled with a working example body), with import instructions and the two collection variables to set — `baseUrl` and `token` (bearer auth). The `openapi.json` spec is offered alongside for generating your own client. The collection is regenerated on every release, so it always matches the reference.

## v3.6.2 — Try any use case from the docs

2026-08-02

New

**Ready-to-send example payloads for every use case.** The [Document & invoice types](<reference/document-types.html#try-it>) page now has a _Test any use case_ section: an example `POST /v1/documents/send` body for each scenario — standard invoice, prepayment/acompte, corrected, credit & debit notes, self-billing, reverse charge, multi-party, orders, quote and event — each with **Try in Playground** (opens the request builder prefilled, your sandbox key loaded), **Copy JSON** and **Copy curl**. Every example uses the sandbox test identifiers, so it runs as-is. Localized in French and Italian.

## v3.6.1 — Cross-tenant isolation on collection endpoints

2026-07-30

Fix

**Collection endpoints now enforce your tenant on a caller-supplied`companyId`.** On `GET /v1/documents`, `POST /v1/documents/search` and `GET /v1/events`, a `companyId` filter is _validated against_ the organization your credentials are bound to instead of being applied as-is; naming an organization you can't access now returns **403`PERMISSION_DENIED`** rather than that tenant's rows. The default (unfiltered) company listing (`GET /v1/companies`) is scoped to your own organization, and `GET /v1/companies/{id}` rejects an id outside your tenant — including one reached via a `vat:`/`peppol:` alias. This closes a cross-tenant read of company records, document metadata and events. Calls that omit `companyId`, or pass your own organization, are unaffected.

## v3.6.0 — Document & invoice types reference

2026-07-28

New

**A single page for every document and invoice type.** [Document & invoice types](<reference/document-types.html>) lays out the seven `type` values (invoice, credit-note, debit-note, purchase-order, sales-order, quote, event), the four invoice subtypes rendered as the UBL `InvoiceTypeCode` (commercial 380, prepayment 386, corrected 384, self-billed 389), and — new — dedicated deep-dives on **prepayment invoices** (_facture d'acompte_ , UNCL1001 386), **self-billing** (_autofacturation_ , the `selfBilled` flag that flips the buyer/seller roles) and reverse-charge **self-invoicing** (_autofattura_ , Italian TD16–TD29), plus a note on how employee **expenses** map onto the model (inbound invoice vs e-reporting). Reachable from the API Reference menu and sidebar.

## v3.5.2 — Fuller Compliance menu

2026-07-15

Changed

The Compliance dropdown's first column now links straight into the overview's key sections — [coverage matrix](<compliance/index.html#matrix>), [mandate timeline](<compliance/index.html#timeline>), [how Flowie handles each regime](<compliance/index.html#how-flowie-handles>), and [regime types](<compliance/index.html#regime-types>) — alongside the France and Italy deep-dive columns.

## v3.5.1 — Multi-column menu panels

2026-07-14

Changed

**Top-menu panels with several groups now lay out as columns.** Compliance shows _All countries · 🇫🇷 France · 🇮🇹 Italy_ side by side; Guides, Sandbox and Build with AI pair their pages with their journey group. Panels that would overflow the viewport flip to right-aligned automatically.

## v3.5.0 — Journey-shaped menus, deeper compliance navigation

2026-07-14

Navigation now follows the integration journey: integrate fast, test extensively, verify it works, then deep-dive the regulations — with the edge cases one click away.

Changed

**Deeper top-menu dropdowns.** Guides gains an _Integrate fast_ group (send an invoice, receive, ERP webhooks, order flow, go-live checklist); Sandbox a _Test extensively_ group (test identifiers, recipient simulators, lifecycle & compliance sims, forced errors, time travel); Build with AI a _Ship faster with AI_ group (MCP server, docs for agents, AI tools); and Compliance now lists the full 🇫🇷 France and 🇮🇹 Italy deep-dives — including the [refusal & rejection edge cases](<compliance/fr/refusal-rejection.html>) and [all 45 cas d'usage](<compliance/fr/use-cases.html>) — from every page.

Changed

**Compliance sidebars nest the deep-dives.** On every country page, the France and Italy entries in the countries list expand with their sub-pages (lifecycle explorer, refusal & rejection, use cases, integration playbook, document types), so the regulation deep-dives are reachable from anywhere in the compliance section — in all three languages.

Changed

The redundant "Exchange" chip next to the logo was removed — it appeared inconsistently and duplicated the _Docs_ menu entry.

## v3.4.2 — Richer top-menu dropdowns

2026-07-13

Changed

**Top-menu dropdowns now tell you what's inside.** Every entry carries a one-line insight under the page name — e.g. _Errors — every error code, with the fix_ , _Sandbox — simulators, test IDs & time travel_ — localized in all three languages, with a refreshed panel design (soft entrance animation, accent highlight on the current page, deeper shadow). Same links, faster orientation.

## v3.4.1 — Sidebar menus aligned with the top menu

2026-07-13

Changed

**Every sidebar now leads with its section.** The first sidebar group on Reference, Guides, Build with AI, Sandbox and Playground pages lists the same pages as that section's top-menu dropdown (with the current page highlighted), so the side menu and the top menu never disagree — e.g. the [error catalog](<reference/errors.html>) now shows its Reference siblings (overview, data model, webhooks) instead of a lone page outline. Page outlines are uniformly titled "On this page", matching the compliance deep-dives, and the French/Italian compliance pages' sidebar titles are now translated. Compliance country pages keep their richer country directory as the section group.

## v3.4.0 — Clean docs architecture: section directories + menu dropdowns

2026-07-13

The docs URL tree now mirrors the menu: every section is a directory, every page is directly reachable from the top menu, and the language suffix is unambiguous everywhere.

Changed

**Every menu section became a directory.** `reference/` ([overview](<reference/index.html>), [data model](<reference/data-model.html>), [errors](<reference/errors.html>), [webhooks](<reference/webhooks.html>)), `guides/` ([overview](<guides/index.html>), [onboarding kit](<guides/onboarding-kit.html>)), `build-with-ai/` ([overview](<build-with-ai/index.html>), [agent onboarding](<build-with-ai/agent-onboarding.html>)), `sandbox/` ([overview](<sandbox/index.html>), [API keys](<sandbox/keys.html>)), `playground/` ([overview](<playground/index.html>), [request inspector](<playground/requests.html>)) — in all three languages. Every old flat URL (e.g. `reference.html`, `keys.html`) permanently redirects to its new home, preserving query strings and anchors, so existing bookmarks, deep links and API-returned `viewerUrl`s keep working.

Changed

**France and Italy compliance deep-dives moved to per-country directories.** `compliance/fr-lifecycle.html` → [`compliance/fr/lifecycle.html`](<compliance/fr/lifecycle.html>), and likewise the France overview ([`compliance/fr/`](<compliance/fr/index.html>)), [refusal & rejection](<compliance/fr/refusal-rejection.html>), [use cases](<compliance/fr/use-cases.html>), [integration playbook](<compliance/fr/integration.html>), the Italy overview ([`compliance/it/`](<compliance/it/index.html>)) and [document types](<compliance/it/document-types.html>). The old flat names read ambiguously next to the `.fr.html`/`.it.html` language suffixes; now the directory is the country and the suffix is the language. Old URLs redirect.

New

**Top-menu section dropdowns.** Sections that own sub-pages (API Reference, Guides, Compliance, Build with AI, Sandbox, Playground) expose them in a dropdown, so pages like the error catalog, webhook cookbook or API-keys manager are one click from anywhere — no more reliance on buried body links. Opens on hover or keyboard focus, with a caret toggle for touch.

Changed

Sitemap, search index, `llms.txt`/`llms-full.txt` exports and the OpenAPI description examples all follow the new paths; the sitemap also gained the previously-missing Build with AI and compliance sub-pages.

## v3.3.0 — Update a document's lifecycle by invoice number

2026-07-06

New

**Transition a document by its invoice number.** `POST /v1/documents/by-number/{number}/lifecycle` targets a document by the human-readable invoice number instead of Flowie's internal `documentId` — for integration partners (e.g. ERP/iPaaS connectors) that only hold the number. Same request body, auth and response as `POST /v1/documents/{documentId}/lifecycle`, and it runs the identical state-machine validation, tx-docs update, PPF/SDI compliance reporting and `lifecycle.updated` webhook. The number is resolved _scoped to your organization_ , so you can never transition another tenant's document. Because invoice numbers are not unique, resolution is strict: **no match → 404** , **exactly one → the transition is applied** , **more than one → 409** (re-issue against the specific `documentId`). The existing id-based route is unchanged.

## v3.2.1 — France: machine-readable lifecycle referential

2026-07-03

New

**The French lifecycle referential is now published as data.** [`/schemas/fr-lifecycle-statuses.json`](<schemas/fr-lifecycle-statuses.json>) carries all 14 statuses (200–213) — tier, phase, emitter, terminality, motif & amount-block rules, UNTDID 1373 mapping, canonical transitions, and the exact Flowie call or webhook per status — validated by [`/schemas/fr-lifecycle-status.schema.json`](<schemas/fr-lifecycle-status.schema.json>) (JSON Schema draft 2020-12). Versioned with the DGFiP / AFNOR spec revisions it was verified against. See [the lifecycle explorer](<compliance/fr/lifecycle.html#machine-readable>).

## v3.2.0 — France: interactive lifecycle reference & integration playbook

2026-07-03

Two new France compliance pages make the docs a full reference for the 2026–2027 reform.

New

**[Lifecycle explorer](<compliance/fr/lifecycle.html>)** — the complete AFNOR XP Z12-012 status referential (codes 200–213) as an interactive, animated state machine: filter by tier (obligatoire / recommandé / libre), play the five canonical scenarios (happy path, dispute, suspension, refusal, platform reject) with a live webhook log, and click any status for its definition, transitions and the exact API call that emits or observes it. Includes the CDAR field guide (MDT-77/105/113/114, MDG-43 amount blocks), the UNTDID 1373 mapping, and a status → API cheat-sheet.

New

**[Integration playbook](<compliance/fr/integration.html>)** — the end-to-end French implementation path: onboarding & annuaire, receiving, sending Factur-X, the buyer/supplier status responsibility matrix, a production-grade webhook handler (idempotent, out-of-order-safe), e-reporting, the sandbox test matrix and a go-live checklist.

Changed

**[France overview](<compliance/fr/index.html>) corrected & re-tiered.** Deadlines fixed (large & mid-sized companies must _send_ from 1 September 2026, not 2027 — SMEs follow in 2027) and the lifecycle section now reflects the official three-tier classification: 4 obligatoires (200, 210, 212, 213), 5 recommandés (203–206, 211) and 5 coded libres (201, 202, 207–209). The French translation drops a legacy status-code table (302/304/309/40x) that never existed in the official referential.

New

[`GET /v1/documents/{id}/lifecycle`](<reference/index.html#get-lifecycle>) now returns `currentStatusReason` — the failing EN 16931 / CTC-FR schematron rule ids behind a validation-driven status.

## v3.1.11 — Directory-line search returns real entries

2026-06-12

Fix

`POST /afnor/directory-service/v1/directory-line/search` was a stub that always returned `totalNumberOfResults: 0`. It now forwards the filter set to the PPF annuaire (`ppf-annuaire`'s `/api/search/ligne-annuaire`) and maps each entry to an AFNOR directory line (`addressingIdentifier`, `routingIdentifier`, `administrativeStatus`, …), so a SIRET that has annuaire lines now returns them.

## v3.1.10 — Original/Converted XML for JSON-created docs

2026-06-12

Fix

Retrieving a document's `Original`/`Converted` XML (AFNOR `GET /afnor/flow-service/v1/flows/{id}?docType=Original` and `GET /v1/documents/{id}/xml`) returned `404` "XML not available… created from JSON without a stored XML file" for documents created from structured JSON. When no XML is physically stored, the UBL 2.4 is now rendered on the fly from the document's JSON (via the same converter the "Convert to UBL" flow uses), so the original/converted content is returned. A genuinely empty document still 404s.

## v3.1.9 — Flow search results match the query

2026-06-12

Fix

`POST /afnor/flow-service/v1/flows/search` could return flows whose `flowType`/`flowDirection` didn't match the request (e.g. a `CustomerInvoice`/`Out` query surfacing `SupplierInvoice`/`In` rows), and lifecycle (`…LC`) searches returned plain invoices. Results are now hard-filtered to the requested `flowType`/`flowDirection` sets, so the response always matches the query (lifecycle searches return an empty set when no lifecycle flows exist rather than mislabeled invoices).

## v3.1.8 — API-key creation hardening

2026-06-12

Closes privilege-escalation gaps in [`POST /v1/api-keys`](<reference/index.html#create-api-key>). Keys remain bound to the caller's organization and tier (no `organizationId` in the request).

Changed

**Scopes are clamped to the caller.** A new key can no longer be granted scopes the caller doesn't hold — requesting an unheld scope (or `*`) now returns `403`. Omitting `scopes` inherits the caller's scopes instead of silently defaulting to `*`.

Changed

**`rateLimit.requestsPerMinute` is clamped to the tier ceiling**, so a key can't grant itself a higher request rate than its tier allows.

Fix

`name` must be non-empty and `expiresAt`, if given, must be in the future (a past value now returns `400` instead of minting a dead key). Keys with no `expiresAt` remain permanent.

## v3.1.7 — Company PATCH fix

2026-06-12

Fix

[`PATCH /v1/companies/{id}`](<reference/index.html#update-company>) returned `500` when only `capabilities` or `compliance` were patched — the response was built from a detached DB row after the session closed. It is now built inside the session.

## v3.1.6 — AFNOR SIREN search pagination

2026-06-11

Fix

**SIREN search pagination.** `POST /afnor/directory-service/v1/siren/search` only fetched `limit` rows then sliced, so pages past the first (`ignore > 0`) came back empty and `totalNumberOfResults` reflected only the fetched window. Pages now resolve correctly with an accurate total.

Fix

**Bounded`ignore`.** `ignore` was unbounded; a very large value forced an oversized upstream fetch. It is now capped (max 10000) and the upstream fetch is bounded regardless.

## v3.1.5 — Deterministic directory ordering

2026-06-11

Fix

Directory search results are now ordered deterministically. The local-registration fallback used a bare `LIMIT` with no `ORDER BY`, so identical [`GET /v1/directory/search`](<reference/index.html#search-directory>) calls could return rows in a different order. All AFNOR directory-service lookups are read-only and idempotent.

## v3.1.4 — Directory by SIREN, list direction, recoverable number lookup

2026-06-10

Fix

**Directory search by SIREN/SIRET in`q`.** A bare 9-digit SIREN (or 14-digit SIRET) typed into [`GET /v1/directory/search?q=…`](<reference/index.html#search-directory>) now routes to the SIREN/SIRET lookup instead of the name search, which never matched a number. The AFNOR endpoints also stop deriving the wrong SIREN from the VAT number.

Fix

**Per-row`direction` in document listings.** [`GET /v1/documents`](<reference/index.html#list-documents>) now stamps each row `incoming`/`outgoing` (previously always `null`). An invalid `direction` now returns `400` instead of silently returning both.

Fix

**Recoverable lookup by invoice number.** When a number matches more than one document, [`GET /v1/documents/{number}`](<reference/index.html#get-document>) now returns the candidate ids in the `409` body instead of a dead-end error.

## v3.1.3 — Lifecycle history shows real statuses

2026-06-10

Fix

[`GET /v1/documents/{id}/lifecycle`](<reference/index.html#get-lifecycle>) returned `currentStatus` and every `history[].status` as `"unknown"` (and `setBy` null) for all real documents. The history now reads the upstream audit-log's real fields (`action`, `actionBy`), and `currentStatus`/`allowedTransitions` resolve from the document's authoritative per-party lifecycle status rather than the most recent audit action.

## Docs — Unified navigation

2026-06-04

Documentation-site improvements only — no API surface change. The top navigation is now generated from a single source of truth, so every page (including the per-country compliance pages) carries the same complete, consistent set of links.

Docs

Top navigation is now consistent across every page. Pages that were missing the **API Keys** or **Playground** links (e.g. [Build with AI](<build-with-ai/index.html>), [agent onboarding](<build-with-ai/agent-onboarding.html>)) now carry the full set, and the per-country [compliance pages](<compliance/fr/index.html>) use the same generated nav.

Docs

Active-link highlighting now works on the French and Italian pages even with JavaScript disabled (their nav hrefs point at the English asset, so the old runtime filename match never fired).

Docs

Browser-storage keys used by the docs site (theme, cached tokens, saved playground vars) are now sourced from a single `window.FLW` registry loaded on every page, removing drift between the individual scripts.

## v3.1.2 — Agent handoff link

2026-05-08

Hand a single URL to your LLM and it does the integration. The user pre-approves a scope set bound to their org; the agent redeems the embedded token for an API key in one POST. No PKCE round-trip, no consent UI — the issued key acts on the user's _real_ organization, not a fresh sandbox.

New

**`POST /v1/oauth/handoff`** (authenticated) — mints a single-use, scope-and-org-bound handoff token and returns a ready-to-paste URL. Default 10-min TTL (60s–60min configurable). Default scopes: `send`, `receive`, `documents.read`, `companies.read`, `stats`. Caller cannot pre-approve scopes their own token doesn't hold.

New

**`POST /v1/oauth/handoff/exchange`** (no auth) — single-use redemption. Returns an `flw_test_…` (or `flw_live_…` for paid tiers) key bound to the original user's organization, company, and tier.

New

Home-page widget under ["Building with an AI agent?"](<index.html#agent-handoff>) — copy the anonymous link, or paste an existing API key to generate a personalized handoff URL in-browser. The pasted key never leaves the page.

Docs

[Agent onboarding](<build-with-ai/agent-onboarding.html#handoff>) documents three paths now: handoff (fastest), sandbox bootstrap (no auth), OAuth consent (PKCE). LLM discovery surface ([llms.txt](<llms.txt>)) updated.

## v3.1.1 — Belgium HERMES retired

2026-05-04

Belgium's regulator-side reporting hub HERMES was decommissioned by FPS Finance on 2025-12-31 (consultation-only access expired 2026-03-31). Flowie's `HermesAdapter` and the BE branch of the compliance dispatcher have been removed. Belgian invoices are now pure Peppol — the delivery itself is the compliance event.

Changed

**No`compliance.reported` events fire for Belgian invoices.** If your webhook router branches on `data.platform == "HERMES"`, drop the branch — see [migration guide](<compliance/be.html#migration>) for the full diff.

Changed

BE-CIUS validation now surfaces synchronously: `POST /v1/documents/send` returns `422` with the BE-CIUS schematron rule code in `error.details[].code` \+ the failing XPath. Replaces the old deferred `compliance.reported.failed` \+ `HER-*` path. Same checks, faster feedback.

Deprec

`HERMES_REPORT_URL` / `HERMES_REPORT_TOKEN` environment variables are no longer read. `simulateCompliance: "reject_HER_001"` sandbox value is also retired. Historical `compliance_reports` rows with `platform=HERMES` are retained for audit; new ones won't be created.

Docs

Belgium compliance page ([compliance/be.html](<compliance/be.html>)) rewritten with sourced timeline (2024-02-06 → 2028-01-01), 4-corner Peppol diagram, end-to-end send example, lifecycle table comparing FR/IT/BE, BR-BE-* error catalog, sandbox tests, and HERMES → Peppol migration table.

## v3.1.0 — AI agents & multi-org

2026-05-03

First-class Model Context Protocol surface for AI agents, plus organization switching for JWT users in multiple orgs.

New

**MCP servers.** `/exchange/mcp` (curated, 34 tools across Documents / Directory / Companies / Lifecycle / Compliance / Partners) and `/exchange/mcp/full` (every documented operation, 94 tools). Same Bearer token as REST, same quotas, same sandbox. [Full guide →](<build-with-ai/index.html>)

New

`POST /v1/documents/send` accepts `type: "event"` — pure audit-trail records, persisted as documents but never routed over Peppol. Useful for ERP-side notifications you want to keep alongside real invoices.

New

Organization switching for multi-org JWT users: switch the active organization without re-login. Existing `flw_*` API keys are unaffected (single-tenant by design).

Changed

MCP transport upgraded from legacy SSE to **streamable-HTTP** (MCP spec `2025-06-18`). Reconfigure existing clients as `"transport": "streamable-http"`.

## v3.0.0 — Unified surface

2026-04-13

First stable cut of the Exchange API. The legacy `/api/…` endpoints continue to work but are deprecated.

New

Resource-oriented surface under `/v1/`: `companies`, `documents`, `partners`, `webhooks`, `events`, `compliance`, `platform`, `api-keys`, `stats`.

New

Lifecycle state machine with auto-reporting to PPF (FR), SDI (IT), HERMES (BE).

New

`Idempotency-Key` is accepted on every `POST` with a 24h TTL.

New

Cursor-based pagination everywhere (`limit`, `cursor`, `hasMore`).

New

Platform keys with `X-Flowie-Company` for tenant-scoped calls.

New

AFNOR XP Z12-013 adapter under `/afnor/flow-service` and `/afnor/directory-service`.

New

cXML PunchOut callback at `/document/callback`.

Deprec

All `/api/…` endpoints. Sunset date: 2027-04-01. Mapping table in the [migration guide](<guides/index.html#migration>).

## v2.9.0

2026-03-28

New

`POST /v1/documents/search` accepts compound `$and`/`$or` filter trees.

New

Webhook deliveries now include `X-Flowie-Attempt` header.

Changed

Directory verify latency dropped from p95 420ms → 90ms via SMP cache.

## v2.8.0

2026-03-10

New

AI tag recommendation: `POST /v1/categorization/objects/tags/auto`.

New

Structured document view at `GET /v1/documents/{id}/structured` — all scalars flattened, ready for warehouses.

Fix

VAT normalization now strips all whitespace (was only stripping leading/trailing).

## v2.7.0

2026-02-14

New

Company identifier resolution: `vat:` and `peppol:` prefixes accepted in any `{company_id}` / `{partner_id}` path param.

New

Batch lifecycle update: `POST /v1/documents/lifecycle/batch`, up to 500 per call.

Changed

HERMES (BE) reporting enabled by default for newly created BE companies. Existing companies untouched.

## v2.6.0

2026-01-22

New

Circuit-breaker visibility at `GET /health/readiness`. Per-upstream state.

New

Webhook secret rotation: `PATCH /v1/webhooks/{id}` with `{"rotateSecret": true}`. Old secret stays valid for 60 minutes.

Fix

Idempotency cache correctly distinguishes requests differing only in a query param.

## v2.5.0

2025-12-05

New

PPF (FR) adapter graduated from beta. Registered PDP status confirmed by DGFiP.

New

ISO 20022 / SEPA export at `POST /v1/payments/export/iso20022`.

## v2.4.0

2025-10-18

New

Events API (`/v1/events`) — durable twin of every webhook, replayable.

New

Rate-limit headers (`X-RateLimit-*`) added to every response.

Changed

Free tier rate limit raised from 30 to 60 req/min.
