VeriFactu Guide
Comply with VeriFactu and Spain Anti-Fraud Law by using B2Brouter API
The Anti-Fraud Law (Law 11/2021) mandates that businesses must ensure their invoicing and accounting systems meet strict technical standards to prevent data manipulation. Royal Decree 1007/2023 further defines the technical requirements for digital billing systems, emphasizing the need for standardized invoice formats that can be easily accessed by tax authorities. It mandates that businesses use certified software to ensure data integrity and make real-time reporting of invoices to tax authorities possible.
These regulations require businesses to adopt systems capable of securely generate, store, and transmit invoices and tax reports. B2Brouter's REST API is a technical solution that allows your business to easily be 100% compliant offloading the complexities to B2Brouter and focusing on the business logic of your system.
What is VeriFactu?
VeriFactu is a compliance system designed to simplify the process of meeting the invoicing requirements outlined in the Anti-Fraud Law. It ensures that all invoices generated by businesses are securely transmitted to the Tax Authority to be verified for integrity and authenticity by both the Tax Authority and the Customer. The Spanish Tax Authority is the AEAT ("Agencia Estatal de Administración Tributaria").
VeriFactu involves the immediate, real-time submission of invoice data to the AEAT as soon as the invoice is issued. This ensures that no modifications can be made after the invoice is issued, guaranteeing its security. Additionally, the invoice contains a QR code, allowing the recipient to verify its tax compliance through the AEAT's platform. This method significantly reduces the burden on businesses, as it eliminates the need for them to store and maintain invoice records themselves. Instead, the AEAT retains these records, ensuring their preservation and providing a streamlined way to access historical data.
In practice, VeriFactu requires:
- Creating a tax report XML file.
- Computing a digital fingerprint for each tax report. This includes a hash chain to create a tamper-proof trail.
- Assembling a "Libro de Registro" or Ledger that may include one or several tax reports.
- Authenticated submission of the "Libro de Registro" to the AEAT using a qualified electronic certificate.
- Obeying the rate-limited requests established by the AEAT (max. 1 call per minute, unless submitting >1,000 invoices).
- Generating a QR code for invoice verification, to be included in the issued invoice.
- Processing the responses from the AEAT for each tax report.
With B2Brouter you can abstract much of the complexities of this process, and comply with the Anti-Fraud Law by issuing just a few REST calls to the API. You will not need your own qualified electronic certificate because B2Brouter is a Social Collaborator in Tax Management. You just need a valid B2Brouter API Key.
B2Brouter provides two modalities of operation depending on whether you are using B2Brouter for both issuing invoices and reporting them to the Tax Authority, or just for tax reporting.
Setting up and working with B2Brouter API
The first step is to set up the configuration for VeriFactu for each one of the Companies for which you want to submit tax reports. You can refer to the TaxReportSettingins Guide for a full description of the process. The call for configuring an account is as follows:
curl --location 'https://app.b2brouter.net/accounts/{account_id}/tax_report_setting' \
--header 'X-B2B-API-Key: xxxxxxx' \
--header 'Content-Type: application/json' \
{
"tax_report_setting": {
"code": "VeriFactu",
"start_date": "2025-04-06",
"auto_generate": true,
"auto_send": true,
"reason_vat_exempt": "E1",
"special_regime_key": "01",
"reason_no_subject": "OT",
"credit_note_code": "R1"
}
}
Invoice API
If you are using B2Brouter for both issuing invoices and reporting them to the Tax Authority, once you have correctly configured your account, you can operate as you'd normally do for issuing invoices.
The issued invoices will contain the mandatory QR code, and the VeriFactu tax report will be sent automatically to the AEAT. This is an example of a call to create and issue an Invoice:
curl --request POST \
--url https://app-staging.b2brouter.net/projects/account/invoices.json \
--header 'content-type: application/json' \
--data '
{
"send_after_import": true,
"project_id": "3-enterprise",
"invoice": {
"date": "2025-04-15",
"due_date": "2025-05-15",
"number": 48,
"payment_method": 1,
"client": {
"name": "client.name",
"address": "client.address",
"address2": "client.address2",
"city": "client.city",
"province": "client.province",
"postalcode": "28938",
"country": "es",
"email": "[email protected]",
"language": "es",
"currency": "EUR",
"website": "client.website",
"terms": "15",
"payment_method": 1,
"tin_scheme": 9920,
"tin_value": "ESA28388510",
"notes": "client.notes",
"description": "client.description"
},
"invoice_lines_attributes": [
{
"quantity": 10.0,
"description": "line.description",
"price": 11.0,
"unit": 9,
"taxes_attributes": [
{
"name": "IVA",
"percent": 21.0,
"category": "S"
}
]
}
]
},
}'
In the response of the POST REST call for creating an invoice, if send_after_import is set to true, you will find a section tax_report_ids in which you'll find the ID of the tax report associated with the invoice. Note that tax_report_ids is an array because each invoice can have more than one tax report for cancellations and corrections. However, when the invoice is freshly created there is only one tax report ID in the tax_report_ids section:
{
"invoice": {
"id": 4919734,
"type": "IssuedInvoice",
"number": "48",
"from_net": "api",
"from_net_id": null,
"project": {
"id": 107926,
"name": "3 - Enterprise Company"
},
"company": {
"id": 100446,
"name": "3 - Enterprise Company",
"taxcode": "ES77167394W",
"address": null,
"address2": null,
"postalcode": "08080",
"city": null,
"province": null,
"country": "es"
},
"client": {
"id": 483183,
"name": "client.name",
"taxcode": "ESA28388510",
"address": "client.address",
"address2": "client.address2",
"postalcode": "28938",
"city": "client.city",
"province": "client.province",
"country": "es",
"party_identification": null
},
"state": "sending",
"date": "2025-04-15",
"due_date": "2025-05-15",
"ponumber": null,
"file_reference": null,
"payment_method_info": "Pagament en efectiu",
"contact_iban": "NL35ABNA1749777983",
"contact_bic": "clientbic",
"is_credit_note": false,
"adjustment_in_cents": 0,
"charge_reason": null,
"charge_amount": 0.0,
"charge_percent": 0.0,
"discount_text": null,
"discount_percent": 0.0,
"discount_amount": 0.0,
"subtotal": 110.0,
"taxes": [
{
"name": "IVA 21,00%",
"percent": 21.0,
"base": 110.0,
"amount": 23.1
}
],
"total": 133.1,
"currency": "EUR",
"amounts_withheld_reason": null,
"withheld_percent": 0.0,
"amounts_withheld": 0.0,
"payable_amount": 133.1,
"extra_info": null,
"tax_report_ids": [
15
],
"created_at": "2025-04-15T09:54:42Z",
"updated_at": "2025-04-15T09:54:45Z",
"state_updated_at": "2025-04-15T09:54:45Z",
"ack_at": null,
"apply_taxes_to_charge": false,
"charge_is_reimbursable_expense": false
}
}
You can access the complete tax report, including the QR code, its identifier, its state, and all other fields, by calling the get tax report endpoint with the tax report ID provided in the response of the create invoice call.
Note that both the chaining process and the sending process of the ledger are asynchronous processes, that is, are performed on the background outside the main process that creates the invoice and generates the tax report.
Therefore, the state of the tax report will always be processing in the response of the POST call to create an invoice because the background process that chains and sends the tax report is performed independently of the process of creating the invoice and its associated tax report.
This is the reason why you must check the life cycle of the tax report. The state field of the tax report after issuing the invoice will change. You have two options to accomplish this:
-
The recommended way is using the tax report web hook. The web hook will issue a POST call to your system each time the state of the tax report reaches a final state. These states are: registered, error, and registered_with_errors
-
Pooling the state of the tax report with get tax report endpoint until the state field is registered, error or registered_with_errors.
[BETA] Tax Report API
If you are not using B2Brouter for issuing invoices, need more control over the process of generating and processing tax reports, or expect a high volume of tax reports, as in a Point of Sale ("Terminal Punto de Venta" in Spanish), you can use the new Tax Report API.
This API is designed for maximum control by the user and high availability. You can either submit tax reports as a JSON payload or as a valid VeriFactu XML.
The structure of the JSON format for B2Brouter tax reports is based on PEPPOL Continuous Transaction Control (CTC). That means that the fields used in the JSON payload have not the same name as in the XML representation of a VeriFactu tax report. The reason behind this design decision is that B2Brouter Tax Report API is an universal API not only geared towards VeriFactu but designed to handle tax reporting around the World. You can check the equivalences between B2Brouter's representation of a Tax Report and VeriFactu nodes in the section Equivalence between B2Brouter internal tax report fields and VeriFactu XML nodes.
It's important to notice that tax reports do not have lines in the same way that invoices have lines; tax reports have tax breakdowns ("desglose" in Spanish) which are the sum of the lines of the original invoice with the same tax. Is the responsibility of your system to aggregate the invoice lines by tax type and category and provide the necessary tax breakdowns. The TaxReport model does not perform any calculations. You must provide all relevant figures in the tax report. B2Brouter will perform error checking on these figures, but will not fill the gaps in incomplete data.
Create a Tax Report
In order to create a tax report you have to call the create tax report endpoint. Please refer tot to the section Equivalence between B2Brouter internal tax report fields and VeriFactu XML nodes for a complete list of equivalences.
As an example, a minimal and valid tax report can be created by the following POST call:
curl -X POST --location 'https://app.b2brouter.net/api/v1/accounts/{account_id}/tax_reports' \
--header 'X-B2B-API-Key: xxxxxxx' \
--header 'Content-Type: application/json' \
--data
'{
tax_report: {
type: 'Verifactu',
invoice_date: '2025-04-03',
invoice_number: '11',
description: 'Lorem ipsum...',
customer_party_tax_id: 'B63354613',
customer_party_country: 'es',
customer_party_name: 'Ingent Grup Systems, SL',
tax_inclusive_amount: 121.0,
tax_amount: 21.0,
invoice_type_code: 'F1',
currency: 'EUR',
tax_breakdowns: [
{
name: 'IVA',
category: 'S',
non_exemption_code: 'S1',
percent: 21.0,
taxable_base: 100.0,
tax_amount: 21.0,
special_regime_key: '01'
}
]
}
}'
The response of this POST call to the create tax report endpoint will already contain the field qr with the QR code (codified in base64) that you must include in the invoice for your customer. The content of this QR code is a URL that will allow your customer to check that AEAT has received and processed the tax report derived from their invoice. This URL is also included in the response to the POST in the field identifier.
You can see the response to the above call:
{
"tax_report": {
"invoice_id": null,
"state": "processing",
"sent_at": null,
"contact_name": null,
"label": "Verifactu 11",
"has_errors": false,
"has_warnings": false,
"from_net": null,
"from_net_id": null,
"to_net": null,
"to_net_id": null,
"ledger_id": null,
"document_type_code": "xml.tax_report.verifactu.alta",
"transport_type_code": "es.verifactu",
"generation_timestamp": null,
"tax_point_date": null,
"chained_at": null,
"invoice_date": "2025-04-03",
"invoice_number": "11",
"invoice_series_code": null,
"fiscal_year": null,
"fiscal_period": null,
"amended_number": null,
"amended_series_code": null,
"amended_date": null,
"amend_type": null,
"amended_line_extension": null,
"amended_tax_amount": null,
"amended_tax_exclusive": null,
"amended_tax_inclusive": null,
"amended_charge_total": null,
"amended_payable": null,
"amended_quota_recargo_equivalencia": null,
"invoice_type_code": "F1",
"operation_type": null,
"external_reference": null,
"description": "Lorem ipsum...",
"annullation": false,
"correction": false,
"ticket": false,
"issued_by_third_party_or_receiver": false,
"customer_party_name": "Ingent Grup Systems, SL",
"customer_party_id": null,
"customer_party_scheme": null,
"customer_party_tax_id": "B63354613",
"customer_party_tax_scheme": null,
"customer_party_country": "es",
"customer_party_endpoint_id": null,
"supplier_party_name": "B2Brouter Global S.L.",
"supplier_party_id": null,
"supplier_party_scheme": null,
"supplier_party_tax_id": "B63276174",
"supplier_party_tax_scheme": null,
"supplier_party_country": null,
"supplier_party_endpoint_id": null,
"third_party_name": null,
"third_party_id": null,
"third_party_scheme": null,
"third_party_tax_id": null,
"third_party_tax_scheme": null,
"third_party_country": null,
"third_party_endpoint_id": null,
"line_extension_amount": null,
"tax_amount": 21,
"tax_exclusive_amount": null,
"tax_inclusive_amount": 121,
"charge_total": null,
"payable_amount": null,
"quota_recargo_equivalencia": null,
"currency": "EUR",
"original_currency": "EUR",
"created_at": "2025-04-03T12:49:53.020Z",
"updated_at": "2025-04-03T12:49:53.086Z",
"identifier": "https://prewww2.aeat.es/wlpl/TIKE-CONT/ValidarQR?nif=B63276174&numserie=11&fecha=03-04-2025&importe=121.0",
"qr": "iVBORw0KVGgoAAAANS[...]",
"previous_id": null,
"rechazo_previo": null,
"simplificada_art7273": false,
"macrodato": "N",
"huella": null,
"digested_at": null,
"annulled_at": null,
"cupon": false,
"base_imponible_a_coste": null,
"tax_breakdowns": [
{
"id": 320703,
"name": "IVA",
"category": "S",
"special_regime_key": "01",
"non_exempt": true,
"non_exemption_code": "S1",
"no_subject": false,
"no_subject_code": null,
"exempt": false,
"exemption_code": null,
"percent": 21,
"taxable_base": 100,
"tax_amount": 21
}
]
}
}
Import a Tax Report from an XML VeriFactu file
If your system is capable of generating VeriFactu XML files (a RegistroAlta or a RegistroAnulacion from the SuministroInformacion namespace), you can import them directly using the API via the import endpoint.
If the creation of the Tax Report is successful, the response of this action will already contain the QR code and the identifier for the Tax Report. Note that if the XML file has chaining information, such as Huella or RegistroAnterior, this information will be ignored and B2Brouter will perform the chaining process regardless. This is the only way to ensure the integrity of the data sent to AEAT.
Check the state of a Tax Report
Notice the that state field of the tax report will always be processing after a successful creation of a tax report. This is because both the chaining process among tax reports and the sending process of the ledger are asynchronous processes, that is, are performed on the background outside the main process that creates the tax report.
This is the reason why you must check the life cycle of the tax report, that is the evolution of the field state of the tax report after issuing the invoice. You have two options to accomplish this:
-
The recommended way is using the tax report web hook. The web hook will issue a POST call to your system each time the state of the tax report reaches a final state. These states are: registered, error, annulled, and registered_with_errors
-
Pooling the state of the tax report by calling the manually the get tax report endpoint until the state field of the tax report is any of: registered, error, annulled, and registered_with_errors.
Get the XML representation of the tax report
If you need the XML representation of the tax report, that is, in the case of VeriFactu, the RegistroAlta that B2Brouter will send to the AEAT, you have available a download tax report endpoint. For your convenience, B2Brouter also includes a field named xml_base64 in the response to the GET call to the get tax report endpoint that includes the XML codified in base64. Thus, you can both check the state of the tax report, and get the XML representation in just one call.
Note however that, for the case of VeriFactu, the XML representation of the tax report can only be generated after the tax report has been chained. Which, as discussed above, is an asynchronous process. This process is usually fast, as it happens entirely in B2Brouter systems without need to communicate with the AEAT. But there is no guarantee that the tax report will already be chained if you call the get tax report endpoint just after creating the tax report. If the tax report is still in the chaining queue its state will be processing and the xml_base64 field will be nil.
Error checking
It is specially difficult for client systems to deal with all the errors that the Tax Authority may return. To avoid this potential issues, B2Brouter has implemented an exhaustive set of validations of VeriFactu tax reports. This allows early error detection and stops an erroneous (or slightly incorrect) tax report before sending it to the Tax Authority.
For the VeriFactu case, there are exhaustive validations based on the specifications published by the AEAT. If there is a validation error the tax report will not be created, and therefore will not be sent to the AEAT. You'll receive a 422: Unprocessable Entity response with all errors in JSON format. You'll have to correct the errors and create a new tax report.
Cancel a Tax Report
In order to cancel a tax report ("anulación" in Spanish) you have to use the DELETE verb calling the annullation endpoint. As in the creation case, you'll have to to check the evolution of the cancellation by ether processing the tax report web hook on your system, or pooling the state of the tax report by calling the get tax report endpoint until the state field of the tax report is error or annulled.
Correct a Tax Report
In order to correct a tax report ("subsanación" in Spanish) you have to use the PATCH or PUT verbs calling the correction endpoint. As in the creation case, you'll have to to check the evolution of the correction by ether processing the tax report web hook on your system, or pooling the state of the tax report by calling the get tax report endpoint until the state field of the tax report is any of: registered, error, annulled, and registered_with_errors.
List Tax Reports
To list your tax reports use the GET verb in list tax reports endpoint.
[BETA] Ledgers API
Verifactu tax reports are not send individually to the AEAT, they are bundled in a "Libro de Registro" (Ledger in B2Brouter terms), that contains up to a 1000 tax reports. Each tax report has a ledger_id field that identifies the internal ID for the ledger to which the tax report belongs. With that ID you can:
- Retrieve the XML representation of the Ledger by calling the download ledger endpoint. This document is the one that B2Brouter sends to the AEAT.
- Retrieve the XML response of the AEAT to the Ledger sent by B2Brouter by calling the download response endpoint.
Equivalence between B2Brouter internal tax report fields and VeriFactu XML nodes
The structure of the JSON format for B2Brouter tax reports is based on PEPPOL Continuous Transaction Control (CTC). That means that the fields used in the JSON payload have not the same name as in the XML representation of a VeriFactu tax report. The reason behind this design decision is that B2Brouter Tax Report API is an universal API not only geared towards VeriFactu but designed to handle tax reporting around the World.
This is a table with the equivalence between the internal B2Brouter representation of a tax report and the VeriFactu fields used in the XML that is sent to the AEAT.
B2Brouter Field (based on PEPPOL CTC) | Verifactu XML Node |
---|---|
invoice_date | IDFactura > FechaExpedicionFactura |
invoice_number | IDFactura > NumSerieFactura |
invoice_series_code | IDFactura > NumSerieFactura |
supplier_party_name | NombreRazonEmisor |
external_reference | RefExterna |
correction | Subsanacion |
annullation | Anulación |
rechazo_previo | RechazoPrevio |
invoice_type_code | TipoFactura |
amend_type | TipoRectificativa |
amended_tax_exclusive | ImporteRectificacion > BaseRectificada |
amended_tax_amount | ImporteRectificacion > CuotaRectificada |
tax_point_date | FechaOperacion |
description | DescripcionOperacion |
simplificada_art7273 | FacturaSimplificadaArt7273 |
ticket | FacturaSinIdentifDestinatarioArt61d |
macrodato | Macrodato |
issued_by_third_party_or_receiver | EmitidaPorTerceroODestinatario |
third_party_name | Tercero > NombreRazon |
third_party_tax_id | Tercero > NIF |
customer_party_name | Destinatarios > IDDestinatario > NombreRazon |
customer_party_tax_id | Destinatarios > IDDestinatario > NIF or IDOtro > IDType |
customer_party_country | Destinatarios > IDDestinatario > IDOtro > CodigoPais |
customer_party_tax_scheme | Destinatarios > IDDestinatario > IDOtro > IDType |
tax_breakdowns[].name | Desglose > DetalleDesglose > Impuesto |
tax_breakdowns[].special_regime_key | Desglose > DetalleDesglose > ClaveRegimen |
tax_breakdowns[].non_exemption_code | Desglose > DetalleDesglose > CalificacionOperacion |
tax_breakdowns[].exemption_code | Desglose > DetalleDesglose > OperacionExenta |
tax_breakdowns[].percent | Desglose > DetalleDesglose > TipoImpositivo |
tax_breakdowns[].taxable_base | Desglose > DetalleDesglose > BaseImponibleOimporteNoSujeto |
tax_breakdowns[].base_imponible_a_coste | Desglose > DetalleDesglose > BaseImponibleACoste |
tax_breakdowns[].tax_amount | Desglose > DetalleDesglose > CuotaRepercutida |
tax_amount | CuotaTotal |
tax_inclusive_amount | ImporteTotal |
previous_id | Encadenamiento > RegistroAnterior > NumSerieFactura |
Updated 11 days ago