Ir al contenido
Log in

v2026-06-26

Changelog

Selecciona esta versión enviando X-B2B-API-Version: 2026-06-26 en las cabeceras de tu petición.

Esta versión implementa cambios incompatibles. Cada cambio incompatible está marcado con la etiqueta Breaking.

CambioÁrea
Eventos de webhook para facturas recibidasWebhooks
Crear oficinas via POST /accounts con parent_idCuentas
CIN independiente para oficinas de contacto 🚨 BreakingContactos
parent_id en las respuestas de contactoContactos
Validación más estricta de contactos al crear facturas 🚨 BreakingFacturas
refuse_reason_code de DGFiP en mark_as y en las respuestas de facturaFacturas
issue_after_import al crear e importar facturasFacturas
Consulta al Annuaire francés en /directory/fr 🚨 BreakingDirectorio
routing_codes del contacto en línea al crear facturas 🚨 BreakingFacturas
routing_codes a nivel de factura al crear facturas 🚨 BreakingFacturas
Base imponible e importe de impuesto en el listado de facturasFacturas
Filtros del listado de facturas: number ahora coincide por prefijo y series_codeFacturas
Generar informe fiscal retorna 201 Created 🚨 BreakingFacturas
Preferencia unit_code_format de la cuentaCuentas
apply_taxes_per_line por facturaFacturas

Dos nuevos eventos de webhook para facturas recibidas:

  • received_invoice.created — se activa cuando se crea una nueva factura recibida en la plataforma (p. ej. importada o subida).
  • received_invoice.state_change — se activa cuando una factura recibida transiciona a un nuevo estado.

Suscríbete a estos eventos al configurar un webhook:

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

Payload para received_invoice.created:

{
"invoice_id": 12345,
"account_id": 42,
"state": "received"
}

Payload para received_invoice.state_change:

{
"invoice_id": 12345,
"event_id": 67890,
"state": "accepted",
"notes": null
}

Crear oficinas via POST /accounts con parent_id

Sección titulada «Crear oficinas via POST /accounts con parent_id»

POST /accounts ahora acepta un campo opcional parent_id. Cuando se proporciona, la cuenta se crea como una oficina (filial) bajo la cuenta padre indicada.

Cuando parent_id está establecido:

  • El padre debe ser una cuenta de nivel superior dentro del mismo grupo (no puede ser ella misma una oficina).
  • Los campos obligatorios habituales (name, postalcode, email, address, city, province, phone, tin_value) pasan a ser opcionales.
  • La cuenta creada hereda la integración del grupo y se retorna con parent_id en la respuesta.

Retorna 422 Unprocessable Entity con código parameter_invalid (parámetro parent_id) si:

  • La cuenta padre no se encuentra o pertenece a un grupo distinto.
  • El padre es a su vez una oficina.

Ejemplo de petición:

POST /accounts
{
"account": {
"parent_id": 12345,
"name": "Paris Establishment",
"address": "1 Rue de la Paix",
"city": "Paris",
"postalcode": "75001",
"province": "Ile-de-France",
"email": "paris@example.com"
}
}

Ejemplo de respuesta:

{
"account": {
"id": 67890,
"parent_id": 12345,
"name": "Paris Establishment",
...
}
}

Notas:

  • tin_value / tin_scheme no se pueden establecer en una oficina — siempre se heredan de la cuenta padre. Enviarlos retorna 422 parameter_cannot_change (parámetro tin_value).
  • cin_value / cin_scheme se almacenan de forma independiente en la oficina. Si no se establecen, la oficina hereda el CIN del padre en las respuestas de lectura.

Todas las respuestas de un único contacto ahora incluyen parent_id: GET /contacts/{id}, POST /accounts/{account}/contacts, PUT /contacts/{id} y DELETE /contacts/{id}.

  • Para los contactos de nivel superior, parent_id es null.
  • Para las oficinas, parent_id contiene el ID del contacto padre.

refuse_reason_code de DGFiP en mark_as y en las respuestas de factura

Sección titulada «refuse_reason_code de DGFiP en mark_as y en las respuestas de factura»

POST /invoices/{id}/mark_as ahora acepta un parámetro opcional reason_code al transicionar al estado refused. Este campo lleva un código de motivo estructurado definido por DGFiP para los rechazos del proceso 210.

Valores aceptados:

