Flowie
Compliance · 🇫🇷 France

The French e-invoicing integration playbook

Why this playbook

From 1 September 2026 every French VAT-liable business must be able to receive e-invoices — and large & mid-sized companies must also send them. SMEs follow on 1 September 2027. There is no free state exchange platform anymore (the PPF was reduced to the directory + tax concentrator in October 2024), so every invoice flows through a Plateforme Agréée. Flowie is PA n° 0040: this page is the complete, code-first path from zero to a compliant integration — sending, receiving, and the full 200–213 lifecycle.

Seven steps, each with copy-paste code. A focused team ships this in days, not months.

Architecture in 60 seconds

Your ERP / app
   │  REST + webhooks (one API for every country)
   ▼
Flowie (PA n° 0040) ── annuaire lookup ──► recipient's PA ──► buyer
   │
   └── e-reporting & mandatory statuses ──► PPF concentrator ──► DGFiP  (≤ 24 h)

Onboard your companies & the annuaire

Create each French legal entity with its SIREN/SIRET, then register it. Registration provisions the receiving flow and (for entities you manage) the annuaire entry that tells every other PA to route invoices for this SIREN to Flowie.

curl -X POST https://back.flowie.ink/exchange/v1/companies \
  -H "Authorization: Bearer $KEY" \
  -d '{
    "name": "Maison Lumière SAS",
    "country": "FR",
    "vatNumber": "FR26921376265",
    "additionalIdentifiers": { "siret": "92137626500017" }
  }'

curl -X POST https://back.flowie.ink/exchange/v1/companies/comp_01H…/register \
  -H "Authorization: Bearer $KEY"

Before invoicing a French counterparty, check how they’re reachable (their PA, their identifiers) with a directory search — a bare SIREN/SIRET is routed to an exact lookup:

curl "https://back.flowie.ink/exchange/v1/directory/search?q=552100554" \
  -H "Authorization: Bearer $KEY"

Receive — you have to be ready first

Reception is the first legal deadline (all companies, September 2026) and the easy half. Register a webhook endpoint, and every inbound invoice — Factur-X, UBL or CII, from any PA — lands as a normalized document:

curl -X POST https://back.flowie.ink/exchange/v1/webhooks \
  -H "Authorization: Bearer $KEY" \
  -d '{
    "url": "https://erp.example.fr/hooks/flowie",
    "events": ["document.received", "lifecycle.updated",
               "compliance.reported", "compliance.reported.failed",
               "document.failed"]
  }'

On document.received, pull whichever view your system prefers — the structured JSON, the original XML, or the rendered PDF:

curl https://back.flowie.ink/exchange/v1/documents/doc_inb1/structured \
  -H "Authorization: Bearer $KEY"     # flat JSON for your ERP
# also available: …/xml (signed original) and …/pdf (human view)
Receiving is also when your lifecycle duties start
The moment an invoice is made available to you (status 203), you are the buyer in the state machine: acknowledging, approving, disputing or refusing are your calls to make — see step 4.

Send — Factur-X by default

The standard send call works unchanged; for French domestic B2B, Flowie generates Factur-X (EN 16931, CIUS-FR) and routes via the annuaire. Mind the French required fields — SIRET, and since 2026 the buyer’s SIREN, delivery address when it differs, and the nature of the operation (goods / services / mixed):

curl -X POST https://back.flowie.ink/exchange/v1/documents/send \
  -H "Authorization: Bearer $KEY" \
  -H "Idempotency-Key: fa-2027-0042" \
  -d '{
    "type": "invoice",
    "from": "comp_01H…",
    "to":   "0009:55210055400013",
    "document": {
      "number": "FA-2027-0042",
      "issueDate": "2027-09-01",
      "dueDate":   "2027-10-01",
      "currency":  "EUR",
      "buyer": { "name": "Grand Client SA", "vatNumber": "FR40552100554",
                 "additionalIdentifiers": { "siren": "552100554" } },
      "lines": [
        { "description": "Prestation de conseil — août 2027",
          "quantity": 8, "unit": "days", "unitPrice": 950.00,
          "vatRate": 20, "vatCategory": "S" }
      ]
    }
  }'

A 201 response means the invoice passed Flowie’s controls — status 200 Déposée is emitted and its data will reach the PPF within 24 h. A platform rejection later (unknown SIRET, duplicate number…) surfaces as document.failed with the motif — that is status 213 Rejetée: fix and send a new invoice, never a mutation of the old one. B2G invoices (Chorus Pro) additionally need buyerReference (Service Exécutant) and orderReference — see required fields.

Emit the lifecycle — who owes which status

The explorer covers every status in depth; here is the split of responsibilities your integration must implement. Everything is one endpoint: POST /v1/documents/{id}/lifecycle.

