Integrar tu software

Tu programa sigue numerando, calculando y maquetando. En el momento de emitir, hace una llamada; con lo que vuelve, imprime el QR y las leyendas. Esta página recorre esa integración de principio a fin.

En qué consiste

Una integración completa hace cuatro cosas, y sólo la primera es imprescindible para empezar:

  1. 1

    Emitir

    Al confirmar una factura, POST /v1/facturas. La respuesta trae el identificador del registro, la huella, la URL del QR y las leyendas. Guarda esos datos junto a la factura.
  2. 2

    Imprimir

    Pinta el QR (30–40 mm) y las dos leyendas en el PDF. Puedes dibujar la URL con tu propia librería o pedir el símbolo ya renderizado a GET /v1/registros/{id}/qr.
  3. 3

    Enterarte del resultado

    La remisión a la AEAT es asíncrona. Suscribe un webhook o consulta GET /v1/facturas/{id} para saber si la AEAT aceptó, aceptó con errores o rechazó.
  4. 4

    Anular cuando toque

    POST /v1/anulaciones emite el registro de anulación encadenado. Corregir es emitir una rectificativa (otra alta, tipo R1–R5).
Lo que la API no hace por ti: no decide el número de la factura (es tu numeración, y va tal cual al registro y al QR), no calcula cuotas (comprueba que cuadren) y no bloquea la edición de facturas en tu programa. Un registro ya emitido es inmutable; asegúrate de que tu software no permita editar ni borrar una factura registrada.

Autenticación e idempotencia

Todas las llamadas a https://verifactu-api.zctechpartners.cloud/v1 llevan Authorization: Bearer <clave>. Usa una clave de perfil integración; el entorno lo decide su prefijo (vf_test_ preproducción, vf_live_ producción), como se explica en Entornos y claves.

Toda petición POST exige además una cabecera Idempotency-Key (mínimo 8 caracteres; un UUID es lo natural). Misma clave y mismo cuerpo durante 24 horas devuelven la misma respuesta, sin crear un segundo registro: un doble clic o un reintento de red nunca producen dos facturas ante la AEAT. Misma clave con un cuerpo distinto es 409. Un error de validación no consume la clave, así que puedes corregir el cuerpo y reintentar con ella.

Cabeceras de toda escritura
Authorization: Bearer vf_test_…
Idempotency-Key: 6f1d0a3e-…        # una por factura; guárdala con ella
Content-Type: application/json

Guarda la Idempotency-Key junto a la factura en tu base de datos antes de llamar. Si el proceso muere a medias, al reintentar con la misma clave recuperas la respuesta original en vez de duplicar.

Emitir una factura

POST https://verifactu-api.zctechpartners.cloud/v1/facturas
{
  "obligado_nif": "B21778451",
  "num_serie_factura": "FAC-2027-0001",
  "serie": "FAC-2027",
  "fecha_expedicion": "2027-01-07",
  "tipo_factura": "F1",
  "descripcion_operacion": "Servicios de consultoría, enero 2027",
  "destinatario": { "nif": "12345678Z", "nombre_razon": "CLIENTE SL" },
  "lineas": [
    { "descripcion": "Consultoría", "base": 1000.00, "tipo_iva": 21, "cuota": 210.00, "calificacion": "S1" }
  ],
  "cuota_total": 210.00,
  "importe_total": 1210.00
}
CampoQué es
obligado_nifNIF del emisor, dado de alta como obligado en el entorno de la clave.
num_serie_facturaEl número completo de la factura, exactamente como figura en el documento (1–60 caracteres, sin caracteres de control). La AEAT no impone formato. Va intacto a la huella y al QR.
serieOpcional. Código de serie para agrupar en consultas. No se concatena al número ni afecta a la cadena.
fecha_expedicionFecha de expedición en formato AAAA-MM-DD.
tipo_facturaF1 completa, F2 simplificada, F3 sustitutiva de simplificadas, R1–R5 rectificativas.
destinatarioObligatorio en F1: nif (nacional) o id_otro (no residente: codigo_pais, id_type, id) y nombre_razon. Su NIF no puede ser el del emisor.
lineasDe 1 a 12 desgloses. Cada uno: base, tipo_iva y cuota cuando aplica, calificacion (S1 sujeta, S2 inversión del sujeto pasivo, N1/N2 no sujeta, E1–E6 exenta), opcionalmente recargo_equivalencia y regimen_especial (por defecto 01, régimen general).
cuota_total / importe_totalSuma de cuotas y total de la factura. importe_total es el que va al QR: base más cuota, sin restar retenciones como el IRPF.
201 Created
{
  "id": "0d3f2c1e-…",
  "tipo": "alta",
  "num_serie_factura": "FAC-2027-0001",
  "huella": "3F0A9C…",
  "estado": "pendiente",
  "qr": {
    "url": "https://prewww2.agenciatributaria.gob.es/wlpl/TIKE-CONT/ValidarQR?nif=B21778451&numserie=FAC-2027-0001&fecha=07-01-2027&importe=1210.00",
    "leyendas": ["VERI*FACTU", "Factura verificable en la sede electrónica de la AEAT"]
  }
}

