Documentación para desarrolladores

Integra VeriFactu y Facturae en una tarde.

Una API REST clara, con ejemplos en cinco lenguajes. Autenticación por API key, emisión asíncrona y webhooks. Prueba gratis en el sandbox.

x-api-key: vsk_…Consigue tu API key

Quick Start

Una API key autentica tu cuenta (el integrador). La cuenta agrupa N emisores (los obligados por los que facturas). Cada emisor tiene su NIF, certificado y domicilio.

1. Crea tu cuenta

Regístrate gratis en el dashboard. Obtienes 10 llamadas API de prueba en live, y una clave de test contra la AEAT de preproducción sin tope.

2. Obtén tu API Key

Cópiala en el dashboard. Va en el header x-api-key de todas las peticiones.

3. Da de alta un emisor y su certificado

Crea el obligado con POST /api/v1/emisores y sube su certificado P12 con POST /api/v1/emisores/:id/certificate. El NIF del certificado debe coincidir con el del emisor.

4. Crea tu primera factura

POST /api/v1/invoices (indica emisorNif si tu cuenta tiene varios emisores). Copia un ejemplo de abajo y prueba en segundos.

5. Entrega el PDF a tu cliente

Descarga el PDF imprimible con GET /api/v1/invoices/:id/pdf: lleva el QR de cotejo, la mención VeriFactu y, si subes el logo del emisor (POST /api/v1/emisores/:id/logo), tu marca bajo el nombre de la empresa.

Ejemplo: Crear tu primera factura

Copia este ejemplo, reemplaza TU_API_KEY y ejecuta. La factura se firmará y encolará automáticamente.

curl -X POST https://factuneo.es/api/v1/invoices \
  -H "Content-Type: application/json" \
  -H "x-api-key: vsk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
  -d '{
    "numeroSerie": "FAC-2026-001",
    "fechaExpedicion": "2026-06-08",
    "baseImponible": 100.00,
    "cuotaIva": 21.00,
    "importeTotal": 121.00,
    "tipoFactura": "F1",
    "nombreRazonEmisor": "Empresa Test SL",
    "descripcionOperacion": "Venta de productos",
    "destinatarios": [
      { "nombreRazon": "Cliente Ejemplo SL", "nif": "B87654321" }
    ],
    "desglose": [{
      "claveRegimen": "01",
      "calificacionOperacion": "S1",
      "tipoImpositivo": 21,
      "baseImponible": 100.00,
      "cuotaRepercutida": 21.00
    }]
  }'

Usar el panel

El panel es la ficha de tu cuenta (una por integrador, modelo 1:1). Lo usas una vez para configurarte; el alta de tus clientes y la emisión van siempre por API desde tu aplicación. No entras al panel cada vez que un cliente te contrata.

1. API key

Regístrate y, en tu cuenta, genera la API key (se muestra una sola vez — guárdala). Va en el header x-api-key de todas las peticiones.

2. Certificado representante (solo si usas modo Representado)

Sube una sola vez tu certificado de colaborador social / apoderado en la tarjeta Certificado representante. Firma por todos tus clientes en modo Representado. Si solo usas modo Propio, no necesitas subir nada aquí.

Para retirarlo, el botón Eliminar de esa misma tarjeta. Pide confirmación y te dice cuántos emisores se quedan sin poder emitir antes de hacerlo. Equivale a DELETE /api/v1/account/certificate.

3. Webhook (opcional)

URL para recibir los eventos de tus facturas y certificados sin tener que consultar. Si lo dejas vacío, consultas con GET /api/v1/invoices/:id.

4. Suscripción

Tu plan y facturación.

5. Borrar cuenta

Si el servicio no te encaja, te vas sin escribirnos: botón Borrar mi cuenta. Elimina de forma permanente tu cuenta, tu API key y tu usuario, previa confirmación. No se puede deshacer.

Exige que la cuenta esté vacía antes: primero eliminas tus emisores y tu certificado representante con sus propios borrados —que ya explican qué se lleva cada uno— y después la cuenta. No arrastramos material de firma ni obligados en cascada desde un solo clic.

Si has emitido facturas, la cuenta no se puede borrar: son registros fiscales con obligación de conservación.

6. Logs y observabilidad

En Logs y observabilidad (/dashboard/logs) ves qué hace tu integración sin tener que escribirnos: peticiones a la API, errores, latencia, IPs, entregas de webhook con reenvío de cualquier evento, facturas por estado, certificados por caducar, consumo por NIF, tu auditoría legal (cada factura enviada/aceptada/ rechazada, cambios de certificados y emisores, login, claves regeneradas, suscripción) y tus alertas (certificados por caducar, facturas rechazadas o con error — filtradas a tus propios NIFs, no las de toda la plataforma). Es una vista del panel (sesión de navegador) — no hay endpoint por API key para consultarlo desde tu propio backend; es para ti, no para tu software.

Importante: el estado de una entrega de webhook (PENDIENTE / ENTREGADO / FALLIDO, de esta sección) y el estado de la factura que que viaja dentro del webhook (el nombre del evento y data.status, ver Webhooks) son dos cosas distintas que comparten nombre por casualidad. Una entrega FALLIDO significa que no logramos avisarte del cambio — no dice nada sobre si la factura en sí fue aceptada o rechazada por la AEAT.

Selector de entorno

Arriba de todo, un selector live / test. Abre en live por defecto a propósito: así un fallo probando en el sandbox nunca se confunde con una incidencia real. Cada pestaña se filtra por el entorno elegido — cambiarlo recarga los datos y vuelve a la página 1 de cualquier listado.

Resumen

Cuatro bloques, todos del entorno seleccionado:

BloqueCampoQué es
APILlamadas (7 días)Total de peticiones a /api/v1/ de los últimos 7 días
Errores (7 días)De esas, cuántas devolvieron statusCode >= 400
Latencia mediaMedia de durationMs de los últimos 7 días, en ms
IPs con más tráficoTop 10 IPs de origen por nº de llamadas
ClientesEmisores activosEmisores no dados de baja en este entorno
Certificados por caducarLos que vencen en menos de 30 días (tuyo de representante + los de tus clientes en modo Propio). Solo se calcula en live — el sandbox usa certificados de prueba, no importa que caduquen
FacturasTotalRegistros de alta creados en este entorno
Por estadoDesglose ACEPTADO / PENDIENTE / ENVIADO / ACEPTADA_CON_ERRORES / RECHAZADO / ERROR — mismo significado que en la tabla de estados del webhook
Webhooks% entregadosENTREGADO ÷ (ENTREGADO + FALLIDO + PENDIENTE) del entorno. si no hay ninguna entrega todavía
FallidosEntregas que agotaron sus reintentos automáticos

Pestaña Peticiones

Una fila por llamada a la API: fecha, método, ruta (patrón, p. ej. /api/v1/invoices/:id — nunca la URL con el id real), código de estado, latencia e IP de origen.

Nunca se guarda el cuerpo de la petición ni las cabeceras — ahí viajan NIFs de terceros, datos fiscales y contraseñas de certificado. Solo se registran esos seis metadatos.

Retención: el detalle se conserva 30 días y se purga automáticamente después. El agregado diario (llamadas y errores por día, usado en el resumen y en Consumo por NIF) es perpetuo, no se purga nunca.

Pestaña Errores

Igual que Peticiones, filtrado a statusCode >= 400. Útil para ver de un vistazo si algo se está rompiendo sin tener que buscarlo entre las llamadas correctas. Si aparece un errorCode (p. ej. EMISOR_QUOTA_EXCEEDED, INVOICE_NOT_FOUND), es el mismo código del cuerpo de la respuesta — consulta la tabla de errores para saber qué significa cada uno.

Pestaña Webhooks

Una fila por entrega (cada intento de POST a tu URL), con su evento, código de respuesta HTTP, nº de intentos y el error si falló.

EstadoSignificado
PENDIENTEEncolada o reintentándose. Puede que veas alguno así y en el siguiente refresco ya haya pasado a ENTREGADO
ENTREGADOTu servidor respondió 2xx
FALLIDOSe agotaron los reintentos automáticos (hasta 5, con espera creciente) sin recibir 2xx

