Payments
E-invoicing (France)
Issue and receive regulated French e-invoices — EN 16931-validated, delivered as Factur-X, with the normalized lifecycle (cycle de vie) and e-reporting the 2026 reform requires.
From 1 September 2026, every French business must be able to receive electronic invoices, and large/mid-size companies must emit them (SMEs follow on 1 September 2027). BaxPay handles the formats, the validation, the routing and the tax reporting so your integration is a handful of REST calls.
How it works
- You create a draft invoice with lines. BaxPay computes the totals and VAT breakdown server-side and validates everything against EN 16931 (the European semantic standard) plus French rules (SIREN checks, mentions obligatoires).
- You send it. BaxPay builds the Factur-X artifact (a human-readable PDF/A-3 with the machine-readable XML embedded), looks the buyer up in the annuaire (the state directory of who is served by which platform), and routes it — internally if the buyer is on BaxPay, otherwise through the certified platform network.
- Both sides then exchange lifecycle statuses — the reform's normalized state machine. Four statuses are mandatory nationwide: déposée, rejetée, refusée, encaissée.
- Payment collection can be automated with payment links, and the encaissée events feed your e-reporting to the tax authority.
Enable e-invoicing
Set the business's SIREN once, from the dashboard (E-invoicing → Settings) or via the dashboard API. Every invoice you emit uses your business profile (name, address, VAT number) as the seller.
Clients
Invoice counterparties come from your general-purpose client book — the same
resource used to group payment links per customer. Create the client once
(POST /clients), then invoice by clientId. Two e-invoicing specifics:
- a client needs a Luhn-valid SIREN (and an address) before you can e-invoice them — a B2C or non-French client legitimately has neither;
- every issued invoice carries a legal snapshot of the buyer, so editing or deleting a client never rewrites an invoice.
Create a draft invoice
/einvoicesIdempotency-Key requiredSupply the buyer inline (one-off invoicing, no client record):
curl -X POST https://sandbox-api.baxpay.com/v1/einvoices \
-H "Authorization: Bearer $BAXPAY_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"invoiceNumber": "FA-2026-0042",
"issueDate": "2026-09-01",
"dueDate": "2026-09-30",
"buyerName": "ACME SAS",
"buyerSiren": "552100554",
"buyerVatId": "FR96552100554",
"buyerAddress": {
"line1": "12 rue de la Paix",
"city": "Paris",
"postalCode": "75002",
"countryCode": "FR"
},
"paymentTerms": "Paiement à 30 jours.",
"lines": [
{
"lineNumber": 1,
"description": "Prestation de conseil — septembre 2026",
"quantity": 3,
"unitCode": "DAY",
"unitPrice": 650,
"vatRate": 20
}
]
}'const res = await fetch('https://sandbox-api.baxpay.com/v1/einvoices', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.BAXPAY_API_KEY}`,
'Idempotency-Key': crypto.randomUUID(),
'Content-Type': 'application/json',
},
body: JSON.stringify({
invoiceNumber: 'FA-2026-0043',
issueDate: '2026-09-01',
dueDate: '2026-09-30',
buyerName: 'ACME SAS',
buyerSiren: '552100554',
buyerVatId: 'FR96552100554',
buyerAddress: { line1: '12 rue de la Paix', city: 'Paris', postalCode: '75002', countryCode: 'FR' },
lines: [
{ lineNumber: 1, description: 'Prestation de conseil — septembre 2026', quantity: 3, unitCode: 'DAY', unitPrice: 650, vatRate: 20 },
],
}),
});
const body = await res.json();
if (!res.ok) throw new Error(JSON.stringify(body));
console.log(JSON.stringify(body, null, 2));{
"success": true,
"data": {
"id": "0d1f9b1e-…",
"status": "DRAFT",
"direction": "OUTBOUND",
"invoiceNumber": "FA-2026-0001",
"typeCode": "COMMERCIAL_INVOICE",
"sellerSiren": "732829320",
"buyerSiren": "552100554",
"totalNet": "1950",
"totalVat": "390",
"totalGross": "2340",
"amountDue": "2340",
"vatBreakdown": [{ "rate": 20, "categoryCode": "S", "taxableAmount": 1950, "taxAmount": 390 }],
"livemode": false
}
}Or reference a client from your client book instead of the inline buyer fields:
{
"invoiceNumber": "FA-2026-0044",
"issueDate": "2026-09-01",
"clientId": "b7e1c2d0-…",
"lines": [{ "lineNumber": 1, "description": "Conseil", "quantity": 1, "unitCode": "DAY", "unitPrice": 650, "vatRate": 20 }]
}When both are present, inline buyer fields override the client's stored values for that invoice only. The client needs a SIREN and a postal address.
Things to know about the draft:
- Numbering is BaxPay's. French rules require a continuous, gap-free sequence per seller, so with
French e-invoicing enabled BaxPay assigns the number itself (
FA-YYYY-NNNN, credit notesAV-YYYY-NNNN) and theinvoiceNumberyou send is replaced. It is still required by validation. - Money is computed for you. Totals and the VAT breakdown come from the lines. Amounts in the
response are decimal strings (
"1950"), so parse them as decimals, not floats. - Corrections are credit notes. A deposited invoice is never edited. Send
typeCode: "CREDIT_NOTE"with the original'sprecedingInvoiceRefandprecedingInvoiceIssueDate. - A draft can still be changed with
PUT /einvoices/{id}(same body).
If validation fails you get a 400 listing each violated rule with its official EN 16931 identifier.
Here the buyer's VAT number has the wrong check digits:
curl -X POST https://sandbox-api.baxpay.com/v1/einvoices \
-H "Authorization: Bearer $BAXPAY_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"invoiceNumber": "FA-2026-0045",
"issueDate": "2026-09-01",
"buyerName": "ACME SAS",
"buyerSiren": "552100554",
"buyerVatId": "FR60552100554",
"buyerAddress": { "line1": "12 rue de la Paix", "city": "Paris", "postalCode": "75002", "countryCode": "FR" },
"lines": [{ "lineNumber": 1, "description": "Conseil", "quantity": 1, "unitCode": "DAY", "unitPrice": 650, "vatRate": 20 }]
}'{
"statusCode": 400,
"message": "The buyer is missing information required for this invoice.",
"code": "EINVOICE_BUYER_INCOMPLETE",
"violations": [
{ "rule": "BR-CO-9", "message": "Buyer VAT identifier (BT-48): VAT number is invalid for FR.", "field": "vatId" }
]
}Branch on code, and show each violations[].message to whoever fixes the data.
Send it
/einvoices/{id}/sendIdempotency-Key requiredValidates again, generates the Factur-X PDF and XML, deposits the invoice (DEPOSITED, the mandatory
first lifecycle step) and emits it towards the buyer's platform (EMITTED):
curl -X POST https://sandbox-api.baxpay.com/v1/einvoices/0d1f9b1e-…/send \
-H "Authorization: Bearer $BAXPAY_API_KEY" \
-H "Idempotency-Key: $(uuidgen)"{
"success": true,
"data": {
"id": "0d1f9b1e-…",
"status": "EMITTED",
"format": "FACTUR_X",
"pdfUrl": "/einvoicing/invoices/0d1f9b1e-…/artifacts/pdf",
"lifecycleEvents": [{ "status": "DEPOSITED" }, { "status": "EMITTED" }]
}
}pdfUrl and xmlUrl are paths in the Business dashboard: download the Factur-X files there. They
aren't served to API keys.
List and retrieve
/einvoices?direction=OUTBOUND&status=EMITTED&page=1&limit=25/einvoices/{id}curl "https://sandbox-api.baxpay.com/v1/einvoices?direction=OUTBOUND&status=EMITTED&limit=25" \
-H "Authorization: Bearer $BAXPAY_API_KEY"{
"success": true,
"data": {
"data": [{ "id": "0d1f9b1e-…", "direction": "OUTBOUND", "status": "EMITTED", "invoiceNumber": "FA-2026-0001" }],
"total": 1,
"page": 1,
"limit": 25
}
}direction is OUTBOUND (you are the seller) or INBOUND (you are the buyer: invoices other
platforms delivered to your SIREN).
Lifecycle statuses
/einvoices/{id}/lifecycleIdempotency-Key requiredAs the seller, record that the buyer paid outside BaxPay:
curl -X POST https://sandbox-api.baxpay.com/v1/einvoices/0d1f9b1e-…/lifecycle \
-H "Authorization: Bearer $BAXPAY_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{ "action": "PAYMENT_TRANSMITTED" }'curl -X POST https://sandbox-api.baxpay.com/v1/einvoices/0d1f9b1e-…/lifecycle \
-H "Authorization: Bearer $BAXPAY_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{ "action": "COLLECTED" }'{ "success": true, "data": { "id": "0d1f9b1e-…", "status": "COLLECTED" } }As the buyer (INBOUND invoices) you may assert TAKEN_INTO_ACCOUNT, APPROVED,
PARTIALLY_APPROVED, DISPUTED, SUSPENDED or REFUSED, for example:
{ "action": "REFUSED", "reasonCode": "GOODS_NOT_RECEIVED", "reason": "Livraison non reçue." }As the seller (OUTBOUND): PAYMENT_TRANSMITTED, COLLECTED. REFUSED, DISPUTED, SUSPENDED
and REJECTED require a reason: a legal requirement, not a BaxPay one. An illegal transition
(COLLECTED → COLLECTED, say) returns 400, and a platform-owned status like DEPOSITED can never be
asserted by an API client.
The full status vocabulary: DRAFT, DEPOSITED, REJECTED, EMITTED, RECEIVED,
MADE_AVAILABLE, TAKEN_INTO_ACCOUNT, APPROVED, PARTIALLY_APPROVED, DISPUTED,
SUSPENDED, REFUSED, PAYMENT_TRANSMITTED, COLLECTED, COMPLETED.
E-reporting
Businesses under the reform also transmit transaction and payment data to the tax authority.
BaxPay generates each period's submission from data it already holds: you review, then submit.
E-reporting needs the fiscal settings (VAT regime, default VAT category and rate) filled in under
E-invoicing → Settings; until then it answers 400 EINVOICING_SETUP_INCOMPLETE.
/einvoices/ereporting/periods?kind=PAYMENT_DATAlegal periods for your VAT regime/einvoices/ereporting/generateIdempotency-Key required/einvoices/ereporting/{id}/submitIdempotency-Key required/einvoices/ereportinglist submissionsPeriods are fixed by law and by your VAT regime: PAYMENT_DATA is reported per calendar month
(every two months under the VAT exemption regime), and any other window is refused with
400 EREPORTING_PERIOD_INVALID. Ask the API for the legal periods instead of computing them:
# The current legal period for payment data
PERIOD=$(curl -s "https://sandbox-api.baxpay.com/v1/einvoices/ereporting/periods?kind=PAYMENT_DATA" \
-H "Authorization: Bearer $BAXPAY_API_KEY" | jq -c '.data[0]')
curl -X POST https://sandbox-api.baxpay.com/v1/einvoices/ereporting/generate \
-H "Authorization: Bearer $BAXPAY_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d "$(echo "$PERIOD" | jq -c '{ kind: "PAYMENT_DATA", periodStart, periodEnd }')"{
"success": true,
"data": {
"id": "a4157f41-…",
"kind": "PAYMENT_DATA",
"status": "GENERATED",
"periodStart": "2026-09-01T00:00:00.000Z",
"periodEnd": "2026-09-30T00:00:00.000Z",
"livemode": false
}
}A period with nothing to report answers 400 EREPORTING_EMPTY_PERIOD: there are no nil filings.
curl -X POST https://sandbox-api.baxpay.com/v1/einvoices/ereporting/a4157f41-…/submit \
-H "Authorization: Bearer $BAXPAY_API_KEY" \
-H "Idempotency-Key: $(uuidgen)"kind is B2C_TRANSACTIONS (sales to consumers), INTERNATIONAL_B2B, or PAYMENT_DATA
(encaissements on service invoices, fed automatically by COLLECTED lifecycle events, including
payment-link payments).
Webhooks
Subscribe to these webhook events (signed, mode-scoped like every other event):
einvoice.status_changed— any lifecycle transition, withstatus,source, and the motif.einvoice.paid— an invoice was paid through its payment link.
Sandbox behavior
With a bx_test_ key everything works end to end with zero external traffic. Any 9-digit
SIREN is accepted (live requires a valid checksum), so the PA's test fixtures work: the annuaire
resolves unknown SIRENs to a simulated platform, deposits return simulated tracking references,
and no invoice, status or e-reporting ever reaches a real platform or the tax authority. If the
buyer's SIREN belongs to another BaxPay business, delivery is real and internal — their INBOUND
copy appears instantly, which is the easiest way to test the full buyer flow.