ADR_ERR, REF_CT_ABSENT, CMD_ERR, TX_TVA_ERR, MONTANTTOTAL_ERR, CALCUL_ERR, DOUBLON, NON_CONFORME, DEST_ERR, TRANSAC_INC, EMMET_INC, CONTRAT_TERM, DOUBLE_FACT, AUTRE

Ejemplo de petición:

POST /invoices/{id}/mark_as
{
"state": "refused",
"reason": "Incorrect total amount",
"reason_code": "MONTANTTOTAL_ERR"
}

Ejemplo de respuesta (extracto):

{
"invoice": {
"id": 12345,
"state": "refused",
"refuse_reason": "Incorrect total amount",
"refuse_reason_code": "MONTANTTOTAL_ERR",
"..."
}
}

issue_after_import al crear e importar facturas

Sección titulada «issue_after_import al crear e importar facturas»

POST /accounts/{account}/invoices y POST /accounts/{account}/invoices/import ahora aceptan un indicador opcional issue_after_import. Cuando es true y la factura pasa las validaciones:

  • La factura transiciona a state=issued (esquina c1) de forma síncrona, dentro de la misma petición.
  • Si la cuenta tiene activada una configuración de informe fiscal before_send (Verifactu, Ksef, Zatca) con auto_generate activado y la factura cualifica, el informe fiscal se genera en línea y se expone via tax_report_ids en la respuesta. Cuando auto_generate está desactivado, la factura se emite igualmente pero no se crea ningún informe — coherente con el flujo de envío, esos informes se gestionan a través de la API de Tax Reports.
  • Las autoridades que informan después del envío (p. ej. SII, SDI) no se activan al emitir; sus informes se crean cuando la factura se envía o se marca como enviada.
  • No se encola ningún job de envío en segundo plano para la factura; el estado a nivel de transporte (c2) permanece intacto. Los informes fiscales que se envían automáticamente (p. ej. Ksef) siguen encolando su propio job de envío.

Mientras la factura permanece en state=issued es de solo lectura: no se puede actualizar, eliminar ni revertir a new hasta que se envía o se descarga.

Si tanto send_after_import como issue_after_import son true, send_after_import gana — enviar ya emite la factura. Se ignora para documentos que no sean facturas.

El endpoint de importación masiva POST /accounts/{account}/invoices/imports acepta el mismo indicador issue_after_import (como campo de formulario multipart). Se aplica por archivo: cada factura importada que cualifica se emite; un archivo que no se puede emitir, o cuya emisión falla, se reporta en ese elemento sin abortar el lote.

Ejemplo de petición:

POST /accounts/{account}/invoices
{
"issue_after_import": true,
"invoice": {
"number": "F-2026-0001",
"..."
}
}

Base imponible e importe de impuesto en el listado de facturas

Sección titulada «Base imponible e importe de impuesto en el listado de facturas»

GET /accounts/{account}/invoices ahora retorna dos campos adicionales por factura:

  • taxable_base — el subtotal de la factura antes de impuestos (float, misma moneda que currency).
  • tax_amount — el importe total de impuestos, calculado como la suma de todos los desgloses de impuesto (float).

Ejemplo de respuesta (parcial):

{
"invoices": [
{
"id": 12345,
"number": "INV-2025-001",
"total": 121.0,
"taxable_base": 100.0,
"tax_amount": 21.0,
"currency": "EUR"
}
]
}

Las cuentas ahora exponen un campo unit_code_format que controla la lista de códigos utilizada para los códigos de unidad de las líneas de factura al leer y escribir facturas a través de la API.

  • internal (por defecto) — la propia lista de códigos de unidad enteros de B2Brouter.
  • unece_rec20 — códigos de la Recomendación 20 de UN/ECE (p. ej. HUR, KGM, H87).

Léelo en GET /accounts/{account} y establécelo en PUT /accounts/{account}:

{
"account": {
"unit_code_format": "unece_rec20"
}
}

Cuando se establece a unece_rec20, el campo unit de la línea de factura cambia de tipo: en lugar del id entero por defecto, se intercambia como un código UNECERec20 en cadena (p. ej. "HUR"). Se retorna así en las lecturas de factura — GET /invoices/{id} y las respuestas de create/update — y se acepta como código en las escrituras (creación e importación de facturas); los códigos desconocidos recaen en other. Las integraciones que interpretan unit como entero deben gestionar la forma en cadena para estas cuentas.