Reenviar a mano: el botón Reenviar está en todas las filas, no solo en las fallidas. Manda de nuevo el mismo payload que se generó la primera vez (no se recalcula nada de la factura) a la misma URL.

Reenviar una entrega ya ENTREGADO es un caso legítimo y frecuente: tu servidor contestó 2xx pero el mensaje se perdió después (una cola que se vació, un despliegue a medias, un bug que ya arreglaste). El reenvío conserva el mismo id de evento que el original, así que si deduplicas por ese id —y deberías— no lo contabilizas dos veces.

Cada reenvío crea una fila nueva, marcada como reenvío; la entrega original se queda como estaba. El historial no se pisa: si algo se entregó, esa prueba no desaparece porque un reenvío posterior falle.

Si tu integración ya reintenta por su cuenta consultando GET /api/v1/invoices/:id, esta pestaña es solo para diagnóstico — no hace falta que la mires si no sospechas un problema.

Pestaña Consumo por NIF

Llamadas y errores de los últimos 30 días, agrupados por emisor (NIF + razón social). Útil para ver qué cliente está más activo o cuál está fallando más de la cuenta. Las llamadas que no van ligadas a un emisor concreto (p. ej. GET /api/v1/account) no aparecen aquí — sí cuentan en el total de la pestaña Peticiones y del Resumen.

Pestaña Auditoría

El registro legal de tu cuenta, con cadena de hashes encadenados (cumple el Esquema Nacional de Seguridad, medida MP.SI.3 "Registro de actividad"): un evento no se puede alterar ni borrar sin que se note. Distinto de las pestañas de arriba, que son diagnóstico operativo — esto es el histórico inalterable de qué pasó.

Qué queda registrado:

EventoCuándo
Envío a la AEAT / Error de envíoCada factura enviada, aceptada, rechazada o con error — incluida cada anulación y cada Facturae generado
Cambio de configuraciónAlta de emisor, certificado subido/borrado (del emisor o representante), logo subido
Inicio de sesiónCada login en el panel
Clave regeneradaCada vez que regeneras tu clave live o test
Suscripción activada / canceladaAltas y bajas de tu plan de pago

Los eventos que no son de un entorno concreto (login, certificado representante, suscripción) se ven en las dos pestañas live y test — no son ni de uno ni de otro, son de la cuenta.

Pestaña Alertas

"Esto necesita tu atención" — distinto del histórico completo de Auditoría y de los conteos del Resumen: aquí solo lo que requiere que hagas algo, filtrado a tus propios NIFs y tus propias facturas, nunca agregados de toda la plataforma.

TipoCuándo se genera
Certificado por caducarEl tuyo de representante, o el de un emisor en modo Propio, a menos de 30 días de caducar
Factura rechazadaLa AEAT devolvió RECHAZADO para un envío
Factura con errorEl envío falló de forma definitiva (sin representación vigente, certificado ilegible, rechazo total del lote) — no incluye fallos transitorios de red que ya se reintentaron solos

Cada certificado o factura genera como mucho una alerta: no se repite en cada ciclo del monitor. No hay forma de marcarla como resuelta todavía — es un historial, no una bandeja de tareas.

Emisores (clientes)

El listado de emisores del panel es informativo. Tus clientes los da de alta tu software con POST /api/v1/emisores y aparecen aquí automáticamente. La gestión (alta, cert por cliente en modo Propio, baja) se hace por API:

AcciónModo PropioModo Representado
Alta del clientePOST /api/v1/emisores {certMode:"PROPIO"}POST /api/v1/emisores {certMode:"REPRESENTADO"}
CertificadoPOST /api/v1/emisores/:id/certificate (cert del cliente)Ninguno por cliente — usa tu cert representante
Emitir facturaPOST /api/v1/invoices {emisorNif}igual

Logo del emisor: además, desde el panel puedes subir el logo de cada emisor (botón de la imagen en su fila). Se imprime en el PDF de sus facturas, bajo el nombre de la empresa. Lo mismo por API con POST /api/v1/emisores/:id/logo.

Eliminar un cliente: el botón de la papelera de su fila. Pide confirmación y avisa de qué va a pasar antes de hacerlo, porque depende de si ese emisor tiene facturas:

  • Sin facturas → se borra su ficha por completo.
  • Con facturas → su ficha se conserva desactivada, porque las facturas son registros fiscales que deben seguir teniendo un emisor identificable.

En los dos casos se borran su certificado, su contraseña y su logo. Equivale a DELETE /api/v1/emisores/:id.

El panel cubre tu cuenta; el ciclo de vida de cada cliente es 100% API.

Autenticación

Todas las peticiones a la API requieren autenticación mediante API Key.

Header requerido

x-api-key: vsk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

Obtener API key

  1. Inicia sesión en el dashboard
  2. En la tarjeta API Key de tu cuenta, pulsa Generar / Regenerar
  3. Cópiala (se muestra una sola vez; puedes regenerarla cuando quieras)

Seguridad: La API key tiene el mismo nivel de sensibilidad que una contraseña. Nunca la expongas en código frontend ni la subas a repositorios públicos.

Idempotencia

En POST /api/v1/invoices puedes enviar el header Idempotency-Key con un valor único por factura (un UUID). Si tu ERP reintenta (timeout, red) con la misma clave, la API devuelve la misma factura en vez de duplicarla. Recomendado en producción.

Idempotency-Key: 7c9e6679-7425-40de-944b-e07fc1f90ae7

Planes

PlanPrecioIncluye
Prueba0 €1 NIF live · 10 llamadas API (total, no mensual) · clave de test siempre gratis y sin tope
Pay-as-you-godesde 6,90 €/NIF/mes500 facturas/NIF/mes · Facturae · envío AEAT

El precio es escalonado por tramos: cada tramo se factura a su propia tarifa, así que la tarifa marginal baja al crecer (6,90 € los 5 primeros NIFs … 1,40 € a partir de 101). Consulta el importe exacto en la calculadora.

Extras opcionales: soporte prioritario (49 €/mes fijos) y facturas ilimitadas por NIF (quita el tope de 500/NIF/mes; también escalonado, desde 0,90 €/NIF).

Todos los precios son sin IVA: se añade el 21% en el cobro, salvo empresas de otros países de la UE con NIF-IVA válido (inversión del sujeto pasivo).

Sandbox: entornos test y live

Cada cuenta tiene dos API keys independientes, siempre — no hay que pedir ni activar nada aparte. La que uses en el header x-api-key decide el entorno, con la misma URL base para las dos:

ClavePrefijoA dónde vaCoste
Livevsk_live_…AEAT real. Con validez fiscal.Se factura por NIF (ver Planes)
Testvsk_test_…AEAT de preproducción. Sin validez fiscal.Siempre gratis, sin tope de facturas
# Con la clave de test: crea, prueba, rompe cosas — no pasa nada.
curl -X POST https://api.factuneo.es/api/v1/emisores \
  -H "x-api-key: vsk_test_..." \
  -H "Content-Type: application/json" \
  -d '{"nif":"B12345678","nombreRazon":"Empresa de prueba SL"}'

Por qué existe

Antes de este sandbox, las únicas "pruebas" disponibles eran las llamadas del plan de prueba, y esas llamadas iban contra la AEAT real: probar tu integración significaba generar registros fiscales de verdad. El sandbox separa completamente eso: puedes construir y romper tu integración sin miedo, y solo cuando estés listo cambias a la clave live.

Aislamiento: dos mundos que no se tocan

  • Los emisores son distintos por entorno. Puedes dar de alta el mismo NIF en test y en live: son dos fichas independientes, con su propio certificado y su propia cadena de huellas. Nunca se mezclan.
  • La clave de test solo ve datos de test. GET /api/v1/emisores con la clave test nunca devuelve tus emisores live, y viceversa. Ni por accidente ni adivinando un id.
  • Las facturas de test no cuentan para nada facturable. No suman al nº de NIFs facturados, no tienen el tope de 500/mes, y no consumen las llamadas del plan de prueba.
  • El QR de una factura de test apunta a preproducción. Si lo escaneas, no cotejará en la sede real de la AEAT — es la prueba de que nunca podría confundirse con una factura real.