En ese momento el registro ya existe, está encadenado y tiene huella; la remisión a la AEAT ocurre después, en lotes que el planificador forma cada minuto. Guarda id, huella, qr.url y el importe registrado con la factura: son los datos que imprimirás y con los que cotejarás.

La cadena de un obligado se escribe en serie. Si dos emisiones del mismo obligado coinciden, la segunda espera; si la espera se agota, la API responde 503 chain-locked con Retry-After. Reintenta con la misma Idempotency-Key.

QR y leyendas en el documento

El reglamento fija cómo va el QR: ISO/IEC 18004, nivel de corrección M, entre 30 y 40 mm de lado, con una zona de silencio de al menos 2 mm, y precedido del rótulo «QR tributario». Junto a él, las dos leyendas que devuelve la API, siempre en castellano aunque la factura esté en otro idioma. Tienes dos opciones:

  • Dibujar tú la URL (qr.url) con cualquier librería de QR, respetando los parámetros anteriores.
  • Pedir el símbolo ya renderizado: GET /v1/registros/{id}/qr devuelve un SVG (image/svg+xml) con la corrección y el margen correctos, listo para incrustar a 30–40 mm. Sólo existe para registros de alta: una anulación no lleva QR.
Ejemplo (Node.js)
const res = await fetch(`${API}/v1/registros/${registroId}/qr`, {
  headers: { Authorization: `Bearer ${CLAVE}` },
})
const svg = await res.text()   // guárdalo con la factura y dibújalo en el PDF

Si la factura lleva retención (IRPF), imprime los dos importes con su rótulo: el total de la factura (el que va en el QR) y el total a pagar. La propia AEAT lo recomienda para que el cotejo cuadre con lo que ve el receptor.

Consultar el estado

GET /v1/facturas/{id} (o GET /v1/registros/{id}, que también sirve para anulaciones) devuelve el registro con su estado, lo que la AEAT respondió en el último intento (aeat: estado, código, descripción, CSV) y el historial completo de envios. acciones_disponibles indica si procede reenvio (envío agotado sin llegar a la AEAT) o subsanacion (rechazado o aceptado con errores).

estadoQué hacer en tu sistema
pendiente · encolado · enviandoNada. La factura ya está registrada y encadenada; la remisión está en curso.
aceptadoMarcar como registrada. El QR es cotejable en la Sede.
aceptado_con_erroresRegistrada. Revisar el detalle (código y descripción) por si conviene una rectificativa; no reenviar.
rechazadoSi el motivo es un fallo técnico de la AEAT (códigos 1129, 1241, 1243, 1256), la API reemite estos mismos datos por su cuenta. En cualquier otro caso el dato está mal y la corrección tiene que venir de tu sistema: mostrar el motivo a la persona, no editar la factura ya emitida y no reintentar el mismo número por POST /v1/facturas.
subsanadoExiste un registro posterior que corrige a éste. Nada que hacer.
error_persistenteNo se pudo entregar a la AEAT tras los reintentos. Avisar; reencolar con POST /v1/registros/{id}/reenvio o desde la consola.

Para listados: GET /v1/registros con filtros estado, tipo, obligado_nif o obligado_id, desde, hasta, paginado por cursor (page.next_cursor). Para no sondear, usa webhooks.