El valor por defecto internal mantiene el id entero, de modo que las integraciones existentes no se ven afectadas a menos que lo activen.


Nuevo campo apply_taxes_per_line (booleano, anulable) en las facturas, tanto escribible como retornado en la respuesta. Sobrescribe, por factura, la configuración a nivel de cuenta del mismo nombre que controla el redondeo de IVA:

  • true — los impuestos se calculan por línea (cada línea se redondea y luego se suman).
  • false — los impuestos se calculan por factura (la base imponible se suma por tipo, y se redondea una sola vez).
  • omitido / null — hereda la configuración apply_taxes_per_line de la cuenta (el comportamiento anterior).

La sobrescritura solo se respeta para cuentas premium. Esto permite alinear el modo de redondeo con administraciones públicas que recalculan los totales y rechazan discrepancias, sin cambiar el valor por defecto a nivel de cuenta. Consulta la guía de cálculo de facturas para ejemplos trabajados.

Endpoints afectados:

  • GET /invoices/{id} — retorna el apply_taxes_per_line efectivo.
  • POST /accounts/{account}/invoices — acepta y retorna apply_taxes_per_line.
  • PATCH /invoices/{id} — acepta y retorna apply_taxes_per_line.

🚨 Breaking

POST /accounts/{account}/contacts y PUT /contacts/{id} ahora almacenan cin_value y cin_scheme de forma independiente en las oficinas. Anteriormente, el CIN siempre se heredaba del contacto padre.

Comportamiento:

  • Si se proporcionan cin_value / cin_scheme, se almacenan en la oficina y se retornan en las respuestas.
  • Si se omiten (o se borran con cin_value: ""), la oficina hereda el cin_value del contacto padre.
  • tin_value / tin_scheme se siguen heredando del padre y no se pueden establecer de forma independiente.
  • Se aplica la unicidad del CIN dentro de la cuenta: enviar un cin_value ya asignado a otro contacto de la misma cuenta retorna 422 Unprocessable Entity.

Endpoints afectados:

  • POST /accounts/{account}/contactscin_value / cin_scheme ahora se almacenan cuando parent_id está establecido.
  • PUT /contacts/{id}cin_value / cin_scheme ahora se almacenan en las oficinas.

Ejemplo: Crear una oficina con su propio CIN

POST /accounts/{account_id}/contacts
{
"contact": {
"parent_id": 12345,
"name": "Branch Office",
"cin_value": "1234567890",
"cin_scheme": 88
}
}

Respuesta:

{
"contact": {
"id": 67890,
"parent_id": 12345,
"name": "Branch Office",
"cin_value": "1234567890",
"cin_scheme": "88",
"..."
}
}

Validación más estricta de contactos al crear facturas

Sección titulada «Validación más estricta de contactos al crear facturas»

🚨 Breaking

Cuando se proporciona un objeto contact al crear una factura, la API lo compara con los contactos existentes por tin_value o cin_value:

  • Si todos los datos coinciden con un contacto existente, la factura se crea y se enlaza con él.
  • Si los identificadores de enrutamiento difieren (cin_value, cin_scheme, routing_codes, pin_value, recipient_code), se crea una oficina automáticamente.
  • Si solo difieren campos descriptivos (address, city, postalcode, etc.), la petición retorna 422 Unprocessable Entity con código data_mismatch. Pasa update_contact: true (campo de la petición a nivel superior, por defecto false) para fusionar los datos descriptivos con el contacto coincidente en su lugar — los identificadores de enrutamiento y fiscales nunca se sobrescriben de esta manera.
  • Si no se encuentra ningún contacto coincidente, se crea un contacto nuevo. Cuando coincide con una empresa publicada en el directorio de B2Brouter, se enlaza con ella: si los datos descriptivos de la factura coinciden con una de las oficinas registradas de esa empresa, el contacto se enlaza con esa oficina; si difiere, el contacto mantiene los datos de la factura (enlazado al directorio pero no verificado contra él).
  • update_contact se ignora para los documentos recibidos: una factura entrante nunca reescribe los datos de un contacto existente.

Nota: el endpoint de contactos es la forma recomendada de crear y mantener contactos y sus oficinas. Para enlazar una factura con un contacto existente sin ninguna lógica de coincidencia, usa contact_id directamente.


Consulta al Annuaire francés en /directory/fr

Sección titulada «Consulta al Annuaire francés en /directory/fr»