Qué NO cambia entre entornos

  • El certificado. Si usas modo Representado, el mismo certificado sirve para las dos claves — la preproducción de la AEAT también autentica por certificado, así que no hace falta uno "de pruebas" aparte. En modo Propio, cada emisor (test o live) necesita el suyo igualmente.
  • El formato de las respuestas. Un 200, un 402, un 404… responden exactamente igual en los dos entornos. Tu código no necesita ninguna rama especial para "modo test".

Dónde conseguir las claves

Desde el panel, tarjeta API Keys: ahí ves las dos, generas o regeneras cada una por separado. También se generan automáticamente al crear una cuenta por API (POST /api/v1/accounts), que devuelve las dos en la misma respuesta.

Trátalas con el mismo cuidado que la live: la clave de test no puede emitir facturas reales, pero sigue dando acceso a tu cuenta — quien la tenga puede leer y modificar tus datos de sandbox. No la publiques ni la subas a un repositorio.

Certificados y firma

Cada emisor firma sus registros ante la AEAT con un certificado cualificado. Hay dos modos, configurables por emisor con el campo certMode:

ModoQuién aporta el certificadoCuántosEndpoint
PROPIO (default)Cada emisor su propio cert1 por emisorPOST /api/v1/emisores/:id/certificate
REPRESENTADOEl integrador, un único cert para todos1 por cuentaPOST /api/v1/account/certificate

PROPIO

El obligado tiene su certificado y tú lo subes por él. Al subirlo se valida la contraseña, la caducidad y que el NIF del certificado coincida con el del emisor.

Comprobación contra la AEAT

Que un certificado sea válido no significa que la AEAT lo acepte: hace falta además que el obligado esté en el censo y, en modo Representado, que exista apoderamiento. Por eso, al subirlo hacemos una consulta de solo lectura a la AEAT con ese certificado (no registra nada) y te devolvemos el resultado en aeatAutorizacion:

ValorQué significa
AUTORIZADOLa AEAT responde correctamente. Puedes facturar
NO_AUTORIZADOEl certificado se guarda, pero las facturas fallarán. El motivo, en aeatDetalle
NO_VERIFICADONo se pudo contactar con la AEAT. No dice nada del certificado

Esta comprobación existe porque la AEAT no devuelve un error legible cuando rechaza un certificado: responde HTTP 200 con una página HTML. Sin mirar el cuerpo, un rechazo parece un envío correcto — y el problema se descubría factura a factura. Si recibes NO_AUTORIZADO, revisa el censo y el apoderamiento antes de emitir.

La comprobación nunca impide guardar el certificado: si la AEAT no responde, el resultado es NO_VERIFICADO y puedes seguir.

REPRESENTADO (colaborador social / apoderamiento)

Subes un solo certificado (el del integrador) a nivel de cuenta y firma por todos los emisores en este modo. El obligado no entrega su certificado: solo te apodera en la Sede de la AEAT para el trámite VeriFactu. Cambia el modo de un emisor con PATCH /api/v1/emisores/:id { "certMode": "REPRESENTADO" }.

Para usar REPRESENTADO, el integrador debe ser colaborador social o estar apoderado por el obligado. La AEAT valida la representación en cada envío.

Custodia del certificado

Un certificado de firma es la credencial más sensible que nos entregas, así que esto es exactamente lo que le pasa:

CifradoAES-256-GCM. La clave maestra se deriva con scrypt y vive solo en la configuración del servidor, nunca en la base de datos: un volcado de la BD no basta para descifrar nada.
UsoSe descifra únicamente en memoria, en el instante de firmar o de abrir la conexión mTLS con la AEAT. No se escribe descifrado en disco ni en logs.
Exposición por APINunca. Ningún endpoint devuelve los bytes: solo hasCertificate: true/false y la fecha de caducidad.
En tránsitoTLS 1.3.

Retirar el certificado

Lo que subes lo retiras tú, cuando quieras, sin escribirnos:

Qué quieresLlamada
Retirar el cert de un emisor (sigue siendo cliente)DELETE /api/v1/emisores/:id/certificate
Retirar tu cert representanteDELETE /api/v1/account/certificate
Eliminar a un cliente y su certificadoDELETE /api/v1/emisores/:id

También desde el panel, en las tarjetas correspondientes.

El borrado es permanente: se eliminan el .p12 y su contraseña de la base de datos. A partir de ahí ese emisor no puede firmar hasta que subas otro.

Lo que NO desaparece —y no puede desaparecer— son las facturas ya comunicadas a la AEAT: constan en su sistema por obligación legal, su cadena de huellas es inalterable, y la firma de cada una quedó dentro del XML en el momento de emitir, donde sigue siendo verificable. Borrar el certificado no invalida ni oculta nada de lo ya emitido; solo impide emitir en el futuro.

Por eso la obligación de conservación recae sobre las facturas, no sobre el certificado: una vez firmado y comunicado el registro, el certificado ya no cumple ninguna función sobre lo pasado.

Endpoints principales

Base URL: https://factuneo.es

Alta de cuenta (sin pasar por el panel)

MétodoEndpointDescripción
POST/api/v1/accountsCrea cuenta + primer emisor y devuelve la API key. No requiere autenticación y la key se muestra una sola vez

Emisores y cuenta

MétodoEndpointDescripción
POST/api/v1/emisoresAlta de emisor (obligado)
GET/api/v1/emisoresListar emisores de la cuenta
GET/api/v1/emisores/:idDetalle de un emisor
PATCH/api/v1/emisores/:idEditar domicilio / certMode
DELETE/api/v1/emisores/:idEliminar emisor y su certificado (conserva la ficha si tiene facturas)
POST/api/v1/emisores/:id/certificateSubir certificado P12 del emisor (modo PROPIO)
DELETE/api/v1/emisores/:id/certificateRetirar el certificado del emisor (sin borrar al emisor)
POST/api/v1/emisores/:id/logoSubir el logo del emisor (PNG/JPG), se imprime en el PDF
GET/api/v1/emisores/:id/logoDescargar el logo del emisor
DELETE/api/v1/emisores/:id/logoEliminar el logo del emisor
GET/api/v1/accountDatos de la cuenta, nº de emisores y webhookSecret
POST/api/v1/account/certificateCert representante de la cuenta (modo REPRESENTADO)
DELETE/api/v1/account/certificateRetirar tu certificado representante (corta la emisión REPRESENTADO)

Facturas VeriFactu

MétodoEndpointDescripción
POST/api/v1/invoicesCrear factura (indica emisorNif si hay varios emisores)
GET/api/v1/invoicesListar facturas (paginado)
GET/api/v1/invoices/:idObtener detalle
GET/api/v1/invoices/:id/xmlDescargar XML del RegistroAlta
GET/api/v1/invoices/:id/pdfDescargar el PDF imprimible de la factura (con QR y logo)
GET/api/v1/invoices/:id/qrObtener datos del QR
GET/api/v1/invoices/:id/statusHistorial de estados
GET/api/v1/invoices/:id/consulta-aeatConsultar a la AEAT + cotejo de huella
POST/api/v1/invoices/:id/anularAnular factura (registro de anulación)
POST/api/v1/invoices/:id/facturaeGenerar XML Facturae (B2B)

Contratos electrónicos

MétodoEndpointDescripción
POST/api/v1/contractsCrear contrato
GET/api/v1/contractsListar contratos
GET/api/v1/contracts/:id/xmlDescargar XML firmado

Mandatos SEPA

MétodoEndpointDescripción
POST/api/v1/sepa-mandatesCrear mandato
GET/api/v1/sepa-mandatesListar mandatos
GET/api/v1/sepa-mandates/:id/xmlDescargar XML firmado