Anular y rectificar

Anulación

POST https://verifactu-api.zctechpartners.cloud/v1/anulaciones
{
  "obligado_nif": "B21778451",
  "num_serie_factura_anulada": "FAC-2027-0001",
  "fecha_expedicion_anulada": "2027-01-07"
}

Emite un registro de anulación encadenado. La respuesta es la misma forma que el alta, sin qr. Si anulas una factura que no llegó a registrarse por la API (anterior a la integración), indica "sin_registro_previo": "S".

Rectificativa

Es un alta con tipo_factura R1–R5, tipo_rectificativa (S por sustitución, I por diferencias), la lista facturas_rectificadas (número y fecha de cada una) y, por sustitución, importe_rectificacion con base_rectificada y cuota_rectificada. Lleva su propio QR.

Errores

Los errores siguen RFC 9457 (application/problem+json): type es un identificador estable bajo https://verifactu-api.zctechpartners.cloud/problems/…, detail es un texto mostrable y, en validación, errors enumera campo y mensaje. Toda respuesta lleva request_id; cítalo si nos escribes.

HTTPtypeCuándo
400malformed-requestJSON ilegible o cabeceras imposibles.
401unauthorizedClave ausente, inválida o revocada.
403forbiddenLa clave no tiene la capacidad o el obligado está fuera de su ámbito.
404not-foundObligado inexistente en el entorno de la clave; registro inexistente.
409conflict / idempotency-conflictFactura ya registrada (mismo número y fecha) / misma Idempotency-Key con otro cuerpo.
413 · 415payload-too-large / unsupported-media-typeCuerpo mayor del permitido / no es JSON.
422validation-errorCampos inválidos o importes que no cuadran. Ver errors[].
429rate-limitedMás de 120 peticiones por minuto con la clave. Respeta Retry-After.
503chain-lockedCadena del obligado ocupada. Reintenta con la misma Idempotency-Key.
422 de ejemplo
{
  "type": "https://verifactu-api.zctechpartners.cloud/problems/validation-error",
  "title": "Error de validación",
  "status": 422,
  "detail": "El cuerpo no supera la validación.",
  "request_id": "7f0c…",
  "errors": [
    { "campo": "lineas.0.cuota", "mensaje": "base × tipo_iva no cuadra con cuota" }
  ]
}

Ejemplo real: el CRM ONE

ZC Tech Partners factura con su propio CRM (ONE) integrado con esta API; es el cliente cero. Sirve como referencia de cómo queda una integración terminada:

  • Un interruptor por empresa activa VERI*FACTU. Apagado, el programa es idéntico al de antes.
  • Con el interruptor encendido, «Marcar enviada» se sustituye por Emitir, que llama a POST /v1/facturas con una Idempotency-Key guardada en la fila y almacena en la factura el id del registro, la huella, la URL del QR, el SVG del QR y el importe registrado.
  • Una factura registrada no se puede editar ni borrar: la base de datos lo impide con disparadores y la interfaz retira los botones. Anular emite POST /v1/anulaciones.
  • El PDF imprime el QR de 35 mm con «QR tributario» encima y «QR VERI*FACTU» debajo, las dos leyendas en castellano y, si hay IRPF, el total de la factura y el total a pagar.
  • Un webhook firmado actualiza el estado del acuse (pendiente, aceptado, aceptado con errores, rechazado) que se muestra en la ficha de la factura, con enlace para cotejar en la Sede.

Lista de comprobación

  • La clave de integración vive en la configuración del servidor, nunca en el navegador ni en el repositorio.
  • Cada factura guarda su Idempotency-Key antes de llamar y reutiliza la misma al reintentar.
  • num_serie_factura es exactamente el número impreso; importe_total es base más cuota, sin retenciones.
  • El PDF lleva el QR a 30–40 mm, «QR tributario», las dos leyendas y, con IRPF, los dos totales.
  • Una factura registrada no se puede editar ni borrar en tu programa.
  • Tu sistema reacciona a rechazado y error_persistente (webhook o consulta) y muestra el motivo.
  • Has probado el ciclo completo en preproducción, incluido cotejar un QR en el entorno de pruebas de la Sede.