Skip to content
Log in

Submit a Flux-10 e-reporting file

This guide explains how to deposit a Flux 10 (e-reporting) file with the PPF when your own system already generates it, without the underlying invoices going through B2Brouter’s invoicing. B2Brouter validates the file against the official v3.2 XSD, normalizes it for deposit, and transmits it to the DGFiP as an Accredited Platform (Plateforme Agréée), returning the CDV lifecycle states to you via webhook and API. This is an alternative path to the native pipeline, where you send invoice data as JSON and B2Brouter builds the F10, and both share the same output channel to the PPF.

For integrators and software vendors who already produce their own F10 <Report> and only need an accredited channel to deposit it. If what you want is for B2Brouter to generate the F10 from your invoices, don’t use this guide: see the e-Reporting (Flux 10) section of the DGFiP guide.

  • A French account on B2Brouter with the dgfip Tax Report Setting enabled. See steps 1 and 2 of the DGFiP guide.
  • X-B2B-API-Version: 2026-03-02 or later.
  • API permissions for the ledger endpoints enabled on your plan. If you get a 403, open a support ticket to have them enabled.
  • An F10 file that validates against the official v3.2 XSD package.

The examples use https://api-staging.b2brouter.net. For production, replace it with https://api.b2brouter.net.

POST /accounts/{ACCOUNT_ID}/ledgers/import

The request body is the raw XML, with Content-Type: application/octet-stream (any other content type is rejected, so Rails doesn’t try to parse the body as parameters).

Terminal window
curl --request POST \
--url https://api-staging.b2brouter.net/accounts/{ACCOUNT_ID}/ledgers/import \
--header 'X-B2B-API-Key: {YOUR_API_KEY}' \
--header 'X-B2B-API-Version: 2026-03-02' \
--header 'Content-Type: application/octet-stream' \
--header 'Idempotency-Key: f10-2026-01-transactions-001' \
--data-binary @flux10-transactions.xml

Response — 201 Created:

{
"ledger": {
"id": 68916,
"type": "dgfip",
"state": "new"
}
}

The ledger id is the identifier you’ll use to track the whole lifecycle. Save it.

The file is a <Report> with no namespace. B2Brouter reads three things from it to decide whether it can be deposited: the <Issuer>, the declared sub-flow, and XSD validity.

<?xml version="1.0" encoding="UTF-8"?>
<Report xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<ReportDocument>
<Id>your-identifier</Id>
<Name>Déclaration Transactions Janvier 2026</Name>
<IssueDateTime>
<DateTimeString>202602110400</DateTimeString>
</IssueDateTime>
<TypeCode>IN</TypeCode>
<Sender>
<!-- Replaced with B2Brouter's PA identity when deposited -->
<Id schemeId="0238">0426</Id>
<Name>PDP426</Name>
<RoleCode>WK</RoleCode>
<URIUniversalCommunication>
<URIID>contact@exemple.fr</URIID>
</URIUniversalCommunication>
</Sender>
<Issuer>
<!-- Must match the SIREN of the destination account's company -->
<Id schemeId="0002">832987911</Id>
<Name>METACORTEX</Name>
<RoleCode>SE</RoleCode>
<URIUniversalCommunication>
<URIID>contact@metacortex.fr</URIID>
</URIUniversalCommunication>
</Issuer>
</ReportDocument>
<TransactionsReport>
<!-- … or <PaymentsReport>, but not both at once -->
</TransactionsReport>
</Report>

One sub-flow per deposit. The PPF transmits transactions and payments separately (spec v3.2 §3.7.7): a <Report> must carry <TransactionsReport> or <PaymentsReport>. The declared sub-flow determines the ledger’s mode. A report carrying both, or neither, is rejected.

#CheckOn failure
1Content-Type: application/octet-stream406Content-Type must be application/octet-stream
2Body size ≤ 100 MB (PPF’s IRR_TAILLE limit, v3.2 §3.4.2)422The payload exceeds the PPF file limit of 100 Mo
3XSD validation against the official v3.2 package422 with schema error details
4API version ≥ 2026-03-02422DGFiP F10 import requires API version 2026-03-02 or later
5Account has the dgfip setting enabled422The account has no enabled DGFiP e-reporting configuration
6<Issuer><Id> SIREN matches the account’s company422The <Issuer> SIREN (…) does not match the account's company
7Exactly one sub-flow (transactions or payments)422The report carries both transactions and payments; split it into two deposits or The report carries neither transactions nor payments