Seguridad y auditoría

MétodoEndpointDescripción
GET/api/v1/security/complianceInforme de cumplimiento ENS
GET/api/v1/security/audit-integrityCadena de hashes: devuelve { validChain, checkedAt }

Monitorización

MétodoEndpointDescripción
GET/healthHealth check (DB + Redis + AEAT). Público, sin datos de cuentas

/metrics (Prometheus) es interno: no está publicado y requiere token propio. Para el estado del servicio usa /health.

En Referencia tienes cada endpoint con su cuerpo, su respuesta y sus errores.

Referencia por endpoint

Cada endpoint con lo que acepta, lo que devuelve y en qué puede fallar. Todos requieren x-api-key salvo donde se indique lo contrario.


Cuenta

POST /api/v1/accounts

Crea cuenta + primer emisor y devuelve la API key. No requiere autenticación: es el alta programática, para no pasar por el panel.

CampoTipoOblig.Notas
namestring(1-255)Nombre del integrador
nifstring(1-9)NIF del primer emisor
razonSocialstring(1-255)Razón social del primer emisor
emailstringTu correo. Recibe el enlace que activa las llamadas de prueba
curl -X POST https://factuneo.es/api/v1/accounts \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Mi ERP",
    "nif": "B12345678",
    "razonSocial": "Cliente Uno SL",
    "email": "dev@miempresa.es"
  }'
{
  "accountId": "9c1b…",
  "emisorNif": "B12345678",
  "apiKey": "vsk_live_…",
  "apiKeyTest": "vsk_test_…",
  "emailVerificado": false
}

Las dos claves se muestran una sola vez: solo guardamos su hash. Si las pierdes, regenéralas desde el panel — cada una por separado. Más sobre qué diferencia apiKey de apiKeyTest en Sandbox (test / live).

Verifica el correo antes de llamar a la API. Con emailVerificado: false la key ya es válida, pero las llamadas del plan de prueba devuelven 403 EMAIL_NOT_VERIFIED hasta que pulses el enlace que te enviamos. Es lo que evita que se creen cuentas en cadena para acumular pruebas gratis. Las cuentas con suscripción activa no pasan por esta comprobación.

Errores: 409 NIF_EXISTS (ya hay una cuenta con ese NIF) · 400 DISPOSABLE_EMAIL (correo de un servicio temporal) · 400 campos inválidos.

GET /api/v1/account

Datos de tu cuenta, nº de emisores facturables y el secreto de webhooks.

{
  "id": "9c1b…",
  "name": "Mi ERP",
  "emisorCount": 3,
  "environment": "live",
  "hasRepresentanteCert": true,
  "representanteNif": "12345678Z",
  "webhookSecret": "a3f2…"
}

environment refleja qué clave usaste en esta llamada (live o test). emisorCount es siempre el nº de NIFs live facturables, aunque llames con la clave de test.

emisorCount es el número de NIFs activos: es la cantidad que se factura.

POST /api/v1/account/certificate

Sube el certificado del integrador (colaborador social / apoderado) que firmará por los emisores en modo REPRESENTADO. Se sube una sola vez por cuenta.

CampoTipoOblig.
certificateBase64string
passwordstring
{ "success": true, "representanteNif": "12345678Z", "certRepresentanteExpira": "2028-12-31T00:00:00.000Z" }

Errores: 400 CERTIFICATE_INVALID (contraseña incorrecta o fichero ilegible) · 400 CERTIFICATE_EXPIRED.

DELETE /api/v1/account/certificate

Retira tu certificado representante. Es la contrapartida de subirlo: el material de firma que nos confías lo retiras tú, cuando quieras, sin pedírnoslo.

Borra de la base de datos, de forma permanente, el .p12, su contraseña, su fecha de caducidad y los datos del representante.

curl -X DELETE https://api.factuneo.es/api/v1/account/certificate \
  -H "x-api-key: TU_API_KEY"
{ "success": true, "hadCertificate": true, "emisoresAfectados": 12 }

⚠️ Corta la emisión de todos tus emisores en modo REPRESENTADO. emisoresAfectados te dice cuántos se quedan sin poder emitir, para que lo sepas en el acto y no lo descubras cuando falle la siguiente factura. Los emisores en modo PROPIO no se ven afectados: firman con el suyo.

Para volver a operar, sube otro con POST /api/v1/account/certificate. No hace falta tocar los emisores: siguen en modo REPRESENTADO con su representacionVigente intacta.

No toca las facturas ya emitidas ni su cadena de huellas.


Emisores

POST /api/v1/emisores

Da de alta un obligado. Al crearlo empieza a contar para la facturación.

El plan de prueba admite 1 NIF en live; el segundo devuelve 403 EMISOR_QUOTA_EXCEEDED (en el sandbox de test no hay tope). Los planes de pago no tienen tope: se factura por NIF. Eliminar o desactivar un emisor libera hueco, porque deja de facturarse.

CampoTipoOblig.Por defecto
nifstring(1-9)
nombreRazonstring(1-120)
tipoPersonaF o JNoJ
certModePROPIO o REPRESENTADONoPROPIO
representacionVigentebooleanNofalse
direccionstring(≤80)No*
codigoPostalstring(≤10)No*
municipiostring(≤50)No*
provinciastring(≤50)No*
paisstring(3)NoESP
  • Opcionales para VeriFactu, obligatorios para generar Facturae.

Quién aporta el certificado

Cada registro se remite a la AEAT autenticado por TLS mutuo, así que toda factura necesita un certificado. Hay dos modelos y los eliges por emisor:

certModeQuién firmaQué tienes que hacer
PROPIOEl propio obligadoQue suba su certificado a POST /emisores/:id/certificate
REPRESENTADOTú, por cuenta de élSubir tu certificado una vez a POST /account/certificate y marcar representacionVigente: true en cada emisor

En modo REPRESENTADO, representacionVigente declara que tienes poder legal (apoderamiento o colaboración social) sobre ese NIF. Sin él, la emisión devuelve 403 EMISOR_REPRESENTATION_NOT_VALID. Ponerlo a false revoca la representación y corta la emisión de ese emisor de inmediato.

curl -X POST https://factuneo.es/api/v1/emisores \
  -H "x-api-key: vsk_…" -H "Content-Type: application/json" \
  -d '{
    "nif": "B12345678",
    "nombreRazon": "Cliente Uno SL",
    "certMode": "PROPIO",
    "direccion": "Calle Ejemplo 123",
    "codigoPostal": "28001",
    "municipio": "Madrid",
    "provincia": "Madrid"
  }'

Devuelve 201 con el emisor. Los bytes del certificado nunca se devuelven: solo hasCertificate.

{
  "id": "7d2e…", "nif": "B12345678", "nombreRazon": "Cliente Uno SL",
  "tipoPersona": "J", "certMode": "PROPIO", "hasCertificate": false,
  "certExpirationDate": null, "representacionVigente": false, "hasLogo": false,
  "isActive": true, "createdAt": "2026-07-27T10:00:00.000Z", "environment": "live"
}

El emisor hereda el entorno de la clave con la que lo creas: con vsk_test_… sale "environment": "test", aislado de tus emisores live aunque uses el mismo NIF. Ver Sandbox (test / live).

Errores: 409 EMISOR_DUPLICADO (ese NIF ya está en tu cuenta en este entorno — puedes repetir el NIF en el otro).

GET /api/v1/emisores

Para qué sirve: ver tu cartera de obligados y su estado. Devuelve todos los emisores de la cuenta (activos y de baja), cada uno con hasCertificate, certExpirationDate, certMode, representacionVigente e isActive. Úsalo para sincronizar tu panel con Factuneo o para comprobar qué clientes tienen certificado antes de facturar por ellos. Misma forma que el objeto de POST /emisores.

GET /api/v1/emisores/:id

Para qué sirve: el detalle de un emisor concreto por su ID — p. ej. para refrescar su estado de certificado justo después de subirlo. 404 EMISOR_NOT_FOUND si no existe o no es tuyo.

PATCH /api/v1/emisores/:id

