Spain Fiscalization Guide
What is Spanish fiscalization?
Spain requires businesses to electronically report every invoice to the government. Think of it as a digital receipt that gets sent to the tax authority in real-time (or near real-time), so they can verify that businesses are accurately reporting their sales and collecting the right amount of VAT (Value Added Tax -- Europe's sales tax).
The twist: Spain doesn't have just one system. Depending on where your merchant operates and how big they are, one of three different systems applies. FiscalAPI handles all three -- you just set the right system in your location config, and the adapter takes care of the rest.
The three systems: TicketBAI, Verifactu, and SII
Here's a simple decision tree:
- Is the merchant in the Basque Country? (three provinces in northern Spain with their own tax rules) → Use TicketBAI
- Does the merchant have annual revenue over EUR 6 million? → Use SII (Suministro Inmediato de Información -- "Immediate Supply of Information")
- Everyone else in Spain → Use Verifactu
TicketBAI -- Basque Country invoice reporting
What it is: A regional fiscalization system used in three provinces (Gipuzkoa, Araba, and Bizkaia) in Spain's Basque Country. These provinces have their own tax authorities, separate from the national one.
What problem it solves: The Basque tax authorities want to see every invoice as it's created, so they can detect unreported sales in real-time.
How it works: When your POS submits a sale, FiscalAPI generates an XML document, digitally signs it (using a cryptographic standard called XAdES-EPES), and sends it to the regional tax authority. Each invoice references the previous one's hash, creating a tamper-evident chain -- if someone tries to delete or modify an invoice, the chain breaks and the authorities will notice.
Example: When your POS submits a EUR 50 sale at a store in San Sebastián (Gipuzkoa), FiscalAPI builds the TicketBAI XML, signs it with the merchant's certificate, and POSTs it to Gipuzkoa's tax authority. Within milliseconds, the authority confirms receipt and FiscalAPI returns a TBAI ID and QR code URL in the transaction response.
FiscalAPI handles XML generation, digital signing, invoice chaining, and submission automatically. You just create a transaction -- the adapter does the rest.
Verifactu -- nationwide invoice verification
What it is: Spain's national invoice verification system for most businesses. Run by the AEAT (Agencia Estatal de Administración Tributaria -- Spain's equivalent of the IRS).
What problem it solves: Same goal as TicketBAI but for the rest of Spain. Every invoice gets reported to the national tax authority with a hash chain guaranteeing integrity.
How it works: When your POS submits a sale, FiscalAPI creates a record with a SHA-256 hash that chains to the previous invoice. This record is submitted to AEAT via a SOAP web service. No digital certificate signing on the document itself -- the integrity comes from the hash chain instead.
Example: When your POS submits a EUR 200 sale at a store in Barcelona, FiscalAPI generates the Verifactu record, computes the chain hash, and submits it to AEAT. The transaction completes with a fiscal ID confirming the record was accepted.
FiscalAPI manages the hash chain automatically. You never need to track which invoice came before which -- the adapter maintains chain state per merchant.
SII -- real-time reporting for large businesses
What it is: A separate AEAT system for large companies (annual revenue >= EUR 6,336,000). Instead of reporting in real-time, businesses get a four-day window to submit each invoice.
What problem it solves: Large companies often have complex invoicing workflows. SII gives them a short grace period while still requiring near-real-time reporting to AEAT.
How it works: FiscalAPI builds a SOAP envelope with the invoice details and submits it to AEAT using mutual TLS (the merchant's digital certificate authenticates the connection). AEAT responds with a verification code (CSV -- Código Seguro de Verificación).
Example: When your merchant's ERP system records a EUR 10,000 consulting invoice to a client, FiscalAPI builds the SII SOAP message, submits it to AEAT over mutual TLS, and returns the CSV verification code. The merchant has up to 4 calendar days from the invoice date to submit -- but since FiscalAPI submits in real-time when you create the transaction, you're covered as long as the transaction is created within the window.
FiscalAPI handles SOAP envelope construction, mutual TLS certificate management, and AEAT communication automatically. You just create transactions -- the adapter does the rest.
Who must use SII?
SII is mandatory for:
- Companies with annual turnover exceeding EUR 6,336,000
- Businesses enrolled in the monthly VAT refund scheme (REDEME -- Registro de Devolución Mensual)
- Groups filing consolidated VAT returns (grupos de IVA -- when multiple related companies file as one)
- Businesses that voluntarily opt in (some companies choose SII for the faster VAT refunds it enables)
Quick comparison
| Feature | TicketBAI | Verifactu | SII |
|---|---|---|---|
| Who uses it | Merchants in the Basque Country | Most businesses in Spain | Large businesses (>= EUR 6M revenue) |
| Where | Gipuzkoa, Araba, Bizkaia only | Nationwide | Nationwide |
| Authority | Regional Basque tax offices | AEAT (national tax agency) | AEAT (national tax agency) |
| Submission timing | Real-time | Real-time | Within 4 calendar days |
| How invoices are secured | Digital signature (XAdES-EPES) + hash chain | SHA-256 hash chain | Mutual TLS (certificate on the connection) |
| Invoice chaining | Yes | Yes | No (each invoice is independent) |
| Status | Active (mandatory in Basque Country) | Rolling out | Active |
Country config fields
The country_config object for Spanish locations tells FiscalAPI which system to use and provides the required credentials.
Common fields (all systems)
| Field | Type | Required | Description |
|---|---|---|---|
system | string | Yes | Which system to use: ticketbai, verifactu, or sii |
legal_name | string | Yes | The merchant's registered company name |
Note: The merchant's Spanish tax ID (NIF or CIF -- a unique business identifier, similar to an EIN in the US) goes in the location's
tax_idfield, not incountry_config.
| certificate_id | string | Yes | Reference to an uploaded certificate used for signing or TLS |
| tsa_url | string | No | Timestamp authority URL for XAdES-T timestamps (optional, for TicketBAI) |
TicketBAI-specific fields
| Field | Type | Required | Description |
|---|---|---|---|
territory | string | Yes | Which Basque province: gipuzkoa, araba, or bizkaia |
tbai_license | string | Yes | The TicketBAI software license code (issued by the regional authority) |
Verifactu-specific fields
| Field | Type | Required | Description |
|---|---|---|---|
software_nif | string | Yes | Tax ID of the software developer (your company's NIF if you built the POS) |
installation_number | string | No | Identifies this specific software installation |
SII-specific fields
SII only needs the common fields (legal_name and certificate_id). The merchant's NIF goes in the location's tax_id.
| Field | Type | Required | Description |
|---|---|---|---|
legal_name | string | Yes | The merchant's full legal name (razón social) |
certificate_id | string | Yes | Reference to an uploaded certificate for mutual TLS with AEAT |
Configuration examples
TicketBAI location (Gipuzkoa)
curl -X POST https://api.zyntem.dev/v1/locations \
-H "Content-Type: application/json" \
-H "Authorization: Bearer zyn_test_abc123def456..." \
-d '{
"name": "Donostia Store",
"country": "ES",
"address": "Calle Mayor 1, 20003 San Sebastián",
"tax_id": "B12345674",
"country_config": {
"system": "ticketbai",
"territory": "gipuzkoa",
"legal_name": "Acme Corp SL",
"certificate_id": "spain-prod-2026",
"tbai_license": "TBAILIC123456"
}
}'
TicketBAI location (Bizkaia)
Bizkaia works differently from the other Basque provinces. Instead of submitting each invoice in real-time, invoices are queued and submitted in a daily batch (called LROE) at 2 AM UTC. Your transaction will show a pending_batch status until the batch runs.
curl -X POST https://api.zyntem.dev/v1/locations \
-H "Content-Type: application/json" \
-H "Authorization: Bearer zyn_test_abc123def456..." \
-d '{
"name": "Bilbao Store",
"country": "ES",
"address": "Gran Vía de Don Diego López de Haro 1, 48001 Bilbao",
"tax_id": "B12345674",
"country_config": {
"system": "ticketbai",
"territory": "bizkaia",
"legal_name": "Acme Corp SL",
"certificate_id": "spain-prod-2026",
"tbai_license": "TBAILIC123456"
}
}'
Verifactu location
curl -X POST https://api.zyntem.dev/v1/locations \
-H "Content-Type: application/json" \
-H "Authorization: Bearer zyn_test_abc123def456..." \
-d '{
"name": "Barcelona Store",
"country": "ES",
"address": "Passeig de Gracia 1, 08007 Barcelona",
"tax_id": "B87654321",
"country_config": {
"system": "verifactu",
"legal_name": "Acme Corp SL",
"certificate_id": "spain-prod-2026",
"software_nif": "B99999999",
"installation_number": "001"
}
}'
SII location
curl -X POST https://api.zyntem.dev/v1/locations \
-H "Content-Type: application/json" \
-H "Authorization: Bearer zyn_test_abc123def456..." \
-d '{
"name": "Madrid HQ",
"country": "ES",
"address": "Calle de Alcalá 50, 28014 Madrid",
"tax_id": "A12345674",
"country_config": {
"system": "sii",
"legal_name": "Gran Empresa SL",
"certificate_id": "spain-sii-prod-2026"
}
}'
Submission flows
What happens when your POS creates a sale
The flow varies by system, but from your perspective it's the same API call -- POST /v1/transactions. FiscalAPI routes to the right system based on the location's country_config.system.
TicketBAI (Gipuzkoa / Araba) -- real-time submission
- Your app creates a transaction via
POST /v1/transactions - FiscalAPI generates TicketBAI XML and chains it to the previous invoice (per merchant tax ID)
- The XML is digitally signed using the merchant's certificate
- The signed XML is sent to the regional tax authority in real-time
- On success, you get back a
fiscal_id(TBAI ID) and a QR code URL - Transaction status:
success
TicketBAI (Bizkaia) -- batch submission (LROE)
- Your app creates a transaction via
POST /v1/transactions - FiscalAPI generates and signs the XML (same as above)
- Transaction status:
pending_batch(queued for batch submission) - At 2 AM UTC, the LROE batch processor collects all pending transactions and submits them
- On acceptance, transaction status transitions to
success
You can monitor batch processing via the batch stats endpoint.
Verifactu -- real-time submission
- Your app creates a transaction via
POST /v1/transactions - FiscalAPI generates the record and computes the SHA-256 chain hash
- The record is submitted to AEAT via SOAP
- On success, you get back a
fiscal_idand transaction status:success
SII -- real-time submission with 4-day window
- Your app creates a transaction via
POST /v1/transactions - FiscalAPI builds a SOAP envelope with the invoice record
- The envelope is submitted to AEAT via mutual TLS
- AEAT responds with a status (
Correcto,Incorrecto, orParcialmenteCorrecto) - On success,
fiscal_idis set to the CSV (verification code) and transaction status:success
SII cancellations
Voiding a transaction submits a cancellation record to AEAT. This tells the tax authority that a previously reported invoice has been annulled.
SII received invoices
To submit a received invoice (recording a purchase), set "book_type": "received" in the transaction metadata along with the supplier's details.
SII book types (Libros Registro)
SII organizes invoice data into four "books" (think of them as categories or ledgers):
| Book | What it contains | When to use |
|---|---|---|
| Issued invoices (Facturas Expedidas) | Invoices your merchant sends to their customers | Every sale your merchant makes |
| Received invoices (Facturas Recibidas) | Invoices your merchant receives from suppliers | Recording purchase invoices |
| Investment goods (Bienes de Inversión) | Capital asset purchases | Buying equipment, vehicles, etc. |
| Intra-community operations (Operaciones Intracomunitarias) | Cross-border transactions within the EU | Selling to or buying from another EU country |
FiscalAPI currently supports issued invoices and received invoices, which cover the vast majority of SII submissions.
SII transaction metadata
SII transactions accept optional metadata fields. All fields have sensible defaults -- you only need to provide them when you want to override the defaults.
Core metadata fields
| Field | Type | Default | Description |
|---|---|---|---|
invoice_number | string | Transaction ID | The invoice number as it appears on the document |
series | string | -- | Optional series prefix (prepended to invoice number) |
issue_date | string | Current date | Invoice date in DD-MM-YYYY format |
description | string | Auto-generated | A brief description of what the invoice is for |
regime_key | string | "01" (general) | Tax regime code (see regime keys table below) |
exercise | string | Current year | Fiscal year (e.g., "2026") |
period | string | Current month | Tax period: "01" through "12", or "0A" for annual |
simplified | boolean | false | Set to true for simplified invoices (small transactions) |
book_type | string | "issued" | "issued" for sales, "received" for purchases |
Counterparty fields (the other party on the invoice)
Required for standard invoices. Not needed for simplified invoices.
| Field | Type | Description |
|---|---|---|
counterparty_nif | string | The other party's Spanish tax ID (for domestic transactions) |
counterparty_name | string | The other party's legal name |
counterparty_id_type | string | For foreign parties: "02" = EU VAT number, "04" = passport |
counterparty_country | string | Country code (ISO 3166-1 alpha-2) |
counterparty_id | string | The foreign party's identifier value |
Credit note fields (when correcting a previous invoice)
| Field | Type | Description |
|---|---|---|
original_invoice_number | string | Number of the invoice being corrected |
original_issue_date | string | Date of the original invoice (DD-MM-YYYY) |
Example: standard issued invoice
Scenario: Your merchant issues a EUR 1,210 consulting invoice to another Spanish company.
curl -X POST https://api.zyntem.dev/v1/transactions \
-H "Content-Type: application/json" \
-H "Authorization: Bearer zyn_test_abc123def456..." \
-d '{
"location_id": "loc_madrid_hq",
"type": "sale",
"amount": 121000,
"currency": "EUR",
"items": [
{
"description": "Consulting services",
"quantity": 1,
"unit_price": 100000,
"total_amount": 121000,
"tax_rate": 2100,
"tax_amount": 21000
}
],
"metadata": {
"invoice_number": "FA-2026/001",
"series": "A-",
"issue_date": "13-03-2026",
"description": "Consulting services Q1 2026",
"regime_key": "01",
"counterparty_nif": "B99999999",
"counterparty_name": "Acme Consulting SL"
}
}'
Example: credit note (correcting a previous invoice)
Scenario: Your merchant needs to issue a full credit note to reverse a previous invoice.
curl -X POST https://api.zyntem.dev/v1/transactions \
-H "Content-Type: application/json" \
-H "Authorization: Bearer zyn_test_abc123def456..." \
-d '{
"location_id": "loc_madrid_hq",
"type": "refund",
"amount": -121000,
"currency": "EUR",
"items": [
{
"description": "Consulting services - correction",
"quantity": 1,
"unit_price": -100000,
"total_amount": -121000,
"tax_rate": 2100,
"tax_amount": -21000
}
],
"metadata": {
"invoice_number": "RA-2026/001",
"issue_date": "13-03-2026",
"description": "Rectificación FA-2026/001",
"counterparty_nif": "B99999999",
"counterparty_name": "Acme Consulting SL",
"original_invoice_number": "A-FA-2026/001",
"original_issue_date": "10-03-2026"
}
}'
SII invoice types
| Code | Name | When to use |
|---|---|---|
| F1 | Factura (Standard invoice) | Normal invoices -- this is the default for sale transactions |
| F2 | Factura simplificada (Simplified invoice) | Small transactions -- set "simplified": true in metadata |
| R1 | Factura rectificativa (Credit note) | Correcting a previous invoice -- automatic for refund transactions |
SII regime keys
The regime_key metadata field tells AEAT what tax regime applies to this invoice. Common values:
| Key | What it means | When to use |
|---|---|---|
01 | General regime | Default -- use this for standard domestic transactions |
02 | Export | Sales to non-EU countries |
03 | Special regime for used goods | Second-hand goods (margin scheme) |
05 | Travel agencies | Travel agency services |
07 | Special cash accounting regime | Businesses on cash-basis accounting |
09 | Intra-community acquisitions | Purchases from other EU countries |
SII AEAT endpoints
SII communicates with AEAT via SOAP 1.1 over mutual TLS.
Production
| Book | Endpoint |
|---|---|
| Issued invoices | https://www1.agenciatributaria.gob.es/wlpl/SSII-FACT/ws/fe/SiiFactFEV2SOAP |
| Received invoices | https://www1.agenciatributaria.gob.es/wlpl/SSII-FACT/ws/fr/SiiFactFRV2SOAP |
Sandbox
| Book | Endpoint |
|---|---|
| Issued invoices | https://prewww1.aeat.es/wlpl/SSII-FACT/ws/fe/SiiFactFEV2SOAP |
| Received invoices | https://prewww1.aeat.es/wlpl/SSII-FACT/ws/fr/SiiFactFRV2SOAP |
Sandbox routing is automatic when using a test API key (zyn_test_...). Test transactions hit sandbox endpoints without affecting real tax records.
SII submission deadline
Invoices must be reported to SII within four calendar days of the invoice issue date (or the accounting date for received invoices). Sundays and national holidays do not count toward this deadline.
FiscalAPI submits invoices to AEAT in real-time when you create the transaction, so you meet the deadline as long as the transaction is created within the four-day window.
SII error handling
AEAT responds with one of three statuses:
| AEAT status | What it means | Your transaction status |
|---|---|---|
Correcto | Invoice accepted | success |
ParcialmenteCorrecto | Some records accepted, some rejected (batch submissions) | partial |
Incorrecto | Invoice rejected | failed |
Common error codes:
| Code | What went wrong | What to do |
|---|---|---|
| 1106 | Invalid NIF (tax ID format error) | Check the merchant's tax_id and counterparty NIF |
| 2000 | Duplicate invoice (already registered with AEAT) | The invoice was already submitted -- no action needed |
| 3000 | XML schema validation error | Check transaction data for missing/invalid fields |
SII certificate setup
SII requires a qualified electronic certificate for mutual TLS authentication with AEAT. This is how AEAT verifies the identity of the submitting business. Upload the certificate through the certificates API:
curl -X POST https://api.zyntem.dev/v1/certificates \
-H "Authorization: Bearer zyn_test_abc123def456..." \
-F "name=spain-sii-prod-2026" \
-F "type=pkcs12" \
-F "file=@certificate.p12" \
-F "password=cert-password"
Then reference the certificate ID in your location's country_config.certificate_id.
FiscalAPI handles the mutual TLS connection setup using the uploaded certificate. You don't need to manage TLS handshakes, certificate chains, or SOAP envelope construction.
Invoice chaining
Both TicketBAI and Verifactu create a tamper-evident chain where each invoice references the hash of the previous one (for the same merchant tax ID). This is how the government detects deleted or modified invoices. FiscalAPI manages the chain automatically -- you never need to track chain state.
Switching systems
If a merchant moves to a different region or crosses the revenue threshold for SII, you can change their fiscalization system by updating the location:
curl -X PATCH https://api.zyntem.dev/v1/locations/{id} \
-H "Content-Type: application/json" \
-H "Authorization: Bearer zyn_test_abc123def456..." \
-d '{
"country_config": {
"system": "verifactu",
"legal_name": "Acme Corp SL",
"certificate_id": "spain-prod-2026",
"software_nif": "B99999999"
}
}'
Validation rules
FiscalAPI validates your configuration when you create or update a Spanish location:
country_configis required -- Spain locations must include acountry_configwith asystemfield- Valid systems --
systemmust beticketbai,verifactu, orsii - Territory -- Required for TicketBAI; must be
gipuzkoa,araba, orbizkaia - NIF -- The merchant's tax ID (provided in the location's
tax_idfield) - Legal name -- Required for all systems
- Certificate -- A valid signing certificate must be uploaded and referenced by
certificate_id
Error examples
Missing country config:
{
"error": "Spain locations require country_config with system field (ticketbai, verifactu, or sii)"
}
Invalid system:
{
"error": "system must be \"ticketbai\", \"verifactu\", or \"sii\", got \"invalid\""
}
Territories
| Territory | System | How invoices are submitted |
|---|---|---|
| Gipuzkoa | TicketBAI | Real-time XML POST to regional authority |
| Araba | TicketBAI | Real-time XML POST to regional authority |
| Bizkaia | TicketBAI | Daily batch (LROE) at 2 AM UTC |
| Rest of Spain (small/medium business) | Verifactu | Real-time SOAP to AEAT |
| Rest of Spain (large business >= EUR 6M) | SII | SOAP to AEAT within 4 days |