When…You callFR status emittedTier
As buyer (inbound invoices)
AP takes the invoice into processing{"status":"under_review"}204 Prise en chargeRecommended
You approve it in full{"status":"approved"}205 ApprouvéeRecommended
You approve it in part{"status":"approved","remainingAmount":…}206 Approuvée partiellementRecommended
You contest it (without refusing){"status":"disputed","reason":"…"}207 En litigeLibre
You need supporting documents{"status":"disputed","reasonCode":"suspended","reason":"…"}208 SuspendueLibre
You refuse it (business decision){"status":"rejected","reasonCode":"…","reason":"…"}210 RefuséeMandatory
Your payment went out{"status":"paid","paymentDate":"…"}211 Paiement transmisRecommended
As supplier (outbound invoices)
Buyer asked for documents (you received 208)POST …/actions {"action":"link","relatedDocumentId":"…"}209 ComplétéeLibre
The money arrived on your account{"status":"paid","paymentDate":"…","paymentAmount":…}212 EncaisséeMandatory
Partial collection{"status":"partially_paid","paymentAmount":…,"remainingAmount":…}212 Encaissée (partial)Mandatory
The two calls you cannot skip
210 when you refuse as a buyer, 212 when you’re paid as a supplier. Both are legally mandatory, both are auto-reported by Flowie to the DGFiP, and 212 is what pre-fills the CA3 VAT return for services. Everything else improves visibility; these two keep you compliant. Statuses 200, 201, 202, 203 and 213 are Flowie’s job — never emit them yourself.

The webhook handler — your single integration point

One endpoint, four event families, out-of-order-safe. This is the reference shape (Node; the logic transposes 1-to-1 to any stack):

import express from "express";
import crypto from "node:crypto";

const app = express();
app.use(express.raw({ type: "application/json" })); // keep the raw body for HMAC

// French statuses can be skipped (only 4 of 14 are mandatory) — never
// assume ordering. Rank them and ignore stale updates.
const RANK = { submitted: 0, received: 1, under_review: 2, disputed: 3,
               approved: 4, partially_paid: 5, paid: 6, rejected: 9 };

app.post("/hooks/flowie", async (req, res) => {
  const sig = crypto.createHmac("sha256", process.env.FLOWIE_WEBHOOK_SECRET)
                    .update(req.body).digest("hex");
  if (!crypto.timingSafeEqual(Buffer.from(sig),
        Buffer.from(req.headers["x-flowie-signature"] ?? ""))) {
    return res.status(401).end();
  }
  const event = JSON.parse(req.body);
  res.status(200).end();               // ack fast, process async

  switch (event.type) {
    case "document.received":          // FR code 203 — you are the buyer
      await queue.push("import-invoice", event.data.documentId);
      break;

    case "lifecycle.updated": {        // any 2xx status, either direction
      const { documentId, currentStatus, previousStatus, reasonCode, reason } = event.data;
      if (RANK[currentStatus] <= RANK[previousStatus]) break;  // stale / dup
      await erp.setInvoiceStatus(documentId, currentStatus, { reasonCode, reason });
      break;
    }

    case "document.failed":            // FR code 213 — technical reject
      await alerting.page("invoice-rejected", event.data);     // fix + re-send
      break;

    case "compliance.reported":        // DGFiP leg confirmed
    case "compliance.reported.failed": // DGFiP leg rejected — investigate
      await audit.log(event.type, event.data);
      break;
  }
});

Three properties make this production-grade, and all three matter in France specifically:

  • Idempotent + monotonic — statuses may arrive twice (retries) or out of order (skipped optional statuses); the rank check handles both.
  • Fast ack — status bursts happen (a buyer platform replaying a backlog); never do ERP writes before responding.
  • Compliance events auditedcompliance.reported is your proof the mandatory statuses reached the DGFiP; keep the trail (FEC audits ask for it).

E-reporting — mostly Flowie’s job, two things are yours

E-reporting covers what e-invoicing doesn’t: B2C and international / intra-community transactions (transmitted as data, not exchanged invoices), plus payment data for services. What that means for you:

  • Domestic B2B — nothing to do. The mandatory statuses you emit (esp. 212 with its collected amount) are the payment e-reporting; Flowie forwards them on schedule.
  • B2C & cross-border — send those transactions through the same API (the recipient just isn’t on the French network); Flowie derives and files the transaction data at your VAT regime’s frequency.
  • Watch the failures. compliance.reported.failed means the DGFiP leg bounced; Flowie retries with backoff, but repeated failures (bad SIREN, closed period) need your action.

Prove it in the sandbox — the French test matrix

Every scenario above is reproducible with a flw_test_ key, deterministic test identifiers and the simulateCompliance org flag:

What you’re provingHowExpect
Happy path 200→212Send, then walk under_review → approved → paidlifecycle.updated ×3, then compliance.reported (with simulateCompliance:"accept")
Refusal (210) with motif{"status":"rejected","reasonCode":"…"} as buyerTerminal state; motif echoed verbatim in the event
Platform reject (213)simulateCompliance: "reject_00058"compliance.reported.failed with code 00058
Illegal transition guardreceived → paid directly409 invalid_transition + allowed next states
DGFiP outage resiliencesimulateCompliance: "timeout_30s"Delayed failure event — exercise your retry/alerting
Flaky networksimulateCompliance: "flaky_50pct"Random accept/fail — your handler must be idempotent

Go-live checklist

Non-compliance has a price tag
€50 per missing e-invoice (capped at €15,000/year) and €500 per missing e-reporting transmission (capped at €15,000/year) — per the 2026 finance law. The DGFiP has signalled leniency for good-faith businesses in the first months, but “we hadn’t integrated yet” is not a defence after the SME deadline.

References