Skip to content
BaxPayDevelopers

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

  1. 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).
  2. 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.
  3. 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.
  4. 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

POST/einvoicesIdempotency-Key required

Supply 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
      }
    ]
  }'
{
  "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 notes AV-YYYY-NNNN) and the invoiceNumber you 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's precedingInvoiceRef and precedingInvoiceIssueDate.
  • 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

POST/einvoices/{id}/sendIdempotency-Key required

Validates 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

GET/einvoices?direction=OUTBOUND&status=EMITTED&page=1&limit=25
GET/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

POST/einvoices/{id}/lifecycleIdempotency-Key required

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

GET/einvoices/ereporting/periods?kind=PAYMENT_DATAlegal periods for your VAT regime
POST/einvoices/ereporting/generateIdempotency-Key required
POST/einvoices/ereporting/{id}/submitIdempotency-Key required
GET/einvoices/ereportinglist submissions

Periods 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, with status, 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.