Edita el emisor. Todos los campos son opcionales; envía solo lo que cambies. Aceptan null para vaciarlos: direccion, codigoPostal, municipio, provincia.

CampoTipo
nombreRazonstring(1-120)
tipoPersonaF o J
direccion / codigoPostal / municipio / provinciastring o null
paisstring(3)
certModePROPIO o REPRESENTADO
isActiveboolean
curl -X PATCH https://factuneo.es/api/v1/emisores/7d2e… \
  -H "x-api-key: vsk_…" -H "Content-Type: application/json" \
  -d '{"certMode":"REPRESENTADO"}'

Errores: 400 NO_CHANGES (cuerpo vacío) · 404 EMISOR_NOT_FOUND.

DELETE /api/v1/emisores/:id

Elimina al emisor y su material de firma. Deja de contar para la facturación.

El certificado y su contraseña se borran siempre, en los dos casos de abajo. Cuando un emisor deja de operar, ese material ya no tiene función —la firma de lo ya emitido vive dentro del XML almacenado— y conservarlo sería exposición sin motivo. La obligación legal de conservación recae sobre las facturas, no sobre el certificado.

Qué pasa con su ficha depende de si tiene facturas:

Sin facturasCon facturas
Ficha del emisorSe borra enteraSe conserva desactivada
Certificado + contraseñaSe borranSe borran
LogoSe borraSe borra
Facturas registradas en AEATIntactas

Por qué no se borra la ficha si tiene facturas: las facturas emitidas son registros fiscales con obligación de conservación, y deben seguir teniendo un emisor identificable. Borrar la ficha las dejaría huérfanas. Se conserva lo mínimo para identificar quién emitió, sin nada sensible.

{ "success": true, "deleted": false, "invoiceCount": 7, "hadCertificate": true }
CampoSignificado
deletedtrue si se borró la ficha entera; false si se conservó desactivada
invoiceCountFacturas que obligan a conservarla
hadCertificateSi había certificado que borrar

No es reversible. Antes esta llamada era una baja lógica que dejaba el certificado guardado. Ya no: si vuelves a operar con ese NIF, hay que subir el certificado otra vez. Para desactivar temporalmente sin borrar nada, usa PATCH /api/v1/emisores/:id con {"isActive": false}.

Errores: 404 EMISOR_NOT_FOUND.

POST /api/v1/emisores/:id/certificate

Sube el certificado del emisor. Solo para certMode: "PROPIO".

CampoTipoOblig.
certificateBase64string (el .p12/.pfx en base64)
passwordstring

Al subirlo se valida la contraseña, la caducidad y que el NIF del certificado coincida con el del emisor. Se cifra con AES-256-GCM antes de guardarlo.

Errores: 400 CERTIFICATE_NIF_MISMATCH · 400 CERTIFICATE_EXPIRED · 400 CERTIFICATE_INVALID · 400 CERT_MODE_REPRESENTADO (el emisor usa el cert de la cuenta).

DELETE /api/v1/emisores/:id/certificate

Retira el certificado de un emisor sin borrar al emisor. Es lo que quieres cuando el certificado caduca, se revoca o se sustituye por otro, y el cliente sigue siendo tu cliente.

Borra de la base de datos, de forma permanente, el .p12 y su contraseña. El emisor se queda con todos sus datos pero no puede firmar ni emitir hasta que subas uno nuevo con POST /api/v1/emisores/:id/certificate.

curl -X DELETE https://api.factuneo.es/api/v1/emisores/{id}/certificate \
  -H "x-api-key: TU_API_KEY"
{ "success": true, "hadCertificate": true }

hadCertificate: false significa que ese emisor ya no tenía certificado: la llamada es idempotente, repetirla no da error.

No toca las facturas ya emitidas ni su cadena de huellas: la firma quedó dentro del XML en el momento de emitir, y ahí sigue siendo verificable.

Errores: 404 EMISOR_NOT_FOUND.

POST /api/v1/emisores/:id/logo

Sube el logo del emisor. Se imprime en el PDF de la factura, bajo el nombre de la empresa. No afecta al XML ni al envío a la AEAT: es solo para el documento imprimible que entregas a tu cliente.

CampoTipoOblig.
logoBase64string (la imagen PNG o JPG en base64)
  • Formato: PNG o JPG. El tipo se detecta por los magic bytes del contenido, no por lo que declares; cualquier otro formato se rechaza.
  • Tamaño máximo: 2 MB.
  • Recomendación: un PNG con fondo transparente se ve mejor. Se escala automáticamente a un máximo de ~46 px de alto en el PDF, respetando la proporción.
# Convierte tu logo a base64 y súbelo
BASE64=$(base64 -w0 logo.png)
curl -X POST https://api.factuneo.es/api/v1/emisores/EMISOR_ID/logo \
  -H "x-api-key: sk_live_..." \
  -H "Content-Type: application/json" \
  -d "{\"logoBase64\": \"$BASE64\"}"

Respuesta: { "success": true, "mimeType": "image/png", "bytes": 12345 }.

Errores: 400 LOGO_UNSUPPORTED_FORMAT (no es PNG/JPG) · 400 LOGO_TOO_LARGE (> 2 MB) · 400 LOGO_INVALID (vacío o base64 no válido) · 404 EMISOR_NOT_FOUND.

GET /api/v1/emisores/:id/logo

Devuelve la imagen del logo con su Content-Type (image/png o image/jpeg). Útil para previsualizarla. 404 LOGO_NOT_FOUND si el emisor no tiene logo.

DELETE /api/v1/emisores/:id/logo

Elimina el logo del emisor. A partir de ahí, sus PDF salen sin logo.

El logo también se puede subir desde el panel (sección Emisores): botón de la imagen en cada emisor. Panel y API operan sobre el mismo dato.


Facturas

POST /api/v1/invoices

Crea el registro de alta VeriFactu: calcula la huella SHA-256 encadenada, genera el XML RegistroAlta firmado (XAdES-EPES), lo valida contra el XSD oficial y encola el envío a la AEAT (asíncrono). Responde al instante con la huella y el QR; el estado final llega por webhook o consultando la factura.

Cabeceras: x-api-key (obligatoria) y Idempotency-Key (recomendada, evita duplicados en reintentos).

Cuerpo

CampoTipoOblig.Notas
emisorNifstring(1-9)Solo si hay varios emisoresObligado bajo el que se emite
numeroSeriestring(1-60)Serie + número, único por emisor
fechaExpedicionYYYY-MM-DDDebe ser una fecha real del calendario
tipoFacturastring(2)Ver tabla de valores abajo
nombreRazonEmisorstring(1-120)Razón social del emisor
descripcionOperacionstring(1-500)Descripción de la operación
baseImponiblenúmeroBase imponible total
cuotaIvanúmeroCuota de IVA total
importeTotalnúmeroImporte total
destinatariosarraySegún tipo{ nombreRazon, nif }; obligatorio en F1/F3/R1-R4, se omite en F2/R5
desglosearray (mín. 1)Desglose por tipo de IVA (ver abajo)
subsanacionS / NNoReenvío corregido de un registro previo
rechazoPrevioN / S / XNoCon S o X exige subsanacion: "S"
generateFacturaebooleanNoSi true, genera también el Facturae en la misma llamada (default false)

tipoFactura (catálogo AEAT):

ValorSignificado
F1Factura completa (identifica al destinatario)
F2Factura simplificada (ticket, sin destinatario)
F3Factura en sustitución de simplificadas
R1R4Rectificativas (distintos supuestos del art. 80 LIVA)
R5Rectificativa en facturas simplificadas

desglose[] — una línea por tipo impositivo:

CampoTipoNotas
claveRegimenstringRégimen de IVA. 01 = general (catálogo AEAT 0120: exportación, criterio de caja, REBU…)
calificacionOperacionstringS1 sujeta y no exenta · S2 con inversión del sujeto pasivo · N1/N2 no sujeta
tipoImpositivonúmero% de IVA (21, 10, 4, 0…)
baseImponiblenúmeroBase de esta línea
cuotaRepercutidanúmeroCuota de esta línea

