---
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.46.3 — AFNOR SIREN search answers from the PPF directory

2026-11-03

Fix

**`POST /afnor/directory-service/v1/siren/search` and `GET /afnor/directory-service/v1/siren/code-insee:{siren}` now read the PPF directory, like every other AFNOR directory route.** They used to query the Peppol Directory. A name search therefore returned Peppol participants that the French directory does not hold, often with an empty `siren`, and could miss legal units the directory does hold. A `siren` filter now returns that legal unit. A `businessName` filter returns the directory's own name-index matches: a case-insensitive substring, without accents, at most 25 rows. `entityType` and `administrativeStatus` come from the directory (`Public` for public bodies, `C` for a ceased unit). A SIREN absent from the directory answers `404` even when Peppol knows it. When the directory itself cannot answer, both routes return `503` (unreachable) or `502` (it answered with an error), never a `404` or an empty page, so a company is never reported absent because of an outage. The Peppol extension fields (`peppolId`, `city`, `postalCode`) are still filled when available. `POST /afnor/directory-service/v1/siret/search` by name now returns the right 9-digit `siren`; it used to keep the VAT check digits. In the sandbox these routes read the PPF test directory, as the other directory routes already do.

## v3.46.2 — A refusal already recorded can still send its lifecycle message, once

2026-10-30

Fix

**`POST /v1/documents/{documentId}/lifecycle` with `rejected` or `failed` now builds the French lifecycle message (CDAR 210 Refusée / 213 Rejetée) when your party already holds that status.** A status set by another route first, a flow for example, used to make the call answer `400` “Transition from 'rejected' to 'rejected' is not allowed”, and no message was ever built. The call is now a re-assert. Nothing is written, no `lifecycle.updated` event is emitted, and the report is dispatched with `compliance.FR.reasserted: true`. It is sent once. If one was already transmitted or is in flight, the response carries `compliance.FR.status: "already_reported"` and its `reportId`. A report that was built but never sent, or a dispatch abandoned for more than 15 minutes, does not count. If the report history cannot be read, the call answers `503` rather than risk a second message. A same-status call on any other status still answers `400`.

## v3.46.1 — Documents sent through the API no longer read as a manual upload

2026-10-29

Fix

**A document sent through the API (`POST /v1/documents/send`, a batch send, or an AFNOR `POST /v1/flows` deposit) is now recorded as arriving through the API.** Before this fix, the Flowie app showed it as “Created by … via a manual upload”, the same as a file a person had dropped in by hand. Documents sent from now on carry the API channel. Documents already sent keep their current label.

## v3.46.0 — Check whether a participant can receive a UBL invoice on Peppol

2026-10-26

New

**`GET /v1/directory/peppol/{participantId}/capabilities` tells you, before you send, whether an address can receive a UBL invoice or credit note on Peppol.** It is answered from the network itself: the OpenPeppol SML names the participant's SMP, and the SMP lists the document types it accepts. The Peppol Directory cannot answer this, because a participant can be absent from it and still be reachable. The response carries `registered`, `acceptsInvoice`, `acceptsCreditNote`, every listed `documentTypes` identifier and the `smpUrl`. `registered: false` is authoritative: the SML has no record, so no access point can deliver to that address. `null` means the network could not be asked, so never block a send on it. Pass the participant as `scheme:value`, for example `0225:552081317` or `0225:552081317_SUFFIX`. A malformed identifier answers `400` without any lookup, and answers are cached for 15 minutes. See [Peppol capabilities](<reference/index.html#peppol-capabilities>).

## v3.45.0 — The two MCP servers name each other, and volume goes to the Exchange API

2026-10-26

Changed

