spanish-tax-compliance
Spanish Tax Compliance (VeriFactu)
Implement Spanish electronic invoicing compliance with VeriFactu regulations, AEAT integration, NIF validation, and tax calculation.
When to Use This Skill
- Implementing VeriFactu electronic invoicing
- Validating Spanish tax identifiers (NIF, DNI, NIE, CIF)
- Calculating IVA (VAT) rates and IRPF retention
- Building the RegistroFactura XML structure for AEAT submission
- Implementing invoice hash chaining
- Understanding invoice types and numbering rules
VeriFactu Regulation Overview
What is VeriFactu?
VeriFactu (Verificación de Facturas) is Spain's anti-fraud electronic invoicing system mandated by the AEAT (Agencia Estatal de Administración Tributaria). It requires:
- Compliant invoicing software that signs and chains invoices
- Real-time or near-real-time submission to AEAT
- Immutable audit trail via cryptographic hash chaining
- Software identification in each submission
Key Requirements
- Each invoice must be signed with a hash linked to the previous invoice
- Invoice numbers must be sequential with no gaps
- The software system must identify itself in submissions
- Invoices must be submitted electronically to AEAT
- Credit/correction invoices must reference the original
Timeline
- Mandatory for all businesses and freelancers (autónomos)
- Implementation dates vary by business size
- Testing available via AEAT staging environment
Invoice Types
Factura Ordinaria (Standard Invoice)
Full invoice with all required fields. Used for most B2B transactions.
from pydantic import BaseModel, Field
from decimal import Decimal
from datetime import date
from enum import Enum
class TipoFactura(str, Enum):
"""VeriFactu invoice types."""
F1 = "F1" # Factura ordinaria
F2 = "F2" # Factura simplificada (ticket)
R1 = "R1" # Factura rectificativa (error en datos)
R2 = "R2" # Factura rectificativa (concurso acreedores)
R3 = "R3" # Factura rectificativa (deuda incobrable)
R4 = "R4" # Factura rectificativa (resto)
R5 = "R5" # Factura rectificativa (simplificada)
class LineaDetalle(BaseModel):
"""Invoice line item."""
descripcion: str = Field(..., max_length=500)
cantidad: Decimal = Field(..., gt=0)
precio_unitario: Decimal = Field(..., ge=0)
tipo_impositivo: Decimal = Field(..., ge=0, le=100) # IVA rate
base_imponible: Decimal # = cantidad * precio_unitario
cuota_repercutida: Decimal # = base_imponible * tipo_impositivo / 100
class FacturaOrdinaria(BaseModel):
"""Full VeriFactu-compliant invoice."""
# Identification
numero_factura: str = Field(..., pattern=r"^[A-Z0-9/-]+$")
serie: str | None = None
fecha_expedicion: date
tipo_factura: TipoFactura = TipoFactura.F1
# Issuer (Emisor)
nif_emisor: str = Field(..., description="NIF of the invoice issuer")
nombre_emisor: str = Field(..., max_length=120)
# Recipient (Destinatario)
nif_destinatario: str = Field(..., description="NIF of the recipient")
nombre_destinatario: str = Field(..., max_length=120)
# Line items
lineas: list[LineaDetalle] = Field(..., min_length=1)
# Totals
base_imponible_total: Decimal
cuota_total: Decimal # Total IVA
importe_total: Decimal # base + cuota
# VeriFactu fields
huella: str | None = None # Hash of this invoice
huella_anterior: str | None = None # Hash of previous invoice (chain)
# Software identification
id_sistema: str = Field(default="FSN-VERIFACTU-001")
nombre_sistema: str = Field(default="VeriFactu by FSN")
version_sistema: str = Field(default="1.0.0")
Factura Simplificada (Simplified Invoice)
For transactions under €400 (retail, hospitality). Simplified data requirements.
class FacturaSimplificada(BaseModel):
"""Simplified invoice (ticket) for amounts < 400€."""
numero_factura: str
fecha_expedicion: date
tipo_factura: TipoFactura = TipoFactura.F2
# Issuer only (no recipient required for simplified)
nif_emisor: str
nombre_emisor: str
# Simplified totals
base_imponible: Decimal
tipo_impositivo: Decimal
cuota: Decimal
importe_total: Decimal = Field(..., le=Decimal("400.00"))
Factura Rectificativa (Correction Invoice)
References and corrects a previous invoice.
class FacturaRectificativa(BaseModel):
"""Correction invoice referencing the original."""
numero_factura: str
fecha_expedicion: date
tipo_factura: TipoFactura # R1-R5
# Reference to original invoice
numero_factura_original: str
serie_original: str | None = None
fecha_original: date
motivo_rectificacion: str = Field(..., max_length=500)
# Correction amount (can be negative)
base_imponible: Decimal
cuota: Decimal
importe_total: Decimal
Invoice Numbering Rules
- Sequential: No gaps allowed within a series
- Unique: Each number + series combination must be unique per fiscal year
- Format: Alphanumeric, typically
YYYY-NNNNNNor with series prefix - Series (optional): Group invoices by type or branch
from datetime import date
def generate_invoice_number(
year: int,
sequence: int,
series: str = "",
) -> str:
"""Generate a sequential invoice number.
Examples:
generate_invoice_number(2026, 1) → "2026-000001"
generate_invoice_number(2026, 42, "A") → "A/2026-000042"
"""
number = f"{year}-{sequence:06d}"
if series:
return f"{series}/{number}"
return number
async def get_next_invoice_number(
session: AsyncSession,
tenant_id: str,
year: int,
series: str = "",
) -> str:
"""Get the next sequential invoice number for a tenant.
IMPORTANT: Must be called within a transaction with row-level locking
to prevent gaps.
"""
result = await session.execute(
text("""
SELECT COALESCE(MAX(sequence_number), 0) + 1
FROM invoices
WHERE tenant_id = :tenant_id
AND fiscal_year = :year
AND series = :series
FOR UPDATE
"""),
{"tenant_id": tenant_id, "year": year, "series": series},
)
next_seq = result.scalar_one()
return generate_invoice_number(year, next_seq, series)
Hash Chaining (Encadenamiento)
Each invoice's hash includes the hash of the previous invoice, creating an immutable chain.
import hashlib
from datetime import date
from decimal import Decimal
def compute_invoice_hash(
nif_emisor: str,
numero_factura: str,
fecha_expedicion: date,
importe_total: Decimal,
huella_anterior: str = "",
) -> str:
"""Compute the SHA-256 hash for a VeriFactu invoice.
The hash chain links each invoice to its predecessor,
preventing retroactive modification.
Based on mdiago/VeriFactu reference implementation.
"""
# Concatenate fields in the specified order
data = (
f"{nif_emisor}"
f"{numero_factura}"
f"{fecha_expedicion.strftime('%d-%m-%Y')}"
f"{importe_total:.2f}"
f"{huella_anterior}"
)
# SHA-256 hash
return hashlib.sha256(data.encode("utf-8")).hexdigest()
# Usage:
# First invoice in chain
hash_1 = compute_invoice_hash(
nif_emisor="B12345678",
numero_factura="2026-000001",
fecha_expedicion=date(2026, 1, 15),
importe_total=Decimal("1210.00"),
huella_anterior="", # No previous for first invoice
)
# Second invoice references the first
hash_2 = compute_invoice_hash(
nif_emisor="B12345678",
numero_factura="2026-000002",
fecha_expedicion=date(2026, 1, 20),
importe_total=Decimal("484.00"),
huella_anterior=hash_1,
)
Software System Identification (SistemaInformatico)
class SistemaInformatico(BaseModel):
"""Software system identification required by VeriFactu."""
nombre: str = "VeriFactu by FSN"
id_sistema: str = "FSN-VERIFACTU-001"
version: str = "1.0.0"
nif_desarrollador: str # NIF of the software developer
nombre_desarrollador: str # Name of the developer/company
AEAT Integration
Endpoints
| Environment | URL |
|---|---|
| Staging | https://prewww1.aeat.es/wlpl/TIKE-CONT/ws/SistemaFacturacion/RegistroFacturacion |
| Production | https://www1.agenciatributaria.gob.es/wlpl/TIKE-CONT/ws/SistemaFacturacion/RegistroFacturacion |
RegistroFactura XML Structure
<?xml version="1.0" encoding="UTF-8"?>
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:sf="https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/SistemaFacturacion.xsd">
<soapenv:Header/>
<soapenv:Body>
<sf:RegistroFacturacion>
<sf:Cabecera>
<sf:ObligadoEmision>
<sf:NombreRazon>Empresa SL</sf:NombreRazon>
<sf:NIF>B12345678</sf:NIF>
</sf:ObligadoEmision>
</sf:Cabecera>
<sf:RegistroFactura>
<sf:RegistroAlta>
<sf:IDFactura>
<sf:IDEmisorFactura>
<sf:NIF>B12345678</sf:NIF>
</sf:IDEmisorFactura>
<sf:NumSerieFactura>2026-000001</sf:NumSerieFactura>
<sf:FechaExpedicionFactura>15-01-2026</sf:FechaExpedicionFactura>
</sf:IDFactura>
<sf:TipoFactura>F1</sf:TipoFactura>
<sf:Destinatarios>
<sf:IDDestinatario>
<sf:NombreRazon>Cliente SA</sf:NombreRazon>
<sf:NIF>A87654321</sf:NIF>
</sf:IDDestinatario>
</sf:Destinatarios>
<sf:Desglose>
<sf:DetalleDesglose>
<sf:ClaveRegimen>01</sf:ClaveRegimen>
<sf:TipoImpositivo>21.00</sf:TipoImpositivo>
<sf:BaseImponible>1000.00</sf:BaseImponible>
<sf:CuotaRepercutida>210.00</sf:CuotaRepercutida>
</sf:DetalleDesglose>
</sf:Desglose>
<sf:ImporteTotal>1210.00</sf:ImporteTotal>
<sf:Encadenamiento>
<sf:PrimerRegistro>S</sf:PrimerRegistro>
</sf:Encadenamiento>
<sf:SistemaInformatico>
<sf:NombreRazon>FSN Development</sf:NombreRazon>
<sf:NIF>12345678Z</sf:NIF>
<sf:NombreSistemaInformatico>VeriFactu by FSN</sf:NombreSistemaInformatico>
<sf:IdSistemaInformatico>FSN-VERIFACTU-001</sf:IdSistemaInformatico>
<sf:Version>1.0.0</sf:Version>
</sf:SistemaInformatico>
<sf:Huella>a1b2c3d4e5f6...</sf:Huella>
</sf:RegistroAlta>
</sf:RegistroFactura>
</sf:RegistroFacturacion>
</soapenv:Body>
</soapenv:Envelope>
Python Submission Client
import httpx
from lxml import etree
class AEATClient:
"""Client for submitting invoices to the AEAT VeriFactu system."""
STAGING_URL = "https://prewww1.aeat.es/wlpl/TIKE-CONT/ws/SistemaFacturacion/RegistroFacturacion"
PRODUCTION_URL = "https://www1.agenciatributaria.gob.es/wlpl/TIKE-CONT/ws/SistemaFacturacion/RegistroFacturacion"
def __init__(
self,
certificate_path: str,
certificate_password: str,
environment: str = "staging",
) -> None:
self.url = self.STAGING_URL if environment == "staging" else self.PRODUCTION_URL
self.cert = (certificate_path, certificate_password)
async def submit_invoice(self, invoice: FacturaOrdinaria) -> AEATResponse:
"""Submit an invoice to AEAT."""
xml = self._build_xml(invoice)
async with httpx.AsyncClient(cert=self.cert) as client:
response = await client.post(
self.url,
content=xml,
headers={"Content-Type": "text/xml; charset=utf-8"},
)
response.raise_for_status()
return self._parse_response(response.content)
def _build_xml(self, invoice: FacturaOrdinaria) -> bytes:
"""Build the SOAP XML envelope for the invoice."""
# XML construction logic
...
def _parse_response(self, content: bytes) -> AEATResponse:
"""Parse the AEAT response."""
...
class AEATResponse(BaseModel):
"""Response from AEAT after invoice submission."""
estado: str # "Correcto" or "Incorrecto"
csv: str | None = None # Código Seguro de Verificación
errores: list[str] = []
Certificate Requirements
- AEAT requires a digital certificate for signing submissions
- Certificates can be obtained from FNMT (Fábrica Nacional de Moneda y Timbre)
- Use
.p12or.pemformat for the certificate - Store certificate securely (Kubernetes Secret or environment variable)
Tax Rules
IVA (VAT) Rates
| Rate | Percentage | Applies To |
|---|---|---|
| General | 21% | Most goods and services |
| Reduced | 10% | Food, water, transport, hospitality |
| Super-reduced | 4% | Bread, milk, medicine, books, first home |
| Exempt | 0% | Healthcare, education, financial services |
from decimal import Decimal
from enum import Enum
class TipoIVA(Enum):
"""Spanish IVA (VAT) rates."""
GENERAL = Decimal("21.00")
REDUCIDO = Decimal("10.00")
SUPERREDUCIDO = Decimal("4.00")
EXENTO = Decimal("0.00")
def calculate_iva(
base_imponible: Decimal,
tipo: TipoIVA = TipoIVA.GENERAL,
) -> tuple[Decimal, Decimal]:
"""Calculate IVA amount and total.
Returns:
Tuple of (cuota_iva, importe_total).
"""
cuota = (base_imponible * tipo.value / Decimal("100")).quantize(Decimal("0.01"))
total = base_imponible + cuota
return cuota, total
IRPF Retention (Freelancers)
class TipoRetencion(Enum):
"""IRPF retention rates for freelancers."""
ESTANDAR = Decimal("15.00") # Standard rate
NUEVO_AUTONOMO = Decimal("7.00") # First 3 years
def calculate_irpf_retention(
base_imponible: Decimal,
tipo: TipoRetencion = TipoRetencion.ESTANDAR,
) -> Decimal:
"""Calculate IRPF retention for freelancer invoices."""
return (base_imponible * tipo.value / Decimal("100")).quantize(Decimal("0.01"))
Intra-Community Operations
# Intra-community (EU B2B) invoices have IVA at 0%
# but require the recipient's EU VAT number
class OperacionIntracomunitaria(BaseModel):
"""Intra-community operation (EU VAT exempt)."""
nif_destinatario_ue: str = Field(
...,
pattern=r"^[A-Z]{2}\d+$",
description="EU VAT number (e.g., DE123456789)",
)
tipo_impositivo: Decimal = Decimal("0.00")
clave_regimen: str = "02" # Intra-community
NIF Validation
DNI Validation
def validate_dni(dni: str) -> bool:
"""Validate Spanish DNI (8 digits + letter).
The check letter is calculated from the number modulo 23.
"""
LETTERS = "TRWAGMYFPDXBNJZSQVHLCKE"
if len(dni) != 9:
return False
number_part = dni[:8]
letter = dni[8].upper()
if not number_part.isdigit():
return False
expected_letter = LETTERS[int(number_part) % 23]
return letter == expected_letter
NIE Validation
def validate_nie(nie: str) -> bool:
"""Validate Spanish NIE (X/Y/Z + 7 digits + letter).
NIE replaces the first letter with a number for check digit calculation:
X → 0, Y → 1, Z → 2
"""
LETTERS = "TRWAGMYFPDXBNJZSQVHLCKE"
NIE_PREFIX = {"X": "0", "Y": "1", "Z": "2"}
if len(nie) != 9:
return False
prefix = nie[0].upper()
if prefix not in NIE_PREFIX:
return False
number_str = NIE_PREFIX[prefix] + nie[1:8]
letter = nie[8].upper()
if not number_str.isdigit():
return False
expected_letter = LETTERS[int(number_str) % 23]
return letter == expected_letter
CIF Validation
def validate_cif(cif: str) -> bool:
"""Validate Spanish CIF (company tax ID).
Format: Letter + 7 digits + control (digit or letter).
"""
if len(cif) != 9:
return False
letter = cif[0].upper()
digits = cif[1:8]
control = cif[8]
if not digits.isdigit():
return False
# Calculate control digit
even_sum = sum(int(d) for d in digits[1::2])
odd_sum = 0
for d in digits[::2]:
doubled = int(d) * 2
odd_sum += doubled // 10 + doubled % 10
total = even_sum + odd_sum
control_digit = (10 - total % 10) % 10
# Control can be letter or digit depending on first letter
CONTROL_LETTERS = "JABCDEFGHI"
letter_types = "KPQS" # Always letter control
digit_types = "ABEH" # Always digit control
if letter in letter_types:
return control.upper() == CONTROL_LETTERS[control_digit]
elif letter in digit_types:
return control == str(control_digit)
else:
# Either is valid
return control == str(control_digit) or control.upper() == CONTROL_LETTERS[control_digit]
Unified NIF Validator
def validate_nif(nif: str) -> bool:
"""Validate any Spanish tax identifier (DNI, NIE, or CIF).
Args:
nif: The tax identifier to validate.
Returns:
True if the NIF is valid.
"""
if not nif or len(nif) != 9:
return False
first_char = nif[0].upper()
if first_char.isdigit():
return validate_dni(nif)
elif first_char in "XYZ":
return validate_nie(nif)
else:
return validate_cif(nif)
Data Retention
Legal Requirements
- Invoices: Must be retained for 4 years (tax inspection period)
- Accounting records: 6 years (commercial code)
- Electronic records: Same as paper, including digital signatures and hashes
- Hash chain integrity: Must be verifiable for the entire retention period
GDPR Considerations
- Invoice data contains personal data (name, NIF, address)
- Retention is justified under legal obligation (Art. 6.1.c GDPR)
- After retention period, data should be anonymized or deleted
- Subject access requests must be fulfilled within 1 month
- Data minimization: only store what's legally required
Reference Implementation
mdiago/VeriFactu (C# / .NET)
GitHub: https://github.com/mdiago/VeriFactu
Use this repository as a protocol and specification reference for:
RegistroFacturaXML structure and field mappingsEncadenamiento(chain linking) implementation detailsSistemaInformaticoidentification requirements- AEAT endpoint URLs and SOAP envelope format
Important: Reference for protocol understanding only. Do not copy code — implement in Python following the monorepo's patterns.
AEAT Official Resources
- Official documentation: https://www.agenciatributaria.es/AEAT.sede/tramitacion/ZZ01.shtml
- VeriFactu technical specs: https://sede.agenciatributaria.gob.es/
- XSD schemas for validation
Guidelines
- Sequential invoice numbers with no gaps — use database locks
- Hash chain every invoice to the previous one (SHA-256)
- Validate NIF/CIF/NIE before accepting invoices
- Use staging environment for AEAT integration testing
- Store digital certificates securely (K8s Secrets)
- Retain invoices 4+ years as required by law
- Reference mdiago/VeriFactu for protocol details, not code
- Use Pydantic models for all invoice data structures
- Apply Decimal type for all monetary amounts (never float)
- Test with real AEAT staging before going to production