Las líneas del desglose deben cuadrar con baseImponible/cuotaIva totales. Si no, la validación semántica devuelve 422 FISCAL_SEMANTIC_ERROR con el detalle en violations.

Corregir un registro (subsanación). Si la AEAT devolvió ACEPTADA_CON_ERRORES o RECHAZADO, reenvía la versión corregida con subsanacion: "S" y rechazoPrevio: "S" (o "X"). La API exige que S/X lleven siempre subsanacion: "S" (regla AEAT §3.1.1).

Respuesta 200

{
  "invoiceId": "3f8a…",
  "hash": "A5C114D6…",
  "qrUrl": "https://…/ValidarQR?…",
  "qrString": "https://…/ValidarQR?…",
  "warnings": []
}
  • hash: huella SHA-256 encadenada (64 hex en mayúsculas).
  • qrString = qrUrl: es lo que codificas en el QR impreso.
  • warnings: avisos semánticos no bloqueantes (la factura se creó y encoló).

La respuesta es inmediata; el envío a la AEAT va en segundo plano. El estado final (ACEPTADO / RECHAZADO…) llega por webhook o con GET /api/v1/invoices/:id.

Errores: 400 VALIDATION_ERROR · 400 TENANT_CERTIFICATE_NOT_CONFIGURED (el emisor aún no tiene certificado) · 400 XSD_VALIDATION_FAILED · 403 (emisor desactivado o sin representación vigente) · 409 LOCK_TIMEOUT (otra factura del mismo emisor en curso: reintenta) · 422 FISCAL_SEMANTIC_ERROR · 429 (límite por minuto o cuota agotada).

GET /api/v1/invoices

Para qué sirve: recorrer o buscar tus facturas emitidas, con filtros por estado y fechas. Úsalo para pintar un listado en tu app, conciliar con tu contabilidad o localizar las facturas que quedaron pendientes o rechazadas. Listado paginado.

ParámetroTipoPor defecto
estadoEnvioPENDIENTE, ENVIADO, ACEPTADO, ACEPTADA_CON_ERRORES, RECHAZADO, ERRORtodos
fechaDesde / fechaHastaYYYY-MM-DDsin filtro
pageentero ≥ 11
limitentero 1-10020
{ "data": [], "meta": { "total": 143, "page": 1, "limit": 20, "totalPages": 8 } }

GET /api/v1/invoices/:id

Para qué sirve: consultar el estado y los datos fiscales de UNA factura por su ID — la alternativa a los webhooks cuando prefieres sondear tú. Devuelve importes, estadoEnvio (dónde va el envío a la AEAT), hashActual, hashAnterior, qrUrl, tipoRegistro (ALTA/ANULACION), anulada y fechaHoraHusoGenRegistro.

GET /api/v1/invoices/:id/xml

Para qué sirve: obtener el XML RegistroAlta firmado tal cual se remite a la AEAT. Úsalo para archivarlo, para auditorías o para aportarlo si Hacienda lo requiere. Se sirve como Content-Type: application/xml.

GET /api/v1/invoices/:id/pdf

Para qué sirve: descargar el PDF imprimible de la factura, listo para entregar a tu cliente. Se genera a partir del registro ya emitido (no recalcula nada ni vuelve a hablar con la AEAT) e incluye los datos fiscales, el desglose de IVA, los totales, el QR de cotejo de la AEAT, la mención VeriFactu y la huella. Si el emisor tiene logo subido, aparece bajo el nombre de la empresa.

Se sirve como Content-Type: application/pdf con Content-Disposition: attachment; filename="<numeroSerie>.pdf".

curl https://api.factuneo.es/api/v1/invoices/INVOICE_ID/pdf \
  -H "x-api-key: sk_live_..." \
  -o factura.pdf

Errores: 404 INVOICE_NOT_FOUND (no existe o no es de tu cuenta).

GET /api/v1/invoices/:id/qr

Para qué sirve: recuperar la URL de cotejo de la AEAT si necesitas regenerar el QR de una factura (también la tienes en la respuesta de POST /invoices). Es la URL que se codifica en el QR impreso.

{ "qrUrl": "https://prewww2.aeat.es/wlpl/TIKE-CONT/ValidarQR?nif=…", "numeroSerie": "FAC-2026-001" }

GET /api/v1/invoices/:id/status

Para qué sirve: ver la traza completa de estados de una factura (PENDIENTEENVIADOACEPTADO/RECHAZADO…), con la respuesta de la AEAT en cada paso. Útil para depurar por qué una factura quedó rechazada. Orden cronológico.

GET /api/v1/invoices/:id/consulta-aeat

Pregunta a la AEAT qué registro conserva y coteja la huella con la tuya. Prueba de integridad extremo a extremo. Solo disponible en VERI*FACTU.

{
  "invoiceId": "3f8a…",
  "encontrada": true,
  "estadoRegistro": "Correcto",
  "huellaAeat": "A5C114D6…",
  "huellaLocal": "A5C114D6…",
  "huellaCoincide": true,
  "timestampPresentacion": "2026-07-24T15:26:33+02:00",
  "codigoError": null,
  "descripcionError": null
}

huellaCoincide: false significa que lo que guarda la AEAT no es lo que tú tienes: revísalo, no lo ignores.

Errores: 502 AEAT_CONSULTA_ERROR si la AEAT no responde.

POST /api/v1/invoices/:id/anular

Genera el registro de anulación. Entra en la misma cadena de huellas que las altas, así que también se envía a la AEAT.

CampoTipoOblig.Notas
refExternastring(1-60)NoReferencia tuya
sinRegistroPrevioS o NNoS declara que no consta alta previa
curl -X POST https://factuneo.es/api/v1/invoices/3f8a…/anular \
  -H "x-api-key: vsk_…" -H "Content-Type: application/json" -d '{}'
{ "anulacionId": "6284…", "invoiceId": "3f8a…", "hash": "E6EF55D1…", "estadoEnvio": "PENDIENTE" }

Errores:

  • 409 INVOICE_NOT_REGISTERED_YET — la AEAT aún no la ha aceptado. Espera: si la anulación llegara antes que el alta, la rechazarían.
  • 409 INVOICE_ALREADY_CANCELLED — ya hay una anulación activa.
  • 409 INVOICE_NOT_REGISTERED — el alta fue rechazada; reintenta con sinRegistroPrevio: "S".
  • 409 NOT_AN_ALTA_RECORD — estás intentando anular una anulación.

POST /api/v1/invoices/:id/facturae

Devuelve el XML Facturae 3.2.2 firmado con XAdES (Content-Type: application/xml). El emisor necesita domicilio fiscal completo.

Facturae y VeriFactu son dos cosas distintas, y este endpoint no habla con la AEAT.

VeriFactu es el registro de facturación que se remite a la AEAT por SOAP: es lo que hace POST /invoices. Facturae es un formato de fichero de factura electrónica (norma de facturae.gob.es, no de la AEAT), y este endpoint se limita a generarlo y firmarlo: no lo envía a ningún sitio. Se valida contra el XSD oficial 3.2.2 antes de devolvértelo.

Qué haces con el fichero depende de a quién factures: si es a una Administración Pública, se presenta en FACe (face.gob.es) o en el punto de entrada de esa Administración; si es B2B, se lo entregas a tu cliente por el canal que useis. Nosotros no presentamos en FACe.

Una misma factura normalmente lleva las dos cosas: el registro VeriFactu a la AEAT y el Facturae para el destinatario. Son independientes.

