Migration guide v2026-04-20 → v2026-06-26
Everything you need to update your code.
This guide helps you upgrade from X-B2B-API-Version: 2026-04-20 to X-B2B-API-Version: 2026-06-26, focusing on what you need to change, not just what changed.
At a glance
Section titled “At a glance”| What changed | Do you need to act? |
|---|---|
Offices now store their own cin_value / cin_scheme instead of inheriting them | Yes — only if your integration creates or reads offices (contacts with parent_id) |
Stricter contact matching on invoice creation with an inline contact | Yes — affects integrations that pass inline contact data on invoice creation |
French Annuaire lookup on /directory/fr can now return 202 Accepted | Yes — only if you call /directory/fr for French identifiers |
Flat cinN_value / cinN_scheme no longer accepted on invoice contact | Yes — move these into contact.routing_codes |
Dedicated routing fields (dire, accounting_unit, etc.) no longer accepted at invoice top level | Yes — move these into the invoice-level routing_codes object |
POST /invoices/{id}/generate_tax_report now returns 201 Created | Yes — only if you check for an exact 200 status |
number invoice list filter now matches by prefix instead of exact match | Only if your integration relies on exact-match behavior |
| New received invoice webhook events | No — opt-in |
Create offices via POST /accounts with parent_id | No — opt-in |
issue_after_import on invoice creation and import | No — opt-in |
Account unit_code_format preference | No — opt-in |
Per-invoice apply_taxes_per_line | No — opt-in |
New taxable_base / tax_amount fields in invoice list | No — additive |
DGFiP refuse_reason_code on mark_as and invoice responses | No — opt-in |
Changes that require action
Section titled “Changes that require action”1. Offices now store their own CIN instead of inheriting it
Section titled “1. Offices now store their own CIN instead of inheriting it”Previously, an office (a contact created with parent_id) always inherited cin_value / cin_scheme from its parent contact. Now these fields are stored independently per office.
- If you send
cin_value/cin_schemewhen creating or updating an office, they are stored on the office and returned in responses. - If you omit them (or clear with
cin_value: ""), the office still inherits the parent's CIN in read responses — no action needed if you never set CIN on offices. tin_value/tin_schemeare unaffected — still always inherited from the parent and cannot be set independently.- CIN uniqueness within the account is now enforced: reusing a
cin_valuealready assigned to another contact in the same account returns422 Unprocessable Entity.
POST /accounts/{account_id}/contacts{ "contact": { "parent_id": 12345, "name": "Branch Office", "cin_value": "1234567890", "cin_scheme": 88 }}// Response — cin_value is ignored, inherited from the parent contact{ "contact": { "id": 67890, "parent_id": 12345, "name": "Branch Office", "cin_value": "<parent's cin_value>", "cin_scheme": "<parent's cin_scheme>" }}POST /accounts/{account_id}/contacts{ "contact": { "parent_id": 12345, "name": "Branch Office", "cin_value": "1234567890", "cin_scheme": 88 }}// Response — cin_value is now stored on the office itself{ "contact": { "id": 67890, "parent_id": 12345, "name": "Branch Office", "cin_value": "1234567890", "cin_scheme": "88" }}Affected endpoints: POST /accounts/{account}/contacts, PUT /contacts/{id}
2. Stricter contact validation on invoice creation with an inline contact
Section titled “2. Stricter contact validation on invoice creation with an inline contact”When you pass a contact hash inline on invoice creation, the API now matches it against existing contacts by tin_value or cin_value before deciding what to do:
- Full match → the invoice is linked to the existing contact.
- Routing identifiers differ (
cin_value,cin_scheme,routing_codes,pin_value,recipient_code) → an office is created automatically. - Only descriptive fields differ (
address,city,postalcode, etc.) → the request now returns422 Unprocessable Entitywith codedata_mismatchinstead of silently updating the contact. - No match → a new contact is created (and linked to the B2Brouter directory if it matches a known company).
If your integration relies on the previous behavior of silently overwriting descriptive contact data on invoice creation, pass update_contact: true (top-level request field, default false) to opt back in. Routing and tax identifiers are never overwritten this way, and update_contact has no effect on received documents.
// Opt in to updating descriptive contact data from invoice creationPOST /accounts/{account_id}/invoices{ "update_contact": true, "invoice": { "contact": { "tin_value": "B12345678", "name": "Acme Corp", "address": "New address 123" } }}The contacts endpoint is the recommended way to create and maintain contacts and their offices. To link an invoice to an existing contact without any matching logic, use
contact_iddirectly.
Affected endpoints: POST /accounts/{account}/invoices
3. French Annuaire lookup on /directory/fr is now asynchronous
Section titled “3. French Annuaire lookup on /directory/fr is now asynchronous”GET /directory/fr/{scheme}/{id} now consults the French PPF Annuaire for SIREN, SIRET, and FRCTC (0225) identifiers. If your integration calls this endpoint for French identifiers, handle the new response shapes:
202 Acceptedwith apolling_url— the company was not yet known; a background job is querying the Annuaire. Retry after a few seconds.424 Failed Dependencywith error codeupstream_unavailableand aRetry-Afterheader — the Annuaire or Peppol is temporarily unavailable. Retry after the indicated delay; do not treat this as a404.422 Unprocessable Entitywith codeinvalid_identifier— the identifier failed length/checksum validation for its scheme.- A new
information_flagsarray is included in successful responses (FR_ASSUJETTI_ACTIVE,FR_ASSUJETTI_INACTIVE,FR_ASSUJETTI_UNKNOWN).
curl --request GET \ --url 'https://app-staging.b2brouter.net/api/v1/directory/fr/0002/123456789' \ --header 'X-B2B-API-Key: {YOUR_API_KEY}' \ --header 'X-B2B-API-Version: 2026-06-26'// 202 Accepted — not yet known, retry polling_url{ "status": "processing", "message": "Querying French PPF Annuaire. Please retry this request.", "polling_url": "https://app-staging.b2brouter.net/api/v1/directory/fr/0002/123456789"}Affected endpoints: GET /directory/fr/{scheme}/{id} (non-FR countries unaffected)
4. Flat cinN_value / cinN_scheme no longer accepted on invoice contact
Section titled “4. Flat cinN_value / cinN_scheme no longer accepted on invoice contact”The inline contact object on POST /accounts/{account}/invoices no longer accepts flat cin1_value/cin1_scheme … cin5_value/cin5_scheme keys. Move them into a contact.routing_codes object — the same shape used by POST /accounts/{account}/contacts.
{ "invoice": { "contact": { "tin_value": "ESA13585625", "name": "Buyer Full Name AS", "cin1_value": "ESR_MISSION_FACTURES_DEPLACEMENTS", "cin1_scheme": "8017" } }}{ "invoice": { "contact": { "tin_value": "ESA13585625", "name": "Buyer Full Name AS", "routing_codes": { "cin1_value": "ESR_MISSION_FACTURES_DEPLACEMENTS", "cin1_scheme": "8017" } } }}Requests still using the flat keys return 422 Unprocessable Entity.
Affected endpoints: POST /accounts/{account}/invoices
5. Invoice-level routing fields replaced by a routing_codes object
Section titled “5. Invoice-level routing fields replaced by a routing_codes object”At the invoice top level, both the flat cinN_value / cinN_scheme keys and the dedicated routing fields (dire, accounting_unit, managing_unit, processing_unit, proponent_unit, nadby, naddp, nadiv, nadpr, nadud) are no longer accepted. Use the invoice-level routing_codes object instead — it carries the invoice's own routing codes (e.g. buyer's DIR3 / Chorus Pro service codes), independent of the contact's.
| Slot | DIR3 (scheme 8014) | DIRe (8015) | EDI/GLN (88) |
|---|---|---|---|
| cin1 | accounting_unit | — | nadby |
| cin2 | managing_unit | — | naddp |
| cin3 | processing_unit | — | nadiv |
| cin4 | proponent_unit | — | nadpr |
| cin5 | — | dire | nadud |
{ "invoice": { "number": "F-2026-0001", "accounting_unit": "ESR_MISSION_FACTURES_DEPLACEMENTS" }}{ "invoice": { "number": "F-2026-0001", "routing_codes": { "cin1_value": "ESR_MISSION_FACTURES_DEPLACEMENTS", "cin1_scheme": "8014" } }}The applied codes are echoed back in a routing_codes object at the invoice top level of the response. Requests still using the old fields return 422 Unprocessable Entity.
Affected endpoints: POST /accounts/{account}/invoices, PUT /invoices/{id}
6. generate_tax_report now returns 201 Created
Section titled “6. generate_tax_report now returns 201 Created”POST /invoices/{id}/generate_tax_report now returns 201 Created on success instead of 200 OK. The response body is unchanged. This only affects clients that check for an exact 200 status — clients checking for any 2xx success are unaffected.
Affected endpoints: POST /invoices/{id}/generate_tax_report
7. Invoice list number filter now matches by prefix
Section titled “7. Invoice list number filter now matches by prefix”GET /accounts/{account}/invoices?number=... now performs a prefix match instead of an exact match. If your integration relies on the previous exact-match behavior, add a client-side check to filter out unwanted prefix matches, or switch to a more specific value.
GET /accounts/{account}/invoices?number=INV-2now also returns INV-20, INV-21, … in addition to an invoice numbered exactly INV-2. A new series_code filter is available for exact-match filtering by series.
Affected endpoints: GET /accounts/{account}/invoices
New fields in existing responses
Section titled “New fields in existing responses”The following fields have been added. They are backwards-compatible — your integration will continue to work without changes. However, if your code fails when it encounters unexpected JSON fields, update your response models to allow them.
Contact responses (GET /contacts/{id}, POST, PUT, DELETE):
parent_id—nullfor top-level contacts, the parent contact's ID for offices
Invoice list (GET /accounts/{account}/invoices):
taxable_base— invoice subtotal before taxestax_amount— total tax amount across all tax breakdown lines
Invoice responses (mark_as, create, update):
refuse_reason_code— DGFiP-defined structured reason code for process 210 refusals (accepted onPOST /invoices/{id}/mark_asalongsidereason)
Account responses (GET /accounts/{account}, GET /accounts):
unit_code_format—internal(default) orunece_rec20; see the opt-in feature below for the effect on invoice lineunit
New features available in this version
Section titled “New features available in this version”Received invoice webhooks — subscribe to received_invoice.created and received_invoice.state_change to be notified when received invoices are created or change state.
Offices via POST /accounts — create an office (subsidiary) directly under a parent account by passing parent_id; standard required fields become optional and the office inherits the group's integration.
issue_after_import — pass this flag on invoice creation or import to synchronously transition the invoice to state=issued inline with the request, instead of relying on the send flow.
unit_code_format account preference — set to unece_rec20 to exchange invoice line unit as a UN/ECE Recommendation 20 string code (e.g. "HUR") instead of B2Brouter's internal integer id. The default internal keeps existing integrations unaffected.
Per-invoice apply_taxes_per_line (premium accounts) — override the account-level VAT rounding mode for a single invoice, useful when a public administration recomputes totals with a different rounding rule. See the invoice calculation guide.
Migration checklist
Section titled “Migration checklist”Use this checklist before switching to v2026-06-26 in production.
Contacts and offices:
- If you set
cin_value/cin_schemewhen creating or updating offices, verify the value is unique within the account (duplicates now return422) - Update any code reading
parent_idon contact responses if you need to distinguish offices from top-level contacts
Invoice creation with inline contact:
- Move any
cinN_value/cinN_schemekeys insideinvoice.contactintoinvoice.contact.routing_codes - Move any invoice top-level
cinN_value/cinN_schemeor dedicated fields (dire,accounting_unit, etc.) into the invoice-levelrouting_codesobject - Decide whether you need
update_contact: trueto preserve the previous descriptive-data overwrite behavior - Test: create an invoice with an inline contact whose descriptive data differs from an existing contact, and confirm you handle the
422 data_mismatchresponse (or passupdate_contact: true)
Tax reports:
- Update any status check on
POST /invoices/{id}/generate_tax_reportthat expects exactly200 OK
Invoice list filtering:
- Review any use of the
numberfilter that depends on exact-match behavior; switch toseries_codeif you need exact matching by series
French directory lookups:
- Handle
202 Acceptedwithpolling_urland retry - Handle
424 Failed DependencywithRetry-Afteras a transient error, not a404 - Handle
422 Unprocessable Entitywith codeinvalid_identifier
Testing your integration
Section titled “Testing your integration”Switch to the new version header in your sandbox or staging environment first:
X-B2B-API-Version: 2026-06-26You can set this header per-request, so you can test against sandbox (or staging) without touching your production configuration. Once you have validated the checklist above, update the version in your production API key settings or request headers.