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 our system 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 AEAT so they can be verified for integrity and authenticity by both the Tax Authority and the Customer.
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.
- Assembling a "Libro de Registro" or Ledger that may include one or several tax reports.
- Authenticated submission of the "Libro de Registro" to AEAT using a qualified electronic certificate.
- Obeying the rate-limited requests established by 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 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 our API without a qualified electronic certificate, just a valid B2Brouter API Key.
We provide 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 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 AEAT. For example, a call to creating an Invoice via the Invoice API might look like:
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 internal 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, you will find 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 processes that chain and send the tax report are 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, 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, 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, and registered_with_errors.
[BETA] Tax Report API
If you are not using B2Brouter for issuing invoices, or if you need more control over the process of generating and processing tax reports, or if you are planning on issuing a high volume of tax reports, eg for 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 directly import a valid XML VeriFactu if you system is capable of generating them.
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 our internal 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 groupings of 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. Our TaxReport model does not perform any calculation, that is you have to provide all relevant figures in the tax report. We'll perform error checking on these figures, but our system will refuse to fill the gaps if incomplete data is provided.
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, that is a RegistroAlta or a RegistroAnulacion from the SuministroInformacion namespace, you can import them directly using our 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 our system 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.
Error checking
In our experience managing tax reporting for several Tax Authorities, a common complication for origin systems (that is, your system) is dealing with errors that come from the Tax Authority. This is why we have implemented an exhaustive set of validations on VeriFactu tax reports created via our Tax Report API in order to detect errors early, and notify them back to your system before sending an erroneous or slightly incorrect tax report to Tax Authority.
For the VeriFactu case, we implemented our exhaustive validations based on the specifications published by the AEAT. In case any error is detected by our system at creation time, the tax report will not be created, and therefore not sent to AEAT. You'll receive a 422: Unprocessable Entity response with all errors in JSON format. You'll have to correct the errors and try o create the tax report again.
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 manually 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 manually 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
In order to list the tax reports that we already have on our system, you have to use the GET verb calling the list tax reports 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.
We provide here 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 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 1 day ago