Campo (dentro de buyer)TipoOblig.Por defecto
taxIdentificationNumberstring(1-30)
personTypeCodeF (física) o J (jurídica)NoJ
residenceTypeCodeR (España) o U (UE) o E (resto)NoR
corporateNamestring(1-80)Si es J
name / firstSurnamestring(1-40)Si es F
address.addressstring(1-80)
address.postCodestring(1-10)
address.townstring(1-50)
address.provincestring(1-50)
address.countryCodestring(3)NoESP
curl -X POST https://factuneo.es/api/v1/invoices/3f8a…/facturae \
  -H "x-api-key: vsk_…" -H "Content-Type: application/json" \
  -d '{
    "buyer": {
      "personTypeCode": "J",
      "residenceTypeCode": "R",
      "taxIdentificationNumber": "B87654321",
      "corporateName": "Cliente Ejemplo SL",
      "address": {
        "address": "Av. del Comercio 5",
        "postCode": "28001",
        "town": "Madrid",
        "province": "Madrid",
        "countryCode": "ESP"
      }
    }
  }'

Errores:

  • 400 SELLER_ADDRESS_NOT_CONFIGURED — falta el domicilio del emisor.
  • 400 INVOICE_NUMBER_TOO_LONG_FOR_FACTURAE — Facturae limita el número de factura a 20 caracteres y VeriFactu permite 60. Usa series más cortas si vas a emitir Facturae.
  • 500 SIGNING_ERROR — no se pudo firmar con el certificado.

Contratos y mandatos SEPA

Módulo aparte de VeriFactu: firma electrónica de documentos. La cuenta firma con su certificado (el mismo que usa para facturar) y devuelve el documento sellado con su huella (contentHash), verificable e inmutable. No se envía a la AEAT.

POST /api/v1/contracts

Para qué sirve: generar y firmar un contrato electrónico (servicio, venta, licencia, NDA) entre varias partes. Devuelve el contrato firmado con su huella.

CampoTipoOblig.
referencestring(1-100)
typeSERVICE o SALE o LICENSE o NDA
partiesarray (mín. 2) de { role: "emitter" o "receiver", name, nif?, email?, address? }
content.titlestring
content.clausesarray de strings (mín. 1)
content.effectiveDateYYYY-MM-DD
content.expirationDateYYYY-MM-DDNo
{ "contractId": "b71c…", "reference": "CT-2026-004", "status": "SIGNED", "contentHash": "9f2a…" }

POST /api/v1/sepa-mandates

Para qué sirve: generar y firmar un mandato SEPA (autorización de adeudo domiciliado) del deudor al acreedor. Devuelve el mandato firmado con su huella.

CampoTipoOblig.
referencestring(1-35)
debtorNamestring(1-70)
debtorIbanIBAN válido
debtorBicBIC (8 u 11 caracteres)No
debtorAddressstring(≤140)No
creditorIdstring(1-35)
creditorNamestring(1-70)

Ambos firman el documento en el momento de crearlo, así que el emisor necesita certificado configurado; si no, responden 400 TENANT_CERTIFICATE_NOT_CONFIGURED.

GET /api/v1/contracts · GET /api/v1/sepa-mandates

Listados. Cada elemento trae id, reference, type/status, contentHash y createdAt.

GET /api/v1/contracts/:id/xml · GET /api/v1/sepa-mandates/:id/xml

Descargan el XML firmado. 404 si no existe o no es tuyo.


Seguridad y auditoría

GET /api/v1/security/compliance

Para qué sirve: un informe de cumplimiento (ENS) de tu cuenta. Evalúa los controles de seguridad aplicables (cifrado de certificados, integridad de la cadena de huellas, vigencia de los certificados…) y devuelve una puntuación y el detalle por control. Úsalo para tu propia auditoría o para justificar el cumplimiento ante un cliente.

{
  "overallStatus": "COMPLIANT",
  "score": 100,
  "controls": [
    { "code": "MP.SI.5.1", "status": "PASS", "description": "…" }
  ]
}

GET /api/v1/security/audit-integrity

Para qué sirve: comprobar que la cadena de auditoría (los eventos del sistema, encadenados por hash) no ha sido manipulada. Recalcula la cadena y confirma que sigue íntegra: es la garantía de inalterabilidad que exige la normativa (ENS).

{ "validChain": true, "checkedAt": "2026-08-05T12:00:00.000Z" }

Monitorización

GET /health

Para qué sirve: comprobar el estado del servicio y sus dependencias (base de datos, Redis, AEAT). Público y sin autenticación — no expone datos de cuentas. Úsalo para tu monitorización de disponibilidad.

{
  "status": "healthy",
  "version": "…",
  "uptime": 138240.5,
  "checks": {
    "database": { "status": "pass", "responseTimeMs": 3 },
    "redis": { "status": "pass", "responseTimeMs": 1 },
    "aeat": { "status": "pass", "responseTimeMs": 210 }
  }
}

Devuelve 200 si tu infraestructura propia está sana (si solo la AEAT va lenta, el estado será degraded pero sigue siendo 200); 503 únicamente si fallan la base de datos o Redis. El endpoint /metrics (Prometheus) es interno y requiere token propio.

Webhooks

El envío a la AEAT es asíncrono. En vez de consultar el estado de cada factura, te avisamos con un POST a tu URL cuando cambia.

Configuración

Se configura en la tarjeta Webhook de tu panel. Déjala vacía si prefieres consultar tú con GET /api/v1/invoices/:id.

Eventos

El nombre del evento te dice qué ha pasado: enruta por event y no necesitas inspeccionar el cuerpo para saber a qué handler mandarlo.

EventoCuándo se emite
invoice.acceptedLa AEAT aceptó la factura (con o sin avisos)
invoice.rejectedLa AEAT la rechazó. Mira data.aeat
invoice.errorNo se pudo enviar. El porqué, en data.reason
invoice.cancelledLa AEAT aceptó una anulación
certificate.expiringUn certificado tuyo caduca pronto (30 días)
certificate.expiredUn certificado tuyo ya ha caducado

Los de certificate.* solo se emiten en producción: los certificados de sandbox son de prueba y da igual que caduquen.

Valores de data.reason en invoice.error:

reasonQué ha pasado
ERROR_EMISOR_SIN_REPRESENTACIONEl emisor está en modo Representado sin representación vigente
ERROR_DESCIFRADO_CERTNo se pudo descifrar su certificado. Vuelve a subirlo
ERROR_LOTE_NO_REINTENTABLELa AEAT rechazó el envío completo del lote
ERROR_RESPUESTA_AEATLa AEAT respondió algo que no se pudo interpretar

Cuerpo que recibes

{
  "id": "evt_3f8a1c22-9d41-4b7e-8a55-1f2e3d4c5b6a",
  "event": "invoice.accepted",
  "environment": "live",
  "tenantId": "9c1b...",
  "timestamp": "2026-07-27T10:15:30.000Z",
  "data": {
    "invoiceId": "3f8a...",
    "invoiceNumber": "FAC-2026-001",
    "recordType": "ALTA",
    "status": "ACEPTADO",
    "acceptedWithWarnings": false,
    "aeat": { "code": null, "message": null }
  }
}

id es el identificador del evento. Es el mismo si reenvías la entrega desde el panel, así que guárdalo y descarta los que ya hayas procesado: es lo que te protege de contabilizar dos veces.

environment distingue live de test. Y no hace falta que confíes solo en ese campo: el secreto de firma es distinto en cada entorno, así que un evento de sandbox no valida contra tu secreto de producción.

En invoice.cancelled, data.invoiceId es la factura original anulada —la que te devolvimos al crearla— y data.cancellationId es el registro de anulación. data.recordType vale ANULACION en los eventos que hablan de una anulación, incluidos los de rechazo o error: si una anulación falla, es la anulación la que falló, no la factura original.

En certificate.*, data trae certificateType (EMISOR o REPRESENTANTE), emisorId, nif, expiresAt y daysUntilExpiry, negativo si el certificado ya caducó.

Cabeceras

CabeceraContenido
X-Webhook-SignatureHMAC-SHA256 del cuerpo, en hexadecimal
X-Webhook-IdId del evento (el mismo id del cuerpo)
X-Webhook-DeliveryId de esta entrega concreta. Cambia en cada reenvío
X-Webhook-EventNombre del evento
X-Webhook-Environmentlive o test
X-Webhook-AttemptNº de intento de entrega (1-5)