🚨 Breaking

GET /directory/fr/{scheme}/{id} ahora consulta el Annuaire francés de la PPF (API REST de la DGFiP) cuando el identificador es un SIREN (9 dígitos, esquema 0002), un SIRET (14 dígitos, esquema 0009), o una dirección electrónica FRCTC (esquema 0225, formato SIREN, SIREN_SIRET, SIREN_SIRET_CodeRoutage o SIREN_Suffix).

El identificador debe ser válido para su esquema — la longitud y el checksum se validan para cada esquema, no solo los franceses. Un identificador que el validador del esquema rechaza retorna 422 Unprocessable Entity con código invalid_identifier (esto incluye un SIREN/SIRET de longitud incorrecta, un checksum fallido, o una dirección FRCTC con un SIREN/SIRET no válido). Los esquemas sin validador se aceptan y se consultan como antes.

Para el esquema 0225 el valor se utiliza tal cual como identificador de direccionamiento del Annuaire, y la empresa resuelta se clasifica por su identificador legal subyacente: un SIREN solo resuelve a cin_scheme 0002, un SIREN_SIRET a 0009 (SIRET), con la dirección FRCTC completa mantenida como pin_scheme 0225; un code routage se expone como código de enrutamiento cin1 (esquema 0224). Esto significa que una consulta 0225 y una posterior consulta 0009/0002 de la misma empresa retornan la misma entrada de directorio.

La consulta es asíncrona para mantener los tiempos de respuesta en milisegundos:

  • Si la empresa ya es conocida en el directorio de B2Brouter, la respuesta es 200 OK con los datos de la empresa.
  • Si aún no es conocida, la respuesta es 202 Accepted con un polling_url. Un job en segundo plano consulta el Annuaire y persiste el resultado. Reintenta la petición al cabo de unos segundos — una vez los datos estén disponibles, la petición retorna 200 OK.
  • Cuando una entrada de directorio conocida no se ha comprobado en las últimas 24 horas (o nunca se ha enriquecido desde el Annuaire), la respuesta sigue siendo 200 OK inmediatamente y se activa una actualización en segundo plano (stale-while-revalidate).
  • Después de un 404 Not Found en un SIREN/SIRET, las peticiones posteriores para el mismo identificador retornan 404 durante 15 minutos sin volver a consultar el Annuaire. Reintenta después de ese periodo si crees que el identificador se ha registrado desde entonces.
  • Si la consulta falla porque un servicio upstream (Annuaire, Peppol) no está disponible, la respuesta es 424 Failed Dependency con código de error upstream_unavailable y una cabecera Retry-After — reintenta después del retraso indicado. Un fallo upstream transitorio nunca se convierte en un 404.

La respuesta incluye un nuevo campo information_flags (array de cadenas) que lleva el estado de registro en el Annuaire, alineado con la especificación CTC de Peppol:

IndicadorSignificado
FR_ASSUJETTI_ACTIVEEl identificador está presente y activo en el Annuaire
FR_ASSUJETTI_INACTIVEEl identificador está presente en el Annuaire pero inactivo
FR_ASSUJETTI_UNKNOWNLa empresa es conocida por B2Brouter pero ausente del Annuaire

Se establece exactamente uno de estos indicadores por empresa. Cuando una empresa ya conocida por B2Brouter (p. ej. via Peppol) está ausente del Annuaire, se retorna con FR_ASSUJETTI_UNKNOWN para que la ausencia sea visible. Un identificador desconocido para B2Brouter y ausente del Annuaire resuelve a 404 Not Found (ver la nota de caché de negación anterior).

Las unidades subordinadas (oficinas SIRET bajo un SIREN) todavía no se exponen en la respuesta.

Ejemplo: empresa aún no conocida — primera petición

Ventana de terminal
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'
{
"status": "processing",
"message": "Querying French PPF Annuaire. Please retry this request.",
"polling_url": "https://app-staging.b2brouter.net/api/v1/directory/fr/0002/123456789"
}

Estado HTTP: 202 Accepted.

Ejemplo: la misma petición unos segundos después

{
"name": "SIREN 123456789",
"country": "fr",
"information_flags": ["FR_ASSUJETTI_ACTIVE"],
...
}

Estado HTTP: 200 OK.

Endpoints afectados:

  • GET /directory/fr/{scheme}/{id} — comportamiento ampliado descrito anteriormente. Los países no franceses no se ven afectados.