The root element decides the authority: a <Report> with no namespace is interpreted as a DGFiP F10; any other root keeps being treated as a Verifactu ledger, so Verifactu import continues to work across all API versions.

Send an Idempotency-Key header with every deposit. If you don’t send one, we use the SHA256 of the payload, so resubmitting an identical file is also idempotent.

ScenarioResponse
New key201 with the created ledger
Same key, same payload200 with the already-existing ledger — no duplicate is created
Same key, different payload409Idempotency-Key already used with a different payload

The key is unique per account. Keys longer than 64 characters or containing non-ASCII characters are stored as a SHA256 digest; the behavior is the same.

Before transmitting, B2Brouter rewrites two nodes of your file:

  • <ReportDocument><Id> becomes the ledger id. It’s the key the PPF uses to correlate returning CDVs; if a client-chosen value were kept, a deposit could claim the responses of a ledger belonging to another account.
  • <ReportDocument><Sender> becomes B2Brouter’s Accredited Platform identity (matricule with schemeId="0238", RoleCode WK), since we’re the one depositing with the PPF. The AIFE has confirmed in writing that this deposit normalization is acceptable.

The rest of the file is left untouched, and the original submission is kept exactly as we received it as an audit attachment on the ledger.

The imported ledger stays in the new state, and the sending cron picks it up on its first run after midnight of the day of deposit: in practice, it’s deposited with the PPF the next day. The transport is an SFTP deposit to the PPF (tar.gz with the F10 envelope).

The VAT regime doesn’t come into play for imported files: periodization by regime is a function of the native pipeline. Here the cadence is that of the deposit, and the declared period is whatever you write into the file.

StatePPF CDVMeaning
newImported and validated, pending deposit
sentDeposited with the PPF
acknowledged500 — ReçueThe PPF has received the file
depositedDeposit accepted by the PPF
registered300Terminal — declaration registered by the DGFiP
refused301Terminal — rejected; error_code and error_description carry the reason
errorTransmission error; error_description carries the detail

Subscribe to the ledger.state_change event to receive every transition without polling. It fires when entering sent, error, acknowledged, deposited, registered and refused.

{
"web_hook": {
"url": "https://example.com/hooks",
"events": ["ledger.state_change"]
}
}

The body you receive carries the account id and the full ledger:

{
"account_id": 42,
"object": {
"object": "ledger",
"id": 68916,
"state": "registered",
"mode": null,
"error_code": null,
"error_description": null,
"document_type_code": "xml.ledger.dgfip.transactions",
"sent_at": "2026-07-25T02:04:11.000Z",
"created_at": "2026-07-24T10:12:03.000Z"
}
}

If two CDVs arrive close together, the already-superseded state isn’t delivered: you always receive the current state, not necessarily a delivery per transition. Check the ledger if you need to confirm the current state. For webhook signature verification, see the webhooks guide.

Terminal window
curl --request GET \
--url https://api-staging.b2brouter.net/ledgers/{LEDGER_ID} \
--header 'X-B2B-API-Key: {YOUR_API_KEY}' \
--header 'X-B2B-API-Version: 2026-03-02'
{
"ledger": {
"id": 68916,
"type": "dgfip",
"state": "refused",
"error_code": "303",
"error_description": null,
"created_at": "2026-07-24T10:12:03.000Z",
"sent_at": "2026-07-25T02:04:11.000Z"
}
}
GET /ledgers/{LEDGER_ID}/download

Returns the normalized XML we deposited (with <Id> and <Sender> rewritten), available from the moment of import. It’s the document to keep as proof of what was declared.

GET /ledgers/{LEDGER_ID}/download_response

Returns the CDV received from the PPF. While none has arrived, it returns 404.

In Sandbox mode, download and download_response return 503: there’s no real deposit or PPF responses.