Las cabeceras están para que puedas enrutar sin parsear el JSON. Lo que va firmado es el cuerpo: si algo importa de verdad, léelo de ahí.

Verificar la firma

Tu secreto está en GET /api/v1/account, campo webhookSecret. Es fijo por cuenta y por entorno: la clave de test te devuelve el secreto de test y la de producción el de producción. Pídelos una vez y guárdalos como variables de entorno.

La firma es el HMAC-SHA256 del cuerpo tal cual llega, en hexadecimal. Compara en tiempo constante para no filtrar información por el tiempo de respuesta:

import crypto from 'crypto';

app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
  const firma = req.headers['x-webhook-signature'];
  const esperada = crypto
    .createHmac('sha256', process.env.FACTUNEO_WEBHOOK_SECRET)
    .update(req.body)          // el cuerpo SIN parsear
    .digest('hex');

  const ok =
    firma?.length === esperada.length &&
    crypto.timingSafeEqual(Buffer.from(firma), Buffer.from(esperada));
  if (!ok) return res.status(401).send('firma inválida');

  const evento = JSON.parse(req.body);
  // Descarta lo ya procesado: un reenvio trae el MISMO evento.id.
  if (yaProcesado(evento.id)) return res.sendStatus(200);
  // Enruta por el nombre del evento: invoice.accepted, invoice.rejected...
  procesar(evento.event, evento.data);
  res.sendStatus(200);   // responde 2xx o reintentaremos
});

Responde con un 2xx lo antes posible: cortamos la conexión a los 10 segundos y eso cuenta como intento fallido. Si fallas, reintentamos hasta 5 veces con espera creciente. Si tu lógica es lenta, encola y responde ya.

SDKs y ejemplos de integración

Ofrecemos ejemplos oficiales en los lenguajes más populares. Aunque aún no hay SDKs empaquetados, los ejemplos de abajo son production-ready y cubren autenticación, creación de facturas, listado y verificación de webhooks.

¿Prefieres una guía paso a paso para tu stack? En el blog tienes integraciones completas para PHP/Laravel, Python/Django, Node.js, Odoo y .NET/C#.

Crear una factura VeriFactu

Calculamos la huella SHA-256 encadenada, generamos el QR y validamos el XML contra el esquema oficial. La respuesta es inmediata; el envío a la AEAT va en segundo plano.

curl -X POST https://factuneo.es/api/v1/invoices \
  -H "Content-Type: application/json" \
  -H "x-api-key: vsk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
  -d '{
    "numeroSerie": "FAC-2026-001",
    "fechaExpedicion": "2026-06-08",
    "baseImponible": 100.00,
    "cuotaIva": 21.00,
    "importeTotal": 121.00,
    "tipoFactura": "F1",
    "nombreRazonEmisor": "Empresa Test SL",
    "descripcionOperacion": "Venta de productos",
    "destinatarios": [
      { "nombreRazon": "Cliente Ejemplo SL", "nif": "B87654321" }
    ],
    "desglose": [{
      "claveRegimen": "01",
      "calificacionOperacion": "S1",
      "tipoImpositivo": 21,
      "baseImponible": 100.00,
      "cuotaRepercutida": 21.00
    }]
  }'

Listar facturas con filtros

Obtén un listado paginado de facturas. Puedes filtrar por tipo, fecha, estado y más.

curl -X GET "https://factuneo.es/api/v1/invoices?page=1&limit=20&tipoFactura=F1" \
  -H "x-api-key: vsk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"

Verificar webhooks (HMAC-SHA256)

Implementa la verificación de firma en tu endpoint de webhooks para garantizar autenticidad e integridad.

# El webhook llega como POST a tu URL configurada
# Headers:
#   X-Webhook-Signature: <hex>   (sin prefijo)
#   X-Webhook-Event: invoice.accepted
#   X-Webhook-Attempt: 1

Manejo de errores

La API utiliza códigos HTTP estándar y devuelve respuestas JSON estructuradas.

Códigos HTTP

CódigoSignificado
200OK
201Creado
400Bad Request - Datos inválidos o schema incorrecto
401Unauthorized - API Key inválida o ausente
403Forbidden - Sin permisos para este recurso
404Not Found - Recurso no existe
409Conflict - Conflicto (ej: serie duplicada, NIF exists)
422Unprocessable Entity - Errores de validación fiscal
429Too Many Requests - Rate limit excedido o cuota de API agotada
500Internal Server Error - Error interno

Códigos de error más habituales

El campo error identifica la causa de forma estable (el message puede cambiar).

CódigoHTTPQué ha pasado
NO_EMISOR400La cuenta no tiene ningún emisor. Da uno de alta con POST /api/v1/emisores
EMISOR_REQUIRED400Tienes varios emisores: indica emisorNif en la petición
EMISOR_NOT_FOUND404No existe un emisor activo con ese NIF en tu cuenta
EMISOR_DUPLICADO409Ya existe un emisor con ese NIF en la cuenta
EMISOR_CERTIFICATE_NOT_CONFIGURED400El emisor está en modo PROPIO y aún no tiene certificado
ACCOUNT_REPRESENTATIVE_CERT_NOT_CONFIGURED400Modo REPRESENTADO sin certificado de representante en la cuenta
EMISOR_REPRESENTATION_NOT_VALID403El emisor no tiene representación vigente
CERTIFICATE_NIF_MISMATCH400El NIF del certificado no coincide con el del emisor
CERTIFICATE_EXPIRED / CERTIFICATE_INVALID400Certificado caducado o ilegible (¿contraseña correcta?)
FISCAL_SEMANTIC_ERROR422Incumple reglas fiscales de la AEAT (ver violations)
XSD_VALIDATION_FAILED400El XML no valida contra el esquema oficial
SELLER_ADDRESS_NOT_CONFIGURED400Falta el domicilio fiscal del emisor (obligatorio para Facturae)
INVOICE_NOT_FOUND404La factura no existe o no es de tu cuenta
INVOICE_NOT_REGISTERED_YET409Aún no la ha aceptado la AEAT: espera para anularla
INVOICE_ALREADY_CANCELLED409Ya existe un registro de anulación activo
INVOICE_CAP_REACHED402Alcanzado el tope de 500 facturas/NIF/mes (actívalo ilimitado)
QUOTA_EXCEEDED429Agotadas las 10 llamadas del plan de prueba
EMAIL_NOT_VERIFIED403Verifica tu correo para activar las llamadas de prueba
EMISOR_QUOTA_EXCEEDED403El plan de prueba admite 1 NIF en live; suscríbete para más
DISPOSABLE_EMAIL400Correo de un servicio temporal: usa una dirección permanente
RATE_LIMIT_EXCEEDED429Más de 100 peticiones por minuto
LOCK_TIMEOUT409Otra factura del mismo emisor se estaba procesando: reintenta
AEAT_CONSULTA_ERROR502La AEAT no respondió a la consulta
UNAUTHORIZED401Falta el header x-api-key o no es válido

Formato de error

{
  "statusCode": 422,
  "error": "FISCAL_SEMANTIC_ERROR",
  "message": "La factura no cumple las validaciones fiscales",
  "violations": [
    {
      "code": "FISCAL_001",
      "message": "La base imponible no coincide con el desglose",
      "field": "baseImponible"
    }
  ]
}

Límites

Son dos límites distintos:

LímiteValorAlcance
Cuota del plan de prueba10 llamadasTotal acumulado en live, no se renueva cada mes (test sin tope)
Peticiones por minuto100 req/minPor cuenta (por IP si no estás autenticado)

Al superar el ritmo de 100 req/min recibes un 429 RATE_LIMIT_EXCEEDED indicando en cuántos segundos reintentar. Al agotar las 10 llamadas de prueba recibes un 429 QUOTA_EXCEEDED: se soluciona suscribiéndote, que quita la cuota (el límite por minuto se mantiene).

Las cabeceras X-RateLimit-Limit, X-RateLimit-Remaining y X-RateLimit-Reset indican tu estado actual.

¿Ves algo incorrecto? Cuéntanos