routing_codes del contacto en línea al crear facturas

Sección titulada «routing_codes del contacto en línea al crear facturas»

🚨 Breaking

El contact en línea en POST /accounts/{account}/invoices ahora acepta un objeto routing_codes — la misma forma utilizada por POST /accounts/{account}/contacts y retornada en las respuestas de contacto. Lleva hasta cinco Números de Identificación de Empresa adicionales (cin1_value/cin1_schemecin5_value/cin5_scheme) además del cin principal. Si se proporciona cinN_value, cinN_scheme es obligatorio (y viceversa). Estos códigos de enrutamiento también se retornan dentro de routing_codes en el objeto invoice.contact incrustado en la respuesta.

POST /accounts/{account_id}/invoices
{
"invoice": {
"contact": {
"tin_value": "ESA13585625",
"name": "Buyer Full Name AS",
"routing_codes": {
"cin1_value": "ESR_MISSION_FACTURES_DEPLACEMENTS",
"cin1_scheme": "8017"
}
}
}
}

Cambio incompatible: las claves planas cinN_value / cinN_scheme dentro de contact ya no se aceptan. Estas peticiones retornan 422 Unprocessable Entity. Envía los códigos de enrutamiento del contacto a través de contact.routing_codes en su lugar.


routing_codes a nivel de factura al crear facturas

Sección titulada «routing_codes a nivel de factura al crear facturas»

🚨 Breaking

POST /accounts/{account}/invoices y PUT /invoices/{id} ahora aceptan un objeto routing_codes en el nivel superior de la factura — la misma forma cin1_value/cin1_schemecin5_value/cin5_scheme utilizada por contact.routing_codes. Lleva los códigos de enrutamiento propios de la factura (p. ej. los códigos de servicio DIR3 / Chorus Pro del comprador), independientes de los del contacto.

Los códigos aplicados se retornan en un objeto routing_codes en el nivel superior de la factura de la respuesta, de modo que un integrador puede verificar qué se ha almacenado.

POST /accounts/{account_id}/invoices
{
"invoice": {
"number": "F-2026-0001",
"routing_codes": {
"cin1_value": "ESR_MISSION_FACTURES_DEPLACEMENTS",
"cin1_scheme": "8017"
}
}
}

Los campos de enrutamiento dedicados que versiones anteriores aceptaban en el nivel superior de la factura ya no se aceptan. Establece la ranura routing_codes equivalente en su lugar, según el esquema:

RanuraDIR3 (esquema 8014)DIRe (8015)EDI/GLN (88)
cin1accounting_unitnadby
cin2managing_unitnaddp
cin3processing_unitnadiv
cin4proponent_unitnadpr
cin5direnadud

Cambio incompatible: en el nivel superior de la factura, tanto las claves planas cinN_value / cinN_scheme como los campos de enrutamiento dedicados (dire, accounting_unit, managing_unit, processing_unit, proponent_unit, nadby, naddp, nadiv, nadpr, nadud) ya no se aceptan, tanto en POST /accounts/{account}/invoices como en PUT /invoices/{id}. Estas peticiones retornan 422 Unprocessable Entity. Envía los códigos de enrutamiento de la factura a través del objeto routing_codes de la factura en su lugar. Versiones anteriores de la API todavía aceptan estos campos.


Cambios de filtrado en GET /accounts/{account}/invoices:

number ahora coincide por prefijo

El parámetro de consulta number ahora realiza una coincidencia de prefijo en lugar de una coincidencia exacta. Se retornan las facturas cuyo número empieza por el valor proporcionado.

GET /accounts/{account}/invoices?number=INV-2

retorna INV-20, INV-21, … además de una factura numerada exactamente INV-2.

En versiones anteriores, el filtro number requería el número de factura completo y retornaba solo una coincidencia exacta.

series_code

El parámetro de consulta series_code filtra facturas por código de serie mediante coincidencia exacta.


🚨 Breaking

POST /invoices/{id}/generate_tax_report ahora retorna 201 Created en caso de éxito en lugar de 200 OK. El cuerpo de la respuesta no cambia.

Antes (≤ 2026-04-20): 200 OK

Después (≥ 2026-06-26): 201 Created

Este es un cambio incompatible menor para los clientes que comprueban un estado exacto de 200. Los clientes que comprueban cualquier éxito 2xx no se ven afectados.