Skip to content

Verifactu — Installation & Operation Manual

Optional module — Spain only. Compliance with the Spanish RRSIF / Veri*Factu regime (RD 1007/2023, Orden HAC/1177/2024). Mandatory for issuing invoices in Spain from 2027. Voluntary phase open since 2026 with no penalties for technical errors.

PopulationMandatory date
Sociedades (IS)2027-01-01
Autónomos / IRPF / others2027-07-01
SIF software vendors2025-07-29 (passed — voluntary mode is live)

Legal sources: RD 1007/2023 (BOE-A-2023-24840), Orden HAC/1177/2024, RDL 15/2025, RD 254/2025.


Contents

  1. What this module does
  2. Architecture & module boundaries
  3. Installation
  4. Setup walkthrough (5 steps)
  5. Daily operation
  6. VAT classification mapping
  7. Test vs production environments
  8. AEAT validation errors — catalogue
  9. Troubleshooting
  10. Data model
  11. API endpoints
  12. Permissions
  13. Pre-production checklist
  14. References

1. What this module does

When a clinic in Spain has the module enabled and issues an invoice, Verifactu silently:

  1. Builds a RegistroAlta (or RegistroAnulacion) XML payload with AEAT-mandated fields, namespaces and ordering.
  2. Computes the SHA-256 chained Huella (links each record to the previous one in the clinic's fiscal chain).
  3. Stores the record in verifactu_records (state=pending).
  4. Returns preliminary compliance_data['ES'] to the invoice (huella, QR URL, tipo_factura, environment).
  5. A periodic worker (every 60 s) drains pending records, signs the SOAP envelope with the clinic's FNMT certificate via mTLS, sends them to AEAT in batches up to 1 000, parses RespuestaRegFactuSistemaFacturacion, updates state + CSV + timestamp, and mirrors the final state back into the invoice.

Each invoice's PDF embeds a QR code with a URL to AEAT's ValidarQR endpoint — anyone holding the printed invoice can verify it in real time on AEAT.


2. Architecture & module boundaries

DentalPin is international software; AEAT/RRSIF is Spain-only. The verifactu module is therefore optional, country-specific, and isolated.

Module dependencies (declared in manifest.depends):

  • billing — to register the country compliance hook.
  • catalog — to read/map vat_types for AEAT classification.

What verifactu does NOT do:

  • Modify vat_types (catalog stays country-agnostic — only rate and names, no AEAT taxonomy).
  • Modify invoices/invoice_items (billing stays fiscal-neutral).
  • Add columns to core/billing/catalog tables. All Spain-specific data lives in tables prefixed verifactu_*.
  • Lock billing flow. If verifactu is disabled, billing operates as if the module wasn't installed.

How it plugs in:

  • Hook is registered via BillingHookRegistry.register(VerifactuHook()) on every backend boot (in VerifactuModule.__init__). Routing is per-invoice based on clinic.settings.country == "ES".
  • The hook reads from its own tables (verifactu_settings, verifactu_certificates, verifactu_records, verifactu_vat_classifications) plus reads (no FK changes) from clinics, vat_types and invoice_items.

This pattern is reusable: a future factur-x-fr (France) or sdi-it (Italy) module ships its own tables, its own classification mapping, and its own UI under /settings/<module>. Each country module owns its complexity.


3. Installation

3.1 Build the backend image

Verifactu adds httpx, lxml, qrcode[pil] and cryptography to the backend dependencies. They are already declared in pyproject.toml, so a fresh build picks them up:

bash
docker-compose up -d --build backend

3.2 Run migrations

bash
docker-compose exec backend alembic upgrade heads

This applies four migrations on the verifactu Alembic branch:

  • vfy_0001_initial — settings, certificates, records.
  • vfy_0002_producer_info — SIF producer fields + signature.
  • vfy_0003_drop_emisor_columns — issuer NIF moved to clinics.
  • vfy_0004_vat_classifications — per-clinic AEAT classification overrides.

3.3 Reconcile the module registry

The host backend reconciles all on-disk modules at startup. Restarting is enough:

bash
docker-compose restart backend

The module appears in Admin → Modules as uninstalled (because auto_install=False).

3.4 Install from the admin UI

  1. Sign in as admin.
  2. Go to Admin → Modules.
  3. Find verifactu in the list and click Install.
  4. The installer:
    • Confirms migrations are at head (no-op if already applied).
    • Runs the lifecycle hook — registers VerifactuHook for ES, starts the APScheduler worker, and seeds default VAT classifications (E1 for every existing zero-rate VAT type).
    • Promotes the record to state=installed.
  5. The verifactu card appears in Configuration with quick links to the configuration sub-pages.

In dev, Nuxt watches frontend/modules.json and restarts itself when the backend rewrites it on install/uninstall — no manual step required. If you do need to force it (e.g. when running a production build):

bash
docker-compose restart frontend

3.5 Optional environment variables

bash
# Defaults shown by the producer wizard. Per-clinic wizard values
# always win; env vars are fallbacks for fresh installs.
VERIFACTU_VENDOR_NIF=                # Producer NIF (real CIF)
VERIFACTU_VENDOR_NAME=               # Producer legal name
VERIFACTU_SOFTWARE_NAME=DentalPin    # Optional, defaults to "DentalPin"
VERIFACTU_SOFTWARE_ID=DP             # 2-char system identifier
VERIFACTU_SOFTWARE_VERSION=0.1.0     # Pin to your deploy's version tag

4. Setup walkthrough (5 steps)

Once installed, every clinic that wants to emit invoices through Verifactu must complete these steps in order. Status of each prereq is shown as a checklist in the verifactu home (/settings/verifactu).

Step 1 — Clinic identity

Verifactu reads the issuer NIF from the clinic itself, not from verifactu settings (single source of truth, shared with all modules).

  1. Go to ConfigurationClinic informationEdit.
  2. Fill CIF/NIF with the clinic's real fiscal NIF.
  3. (Recommended) Fill Razón social if the legal name differs from the commercial name. AEAT's NombreRazonEmisor falls back to the clinic's name when legal_name is empty.
  4. Set Country to España (or any country code; the verifactu hook auto-sets country=ES when verifactu is activated, so this is optional).
  5. Save.

Step 2 — Digital certificate (FNMT)

Verifactu signs every SOAP submission with the clinic's certificate via mTLS. Real production needs a real cert; preproduction accepts AEAT-issued test certs.

  1. Obtain a .pfx / .p12 certificate. Valid types:
    • Certificado de Representante de Persona Jurídica (companies).
    • Certificado de Persona Física (autónomos whose NIF matches the clinic's tax_id).
    • Sello de Empresa (alternative for entities). Request from the FNMT portal.
  2. Go to /settings/verifactu/certificate.
  3. Drag & drop the .pfx file (or click the dropzone). Format and size are validated client-side.
  4. Enter the certificate password.
  5. Click Subir certificado.

The file and password are encrypted with the server key (Fernet) and stored in verifactu_certificates. The cert subject CN is parsed from the bundle for display. Replacing the cert deactivates the previous one but keeps it in history (audit trail).

A status panel at the top shows expiry: green if >60 days remain, amber 15-60 days, red <15 days or expired.

Step 3 — SIF producer

The "productor del SIF" is the legal entity that puts the software into production and signs the declaración responsable per RD 1007/2023 art. 13.

Deployment modelProducer is
Managed SaaS at e.g. dentalpin.comThe SaaS operator
Self-hosted by a clinic with own ITThe clinic (autodesarrollo)
Self-hosted via integrator/partnerThe integrator
Local developmentNone — keep verifactu disabled
  1. Go to /settings/verifactu/producer. Three numbered cards.
  2. Step 1 — Datos del productor: fill NIF, razón social, system ID (2 chars, default DP), version. Click Guardar datos sin firmar to persist a draft.
  3. Step 2 — Firmar declaración responsable: read the inline declaration text (the same content as the downloadable PDF). Tick "He leído y acepto la declaración responsable…". Click Firmar electrónicamente. The server seals declaracion_responsable_signed_at and _by with your timestamp and user ID — sufficient evidence for an AEAT audit.
  4. Step 3 — Descargar PDF firmado: opens a printable HTML page with the declaration + electronic-signature stamp. Useful as evidence for AEAT, internal archive, or distribution to client clinics if you operate as SaaS.

After signing, all producer fields are locked. The Step 1 alert shows an "Anular firma para editar" button that opens a confirmation modal: revoking clears the signature, force-disables Verifactu, and unlocks the fields. Records already submitted to AEAT are not affected — only future submissions reflect the new producer.

Step 4 — VAT classification mapping (AEAT)

Each catalog VAT type must map to an AEAT classification (S1, E1, N1, etc.). The module seeds defaults at install time.

  1. Go to /settings/verifactu/vat-mapping.
  2. For each VAT type (e.g. General 21%, Reducido 10%, Exento 0%), pick its AEAT classification:
    • S1 — Sujeto, no exento (régimen general) for VAT >0%.
    • E1 — Exento (art. 20 LIVA — sanitario, financiero…) for dental clinical services with rate=0% (default seeded for any existing 0% type).
    • E2-E6 for export, intracomunitario, etc.
    • N1/N2 — No sujeta (regla localización) for non-VAT lines where 0% reflects no taxability rather than exemption.
    • Auto to fall back to the rate-based heuristic.
  3. Click the per-row Guardar. Each row's effective classification is shown next to the dropdown.

The mapping table (verifactu_vat_classifications) lives inside the verifactu module. The catalog vat_types table stays country-agnostic. Other country compliance modules can ship their own equivalent table without touching catalog.

Step 5 — Activate Verifactu

  1. Go to /settings/verifactu. The status hero will say "Listo para activar" (orange checklist becomes a green/blue ready panel) when steps 1-4 are complete.
  2. Click Activar Verifactu. The hook is now live: every issued invoice produces a Verifactu record and queues for AEAT submission.
  3. The hero turns blue: "Verifactu activo en pruebas". The environment badge says Pruebas (test).

To switch to production, click Cambiar a producción in the same hero. A red modal asks for confirmation: "Vas a pasar a producción real. Las facturas que se emitan a partir de ahora se enviarán a la AEAT como datos fiscales reales."


5. Daily operation

Issuing an invoice

The flow is automatic — billing-side UX is unchanged. From the user's perspective:

  1. Create an invoice (draft). Add items. Save.
  2. Click Emitir factura. Workflow validates billing data, runs the validate_before_issue hook (Verifactu checks: NIF emisor, producer signed, certificate active, F2 cap ≤400€ if no destinatario NIF), assigns invoice number, writes compliance_data['ES'] with huella + QR URL + record_id.
  3. The invoice transitions to issued. The Verifactu panel on the invoice detail page shows the QR + huella + state.
  4. Within 60 s, the worker picks up the record, sends it to AEAT, updates state (Correcto / AceptadoConErrores / Incorrecto), and stores the AEAT CSV.

Monitoring

Two pages:

  • /settings/verifactu/queue — current submissions, three tabs:
    • Pendientesstate in (pending, sending).
    • Rechazadosstate=rejected (AEAT business rejection).
    • Errores temporalesstate=failed_transient (transport issues, AEAT side faults). Each row shows the AEAT error code + message, submission attempt count, and a Reintentar (Subsanación) action that resends with Subsanacion=S (and RechazoPrevio=X when the previous attempt was Incorrecto).
  • /settings/verifactu/records — immutable fiscal ledger with every record (any state). Filters by state and tipo_factura.

Worker — internal flow

APScheduler IntervalTrigger(60 s) → process_verifactu_submissions()

        ▼ for each clinic with enabled=true, in its own session:
process_clinic(db, clinic_id)
        ├─ pg_try_advisory_xact_lock(hash('verifactu:'||uuid))
        │     └─ if not acquired: skip (another worker has the clinic)
        ├─ if now < next_send_after: skip   ← TiempoEsperaEnvio
        ├─ load active certificate
        ├─ load clinic.tax_id + .legal_name|name (issuer)
        ├─ SELECT records WHERE state IN (pending, failed_transient)
        ├─ mark batch state=sending
        ├─ decrypt PFX + password (Fernet)
        ├─ build_ssl_context (tempfile 0600 + immediate unlink)
        ├─ render envelope (Cabecera + N RegistroFactura)
        ├─ httpx.AsyncClient(verify=ssl_ctx).post(endpoint, body)
        ├─ parse RespuestaRegFactuSistemaFacturacion
        ├─ for each record: match by NumSerieFactura → update state
        │     ├─ Correcto / AceptadoConErrores → mirror into
        │     │   invoice.compliance_data['ES']
        │     └─ Incorrecto → state=rejected, surface code+desc
        ├─ if response is a SOAP Fault (no lineas): copy faultstring
        │   into aeat_descripcion_error for every record (codigo=-2)
        └─ settings.next_send_after = now + tiempo_espera_envio

On transport-level failure (timeout, HTTP 4xx/5xx), records are marked failed_transient with aeat_codigo_error=-1 and the message captured. The worker retries on next tick — exponential backoff via submission_attempt.


6. VAT classification mapping

This is the architectural decision that keeps the module country-isolated.

What it solves

Catalog vat_types stores rate and a localized name. That's enough for billing math (compute IVA per line) but not for AEAT reporting — AEAT requires every line to declare a CalificacionOperacion (S1, S2, N1, N2) or OperacionExenta (E1..E6) value.

Naively encoding S1/E1/N1 in vat_types would force every other country compliance module to share Spanish concepts. So verifactu ships its own per-clinic mapping table.

How it works

Table verifactu_vat_classifications (per clinic):

ColumnPurpose
clinic_idOwner
vat_type_idFK to vat_types (CASCADE)
classificationS1, S2, E1-E6, N1, N2
exemption_causeE1-E6 redundant copy when classification is exempt
notesFree text (e.g. "art. 20.uno.3.º LIVA")

When the hook builds a Desglose, it loads {vat_type_id: override} for the clinic. For each invoice item:

  • If override exists → use literal classification.
  • Else → fall back to iva_classifier.classify(rate, is_exento_sanitario) heuristic.

Default seed

At install time, verifactu inserts an E1 override for every existing vat_type with rate=0, with the note "Servicios sanitarios — art. 20.uno.3.º LIVA (semilla por defecto).". This is the legally-correct classification for dental clinical services in Spain.

The admin can override any row from /settings/verifactu/vat-mapping, including reverting to Auto (heuristic) or picking any of the 10 AEAT classifications.

Heuristic (fallback when no override)

RateClassificationCausa
21 / 10 / 4S1
0 (with vat_exempt_reason on the line)OperacionExenta=E1E1
0 (no exempt reason)N1
OtherValueError

7. Test vs production environments

The only differences between test and prod are two URLs:

SOAP submission endpoint (services/aeat_client.py):

python
"test": "https://prewww1.aeat.es/wlpl/TIKE-CONT/ws/SistemaFacturacion/VerifactuSOAP"
"prod": "https://www1.agenciatributaria.gob.es/wlpl/TIKE-CONT/ws/SistemaFacturacion/VerifactuSOAP"

QR code host (services/qr.py):

python
_TEST_HOST = "prewww1.aeat.es"
_PROD_HOST = "www1.agenciatributaria.gob.es"

Everything else is identical: XML, hash chain, mTLS handshake, worker, persistence, retry behaviour. There is no mock or dry-run.

Implications:

  • Test mode hits AEAT's real preproduction sandbox (separate database, separate certificates allowed, no fiscal effect).
  • Production mode hits the real AEAT. Every record becomes part of the clinic's official fiscal ledger.
  • Pre-prod and prod are separate AEAT systems — what's in test is not in prod and vice-versa.
  • AEAT pre-prod tends to be unstable. Random Codigo[103] errors and 503 responses are AEAT-side, not client bugs (see §8).
  • Switching prod → test mid-stream doesn't break our local hash chain (the chain is global per NIF), but AEAT will see gaps in whichever environment was skipped.

The same uploaded certificate works in both environments — TLS doesn't differ. AEAT pre-prod accepts: real FNMT certs, AEAT-issued test certs (e.g. 00000000T Nombre ApellidoUnoÑ (R: Q0000000J)). Prod requires real FNMT certs only.


8. AEAT validation errors — catalogue

Real errors observed during preproduction integration, with root cause and fix. Use this as a debugging checklist when records appear in Errores temporales or Rechazados.

Codigo[103] — "Error interno del Servidor, Id. del Error: null"

HTTP 403 + SOAP Fault on prewww1.aeat.es.

Documented as AEAT-side DB2 write failure. Their internal validator died before assigning an incident ID. Same family as Codigo[-904] (resource unavailable).

CauseAEAT internal infrastructure
FixWait for AEAT to restore. The worker auto-retries every 60 s.

Codigo[4102] — "El XML no cumple el esquema. Falta informar campo obligatorio.: Cabecera"

XML schema violation. The validator looked for Cabecera in the namespace it expected and didn't find it.

CauseSOAP envelope namespaces inverted: sum: was bound to SuministroInformacion, but AEAT expects it bound to SuministroLR (Cabecera, RegFactuSistemaFacturacion, RegistroFactura live in LR; ObligadoEmision and inner data elements live in Information).
Fixtemplates/soap_envelope.xml.j2: xmlns:sum="…SuministroLR.xsd" and default xmlns="…SuministroInformacion.xsd". Inner elements (ObligadoEmision, RegistroAlta, IDFactura, Desglose, …) carry no prefix and inherit the default namespace.

Codigo[1237] — "CalificacionOperacion N1/N2 con IVA. No se puede informar TipoImpositivo, CuotaRepercutida…"

Business validation. The line declares "no sujeta" but carries IVA quota fields.

Causeiva_classifier.classify returned tipo_impositivo=Decimal("0") for vat_rate=0 (N1). The XML emitted <TipoImpositivo>0.00</TipoImpositivo> and <CuotaRepercutida>0.00</CuotaRepercutida>, both forbidden under N1/N2.
FixSet tipo_impositivo=None for N1. In hook._build_desglose, also force cuota_repercutida=None when calificacion_operacion in ("N1","N2") or operacion_exenta=True.

Codigo[4109] — "Error en el bloque de SistemaInformatico. El formato del NIF es incorrecto."

The producer NIF doesn't satisfy AEAT's CIF/DNI check-digit algorithm.

CauseQ0000000J (the AEAT test issuer NIF) was used as producer_nif. Q0000000J is accepted as IDEmisorFactura (special test census entry) but AEAT validates the SIF producer NIF strictly. The standard check digit for Q0000000 is A, not J.
FixUse a real CIF (your real software vendor entity) or a structurally-valid test CIF. Update via /settings/verifactu/producer wizard.

Codigo[1100] — "Valor o tipo incorrecto del campo: IdSistemaInformatico"

The 2-character system identifier is malformed.

CauseIdSistemaInformatico must be exactly 2 alphanumeric characters. Empty, longer values or non-alphanumeric chars trigger this.
FixSet in producer wizard. DP is a safe default.

Codigo[1103] — "El valor del campo ID es incorrecto"

Generic ID validation. Triggered by malformed IDFactura blocks (e.g. wrong issuer NIF format, invalid date FechaExpedicionFactura, empty NumSerieFactura).

| Fix | Inspect the record's xml_payload and confirm: NIF uppercase 9 chars, fecha DD-MM-YYYY, numerador no vacío. |

General principle

If the error code is numeric ≥ 1000 with a meaningful description, it's an AEAT business validation — fix on the client side. If the code is 103 / -904 / null / 5xx → AEAT-side, just wait.

The full official error catalogue is at Validaciones_Errores_Veri-Factu.pdf.


9. Troubleshooting

"Verifactu emitido pero no aparece en cola"

The hook only runs on the draft → issued transition. If the invoice was issued before Verifactu was activated (or while producer signature was revoked), no record exists.

sql
-- Diagnostic
SELECT enabled FROM verifactu_settings;
SELECT compliance_data FROM invoices WHERE invoice_number = 'FAC-XXXX';
SELECT * FROM verifactu_records WHERE serie_numero = 'FAC-XXXX';

The invoice cannot retroactively enter Verifactu after issuance. Workaround: emit a credit note (R1) referencing the original, then emit a new invoice with the same items — both will queue normally.

"El productor wizard no me deja editar tras firmar"

By design — the signature attests to specific data. Click "Anular firma para editar" in the Step 1 alert. This:

  • Clears declaracion_responsable_signed_at / _by.
  • Forces enabled=false.
  • Unlocks fields.

After re-signing, you must reactivate Verifactu manually from the verifactu home.

"Tras anular firma, las facturas siguientes no entran a Verifactu"

enabled is forced to false on revoke. Re-sign + go to /settings/verifactu + click Activar Verifactu.

Stale frontend cache. Hard-refresh (Cmd+Shift+R). Modals use Nuxt UI v4 syntax: <UModal v-model:open="…"> plus <template #content> slot.

"AEAT 4109 con cert real pero clinic NIF de pruebas"

Mismatch: cert holder ≠ ObligadoEmision. AEAT verifies the cert NIF matches IDEmisorFactura (or is an authorized representative). Either:

  • Update clinic.tax_id to your real NIF (matches cert).
  • Or use AEAT's test cert (00000000T … (R: Q0000000J)) with clinic.tax_id = Q0000000J.

Mixing real cert + test issuer NIF (or vice versa) → 4109.

Reset everything for a clean test

sql
-- Wipe failed records + reset chain head + clear ES compliance.
DELETE FROM verifactu_records WHERE state IN ('failed_transient','rejected','pending');
UPDATE verifactu_settings SET last_huella=NULL, last_record_id=NULL, next_send_after=NULL;
UPDATE invoices SET compliance_data = compliance_data - 'ES' WHERE compliance_data ? 'ES';

DO NOT delete records in state='accepted' — they're legally retained for 4 years.


10. Data model

verifactu_settings — one row per clinic

ColumnTypePurpose
clinic_idUUID, uniqueOwner
enabledboolHook only acts if true
environmenttest / prodAEAT endpoint
numero_instalacionstr(60)UUID per clinic for <NumeroInstalacion>
last_huellastr(64)Chain head SHA-256
last_record_idUUIDFK to last accepted record
next_send_aftertimestamptzAEAT-imposed back-pressure
last_aeat_response_attimestamptzDiagnostics
producer_nifstr(20)NIF of the SIF producer
producer_namestr(200)Producer legal name
producer_id_sistemastr(2)2-char system ID, default DP
producer_versionstr(20)Software version string
declaracion_responsable_signed_attimestamptzSealed when wizard signed
declaracion_responsable_signed_byUUID FK usersAuditor of the signature

The clinic's NIF and legal name are NOT duplicated here — they live in clinics.tax_id and clinics.legal_name (single source of truth).

verifactu_certificates — encrypted PFX storage

ColumnPurpose
pfx_encryptedFernet-encrypted PFX bytes
password_encryptedFernet-encrypted password
subject_cn, issuer_cn, nif_titularDisplay metadata
valid_from, valid_untilExpiry banner thresholds
is_activeOne active per clinic (partial unique idx)
uploaded_byAudit

verifactu_records — append-only fiscal ledger

Legal retention: 4 years (LGT plazo de prescripción).

ColumnPurpose
clinic_id, invoice_idMulti-tenant filtering
record_typealta / anulacion
tipo_facturaF1, F2, F3, R1, R2, R3, R4, R5
huella, huella_anterior, is_first_recordChain links
xml_payloadVerbatim XML — legal req
statepending, sending, accepted, accepted_with_errors, rejected, failed_transient, failed_validation
aeat_csvAEAT receipt code
aeat_estado_envio / aeat_estado_registroResponse status
aeat_codigo_error / aeat_descripcion_errorReal AEAT codes (positive ints) or sentinels: -1=transport, -2=SOAP Fault
aeat_response_xmlRaw response for forensics

Indexes: (clinic_id, created_at desc), (clinic_id, state), (invoice_id), unique (clinic_id, huella).

verifactu_vat_classifications — AEAT mapping per VAT type

ColumnPurpose
clinic_id, vat_type_idComposite key (unique)
classificationS1, S2, E1-E6, N1, N2
exemption_causeE1-E6 (redundant of classification when exempt)
notesFree text legal reference

11. API endpoints

Base prefix: /api/v1/verifactu/. Every endpoint requires ClinicContext (auth + clinic membership).

MethodPathPermissionNotes
GET/settingsverifactu.settings.readRead or lazily create. NIF emisor / razón social derived from clinics.
PUT/settingsverifactu.settings.configureToggle enabled / environment. Auto-sets clinic.settings.country='ES' when enabled. Rejects enable if any prereq missing.
GET/producer/defaultsverifactu.settings.readEnv-var defaults for the wizard
PUT/producerverifactu.settings.configureWizard: write producer info + sign
DELETE/producer/declaracionverifactu.settings.configureRevoke signature + force-disable Verifactu
POST/certificateverifactu.settings.configureMultipart upload (file=*.pfx, password=...)
GET/certificateverifactu.settings.readActive cert metadata
GET/certificate/historyverifactu.settings.readAll uploaded certs
DELETE/certificate/{id}verifactu.settings.configureSoft-deactivate
GET/recordsverifactu.records.readPaginated ledger
GET/records/{id}verifactu.records.readDetail incl. XML
GET/records/{id}/xmlverifactu.records.readPlain text XML payload
GET/queueverifactu.queue.manageFilter by state
POST/queue/{id}/retryverifactu.queue.manageMark for resubmit (Subsanacion=S)
POST/queue/process-nowverifactu.queue.manageTrigger worker for this clinic immediately
GET/vat-mappingverifactu.settings.readList VAT types + AEAT override + inferred default
PUT/vat-mapping/{vat_type_id}verifactu.settings.configureUpsert/clear override
GET/healthverifactu.settings.readSummary: enabled, env, has_cert, valid_until, pending+rejected

Immutable: no endpoint allows editing huella, xml_payload, aeat_csv on records. The libro fiscal is append-only by design.


12. Permissions

Module returns these from get_permissions() (registry namespaces to verifactu.*):

settings.read
settings.configure
records.read
queue.manage

Default role grants:

  • admin* (all)
  • dentistrecords.read
  • hygienist, assistant → none
  • receptionistrecords.read

13. Pre-production checklist

Before flipping any clinic's environment to prod:

  • [ ] Read the AEAT spec PDFs:
    • Veri-Factu_especificaciones_huella_hash_registros.pdf
    • DetalleEspecificacTecnCodigoQRfactura.pdf
    • Veri-Factu_Descripcion_SWeb.pdf
  • [ ] Obtain a real FNMT certificate (representante PJ or sello de empresa) for the clinic.
  • [ ] Decide producer model and sign the declaración responsable in the wizard (/settings/verifactu/producer).
  • [ ] Publish the declaración responsable on your public website AND deliver a copy to each clinic that uses the software.
  • [ ] Engage gestoría / legal review specialised in SII / Verifactu (~500-1500 €). LGT 201 bis exposure makes this worthwhile.
  • [ ] Define internal AEAT inspection response process: who receives the requerimiento, response window (typically 10 business days), evidence package contents.

Technical validation against prewww1.aeat.es

  • [ ] Real FNMT cert + minimum 20 invoices covering: F1 with NIF / IVA general (21%) / IVA reducido (10%) / sanitario exento (E1) / F2 simplificada <400 € / R1 rectificativa / anulación / multi-line con IVAs mixtos / subsanación tras rechazo simulado.
  • [ ] Verify each accepted record's CSV in the AEAT preproduction portal manually.
  • [ ] Test certificate expiry: load a cert expiring in <60 days and confirm the UI shows the amber/red banner.
  • [ ] Test mTLS with bad password — must surface a clear error.
  • [ ] Concurrency test: emit 100 invoices in parallel, verify the chain is intact (no missing or duplicate huella).
  • [ ] Round-trip uninstall: with no records, uninstall must succeed; with accepted records, must fail with the legal warning.
  • [ ] Restart resilience: kill worker mid-batch, verify state=sending records get re-picked. (Currently they don't — watchdog reaper is a known TODO.)

Frontend wiring

  • [ ] Embed <InvoiceVerifactuPanel> in the host's frontend/app/pages/invoices/[id].vue so users see QR + CSV + huella on the invoice detail.
  • [ ] Embed the QR in the invoice PDF — hook.enhance_pdf_data returns verifactu_qr_png_b64. Billing's pdf.py must place it in the upper-right corner per AEAT spec (size 30-40 mm, label "QR tributario", text "VERI*FACTU" nearby).
  • [ ] Restrict prod toggle to admin role (consider a separate verifactu.environment.promote permission).

Operational

  • [ ] Set production env vars:
    VERIFACTU_VENDOR_NIF=…
    VERIFACTU_VENDOR_NAME=…
    VERIFACTU_SOFTWARE_VERSION=…  # tied to your deploy tag
  • [ ] Backups: confirm pg_dump runs nightly and includes verifactu_records. Records must survive 4 years.
  • [ ] KMS / secret rotation: SECRET_KEY derives the Fernet key. Rotating it would render every encrypted PFX + password unreadable. Document a key-rotation procedure (re-upload all certs after rotation) before going live.
  • [ ] Monitoring: alert on next_send_after slipping > 1 h (suggests AEAT outage) and on rejected count growing.
  • [ ] Refresh AEAT XSDs: vendor schemas may shift. Plan periodic refresh from https://prewww2.aeat.es/static_files/.../tikeV1.0/cont/ws/.

Module gaps still open

  • [ ] Worker stuck-record reaper — records in state=sending after a worker crash are not rescued. Add a watchdog that demotes them back to pending after 10 minutes.
  • [ ] AEAT error code → user-friendly message map — translate the most common 100 codes from errores.properties into Spanish strings shown in the queue UI.
  • [ ] Certificate expiry email — daily APScheduler job that emails admins when any clinic's cert expires in <30 days.
  • [ ] PDF embedding of QR — see frontend wiring TODO above.
  • [ ] is_exento_sanitario first-class column on invoice_items — currently the hook deduces it from vat_exempt_reason.

14. References

Official AEAT

FNMT certificates

Reference open-source implementations (for cross-checking XML)

Endpoints

EnvironmentURL
Test (preproducción)https://prewww1.aeat.es/wlpl/TIKE-CONT/ws/SistemaFacturacion/VerifactuSOAP
Productionhttps://www1.agenciatributaria.gob.es/wlpl/TIKE-CONT/ws/SistemaFacturacion/VerifactuSOAP
Test QR validationhttps://prewww1.aeat.es/wlpl/TIKE-CONT/ValidarQR
Production QR validationhttps://www1.agenciatributaria.gob.es/wlpl/TIKE-CONT/ValidarQR

Support

For preproduction outages or specific developer questions: verifactu@correo.aeat.es.