**Each MCP server’s`instructions` now names the other and says which to use for what.** This server (`/exchange/mcp`) presents itself as the high-volume route — an API key’s budget of 120 to 2,400 requests a minute by plan, `send_documents_batch`, and creating a document as one `send_document` call — and points in-app work (approvals, flows, tasks, budgets, reports) at the app server. The app server (`/agent-hub/mcp`) presents itself as an assistant working at a person’s pace and points volume here. See [Which server to use](<build-with-ai/index.html#which-server>).

Changed

**The app MCP server (`/agent-hub/mcp`) allows 60 requests a minute per user**, one object per call; it was 600. Over it, the `429` (with `Retry-After` and the `RateLimit-*` headers) names the Exchange API and this MCP server as the route for higher volume, with the links in `error.data.higherVolume`. The Exchange API’s own limits are unchanged.

Fix

**Creating a document from the app MCP server works.** `create_transaction_document` sent fields the platform does not have (`documentNumber`, `dueAt`, `buyerId`) and left out what it requires, so no call could succeed. It now takes the type, the number, the issue date, the other party (`buyerId` when you sell, `sellerId` when you buy) and the lines (`quantity`, `unitPrice`, `taxRate` as a percentage), computes every amount, accepts `idempotency_key`, and still reads the old argument names.

## v3.44.7 — Search sorts on the key you asked for, and documents `totalAmountInclTax`

2026-10-26

Fix

**`POST /v1/documents/search` now sorts on the first key of `sort`.** Results are sorted on one key. A `sort` with several keys, a direction in capitals (`"DESC"`) or stray spaces used to be ignored without an error, and the results came back by `createdAt` descending. Now the first key applies, case and spaces are normalised, and a field with no direction sorts ascending. The reference lists the sortable fields.

Changed

**The total including VAT (BT-112) is documented as`totalAmountInclTax` in search filters and sorts.** `totalAmountDue` keeps working as a synonym, so existing requests are unaffected. A filter that uses both names keeps both conditions.

## v3.44.6 — A prepaid UBL invoice reports its total including VAT

2026-10-22

Fix

**`grossAmount` is the total including VAT (BT-112) for a UBL invoice that states a prepayment.** A document sent with `format=ubl-xml` stored its amount due for payment (BT-115, `PayableAmount`) as its total including VAT, so `GET /v1/documents/{documentId}` and the document list showed the amount left to pay as `grossAmount`. The two are now stored apart. `balanceDue` on `GET /v1/payments/documents/{documentId}`, and the move to `paid` after a recorded payment, are measured against the amount due for payment when the document states one, otherwise against the total including VAT. An amount due for payment of zero, a fully prepaid invoice, counts as stated: `balanceDue` starts at `0.00` rather than `null`. Field names are unchanged; documents already stored keep their amounts.

## v3.44.5 — Two identical sends at once: the second waits for the first

2026-10-22

Fix

**An identical send that arrives while the first is still being processed now gets the document, not an opaque`409`.** On `POST /v1/documents/send` and the AFNOR deposit `POST /afnor/flow-service/v1/flows`, the Exchange API waits and retries while the request can still finish within about 20 seconds, then answers like any resend: `200` with `Idempotent-Replayed: true`, or `201` if the first attempt had failed. If the first send is still running, the answer is `409` `IDEMPOTENCY_IN_PROGRESS` with a `Retry-After` header and `{"detail": {"code", "message", "retryAfter"}}` (`errorCode` on the AFNOR facade, which now passes `Retry-After` through). `POST /v1/documents/send/batch` does not wait: such an item fails at once with the same error under `results[i].error.detail`. Nothing was done for that request: resend it unchanged after the delay. A `409` for a duplicate document is unchanged. A resend that transaction-documents answers with the existing document is no longer counted as a second sent document and no longer re-links its files and metadata, and a replayed AFNOR deposit, whether served from the idempotency cache or not, no longer emits a second lifecycle message (CDV). See [409 · Conflict](<reference/errors.html#conflict>).

## v3.44.4 — Consent screen: use another account, and a notice for accounts with no organization

2026-10-22

New

**“Not you? Use another account” on the consent screen.** It forgets the consent session and sends you back to Flowie sign-in with the account picker (`prompt=login`), for the same consent request. Before, the screen named the signed-in account with no way to change it.

Changed

**An account that belongs to no organization is told so before it approves.** The screen says to create an organization in the Flowie app or ask to be invited, instead of granting access that cannot read or do anything.

New

**`/.well-known/openai-apps-challenge`** serves the domain-verification token the ChatGPT apps directory issues for a submitted MCP server (`404` until one is configured).

Fix

**A logged`POST` no longer drops all but one `Set-Cookie` header.** The request logger rebuilt the response from a header dictionary, which keeps only one of several cookies.

## v3.44.3 — Every MCP tool has a title

2026-10-20

New

**Each tool on`/exchange/mcp` now carries a `title`**, at the top level and in its `annotations`: the operation’s summary from the [API reference](<reference/index.html>) (for example _Create (or find-and-link) a company from a VAT number_). Assistants show it instead of the raw tool name, and connector directories expect it. Tool names, arguments and the existing `readOnlyHint` / `destructiveHint` / `idempotentHint` / `openWorldHint` values are unchanged. The Flowie MCP servers are also listed on the official MCP Registry as `com.get-flowie/exchange`, `com.get-flowie/app`, `com.get-flowie/docs` and `com.get-flowie/website`.

## v3.44.2 — One copy-ready page to connect an AI assistant over MCP

2026-10-19

Docs

**New page:[`/mcp.md`](<mcp.md>).** Copy it whole into an assistant and it has a working configuration: which server to pick (the App server, `/agent-hub/mcp`, for a person using Flowie through Claude, ChatGPT or Cursor; the API server, `/exchange/mcp`, for integrations), ready-to-paste setups for Claude Code, Claude.ai / Claude Desktop, ChatGPT, Cursor and VS Code on production then staging, how sign-in works, the API-key option, rate limits and troubleshooting. `llms.txt`, [Build with AI](<build-with-ai/index.html#mcp>) and the agent plugin page lead with it.

Docs

**`auth.md` no longer says OAuth sign-in, SSO or dynamic client registration are unavailable to MCP clients.** It said dynamic registration was disabled, gave no production MCP URL and pointed at the identity provider instead of Flowie’s authorization server. It now documents what the servers do: a `401` naming the protected-resource metadata, `authorization_servers: ["https://<host>/exchange"]`, dynamic client registration at `/exchange/v1/oauth/register` (public client, PKCE `S256`), the normal Flowie sign-in with SSO and Google, and a 24 h token renewed with a refresh token. The site’s protected-resource documents and MCP server card say the same, and list both production servers.

## v3.44.1 — Signing in to connect an MCP client no longer fails at random

2026-10-19

Fix

**The OAuth consent flow works whichever server answers each step.** A connection (Claude, Claude Code, VS Code, Cursor…) could fail after you signed in with `{"detail":"Consent request not found or expired."}`, leaving the client without a token: every call to `/agent-hub/mcp` or `/exchange/mcp` then answered `401`. The consent request, the authorization code and handoff tokens were kept by the one server that created them, and the next step could reach another. They are now shared, encrypted at rest, and an authorization code or a handoff token still works exactly once. Nothing changes for clients; reconnect if a connection failed this way.

## v3.44.0 — Linear webhooks arrive through the Exchange API

2026-10-18

New

**`POST /exchange/v1/integrations/linear/webhook` receives Linear’s webhooks** for organizations that connected Linear to their Flowie tasks (staging). It is public like the PunchOut callbacks: Linear cannot hold a Flowie key, so trust comes from Linear’s own `Linear-Signature`, which the Flowie integration bridge verifies. The body and the Linear headers are forwarded unchanged and the bridge’s answer is returned as is; when the bridge does not answer, the route returns `503` and Linear retries. The signature is not kept in the request log. The route is not part of the documented API and is served only where the integration is enabled.

## v3.43.1 — One-click copy for the MCP URLs, and no more sideways scroll

2026-10-18

New

**The home page has a “Connect an MCP client” box** with the production URLs of the Exchange API and Flowie app MCP servers, each with a Copy button. Every MCP URL in [Build with AI](<build-with-ai/index.html#endpoints>) and on the agent plugin page has one too. The App server is no longer marked “coming”: it is live in production.

Fix

**The documentation no longer scrolls sideways** at window widths between about 1000 and 1650 px when the session pill showed a long label such as “Session expired”. The pill now shortens its label instead of widening the top bar.

Fix

The agent onboarding FAQ still said OAuth would come to production later. It now describes what OAuth issues today: 7-day keys and a 90-day single-use refresh token.

## v3.43.0 — One MCP server for the API, one for the app

2026-10-17

Changed

**`/exchange/mcp` now exposes the whole Exchange API**: every documented operation is a tool, where it used to offer a curated subset of eight tags. `/exchange/mcp/full` serves the same tools and stays available for existing configurations. Existing tools keep their names and arguments; a client that wants fewer tools selects them in its host.

New

**The Flowie app is an MCP server too, at`/agent-hub/mcp`** (staging). It offers the tools of Flowie’s in-app agent — documents, workflows, approvals, purchasing, budgets, organizations, payments, inbox and more — and acts as the signed-in user, with their permissions. Sign-in uses the same OAuth flow; see [Build with AI](<build-with-ai/index.html#mcp>).

## v3.42.0 — Sign in to the Flowie app MCP server with the same OAuth flow

2026-10-17

New

**The authorization server now grants _app access_ to the Flowie app MCP server (`/agent-hub/mcp`).** A client whose `/oauth/authorize` request names that server as its `resource` (RFC 8707) is shown a consent screen for full access to the Flowie app as the signed-in user, and `POST /v1/oauth/token` returns that user’s own token instead of an Exchange API key: the app server calls services that act with the user’s permissions. The token lasts as long as the identity provider issues it for (24 hours); when a refresh token is returned (`flw_apprt_…`), `grant_type=refresh_token` renews it here. Only a server on this deployment’s own host qualifies. Every other client is unchanged and still receives an API key.

Changed

`organization_id` in the token response may be `null` for app access, which carries the user’s organizations in `organization_ids`.

## v3.41.1 — Static files under /docs-public load again

2026-10-17

Fix

**Every file served from this API’s`/exchange/docs-public/` copy of the documentation answered `500`** on the deployed version: stylesheets, scripts, images, fonts and most pages. The web framework now listens for a client disconnect while it sends a file, and the API’s request middleware answered it with the request again, which the framework rejects. Static files are now served whole. The documentation at docs.get-flowie.com was not affected, and neither was any `/v1` route.

## v3.41.0 — A clearer consent screen: who gets access, as whom, to what

2026-10-17

Changed

**The hosted consent page at`/oauth/consent` now names the application that receives the access.** The name comes from where the authorization code is sent (its `redirect_uri`): Claude, Visual Studio Code, ChatGPT, Cursor, or an application on your own computer, with the exact destination shown next to it. It used to show the shared public client id every MCP client registers with. A name the client gives itself is shown as an unverified claim and never takes the headline when it looks like a known application. The page loads no third-party resources: its font is served from this API.

Changed

**It shows the signed-in account by email and the number of organizations the key will reach, and folds the permissions into a one-line summary** (how many, how many can change data) that opens to the full list. The page now uses the look of the Flowie web app.

Fix

**Clicking Approve a second time no longer shows raw JSON.** It answers `404` with a page saying the request was already answered. The deny confirmation uses the same page.

## v3.40.5 — Consent sign-in no longer loops back to the identity provider

2026-10-16

Fix

**Signing in on the consent page now lands on the consent screen instead of failing with`ERR_TOO_MANY_REDIRECTS`.** The sign-in session cookie carried the user’s whole access token. For a user who belongs to many organizations that token passes 11 KB, so the cookie exceeded the 4 KB browsers accept and was silently dropped. The consent page then saw no session and sent the browser back to sign in, again and again. The cookie now holds only who signed in. The token stays on the server, bound to the consent request it was obtained for and to that user. An unknown or expired consent request now answers `404` before any redirect to sign-in.

## v3.40.4 — Signing in on the consent page no longer ends on a 404

2026-10-16

Fix

**After signing in, the consent flow now returns to`/oauth/consent?req=…`.** The sign-in callback redirected to a relative `./consent`, which a browser resolves against `/oauth/consent/callback` to `/oauth/consent/consent`. That page does not exist, so an MCP client’s sign-in stopped on `{"detail":"Not Found"}` right after Auth0. The redirect is now absolute and keeps the deployment prefix. Approve and deny are unchanged.

## v3.40.3 — VS Code can sign in to the MCP servers

2026-10-16

Fix

**The`401` from `/exchange/mcp` and `/exchange/mcp/full` now points at a protected-resource document for that server, whose `resource` is the server’s own URL.** It pointed at the shared `/.well-known/oauth-protected-resource`, whose `resource` is the token audience `https://auth.flowie.me`. RFC 9728 §3.3 tells a client to reject a document that names another resource. VS Code does, so its `initialize` hung and then failed with `Cannot read properties of undefined (reading 'startsWith')`, without ever opening a sign-in. The new documents are served at `/.well-known/oauth-protected-resource/mcp` and `/.well-known/oauth-protected-resource/mcp/full`, and at their path-inserted forms `/.well-known/oauth-protected-resource/exchange/mcp` and `…/exchange/mcp/full`. Tokens, scopes, the authorization servers and the shared document are unchanged.

## v3.40.2 — MCP connections no longer drop with “Session not found”

2026-10-15

Fix

**`/exchange/mcp` and `/exchange/mcp/full` no longer answer `404 "Session not found"` partway through a connection.** Each MCP session lived in the memory of the one server that opened it, so on an environment running several replicas a client’s next request regularly reached another one and was refused. Both servers are now stateless: every request stands on its own Bearer token, whichever replica answers it, and `initialize` no longer returns an `Mcp-Session-Id`. Clients need no change. Tools, arguments and authentication are unchanged.

Fix

**The documentation no longer prints MCP tool counts.** The [endpoints table](<build-with-ai/index.html#endpoints>), the agent plugin page and the `connect-mcp` skill said 34 curated and 94 full tools; the servers serve more, and the number moves with every release. They now describe what each server covers, and `tools/list` returns the live set.

## v3.40.1 — Final invoice after deposits: both deductions the norm allows

2026-10-12

Fix

**The guidance for a final invoice after deposits (XP Z12-014 cases 20 and 21) said never to deduct a deposit as a negative line. That was wrong.** The norm allows two options and recommends the first: one take-back line per deposit at quantity `-1`, referencing the deposit invoice (type `386`), which keeps the VAT and the pre-filled VAT return right. The second, `prepaidAmount` (BT-113), stays available, but the norm notes that it makes the pre-filled VAT return count the deposits’ VAT twice. Do not combine the two. The [deposit section](<compliance/fr/use-cases.html#acompte>), the API reference, the description of `document.prepaidAmount` and the UBL generator’s scenario `uc-21-final-after-deposit` now say so. No change to how documents are processed.

## v3.40.0 — A final invoice can reference every deposit invoice and state the amount already paid

2026-10-10

New

**`POST /v1/documents/send` accepts `document.billingReferences` and `document.prepaidAmount`.** `billingReferences` is a list of `{id, issueDate}`, one per preceding invoice (BG-3, 0..n), so a final invoice after several deposit invoices can reference each of them; the first entry also fills `billingReference` / `billingReferenceDate`, and sending both with different first invoices is refused with `422`. An invoice number containing `;` or `|` is refused. `prepaidAmount` is the amount already paid, VAT included (BT-113): it is stored on the document and reduces the amount due (BT-115 = BT-112 − BT-113) without touching the taxable base; a `prepaidAmount` above the invoice total incl. VAT is refused with `422`. Each reference is rendered as its own `BillingReference` when your organization's e-invoicing flow reads the `precedingInvoiceReferences` field; otherwise only the first one is. Until now this case could only be sent as `format=ubl-xml`. See [Deposit then final invoice](<compliance/fr/use-cases.html#acompte>).

## v3.39.3 — Conformance checks return the validator’s real verdict

2026-10-07

Fix

**`POST /v1/documents/validate` and the deposit-time conformance check now return the French validator’s actual verdict.** When the caller authenticated with a Flowie JWT, the request to the validator carried that token, and the validator’s front end rejected the oversized header before checking anything: every call came back `valid: false` with a single `validator_error`, and the check at deposit time was skipped. The validator is now called without the caller’s credentials, so a conforming document is reported valid and a non-conforming one returns its rule failures. Expect real rule ids (XSD, EN 16931, BR-FR) where you previously saw `validator_error`.

## v3.39.2 — No zero-amount stand-in for a document’s XML

2026-10-05

Fix

**`GET /v1/documents/{document_id}/xml` and the AFNOR `GET /flows/{flowId}` (`docType=Original` or `Converted`) answer `404` rather than a stand-in that misstates the amounts.** When no XML is stored for a document, the UBL rendered from its structured data is now served only if it carries the document’s amount. A document with no lines and no amount, or a rendering whose payable amount is zero or absent while the document has a total, used to come back as a minimal UBL for 0.00, presented as the document’s XML; it now answers `404` “XML not available for this document”, like a document with nothing to render. Documents whose XML is stored are unaffected. `docType=ReadableView` now applies the same rule to decide whether a document has anything to render as a PDF, so a document is never renderable for one and empty for the other.

## v3.39.1 — Basikon no longer carries another platform’s address

2026-10-04

Fix

**The platforms directory gave Basikon the contact address of FISKALTRUST, a different platform.** The DGFiP list we import carries `info@fiskaltrust.fr` on both rows. Basikon publishes no contact address, so `GET /v1/portability/platforms` and the public directory now return `contactEmail: null` for it, and an inter-platform portability message to Basikon stays unresolved until you name the recipient in `to`, instead of being written to FISKALTRUST. The correction is applied on every refresh of the list, so it cannot come back.

## v3.39.0 — Every credential of your organization, in one list

2026-09-30

New

**`GET /v1/credentials` lists every credential that can act on your organization**: API keys (including multi-organization agent keys), the OAuth clients your admins created, and the OAuth clients Flowie set up for an integration (`managedBy: flowie`). Each entry gives its type, name, identifier, access, creation and last use, and never a secret. Admins only; `?includeInactive=true` adds revoked and expired ones. The same list is on the API keys page of these docs and on the API credentials page of the Flowie app.

## v3.38.1 — `/v1/me` no longer slows every other call for users in many organizations

2026-09-30

Fix

**`GET /v1/me` looks up at most eight organizations at a time.** For a user in a hundred organizations it used to look them all up at once, and on a busy pod the rest of that user’s requests could then fail with `504` “Internal service timeout” for several seconds. Past its time budget the endpoint still answers, with the organizations it did not reach returned by id only (no name) — now alongside the ones it did, instead of dropping every name.

## v3.38.0 — Create your own OAuth2 client credentials

2026-09-30

New

**`POST /v1/oauth/clients` lets an organization’s admin create a `client_id` / `client_secret` pair for the OAuth 2.0 client-credentials grant, without asking Flowie.** The client acts as its own technical user, never as the admin who created it, with exactly one role in each organization listed — the read-only viewer role unless another is named. The secret is returned once. `GET /v1/oauth/clients` lists the organization’s clients, `POST /v1/oauth/clients/{client_id}/rotate-secret` issues a new secret and `DELETE /v1/oauth/clients/{client_id}` revokes the client. Only an admin of every target organization can call these, with a user token: API keys and OAuth clients are refused with `403`. See the [authentication guide](<auth.md>), section _Machine-to-machine_.

## v3.37.1 — The other party’s files need access to the other party

2026-09-29

Fix

**`GET /v1/documents/{document_id}/pdf` considers the files attached by the other party of the document only when your credentials give you access to that party’s organization.** Since v3.37.0 the endpoint looked for the issuer’s PDF among the files of both parties as soon as you could read the document. A buyer with no access to the seller’s organization now gets the next source instead — a Flowie rendering or a PDF generated from the document — and `X-Flowie-Pdf-Source` says which.

## v3.37.0 — The invoice PDF in one call, and the right one

2026-09-28

New

**`GET /v1/documents/{document_id}/pdf` accepts the invoice number as well as the id.** With `X-Flowie-Organization-Id` naming the entity, `GET /v1/documents/FA-2026-0042/pdf` returns the PDF directly — no search call first. A number that matches several documents in that organization answers `409` with the candidate ids, as `GET /v1/documents/{document_id}` does.

Changed

**Which PDF is served is now fixed, and named in the`X-Flowie-Pdf-Source` response header.** In order: the document’s external file when it is a PDF (`external`); a PDF the issuer attached, deposited with the invoice or embedded in it (`issuer`); a Flowie rendering already attached (`flowie`); a PDF generated from the structured document (`generated`); and only then a platform’s rendering (`pa`). The endpoint used to serve whichever attached PDF came first, which on documents routed through a partner platform was that platform’s rendering, with its own layout and submission pages, rather than the issuer’s invoice.

Fix

**An XML file is never returned as a PDF.** When the document’s external file is the e-invoice XML (UBL, CII, IDOC) and no PDF is attached, the PDF is generated from the document instead of the XML being served with an `application/pdf` media type.

## v3.36.2 — A lifecycle message says it is about one invoice, and rejects with a rejection motif

2026-09-25

Fix

**Every CDAR lifecycle message now carries`MultipleReferencesIndicator` (MDT-74), set to `false`.** The field is mandatory in XP Z12-012 annex A — it states that the status is about a single invoice — and it was missing from every lifecycle flow served by `GET /afnor/flow-service/v1/flows/{flowId}`, so a receiving platform that validates it refused the message. The FNFE CDV schematron does not test the field, which is why ours passed without it.

Fix

**A deposit Flowie rejects at ingress (`213` Rejetée) now gives the motif `REJ_SEMAN` instead of `NON_CONFORME`.** `NON_CONFORME` is a motif for `210` Refusée and `207` En litige only, so the rejection broke `BR-FR-CDV-CL-09`. The failing rules still travel in the free-text `Reason`.

## v3.36.1 — A document webhook tells you about your document, and only once it is yours

2026-09-24

Fix

**`document.received`, `document.updated` and `document.deleted` carry the documented `data`, built for your organization.** They carried the platform’s internal event instead, the same one for every organization on the document — which included the counterparty’s templates (its internal custom field names and their options, such as cost centres) and the counterparty’s own custom field values. `data` now holds `documentId`, `type`, `number`, `direction`, `issueDate`, `dueDate`, `currency`, `netAmount`, `grossAmount`, your own `status`, `sender` and `receiver` (`organizationId`, `name`, `vatNumber`, `peppolId`), `createdAt` and `updatedAt`. `document.updated` adds `changes`, the before/after of each of those fields that moved, and is sent only when one of those fields changed, so `changes` is never empty — an edit to custom fields, yours or the counterparty’s, is no longer a webhook. `document.deleted` carries `documentId`. See [Payload shape](<reference/webhooks.html#payload>).

Fix

**A draft reaches the counterparty’s webhooks only once it is shared with it.** A draft names its counterparty before it is shared, and every organization named on a document was notified — so a supplier’s endpoint heard `document.received` for an invoice its customer had not yet sent, and `document.updated` for every edit to it. Document webhooks now go only to the organizations the document is visible to.

## v3.36.0 — A connection can outlive the key it started with

2026-09-23

New

**`grant_type=refresh_token` on `POST /v1/oauth/token`.** The token response now carries a `refresh_token`; present it at the same endpoint to get a fresh key. The grant has been advertised in `grant_types_supported`, and echoed back by client registration, since those documents were written — and nothing implemented it. That is not a harmless gap: clients act on the advertisement, refreshing on a `401` and shortly before the stored expiry, so a connection that worked would simply stop at the key’s 7-day expiry, having asked to renew and been refused.

New

**Refresh tokens rotate.** Each redemption issues a new key _and_ a new refresh token, and revokes the pair presented — OAuth 2.1 requires that of a public client, which every MCP client is. A replayed token answers `invalid_grant`, as does an unknown or expired one; that is the code clients branch on, and anything else reads to them as a transient error worth retrying. A refresh token stays redeemable for 90 days, so an agent in regular use never reaches the bound.

Changed

**The renewed credential keeps exactly the reach it had** — same organizations, same per-organization scopes, same owning user. Permissions are not recomputed at refresh: they were resolved from `roles-permissions` using the signing-in user’s own token, which is long gone by then. Revoking the key, or letting the refresh token lapse, is what ends the access.

Fix

**A malformed token request answers`400`, not `500`.** The validation branch put the validator’s own error objects in `error_description`, which are not JSON-serialisable, so the response failed while being written. It is a string now, as [RFC 6749 §5.2](<https://www.rfc-editor.org/rfc/rfc6749#section-5.2>) specifies.

## v3.35.0 — A document comes back with its lines

2026-09-23

Fix

**`GET /v1/documents/{id}` returns the document’s lines.** It fetched them — and even resolved their custom field names — then left them out of the response, so an integrator got the header and totals of an invoice with no way to read what was invoiced short of downloading the XML. The response now carries `lines`, one entry per line as recorded on the platform, in the same field names `InvoiceLine` takes on input: `description`, `quantity`, `unit`, `unitPrice`, `netAmount`, `vatRate`, `vatCategory`, plus `vatAmount` and `grossAmount`. `POST /v1/documents/{id}/actions` returns the same shape. The route’s description, which promised the lines in a nested `document` body that was never filled, now says what is returned.

New

**What a line says about its item comes back too.** `itemAttributes` carries the additional item properties (BG-32, each `name` / `value`), `itemStandardId` and `itemClassificationId` the identifiers (BT-157, BT-158), `allowancesCharges` the line-level allowances and charges (BG-27 / BG-28), and `customFields` the line’s custom fields, keyed by name — your organization’s only: a counterparty’s line fields are never returned. A sandbox document now carries a line as well, so the shape can be built against before going live.

Changed

**Values read the way the API writes them.** `vatRate` is a percentage (`20.0`), as on input, although the platform stores it as a fraction; `discountRate` is a percentage too, whichever way the platform recorded it; `unit` and `priceBaseUnit` are UN/ECE Rec 20 codes (`KGM`, `HUR`), although the platform stores a unit name. A line is returned as recorded, whatever recorded it: every field is optional, and a line the input schema would refuse — no description, a negative price — still comes back rather than failing the read.

## v3.34.0 — The Approve button works behind the gateway

2026-09-23

Fix

**The consent form posted to a URL one prefix short, and answered`404`.** This service is served under `/exchange`, and the paths it declares internally carry no such prefix. Every other URL on the consent page was already built from the incoming request — the form’s own `action` was not, so a reader could register a client, sign in, and reach a correctly rendered consent screen that could not be submitted. It is built from the request now, like the rest of the page.

## v3.33.0 — Every MCP client can sign in, not just the ones we listed

2026-09-22

A redirect to your own machine is now accepted on any port and any path, so a native client needs no entry in any list — today or in future.

Fix

**A loopback redirect URI matches on any path, not only`/callback`.** The port was already ignored, as [RFC 8252 §7.3](<https://datatracker.ietf.org/doc/html/rfc8252#section-7.3>) requires; the path was not, and that quietly excluded most of the ecosystem. The MCP SDK’s own default is `/oauth/callback`, so `mcp-remote` — the generic bridge most remote servers are reached through — and the MCP Inspector were both refused, while Claude Code’s `/callback` worked. Matching the path bought nothing: the only attacker who can receive a loopback redirect is another process on the same machine, and it can serve `/callback` exactly as easily. A URI with no path at all is accepted too.

New

**Loopback is a rule now, not a list.** Any `http` redirect to `localhost`, `127.0.0.1` or `[::1]` is accepted whatever port and path it carries, so Claude Code, VS Code, Cursor, `mcp-remote`, the MCP Inspector, opencode, Cline, Continue, Goose, Zed and anything built on the official SDKs all connect with no configuration on our side. `ALLOW_LOOPBACK_REDIRECT_URIS=false` turns it off for a deployment that deliberately serves hosted clients only.

Changed

**The exact-match list is now only for hosted clients.** Those are the ones that redirect to an https URL — Claude on the web, Desktop and mobile; VS Code and Insiders through their `vscode.dev` handler; ChatGPT connectors. Cursor’s old `cursor://` scheme is kept for anyone still on a build before 3.10.17, which is when Cursor moved to a loopback callback because, in their words, many OAuth providers will not accept a custom-scheme URI. Nothing about matching a public host changed: its port and path are part of its origin and are still compared exactly.

New

**A refused registration is logged with the callback it asked for.** Every hosted entry on that list was found by reading somebody’s failure; now the next unsupported client arrives as a log line rather than a support thread. The `400` already named the URI back to the caller — this records it for us too.

## v3.32.0 — Any MCP client can sign in, including the ones on a loopback port

2026-09-22

This server now runs the authorization-code flow itself instead of sending clients to the identity provider, which is what stopped native clients working at all.

New

**`GET /oauth/authorize` — a standard [RFC 6749](<https://www.rfc-editor.org/rfc/rfc6749#section-4.1.1>) authorization endpoint.** The existing `POST /v1/oauth/authorize` takes JSON and returns a consent URL, a shape only a client written against this API can use. Every general OAuth client — and every MCP client is one — opens a browser `GET` with query parameters, so the metadata had to advertise somebody else’s `/authorize`, and it advertised the identity provider’s. Both endpoints work; the JSON one is unchanged.

Changed

**`authorization_endpoint` and `token_endpoint` are this server’s.** They pointed at the identity provider, and that quietly excluded every native client. The identity provider matches redirect URIs exactly — no wildcards, no port-agnostic loopback — while a native client binds an _ephemeral_ port it cannot know in advance: Claude Code declares `http://localhost/callback` and then listens on, say, `:3118`. It was refused every time, on a different port every time, so Claude Code, VS Code and Cursor could not have worked whatever anyone allowlisted. The identity provider now sees exactly one redirect URI — this server’s own consent callback — and adding an MCP client is one entry in this server’s allowlist rather than a change in the identity tenant. **Nothing about how a person signs in changes:** the consent page still hands them to Auth0 Universal Login, with whatever their organization has configured — SSO, username and password, social.

Fix

**A loopback redirect URI now matches on any port.** [RFC 8252 §7.3](<https://datatracker.ietf.org/doc/html/rfc8252#section-7.3>) requires this for `127.0.0.1`, and the same treatment is applied to `localhost`, which is what Claude Code needs. Scheme, host, path and query must still match exactly, and the rule applies _only_ to loopback — for any other host the port is part of the origin, and ignoring it would widen the allowlist to a different service.

Fix

**`POST /v1/oauth/token` accepts `application/x-www-form-urlencoded`.** That is what [RFC 6749 §4.1.3](<https://www.rfc-editor.org/rfc/rfc6749#section-4.1.3>) specifies and what every general OAuth client sends; the endpoint had only ever been driven by our own client, so it accepted JSON alone and answered `415` to everyone else. JSON still works.

New

**The default redirect allowlist covers the hosted clients.** Claude (web, Desktop, mobile), Claude Code, VS Code and Insiders, Cursor, ChatGPT connectors, and out-of-band. A client whose callback is not listed is refused at `/authorize` with `invalid_redirect_uri` and is never redirected to — bouncing an error to an untrusted URI is the open-redirect problem the allowlist exists to prevent. Protocol errors _after_ that check are returned through the redirect URI, per [RFC 6749 §4.1.2.1](<https://www.rfc-editor.org/rfc/rfc6749#section-4.1.2.1>), because a client waiting on a loopback listener cannot read an HTML error page.

## v3.31.0 — An agent credential reaches your own data, and the tools are named after the job

2026-09-21

Signing in to authorise an agent now produces a credential on your real organizations, bounded by what you are actually allowed to do in each of them.

Changed

**Approving an agent binds it to your organizations, not to a fresh sandbox.** The sign-in was real and the consent screen was real, and then `POST /v1/oauth/token` minted `org_sbx_…` — an empty organization created on the spot — so a connection that reported success showed the agent none of your documents. The credential is now issued against every organization the signing-in user can reach, and it is a `flw_live_…` key because it touches live data. A deployment with no sign-in configured still issues the sandbox key it always did.

Changed

**Scopes come from your permissions, not from what the agent asked for.** They were checked against this service's own scope list, so a credential said `documents.write` because the client requested it. Each scope is now granted only where `roles-permissions` — the authority every other Flowie service keys on — says the user holds the matching right: `documents.read` needs `read:transaction:invoices`, `send` needs `create:transaction:invoices`, `lifecycle` needs `update:transaction:invoice-lifecycle`. An account holding none of them in any organization is refused at the consent screen rather than handed a key that fails every call.

New

**`organization_ids` on the token response.** Every organization the credential may act on. Name one in `X-Flowie-Organization-Id` to act there; any other is refused with `403`. Scopes are kept _per organization_ , so an agent whose user approves invoices in one company and only reads them in another cannot carry the first company's rights into the second — the header selects the organization and the rights together.

Changed

**MCP tools, SDK methods and endpoint anchors are named after the job.** `send_document_v1_documents_send_post` is now `send_document`; `list_documents`, `search_documents`, `get_lifecycle` likewise, across all 139 operations. The old names were FastAPI's generated default — handler, path and method concatenated — and they are what an agent host shows the model on every single request. Six pairs of operations turned out to have been relying on the path suffix for uniqueness (the AFNOR certification twins of the webhook routes, two health checks, and the OCI callback's `GET`/`POST` pair); they are named apart explicitly now. **If you generate a client from`openapi.json`, your method names change with this release.**

Fix

**The consent page no longer promises a sandbox to someone signing in.** It told every visitor that approving would mint a sandbox key on a fresh organization, including the users about to grant access to their real one. It now names who is signed in and what the key will reach.

## v3.30.0 — What is on screen is what gets signed, and a signed act proves it

2026-09-21

Changed

**The agreement form saves itself; the _Update the agreement_ button is gone.** A field that has stopped changing is saved, a value picked from a list is saved at once, and the addresses filled in from the annuaire are saved with them. The deed, its digest and the « still to do » line follow — the inputs are never re-rendered, because replacing a form somebody is typing in steals the caret. Downloading and signing still flush anything in flight first.

Fix

**The page saves the agreement before downloading or signing it.** The form is a draft; the document is rendered from the stored mandate. So a reader who filled the addresses in and pressed _Download_ got a deed reading « Aucune adresse » and « Pour l’assujetti : — », and one who pressed _Sign_ was refused for a missing `addressScope` sitting two centimetres above the button. Downloading, signing here and uploading a signed copy each flush the draft first. An amendment that changes nothing still changes nothing, digest included.

Fix

**The attestation quotes the digest that was signed, never a fresh one.** Re-rendering a mandate under a newer template gives different bytes, so a document signed before a template change would have printed today’s hash under « Texte signé » — an empreinte nobody ever signed. The digests now come from the signature record; when the reproduction differs, the act says so, prints both, and names the stored piece holding the text that was signed.

New

**A signed agreement carries the proof of its own signature.** The `.docx` gains a second page — _Attestation de signature électronique_ — naming who signed, when, by which means, and the recorded digests: the signed text, the mandate, and the evidence-chain entry with the one before it. The agreement’s own pages are untouched, because their bytes are what the first of those digests is taken over, and the attestation says in plain French how to recompute it. An unsigned draft attests nothing.

Fix

**The act states its date in words.** « Le présent accord prend effet le 2026-09-29 » — an ISO date, in a French deed, in the one line of it carrying a date. It reads _29 septembre 2026_ now (and _1er_ for the first of the month), from a table rather than a locale so the digest cannot depend on the server.

Fix

**An empty address scope reads as empty.** It printed as a bullet holding a dash — a list of one nothing. It is a sentence: _aucune adresse électronique n’est nommée dans le présent accord : le périmètre reste à compléter_.

## v3.29.0 — A port is live on the next nightly run

2026-09-21

Changed

**A request opened without a date d’effet now takes effect on the first nightly annuaire run after the objection window — not a business day later.** The directory is re-pointed by a batch that runs every night at 02:00 Europe/Paris, so a port agreed on day D is live on D+1, weekends included. Planning against the fifteen-business-day backstop cost the taxpayer two or three days of invoices still arriving at the platform they were leaving. Pass `effectiveDate` to choose any other date, exactly as before.

New

**`clocks.annuaireNextRun` and `clocks.annuaireRunsAt`.** When the directory will actually move, beside `annuaireUpdateBy`, the date nobody may go past. Both are published because the difference between them is what a taxpayer is entitled to know before agreeing a date.

Changed

**A picked platform is shown by name.** The fields carried `0022`, or the slug `flowie`, while the schema two lines above named the same platform properly. They now read `PENNYLANE (0022)` — the way the deed names it — and the matricule is resolved back out on save, because that is what the annuaire routes on.

Fix

**The agreement can be downloaded at any moment.** The link lived inside _Sign your own copy_ , a panel that disappears the moment the mandate is signed — so the one document anybody would want to keep became undownloadable exactly when it started to matter. It sits beside the agreement now, signed or not.

## v3.28.0 — An organization you name is one you hold

2026-09-20

Fix

**Naming an organization in`X-Flowie-Organization-Id`, `Organization-Id` or `companyId` now only widens access to organizations underneath one you already hold.** The check behind those three was “does org-v2 know this id”, and org-v2 answers that for any organization to any authenticated caller — so a token could be pointed at an organization it had no standing in. Access is now read from the ownership hierarchy: a sub-organization of yours still resolves, a parent, a sibling or an unrelated tenant answers `403`, and an ownership lookup that fails refuses rather than widens. API keys are unaffected — they were already bound to their organization.

## v3.27.0 — The panel reads like a move, and asks each thing once

2026-09-20

New

**`clocks.infoHandoverBy`: the handover deadline as a date.** It was published as `infoHandoverBusinessDays: 5` and nothing else, leaving a caller to count _jours ouvrés_ over bank holidays by hand — the one arithmetic this API exists to do. It runs from the notification once one is recorded, and from the opening until then, the same conservative basis as the objection window.

Changed

**The schedule shows all six deadlines, not four.** The two it hid are the ones the _outgoing_ platform owes — the continuity information it has to hand over, and the date the annuaire entry is re-pointed by — which are exactly the dates a taxpayer chasing a late port has to quote.

Changed

**The port reads left to right.** The platform being left comes first, under a drawing of the move: _leaving_ → _designated_ , each with the name and matricule the deed will carry. Two stacked fields labelled « désignée » and « quittée » carried a direction only a reader who already knew it could see.

Changed

**The addresses the annuaire publishes are filled in, not offered.** The page had already read them to say who routes the company; it then reported the empty field as a gap and made the reader press a button to copy them across. The button stays, because an edit has to be undoable, and nothing is written until _Save_.

Fix

**« Signed by » was asked three times on one screen.** Once as the blank of the deed, then again in each of the two ways to sign — three inputs for one datum, any two of which could disagree, with the last one written winning silently. It is asked once, in the agreement, and each way says whose name it is about to put on the act.

Changed

**« This company is fully identified » is gone.** The panel above it lists what was found; a line announcing that it found it was one more thing to read where everything else is something to act on.

## v3.26.0 — The platforms are picked from the registry, not typed

2026-09-20

Changed

**Both platform fields search the 165 registered PAs.** A dropdown that opens on focus and filters by name or by matricule, fed by `GET /v1/portability/platforms` — one fetch, because a directory of 165 rows is not a search space. Nobody should be typing four digits from memory into a deed.

New

**Every offered matricule says how sure it is.** `confirmed` (the operator told us), `published` (the operator says so) or `inferred` (its own SIREN routes on it) — as a chip in the list and a full sentence under the field. `0003` looks equally certain whichever way we came by it, and a reader about to sign is entitled to the difference. A platform with no provable number is offered too, and the agreement names it as typed rather than inventing one.

Changed

**The platform being left arrives already filled in.** The annuaire answered before the reader pressed anything — the panel above the button had just named it — so opening the request carries it. The field says it was pre-filled, and it stays editable.

Changed

**The agreement names the platform it numbers, as far as it can prove it.** « Plateforme quittée : 0003 » names nobody, so the same registry join the rest of the page uses now reaches the deed — carrying its evidence. The platform designated is named as the annuaire spells it, not by the service's own slug. A matricule its operator confirmed or published is named plainly (`CEGID (0007)`); one we _inferred_ is named with the reservation written into the act itself, because 63 of the 165 registered platforms are in that tier and a binding document may neither drop the name nor vouch for it. Re-rendering an unsigned mandate therefore moves its digest, as any template change does — the digest recorded with a signature is kept beside it and is not recomputed.

## v3.25.0 — Every blank can be filled, and a port cannot start here

2026-09-20

Changed

**The designated platform is amendable, and defaults to Flowie.** `incomingPlatform` joins `effectiveDate`, `addressScope`, `signatory` and `outgoingPlatform` on `PATCH …/mandate`. It reads like identity and is not: the decree calls it _plateforme agréée désignée_ , it defaults to us because this is our API, and a taxpayer designating somebody else was previously obliged to open a new request — after reading an agreement that named the wrong platform.

New

**Every blank in the deed now has a field on the page.** The platform designated and the platform being left had none, so “Plateforme quittée : —” could only ever be a dash.

Changed

**A port that would start at Flowie is refused with somewhere to go.** A designation moves a taxpayer _to_ us; from us to us is not a port. Naming Flowie as the platform being left — by its id or its matricule, at opening or on an amendment — answers `409` and points at `support@flowie.fr`. A caller who names the platform being left is taken at their word; when nobody names one, the annuaire is asked at opening, best-effort: an annuaire that cannot be read never refuses a port, because silence is not evidence. The page shows the same address instead of a button nobody could use.

## v3.24.0 — The agreement as a real Word document

2026-09-20

New

**`GET /v1/portability/requests/{requestRef}/agreement.docx` serves the agreement as an OOXML package.** A genuine `.docx` — content types, relationships, document and styles — rather than markup wearing a Word extension, so Word, LibreOffice and Google Docs open it without argument. It is generated from the text `GET …/agreement` returns, which is the text the digest is taken over, and **nothing is added** : no generation date, no page numbers, so two downloads of one agreement are byte-identical. The response carries `X-Agreement-Sha256`, so a caller holding an older copy can tell it apart from the current text without opening either.

Changed

** _Sign your own copy_ hands over that document.** The page fetched a `.doc` it assembled itself; it now asks the API for the real package. One renderer instead of two, and the file the taxpayer signs is the one the API vouches for.

Fix

**Article 1 was cut in half.** It opens “L’assujetti désigne la plateforme désignée…”, which is how the identity lines in the parties block open too — so its first line was set apart from the rest of its own sentence, on the page and in the document. Identity lines are now recognised only between the parties banner and the clauses.

## v3.23.0 — The agreement looks like the document it is

2026-09-20

Changed

**The designation agreement is set as a deed, not as a code listing.** A sheet with margins, a serif face, a centred title, section banners, numbered articles and the address scope as a list — on screen it now reads as the thing somebody is about to be bound by. **The bytes are untouched:** the sheet is built from the exact text the signature is taken over, the digest below it is unchanged, and the verbatim text is kept beside the rendering rather than read back out of it.

Changed

**Downloading it gives you a document a word processor opens.** _Sign your own copy_ hands over the deed with its formatting, under a `.doc` name, so it arrives looking like what was on screen instead of a wall of monospace. The plain text travels inside the file, so the bytes that were hashed stay with the document they came from.

## v3.22.2 — Signing here says what it kept

2026-09-20

Fix

**The new “sign it here” path fell through to the generic confirmation.** Each way of signing has its own sentence — the uploaded document, the agreement approved in Flowie, a signature given on paper — and `accepted`, added with the in-page path, had none. So the way a reader of this page is most likely to sign was the one that could not say what it had kept. It now reads “the text you read here is kept and hashed, as the proof”, and a test fails if a recorded method ever lacks a sentence again.

## v3.22.1 — A refused signature leaves nothing behind

2026-09-20

Fix

**`POST /v1/portability/requests/{requestRef}/mandate/signature/accepted` wrote the signatory before it had somewhere to keep the agreement.** The name was amended onto the mandate — an entry in the evidence chain — and only then was the rendered text stored. When the documents service could not take it, the call answered `502` saying nothing had been recorded, while the mandate already named somebody who had signed nothing. The text is now rendered from the mandate as it will be, stored first, and nothing reaches the chain until the artefact is safe.

## v3.22.0 — A formal agreement, and two ways to sign it

2026-09-20

The designation agreement now reads as the deed it is, and the taxpayer who has just read it can sign it without producing a document of their own.

Changed

**The agreement is written as a formal deed.** Parties identified, numbered articles (objet, plateforme quittée, date d’effet, périmètre des adresses, portée et durée), a signature block, and the reference to article 242 _nonies_ E bis. The five decree items are unchanged — this is how they are set out. **The rendered text therefore differs from the previous wording:** a mandate signed before this release keeps the digests recorded with its signature, which are what that act was taken over, and re-rendering it today shows the current template rather than the bytes that were signed.

New

**`POST /v1/portability/requests/{requestRef}/mandate/signature/accepted` signs the text as rendered.** The fourth way to sign, and the only one where the artefact and the rendering are the same document: our text is stored and the signature is taken over exactly those bytes. The instant is observed rather than asserted, because the act happens in the call. Refused (`409`) on an incomplete mandate or one already signed, and (`502`) when the text cannot be stored — without the artefact it would be a paper assertion again, so nothing is recorded.

New

**The page offers both ways, named, side by side.** _Sign it here_ takes the name of whoever signs and records the agreement on screen. _Sign your own copy_ downloads that same text to sign by hand, by e-signature or in your advocate’s wording, then takes the file back. The download is the bytes already on screen, never a second fetch that might differ.

New

**The addresses covered fill themselves.** The annuaire publishes every addressing line declared for the SIREN, and the page has already read them — so the field offers them in one click and says how many. They stay editable: a scope is the taxpayer’s to decide, and a subsidiary’s line may belong to a port they are not making today.

## v3.21.2 — A recorded signature says so

2026-09-20

Fix

**Signing the agreement left no trace on the page.** The signature block is shown while the signature is missing, so recording one removed the block — and with it the line confirming what had just happened. The request carried the signatory, the instant and both digests, and the page said nothing at all. It now states “signed by _name_ on _date_ ”, rendered from the request rather than shown once, so it is still there on the next read.

## v3.21.1 — The “updated” line stays on screen

2026-09-20

Fix

**Completing the mandate said nothing, although it worked.** On the Change platform page, filling in the _date d’effet_ , the addresses or the signatory re-rendered the agreement and moved its digest correctly — and the confirmation explaining that the digest had moved was painted onto the panel a moment before the re-read of the request replaced it. The message is now shown on the panel that comes back.

## v3.21.0 — The agreement, filled in, completed and signed in one place

2026-09-19

A request could be opened and signed, but the document it rests on could not be read without asking somebody to sign it — and what opening could not know could never be filled in afterwards.

New

**`GET /v1/portability/requests/{requestRef}/agreement` returns the designation agreement as it currently reads.** The five items article 242 _nonies_ E bis requires, rendered from the mandate and filled in from whatever was resolved, plus `agreementSha256` — the digest of exactly those bytes, so a caller can show the text and prove afterwards which version was on screen. It reads and writes nothing: until now the agreement came back only from the call that opens an approval check, so seeing the document meant starting a signature.

New

**`PATCH /v1/portability/requests/{requestRef}/mandate` completes an unsigned mandate.** A request opens from a single identifier, so the address scope is derived and the signatory is usually nobody yet; `mandateGaps` said so and said so for ever. `effectiveDate`, `addressScope`, `signatory` and `outgoingPlatform` can now be sent, singly or together. Each amendment is an entry in the evidence chain carrying only the fields that actually moved, and an amendment that changes nothing changes nothing — including the digest.

Changed

**The digest moves with the text, and a signed mandate is frozen.** Amending re-renders the agreement, so an approval opened on the old one stops answering for the new one — it is not the agreement that was put in front of anybody. Once a signature exists the text no longer moves: `PATCH` answers `409`, because that digest is what somebody put their name to. What the request _is_ — its reference, its taxpayer, the incoming platform — was never amendable and still is not.

New

**The Change platform page shows the agreement, and lets you finish it.** Opening a request now displays the rendered text with its digest, above inputs for the date d’effet, the addresses covered and who signs. Updating re-reads the request, so “nobody is named as signing it” disappears the moment somebody is, and signing re-reads it again — signed is not the end of the procedure, it is the start of the clocks.

## v3.21.0 — An agent can register itself and sign you in

2026-09-20

Connecting this server to an AI client now takes a URL and a sign-in, with nothing to configure.

New

**`POST /v1/oauth/register` — RFC 7591 client registration.** It returns the public application this server already owns rather than creating one. An MCP client will not start an authorization-code flow without a client id, and neither usual way of getting one works here: our identity provider has dynamic registration disabled, and advertises Client ID Metadata Document support that it then refuses at authorization time. Registering against this endpoint gives a client id that is already known and already authorized. The `redirect_uri` is checked here, so an unregistered callback is refused at registration instead of one redirect later.

Changed

**The authorization-server document describes a complete flow.** It now carries `authorization_endpoint`, `token_endpoint`, `registration_endpoint`, `response_types_supported` and `authorization_code` among its grant types. It previously advertised only the handoff grant with no way to start an interactive sign-in, so every connector that followed it stopped there. The handoff grant keeps its own `handoff_token_endpoint`.

Fix

**The advertised identity provider is the one this deployment trusts.** It was hardcoded to production, so staging published the production tenant while validating tokens against its own — sending clients to look for an application that exists only elsewhere.

## v3.20.0 — Approving an agent now requires signing in

2026-09-19

The consent flow can be used with a real account, and can no longer be used by a stranger.

Changed

**Approving an agent requires a signed-in user.** `POST /oauth/consent/{req}/approve` authenticated nobody: it took a request id and minted an authorization code, with no session and no check that the person clicking Approve was the account holder, or anyone at all. Opening a consent link now sends the user to sign in first, and Approve is refused without that identity.

New

**Sign in with the credentials you already use.** The consent page redirects to Flowie’s own login, so whatever your organization is set up with — SSO, username and password, social — is what you see. Authorization code with PKCE (S256); no new account, and no password ever reaches this API.

Changed

**A deployment without consent sign-in configured refuses to approve rather than approving anonymously.** It answers `503` and points at `POST /v1/sandbox/bootstrap` for a sandbox credential or the handoff flow for a real account. The previous behaviour is available deliberately, via `CONSENT_ALLOW_UNAUTHENTICATED`.

## v3.19.0 — An unauthenticated MCP request now says how to authenticate

2026-09-19

A client with no credential could not discover that it needed one, or where to get it. It can now.

Changed

**A request to`/mcp` or `/mcp/full` with no `Authorization` header returns `401` with `WWW-Authenticate: Bearer resource_metadata="…"`.** It previously opened a session, served the full tool catalogue, and reported the 401 as text inside a `200` when a tool was finally called — indistinguishable, to an MCP client, from a broken tool. Only the no-header case changes: a token that is present is still judged by the route that receives it, exactly as before, so every working integration is unaffected.

New

**`/.well-known/oauth-protected-resource` (RFC 9728).** The document the challenge points at. It names both authorization servers — this API, which issues agent credentials with no human in the loop, and Auth0, which signs a person in — so a compliant client can pick the right one for its caller. Served at the path-inserted variant too.

Fix

**The curated tool count in the documentation was wrong.** The `/mcp` endpoint serves 47 tools, not the 34 the docs claimed.

## v3.18.4 — A lookup that fails says which failure it was

2026-09-19

Found by driving the published page rather than reading it: a real company could be offered by the picker and then refused by the lookup, with nothing said about why.

Fix

**`POST /v1/portability/resolve` says which kind of nothing it found.** A 404 covered a company struck off years ago and one incorporated last week alike. The public French register separates them, and its answer now carries into the `detail`: struck off (with the date) means there is no request to open, ever; active but undeclared means there is nothing to port _yet_. The register is asked only when the resolve has already failed, and a register that does not answer leaves the original 404 untouched — a timeout is not evidence that someone’s company is gone.

Fix

**The Change platform page resolves against the same registry it searches.** The picker read production while resolving read the page’s own sandbox, whose PPF annuaire is the PISTE test directory — so a company the picker had just offered came back unresolvable. Opening a request still writes to the throwaway sandbox key, and when that write fails on a company the page has just shown as routed, it now says so rather than blaming the company.

Fix

**A rejected sandbox key is replaced, not believed.** The key lives in `sessionStorage`, so one 401 made every later call on the tab fail the same silent way until the tab was closed: press the button, nothing happens. A 401 or 403 now mints a fresh key and retries once, and a failure that survives that is shown with its reason where the button is.

## 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.
