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.
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.
Con el certificado subido aparece también el domicilio del representante: Facturae
lo exige cuando la factura la firma el representante como tercero (emisores en modo
Representado). Equivale a PATCH /api/v1/account.
2 bis. FACe (solo si facturas a Administraciones Públicas)
La tarjeta FACe está plegada si no la usas. Ahí subes el certificado con el que diste
de alta tu plataforma en el portal de integradores de FACe y ves si FACe lo reconoce en
pruebas y en producción (con el botón Volver a comprobar tras dar de alta la
plataforma). Equivale a POST /api/v1/account/face-certificate. Ver
FACe (Administraciones Públicas).
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.
Por servicio: VeriFactu, Facturae y FACe
Cada petición lleva su servicio: VeriFactu (alta, anulación, consulta, PDF…),
Facturae (/facturae), FACe (/face y el certificado de plataforma) o General
(cuenta, emisores, certificados). Las pestañas Peticiones y Errores tienen un filtro
Servicio. Si usas FACe verás además un bloque FACe en el resumen, una pestaña
FACe y el estado en FACe de cada factura en la pestaña Facturas. Si no lo usas, no
aparece nada de FACe.
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 (cinco si usas FACe), todos del entorno seleccionado:
Bloque
Campo
Qué es
API
Llamadas (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 media
Media de durationMs de los últimos 7 días, en ms
IPs con más tráfico
Top 10 IPs de origen por nº de llamadas
Clientes
Emisores activos
Emisores no dados de baja en este entorno
Certificados por caducar
Los 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
VeriFactu · AEAT
Total
Registros de alta creados en este entorno
Por estado
Desglose ACEPTADO / PENDIENTE / ENVIADO / ACEPTADA_CON_ERRORES / RECHAZADO / ERROR — mismo significado que en la tabla de estados del webhook
FACe (si lo usas)
Presentadas
Facturas presentadas en FACe en este entorno, con cuántas están pagadas, en trámite, rechazadas y con anulación solicitada
Webhooks
% entregados
ENTREGADO ÷ (ENTREGADO + FALLIDO + PENDIENTE) del entorno. — si no hay ninguna entrega todavía
Fallidos
Entregas 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 FACe (si lo usas)
Una fila por factura presentada: serie, emisor, organismo (unidad tramitadora DIR3; las
tres unidades al pasar el ratón), número de registro de FACe, estado (registrada,
registrada en RCF, en trámite, pagada, rechazada, anulada) con el comentario del
organismo si lo hay, cuándo se consultó por última vez y el estado de la anulación.
El estado se consulta solo en FACe cada hora mientras pueda cambiar. Se puede filtrar
por estado.
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ó.
Estado
Significado
PENDIENTE
Encolada o reintentándose. Puede que veas alguno así y en el siguiente refresco ya haya pasado a ENTREGADO
ENTREGADO
Tu servidor respondió 2xx
FALLIDO
Se 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:
Evento
Cuándo
Envío a la AEAT / Error de envío
Cada factura enviada, aceptada, rechazada o con error — incluida cada anulación
Facturae generado
Cada Facturae firmado que se genera (no se envía a la AEAT)
Envío a FACe
Cada presentación en FACe (con su número de registro), cada fallo al presentar y cada solicitud de anulación
Cambio de configuración
Alta de emisor, certificado subido/borrado (del emisor, del representante o de la plataforma FACe), logo subido
Inicio de sesión
Cada login en el panel
Clave regenerada
Cada vez que regeneras tu clave live o test
Suscripción activada / cancelada
Altas 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.
Tipo
Cuándo se genera
Certificado por caducar
El tuyo de representante, o el de un emisor en modo Propio, a menos de 30 días de caducar
Factura rechazada
La AEAT devolvió RECHAZADO para un envío
Factura con error
El 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
FACe: factura rechazada
El organismo rechazó en FACe una factura presentada
Certificado de plataforma FACe
El de tu plataforma FACe está a menos de 30 días de caducar, o ya ha caducado
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ón
Modo Propio
Modo Representado
Alta del cliente
POST /api/v1/emisores{certMode:"PROPIO"}
POST /api/v1/emisores{certMode:"REPRESENTADO"}
Certificado
POST /api/v1/emisores/:id/certificate (cert del cliente)
Ninguno por cliente — usa tu cert representante
Emitir factura
POST /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.
En la tarjeta API Key de tu cuenta, pulsa Generar / Regenerar
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.
1 NIF live · 10 llamadas API (total, no mensual) · clave de test siempre gratis y sin tope
Pay-as-you-go
desde 6,90 €/NIF/mes
500 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, de 9:00 a 17:00 en días laborables), soporte hostelería
(149 €/mes fijos, de 9:00 a 0:00 todos los días, con disponibilidad del 99,5 %) y facturas ilimitadas por NIF
(quita el tope de 500/NIF/mes; también escalonado, desde 0,90 €/NIF).
Si un NIF pasa de 500 facturas en el mes sin el extra de ilimitadas, no se corta la
emisión: se añaden 1,80 € (más impuestos) a tu próxima factura, una sola vez por NIF
y mes, y te avisamos por correo y en el panel. Si te pasa a menudo, te sale más barato el
extra de ilimitadas. Sin suscripción de pago, al llegar a 500 la API responde
402 INVOICE_CAP_REACHED.
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) y clientes de Canarias,
Ceuta y Melilla, que quedan fuera del ámbito del IVA.
Sandbox: entornos test y live
URL base de la API:https://factuneo.es/api/v1, la misma con la clave de test y con la de live.
El estado del servicio está aparte, en GET https://factuneo.es/health.
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:
Clave
Prefijo
A dónde va
Coste
Live
vsk_live_…
AEAT real. Con validez fiscal.
Se factura por NIF (ver Planes)
Test
vsk_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://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. Como lo comparten los dos entornos, subirlo, sustituirlo o borrarlo exige la
clave live (o el panel): con la de test esas llamadas devuelven 403 LIVE_KEY_REQUIRED.
Lo mismo vale para el certificado de la plataforma FACe y el domicilio del representante
(PATCH /api/v1/account). Así una clave de test filtrada no puede dejar sin emitir a
producción.
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:
Modo
Quién aporta el certificado
Cuántos
Endpoint
PROPIO(default)
Cada emisor su propio cert
1 por emisor
POST /api/v1/emisores/:id/certificate
REPRESENTADO
El integrador, un único cert para todos
1 por cuenta
POST /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:
Valor
Qué significa
AUTORIZADO
La AEAT responde correctamente. Puedes facturar
NO_AUTORIZADO
El certificado se guarda, pero las facturas fallarán. El motivo, en aeatDetalle
NO_VERIFICADO
No 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:
Cifrado
AES-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.
Uso
Se 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 API
Nunca. Ningún endpoint devuelve los bytes: solo hasCertificate: true/false y la fecha de caducidad.
En tránsito
TLS 1.3.
Retirar el certificado
Lo que subes lo retiras tú, cuando quieras, sin escribirnos:
Qué quieres
Llamada
Retirar el cert de un emisor (sigue siendo cliente)
DELETE /api/v1/emisores/:id/certificate
Retirar tu cert representante
DELETE /api/v1/account/certificate
Eliminar a un cliente y su certificado
DELETE /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étodo
Endpoint
Descripción
POST
/api/v1/accounts
Crea cuenta + primer emisor y devuelve la API key. No requiere autenticación y la key se muestra una sola vez
Emisores y cuenta
Método
Endpoint
Descripción
POST
/api/v1/emisores
Alta de emisor (obligado)
GET
/api/v1/emisores
Listar emisores de la cuenta
GET
/api/v1/emisores/:id
Detalle de un emisor
PATCH
/api/v1/emisores/:id
Editar domicilio / certMode
DELETE
/api/v1/emisores/:id
Eliminar emisor y su certificado (conserva la ficha si tiene facturas)
POST
/api/v1/emisores/:id/certificate
Subir certificado P12 del emisor (modo PROPIO)
DELETE
/api/v1/emisores/:id/certificate
Retirar el certificado del emisor (sin borrar al emisor)
POST
/api/v1/emisores/:id/logo
Subir el logo del emisor (PNG/JPG), se imprime en el PDF
GET
/api/v1/emisores/:id/logo
Descargar el logo del emisor
DELETE
/api/v1/emisores/:id/logo
Eliminar el logo del emisor
GET
/api/v1/account
Datos de la cuenta, nº de emisores y webhookSecret
POST
/api/v1/account/certificate
Cert representante de la cuenta (modo REPRESENTADO)
DELETE
/api/v1/account/certificate
Retirar tu certificado representante (corta la emisión REPRESENTADO)
Facturas VeriFactu
Método
Endpoint
Descripción
POST
/api/v1/invoices
Crear factura (indica emisorNif si hay varios emisores)
GET
/api/v1/invoices
Listar facturas (paginado)
GET
/api/v1/invoices/:id
Obtener detalle
GET
/api/v1/invoices/:id/xml
Descargar XML del RegistroAlta
GET
/api/v1/invoices/:id/pdf
Descargar el PDF imprimible de la factura (con QR y logo)
GET
/api/v1/invoices/:id/qr
Obtener datos del QR
GET
/api/v1/invoices/:id/status
Estado y respuesta de la AEAT (CSV, motivo de rechazo)
GET
/api/v1/invoices/:id/consulta-aeat
Consultar a la AEAT + cotejo de huella
POST
/api/v1/invoices/:id/anular
Anular factura (registro de anulación)
POST
/api/v1/invoices/:id/facturae
Generar XML Facturae firmado
POST
/api/v1/invoices/:id/face
Presentar la factura en FACe (Administraciones Públicas)
GET
/api/v1/invoices/:id/face
Estado de la factura en FACe
POST
/api/v1/invoices/:id/face/anulacion
Pedir la anulación en FACe
POST
/api/v1/account/face-certificate
Subir el certificado de tu plataforma FACe
POST
/api/v1/account/face-certificate/verificar
Volver a comprobarlo con FACe
DELETE
/api/v1/account/face-certificate
Borrar el certificado de plataforma
PATCH
/api/v1/account
Domicilio del representante
Seguridad y auditoría
Método
Endpoint
Descripción
GET
/api/v1/security/compliance
Informe de cumplimiento ENS
GET
/api/v1/security/audit-integrity
Cadena de hashes: devuelve { validChain, checkedAt }
Monitorización
Método
Endpoint
Descripción
GET
/health
Health 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.
Campo
Tipo
Oblig.
Notas
name
string(1-255)
Sí
Nombre del integrador
nif
string(1-9)
Sí
NIF del primer emisor
razonSocial
string(1-255)
Sí
Razón social del primer emisor
email
string
Sí
Tu correo. Recibe el enlace que activa las llamadas de prueba
{"accountId":"9c1b…","emisorNif":"B12345678","apiKey":"vsk_live_…","apiKeyTest":"vsk_test_…","emailVerificado":false,"aviso":"Estas claves son provisionales: …"}
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).
Las claves de esta respuesta son provisionales. Valen para probar en sandbox
desde el primer momento, pero se anulan cuando verificas el correo: como esta
llamada no requiere autenticación, verificar el correo es lo que demuestra que la
cuenta es tuya. Después de verificar, entra en el panel y genera tus claves
definitivas (test y live). Así nadie que haya dado de alta una cuenta con tu
correo conserva acceso a ella.
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) ·
409 EMAIL_EXISTS (ese correo ya tiene usuario: crea la cuenta desde el panel, con tu sesión) ·
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","domicilioRepresentante":{"direccion":"Calle Mayor 1","codigoPostal":"28001","municipio":"Madrid","provincia":"Madrid","pais":"ESP"},"plataformaFace":{"nif":"B12345678","nombre":"MI SOFTWARE SL","caduca":"2028-01-01T00:00:00.000Z","verificadaPruebas":"2026-09-27T10:00:00.000Z","verificadaProduccion":null},"webhookSecret":"a3f2…"}
domicilioRepresentante y plataformaFace son null si no los has configurado.
verificadaPruebas / verificadaProduccion es la última vez que FACe reconoció tu plataforma en
cada entorno (null = no la reconoce o no se ha comprobado).
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.
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.
⚠️ 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.
Campo
Tipo
Oblig.
Por defecto
nif
string(1-9)
Sí
nombreRazon
string(1-120)
Sí
tipoPersona
F o J
No
J
certMode
PROPIO o REPRESENTADO
No
PROPIO
representacionVigente
boolean
No
false
direccion
string(≤80)
No*
codigoPostal
string(≤10)
No*
municipio
string(≤50)
No*
provincia
string(≤50)
No*
pais
string(3)
No
ESP
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:
certMode
Quién firma
Qué tienes que hacer
PROPIO
El propio obligado
Que suba su certificado a POST /emisores/:id/certificate
REPRESENTADO
Tú, por cuenta de él
Subir 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.
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) · 409 NIF_EN_OTRA_CUENTA (ese NIF ya emite en live
desde otra cuenta: un mismo obligado solo puede tener una cadena de facturación) ·
403 EMISOR_QUOTA_EXCEEDED (plan de prueba: 1 NIF en live).
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.
Campo
Tipo
nombreRazon
string(1-120)
tipoPersona
F o J
direccion / codigoPostal / municipio / provincia
string o null
pais
string(3)
certMode
PROPIO o REPRESENTADO
representacionVigente
boolean (solo modo REPRESENTADO; false corta la emisión)
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 facturas
Con facturas
Ficha del emisor
Se borra entera
Se conserva desactivada
Certificado + contraseña
Se borran
Se borran
Logo
Se borra
Se borra
Facturas registradas en AEAT
—
Intactas
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.
true si se borró la ficha entera; false si se conservó desactivada
invoiceCount
Facturas que obligan a conservarla
hadCertificate
Si 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".
Campo
Tipo
Oblig.
certificateBase64
string (el .p12/.pfx en base64)
Sí
password
string
Sí
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.
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.
Campo
Tipo
Oblig.
logoBase64
string (la imagen PNG o JPG en base64)
Sí
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://factuneo.es/api/v1/emisores/EMISOR_ID/logo \
-H "x-api-key: vsk_live_..." \
-H "Content-Type: application/json" \
-d "{\"logoBase64\": \"$BASE64\"}"
Errores:400 LOGO_UNSUPPORTED_FORMAT (no es PNG/JPG) · 400 LOGO_TOO_LARGE
(> 2 MB) · 400 LOGO_DIMENSIONS (más de 2.000 píxeles de ancho o de alto) ·
400 LOGO_INVALID (vacío, base64 no válido o imagen dañada) · 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, 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
Campo
Tipo
Oblig.
Notas
emisorNif
string(1-9)
Solo si hay varios emisores
Obligado bajo el que se emite
numeroSerie
string(1-60)
Sí
Serie + número, único por emisor
fechaExpedicion
YYYY-MM-DD
Sí
Debe ser una fecha real del calendario
tipoFactura
string(2)
Sí
Ver tabla de valores abajo
nombreRazonEmisor
string(1-120)
Sí
Razón social del emisor
descripcionOperacion
string(1-500)
Sí
Descripción de la operación
baseImponible
número
Sí
Base imponible total. Admite negativos (abonos, rectificativas por diferencias)
cuotaIva
número
Sí
Suma de las cuotas del desglose (IVA, IGIC o IPSI). No incluye el recargo de equivalencia
importeTotal
número
Sí
Base + cuotas + cuotas de recargo de equivalencia
fechaOperacion
YYYY-MM-DD
No
Solo si la operación es de otra fecha que la expedición (p. ej. factura emitida días después de la entrega)
rectificativa
objeto
Sí en R1-R5
Tipo de rectificación y factura que se rectifica (ver Rectificativas)
facturasSustituidas
array
No
Solo en F3: { numeroSerie, fechaExpedicion } de las simplificadas que sustituye
destinatarios
array
Según tipo
{ nombreRazon, nif } o, sin NIF español, { nombreRazon, idOtro } (ver Clientes extranjeros), más su domicilio (ver Factura completa). Obligatorio en F1/F3/R1-R4, se omite en F2/R5
desglose
array (1-12)
Sí
Una línea por impuesto y tipo (ver abajo)
lineas
array (1-500)
No
Conceptos de la factura: descripción, cantidad, precio unitario y descuento. No van a la AEAT; los imprimen el PDF y el Facturae (ver Factura completa)
emitidaPor
T / D
No
Solo si no la expide el propio emisor: T la expide un tercero en su nombre (p. ej. una gestoría) · D la expide el destinatario (autofacturación). Si se omite, la expide el emisor
tercero
objeto
Con emitidaPor: "T"
Quién la expide materialmente: { nombreRazon, nif } o { nombreRazon, idOtro }. No puede ser el propio emisor
subsanacion
S / N
No
Reenvío corregido de un registro previo
rechazoPrevio
N / S / X
No
Con S o Xexigesubsanacion: "S"
generateFacturae
boolean
No
Si true, genera también el Facturae en la misma llamada (default false)
tipoFactura (catálogo AEAT):
Valor
Significado
F1
Factura completa (identifica al destinatario)
F2
Factura simplificada (ticket, sin destinatario)
F3
Factura en sustitución de simplificadas
R1–R4
Rectificativas (distintos supuestos del art. 80 LIVA)
R5
Rectificativa en facturas simplificadas
desglose[] — una línea por impuesto y tipo impositivo (máximo 12, límite del XSD de la AEAT):
Campo
Tipo
Notas
impuesto
01 / 02 / 03 / 05
01 IVA (por defecto si se omite) · 02 IPSI (Ceuta y Melilla) · 03 IGIC (Canarias) · 05 otros (ver IGIC e IPSI)
claveRegimen
string
Régimen: 01 general, 02 exportación, 07 criterio de caja… (catálogo AEAT L8A con IVA, L8B con IGIC). Obligatoria salvo con impuesto: "05", que no la admite
calificacionOperacion
S1 / S2 / N1 / N2
S1 sujeta y no exenta · S2 inversión del sujeto pasivo · N1/N2 no sujeta. Excluyente con operacionExenta
operacionExenta
E1…E8
Causa de exención, en lugar de calificacionOperacion (ver Operaciones exentas). E7 y E8 solo con IGIC
tipoImpositivo
número
% del impuesto. Obligatorio en S1; 0 en S2; no se envía en exentas ni no sujetas
baseImponible
número
Base de esta línea (con signo)
cuotaRepercutida
número
Cuota del impuesto de la línea. Obligatoria en S1; 0 en S2; no se envía en exentas ni no sujetas
tipoRecargoEquivalencia
número
% de recargo de equivalencia (solo S1)
cuotaRecargoEquivalencia
número
Cuota del recargo (va junto a tipoRecargoEquivalencia)
Rectificativas (R1–R5)
Toda rectificativa lleva el bloque rectificativa. La AEAT rechaza una R1–R5 sin él.
Campo
Notas
tipo
Spor sustitución: la factura lleva los importes nuevos completos · Ipor diferencias: solo la variación, con signo
facturasRectificadas
[{ numeroSerie, fechaExpedicion }] de las facturas que corriges (del mismo emisor). Opcional para la AEAT, recomendable siempre
importeRectificacion
Solo y obligatorio en S: { baseRectificada, cuotaRectificada, cuotaRecargoRectificado? } con los importes de la factura original
motivo
Causa de la rectificación, en texto (hasta 500). No va a la AEAT, pero la factura rectificativa tiene que decirla: la imprime el PDF y pasa al Facturae. Sin ella, la respuesta trae el aviso FISCAL_021
Qué tipo usar: R1 error fundado en derecho y art. 80.Uno, Dos y Seis LIVA · R2 concurso de
acreedores (art. 80.Tres) · R3 créditos incobrables (art. 80.Cuatro) · R4 resto de casos ·
R5 rectificativa de una simplificada (va sin destinatario).
Abono de 50 € de base sobre una factura anterior (por diferencias, importes en negativo):
{"numeroSerie":"R-2026-001","fechaExpedicion":"2026-09-26","tipoFactura":"R1","rectificativa":{"tipo":"I","facturasRectificadas":[{"numeroSerie":"F-2026-120","fechaExpedicion":"2026-09-02"}],"motivo":"Devolución de 5 unidades defectuosas"},"nombreRazonEmisor":"MI EMPRESA SL","descripcionOperacion":"Devolución parcial de la factura F-2026-120","destinatarios":[{"nombreRazon":"CLIENTE SA","nif":"A28000024"}],"desglose":[{"claveRegimen":"01","calificacionOperacion":"S1","tipoImpositivo":21,"baseImponible":-50,"cuotaRepercutida":-10.5}],"baseImponible":-50,"cuotaIva":-10.5,"importeTotal":-60.5}
Para sustituir la factura entera, usa "tipo": "S", pon en el desglose los importes
correctos completos y añade "importeRectificacion": { "baseRectificada": 100, "cuotaRectificada": 21 }
con los de la original. En las rectificativas por diferencias no se exige que cuota y base tengan
el mismo signo (una R5 que solo corrige el tipo de IVA puede ir con base 0 y cuota negativa).
Para anular una factura que nunca debió existir no emitas una rectificativa: usa
POST /api/v1/invoices/:id/anular.
Operaciones exentas
La línea lleva operacionExenta en lugar de calificacionOperacion, y sintipoImpositivo,
cuotaRepercutida ni recargo. El PDF imprime la mención legal de la exención.
Valor
Supuesto
E1
Exenta por el art. 20 LIVA (sanidad, educación, seguros, alquiler de vivienda…)
E2
Exportación (art. 21). Va con claveRegimen: "02"
E3
Operaciones asimiladas a la exportación (art. 22)
E4
Regímenes aduaneros y fiscales (arts. 23 y 24)
E5
Entrega intracomunitaria (art. 25). El cliente se identifica con su NIF-IVA europeo (idOtro, idType: "02")
Código ISO de 2 letras (FR, DE, US…). Obligatorio salvo con idType: "02"
idType
02 NIF-IVA (empresas de la UE) · 03 pasaporte · 04 documento oficial del país · 05 certificado de residencia · 06 otro documento · 07 no censado (solo con codigoPais: "ES")
Cada destinatario lleva nifoidOtro, nunca los dos.
Recargo de equivalencia
Para ventas a comerciantes minoristas en recargo de equivalencia, añade a la línea S1 el tipo y
la cuota del recargo. La cuota del recargo no va en cuotaIva, pero sí suma en importeTotal.
Cada línea del desglose indica su impuesto con impuesto. Si se omite, la AEAT entiende IVA, así
que las facturas con IVA no cambian. En una misma factura puede haber líneas de impuestos distintos.
impuesto
Claves de régimen admitidas
Notas
01 IVA
L8A: 01–11, 14, 15, 17–20
Tipos limitados a 0, 4, 10 y 21 % (y los temporales en su periodo); recargo de equivalencia
03 IGIC
L8B: 01–11, 14, 15, 17–19, más 20 (sujetas al IPSI, calificación N2) y 21 (régimen simplificado)
Cualquier tipo (0, 3, 7, 9,5, 15…). Exenciones E1–E8
02 IPSI
01, 08, 11, 18, 19, 20
Cualquier tipo. Desde el 1/1/2027 la AEAT rechaza el registro sin clave de régimen
cuotaIva es la suma de las cuotas de todas las líneas, sea cual sea su impuesto. El PDF nombra el
impuesto en cada línea y en los totales («Cuota IGIC»), y cita la ley de cada exención (Ley 20/1991 del
IGIC, Ley 8/1991 del IPSI…). En el Facturae cada impuesto va con su código (TaxTypeCode 01, 02 o 03).
Factura completa: domicilios y conceptos
El registro de la AEAT no lleva domicilios ni conceptos, pero la factura que entregas a tu cliente
sí tiene que llevarlos (art. 6 del Reglamento de facturación, RD 1619/2012). Envíalos en la misma
llamada y el PDF será una factura completa:
Domicilio del emisor: sale de la ficha del emisor (direccion, codigoPostal, municipio, provincia).
Conceptos: lineas[], con precio unitario sin impuestos y descuentos.
Campo de lineas[]
Tipo
Oblig.
Notas
descripcion
string(1-500)
Sí
cantidad
número
Sí
Distinta de 0. Admite decimales y negativos (devoluciones)
precioUnitario
número
Sí
Sin impuestos. Hasta 6 decimales
descuento
número
No
En euros, sobre el total de la línea
impuesto
01 / 02 / 03 / 05
No
Por defecto 01 (IVA)
tipoImpositivo
número
No
% de la línea. Omítelo en exentas y no sujetas
El importe de cada concepto es cantidad × precioUnitario − descuento, redondeado a céntimos. La
suma de los conceptos tiene que ser la base del desglose; si todos llevan tipoImpositivo, además
tienen que cuadrar tipo a tipo. Si no, la API responde 422 con FISCAL_020.
"destinatarios":[{"nombreRazon":"CLIENTE SA","nif":"A28000024","domicilio":{"direccion":"Av. del Comercio 5","codigoPostal":"28001","municipio":"Madrid","provincia":"Madrid"}}],"lineas":[{"descripcion":"Horas de consultoría","cantidad":12.5,"precioUnitario":60,"descuento":50,"tipoImpositivo":21},{"descripcion":"Licencia anual","cantidad":1,"precioUnitario":300,"tipoImpositivo":21}],"desglose":[{"claveRegimen":"01","calificacionOperacion":"S1","tipoImpositivo":21,"baseImponible":1000,"cuotaRepercutida":210}],"baseImponible":1000,"cuotaIva":210,"importeTotal":1210
Si falta algún domicilio (o la causa en una rectificativa), la factura se emite igual y la
respuesta lo avisa en warnings con el código FISCAL_021: el registro de la AEAT es válido, pero
el PDF no sería una factura completa. Las simplificadas (F2, R5) no los necesitan.
Facturas expedidas por un tercero o por el destinatario
Si tu software emite facturas en nombre de tus clientes (gestorías, asesorías, plataformas),
la AEAT pide indicarlo: "emitidaPor": "T" y en tercero los datos de quien la expide. En la
autofacturación, cuando es el cliente quien expide la factura del proveedor, usa "emitidaPor": "D";
el PDF incluye la mención «Facturación por el destinatario».
Las facturas de 100.000.000 € o más llevan automáticamente la marca Macrodato que exige la AEAT.
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).
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 EMISOR_CERTIFICATE_NOT_CONFIGURED
(el emisor aún no tiene certificado) · 400 ACCOUNT_REPRESENTATIVE_CERT_NOT_CONFIGURED
(modo REPRESENTADO sin certificado de la cuenta) · 402 INVOICE_CAP_REACHED · 400 XSD_VALIDATION_FAILED ·
403 (emisor desactivado o sin representación vigente) ·
409 LOCK_TIMEOUT (otra factura del mismo emisor en curso: reintenta) ·
409 FACTURA_DUPLICADA (ya emitiste esa serie y fecha para este emisor; para
corregirla, reenvíala con subsanacion: "S") ·
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.
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.
También origen: FACTUNEO si lo generó la API, o EXTERNO si lo generó otro sistema y
solo lo remitimos (ver Registros de otro sistema); en ese caso, sistemaExterno dice qué
sistema e instalación lo generó y si se remitió con incidencia.
Incluye el bloque aeat con lo que respondió la AEAT, para saber por qué se
rechazó una factura sin depender de los webhooks:
"aeat":{"destino":"PRODUCCION","csv":null,"estadoRegistro":"Incorrecto","codigoError":"3239","descripcionError":"Error en el bloque de Destinatario. NIF no identificado","motivoError":null}
csv es el Código Seguro de Verificación cuando la AEAT acepta. motivoError
explica un fallo antes de llegar a la AEAT (certificado ilegible, emisor sin
representación…). Mientras la factura está PENDIENTE, todo va a null.
GET /api/v1/invoices/:id/xml
Para qué sirve: obtener el XML RegistroAlta tal cual se remite a la AEAT.
En la modalidad VERI*FACTU el registro no lleva firma electrónica: lo autentica el
envío con el certificado del obligado (TLS mutuo) y lo encadena la huella. Ú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 lo que pide el art. 6 del Reglamento de
facturación: número y fecha, datos y domicilio del emisor y del destinatario, los conceptos
con cantidad, precio unitario y descuento (si los enviaste en lineas), la base, el tipo y la cuota
de cada impuesto (IVA, IGIC o IPSI), la fecha de operación, los totales, el QR de cotejo de la
AEAT y la huella. Si no cabe en una página, sigue en la siguiente, con la huella y el número de
página en cada una.
Si el emisor tiene logo subido, aparece bajo el nombre de la empresa.
El QR se coloca como exige la especificación de la AEAT: al principio de la factura, centrado,
de 38,8 mm, precedido de «QR tributario:» y seguido de «Factura verificable en la sede electrónica
de la AEAT». El PDF añade las menciones obligatorias que correspondan: rectificativa, factura
rectificada y causa de la rectificación, precepto de la exención, inversión del sujeto pasivo, regímenes especiales (criterio de
caja, agencias de viajes, bienes usados), facturación por el destinatario o por un tercero.
Se sirve como Content-Type: application/pdf con
Content-Disposition: attachment; filename="<numeroSerie>.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.
Para qué sirve: saber en qué estado está una factura y qué respondió la AEAT,
sin descargar la factura entera. Devuelve estadoEnvio, actualizadoEn y el mismo
bloque aeat que GET /api/v1/invoices/:id (código y motivo de un rechazo, CSV).
statuses es el historial de estados de Facturae/FACe (aceptación, pago…), si
la factura se presentó por esa vía; en las facturas solo VeriFactu va vacío.
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.
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-EPES conforme a la política de firma de Facturae v3.1 (Content-Type: application/xml): firma el documento, las propiedades de firma y el certificado, con rol de emisor. Admite compradores extranjeros (su domicilio va como dirección en el extranjero).
El emisor necesita domicilio fiscal completo.
Las líneas del Facturae son los conceptos de la factura (lineas, con su precio unitario y
descuento) o, si no se enviaron, una por línea del desglose. Los impuestos de la factura son
exactamente los del registro de la AEAT, cada uno con su código (IVA, IGIC o IPSI).
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 B2B, se lo entregas a tu
cliente por el canal que uséis (el intercambio obligatorio del Real Decreto 238/2026, por la
solución pública de la AEAT o una plataforma privada, con comunicación de estados, aún no lo
ofrecemos); si es a una Administración Pública, la podemos
presentar nosotros en FACe con POST /api/v1/invoices/:id/face (ver más abajo).
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)
Tipo
Oblig.
Por defecto
taxIdentificationNumber
string(1-30)
Sí
personTypeCode
F (física) o J (jurídica)
No
J
residenceTypeCode
R (España) o U (UE) o E (resto)
No
R
corporateName
string(1-80)
Si es J
name / firstSurname
string(1-40)
Si es F
address
objeto
Sí, salvo que la factura se emitiera con el domicilio español completo del destinatario
Si se omite, se usa ese domicilio
address.address
string(1-80)
Sí
address.postCode
string(1-10)
Sí
address.town
string(1-50)
Sí
address.province
string(1-50)
Sí
address.countryCode
string(3)
No
ESP
dir3
objeto
Solo para Administraciones
{ oficinaContable, organoGestor, unidadTramitadora }: códigos DIR3 del organismo. Obligatorio para FACe
Si el emisor es persona física, Facturae exige su nombre y apellidos por separado. Si firma con
su propio certificado se toman de él; si no, envíalos en seller: { name, firstSurname, secondSurname? }
(si faltan: 400 SELLER_NAME_REQUIRED).
Si el emisor está en modo REPRESENTADO, la factura la firma el representante de la cuenta y
Facturae lo identifica como tercero (InvoiceIssuerTypeTE). Facturae exige su domicilio:
envíalo en tercero: { address: { address, postCode, town, province, countryCode } }. Su NIF y
nombre salen de la cuenta; si es persona física, el nombre y los apellidos se toman de su
certificado o de tercero.name / firstSurname / secondSurname (si faltan:
400 THIRD_PARTY_DATA_REQUIRED).
Rectificativas. Si la factura es rectificativa, el Facturae es una «original rectificativa»
(InvoiceClass OR) con el bloque Corrective: número y fecha de la factura rectificada (del
registro), motivo, periodo, método de corrección y la causa (rectificativa.motivo del alta). Admite
importes negativos. En el cuerpo indica al menos el motivo con su código oficial de Facturae:
Campo (dentro de rectificativa)
Oblig.
Notas
motivo
Sí
01–16 error en un dato de la factura (01 número, 03 fecha, 07 NIF del receptor, 09 domicilio del receptor, 10 detalle de la operación, 11 tipo impositivo, 16 base imponible…) · 80–85 art. 80 LIVA (82 devolución de envases, 83 descuentos y bonificaciones, 84 resolución firme, 85 impago con auto de concurso)
metodo
No
01 íntegra · 02 por diferencias · 03 descuento por volumen · 04 autorizada por la AEAT. Por defecto, 01 si la rectificativa es por sustitución y 02 si es por diferencias
periodo
No
{ desde, hasta } (YYYY-MM-DD). Por defecto, el mes de la factura rectificada
400 SELLER_ADDRESS_NOT_CONFIGURED — falta el domicilio del emisor.
400 BUYER_ADDRESS_REQUIRED — falta buyer.address y la factura no tiene guardado el domicilio del destinatario.
400 FACTURAE_MOTIVO_RECTIFICACION_REQUERIDO — es una rectificativa y falta rectificativa.motivo.
400 FACTURAE_FACTURA_RECTIFICADA_REQUERIDA — con los métodos 01 y 02 Facturae exige el número de la factura rectificada, y el registro no la cita.
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.
400 SELLER_NAME_REQUIRED — el emisor es persona física y no hay nombre y apellidos (ver seller).
400 THIRD_PARTY_DATA_REQUIRED — modo REPRESENTADO sin el domicilio (o el nombre) del representante (ver tercero).
500 SIGNING_ERROR — no se pudo firmar con el certificado.
POST /api/v1/invoices/:id/face
Para qué sirve: presentar la factura a una Administración Pública en FACe, el punto
general de entrada de facturas electrónicas del Estado (obligatorio para facturar al sector
público, Ley 25/2013). Genera el Facturae firmado igual que el endpoint anterior, con las
unidades DIR3 del organismo, y lo remite a FACe. Devuelve el número de registro de FACe.
Quién firma y con qué plataforma. La factura la firma el emisor (o su representante) con su
certificado, como en /facturae. El envío se autentica ante FACe con tu plataforma FACe: la
que das de alta en el portal de integradores de FACe con tu certificado. Factuneo no firma tus
facturas ni presenta con una plataforma propia. Ver FACe (Administraciones Públicas).
Con una clave de test se presenta en el entorno de pruebas de FACe (puedes usar la unidad de
pruebas P00000010 como oficina contable, órgano gestor y unidad tramitadora); con una clave
live, en FACe de producción. Cada entorno necesita tu plataforma dada de alta en él.
Cuerpo: el mismo que /facturae, con buyer.dir3obligatorio, más:
Campo
Tipo
Oblig.
Notas
email
string
Sí
Correo al que FACe enviará los avisos de la factura
incluirPdf
boolean
No
Adjunta el PDF de la factura como anexo (por defecto false)
Los códigos DIR3 te los da el organismo (suelen figurar en el pedido o el contrato) y se pueden
consultar en el directorio de FACe.
Estados de FACe:1200 registrada · 1300 registrada en el registro contable · 2400 en
trámite (contabilizada) · 2500 pagada · 2600 rechazada · 3100 anulada.
Errores:
409 FACE_YA_ENVIADA — la factura ya está en FACe. Solo se puede volver a presentar si FACe la rechazó o la anuló.
422 FACE_RECHAZADA — FACe no la ha admitido; detalle trae su motivo (p. ej. una unidad DIR3 inexistente).
422 FACE_PLATAFORMA_NO_REGISTRADA — FACe no reconoce el certificado con que se autentica el envío: da de alta tu plataforma con él.
422 FACE_PLATAFORMA_NO_CONFIGURADA — no hay certificado con el que presentar (ni de plataforma ni del emisor).
502 FACE_NO_RESPONDE — FACe no responde. Reintenta más tarde.
Además, los mismos errores de /facturae.
GET /api/v1/invoices/:id/face
Presentaciones de la factura en FACe con su estado, consultado a FACe en ese momento (registrada,
en trámite, pagada, rechazada, anulada…). Si FACe no responde, devuelve el último estado conocido
con "actualizado": false. No hace falta que lo consultes tú: el estado se sigue solo y cada
cambio te llega por el webhook invoice.face_status_changed.
POST /api/v1/invoices/:id/face/anulacion
Pide al organismo que anule la factura en FACe, con el motivo: { "motivo": "Importe erróneo" }.
La decisión es del organismo; se ve en anulacion (4200 solicitada, 4300 aceptada, 4400 rechazada).
Para corregir una factura ya registrada en la AEAT, además, emite la rectificativa correspondiente.
Seguridad y auditoría
GET /api/v1/security/compliance
Para qué sirve: un informe de cumplimiento (ENS) de tu cuenta, en el entorno de la
clave con la que lo pides (live o test; valen las dos). Devuelve una puntuación y el
detalle por control. Úsalo para tu propia auditoría o para justificar el cumplimiento
ante un cliente.
Control
Qué comprueba
MP.SI.1.1
Que la cuenta tiene clave de API en ese entorno
MP.SI.6.1
Que el límite de peticiones por minuto está en un rango razonable
MP.SI.5.1
Que cada emisor activo puede firmar: su certificado (modo Propio) o el del representante con la representación vigente (modo Representado). Si alguno no puede, lo dice con su NIF
MP.SI.5.2
Que los certificados en uso están vigentes: FAIL si alguno ha caducado, WARN si caduca en menos de 30 días o no se conoce su caducidad
MP.SI.5.3
Cómo se protegen las claves privadas (cifradas con AES-256-GCM, nunca se devuelven)
MP.SI.4.1
Que la cuenta está activa
MP.SI.3.1
Que la auditoría encadenada por hash está habilitada
{"tenantId":"3f8a…","environment":"live","overallStatus":"PARTIAL","score":86,"checkedAt":"2026-09-29T10:00:00.000Z","controls":[{"code":"MP.SI.5.2","name":"Vigencia de los certificados","status":"WARN","message":"Certificado que caduca en menos de 30 días en 1 emisor(es): B12345678.","category":"CRIPTOGRAFIA"}]}
overallStatus es COMPLIANT si todo pasa, PARTIAL si hay avisos y NON_COMPLIANT
si algún control falla.
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 (con la clave live o la de test). Recalcula la cadena y confirma que
sigue íntegra: es la garantía de inalterabilidad que exige la normativa (ENS). La
verificación se repite como mucho cada 10 minutos: checkedAt dice cuándo se hizo.
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.
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.
Recetas: hostelería y tickets
Peticiones completas para un TPV de restaurante, probadas contra la API: cada ejemplo
pasa la validación fiscal, el XSD de la AEAT y genera su huella y su QR. Cópialas con tu
emisorNif y tus series.
Lo básico
Qué
Valor
URL base
https://factuneo.es/api/v1. La misma con la clave de test y la de live: la clave decide el entorno
Autenticación
Cabecera x-api-key
Salud del servicio
GET https://factuneo.es/health (público, fuera de /api/v1)
Fechas
AAAA-MM-DD (2026-10-22). La de expedición no puede ser futura
Importes
Número JSON, no texto: 45, 5.82. Con signo menos en las rectificativas por diferencias
IVA
En desglose[] (obligatorio), una línea por tipo. En lineas[] (opcional) cada concepto lleva su tipoImpositivo y su precioUnitariosin IVA; si los envías, tienen que cuadrar con el desglose tipo a tipo (422 FISCAL_020)
Reintentos
Cabecera Idempotency-Key con un valor único por ticket: repetir la petición con la misma clave devuelve la misma factura (200), sin duplicarla
Modo representado: el cuerpo es el mismo. Cada ticket indica el restaurante en
emisorNif y nombreRazonEmisor, y se envía con tu certificado de representante. El
restaurante tiene que estar dado de alta con certMode: "REPRESENTADO" y
representacionVigente: true; si no, 403 EMISOR_REPRESENTATION_NOT_VALID.
Ticket simplificado (F2)
Un ticket con el IVA de hostelería (10 %) y una venta para llevar al 21 %. Sin
destinatarios: el ticket no identifica al cliente.
Un ticket F2 no puede pasar de 3.000 € (base más cuotas): la AEAT lo rechaza (error 1150) y la API
lo frena antes con 422 FISCAL_001. Por encima, emite una factura completa (F1).
{"emisorNif":"B12345674","numeroSerie":"T1-2026-000123","fechaExpedicion":"2026-10-22","tipoFactura":"F2","nombreRazonEmisor":"RESTAURANTE EJEMPLO SL","descripcionOperacion":"Consumo en mesa 7","lineas":[{"descripcion":"Menú del día","cantidad":2,"precioUnitario":15,"tipoImpositivo":10},{"descripcion":"Café","cantidad":2,"precioUnitario":1.5,"tipoImpositivo":10},{"descripcion":"Botella de vino para llevar","cantidad":1,"precioUnitario":12,"tipoImpositivo":21}],"desglose":[{"claveRegimen":"01","calificacionOperacion":"S1","tipoImpositivo":10,"baseImponible":33,"cuotaRepercutida":3.3},{"claveRegimen":"01","calificacionOperacion":"S1","tipoImpositivo":21,"baseImponible":12,"cuotaRepercutida":2.52}],"baseImponible":45,"cuotaIva":5.82,"importeTotal":50.82}
Id de la factura en Factuneo: para consultarla, anularla o descargar su PDF
hash
Huella SHA-256 encadenada del registro
qrUrl
URL de cotejo de la AEAT: es la URL de verificación de la factura
qrString
Igual que qrUrl. Es el texto que codificas en el QR del ticket
warnings
Avisos no bloqueantes, si los hay (la factura se ha creado)
La respuesta no trae una URL del PDF: se descarga con GET /api/v1/invoices/{invoiceId}/pdf
(con tu x-api-key). El resultado del envío a la AEAT llega después por webhook o con
GET /api/v1/invoices/{invoiceId}.
Entre 30×30 y 40×40 mm, norma ISO/IEC 18004:2015 y nivel M de corrección de errores.
Al principio del ticket, antes del resto del contenido, con al menos 2 mm de margen en blanco alrededor (se recomiendan 6 mm).
Encima, el texto «QR tributario:». Justo debajo, «Factura verificable en la sede electrónica de la AEAT» o «VERI*FACTU», con letra igual o mayor que la del resto del ticket.
Factura completa que sustituye al ticket (F3)
Cuando el cliente pide factura de un ticket ya emitido. Lleva los datos y el domicilio del
cliente y, en facturasSustituidas, el ticket que sustituye.
{"emisorNif":"B12345674","numeroSerie":"F-2026-000045","fechaExpedicion":"2026-10-22","tipoFactura":"F3","nombreRazonEmisor":"RESTAURANTE EJEMPLO SL","descripcionOperacion":"Consumo en mesa 7 (sustituye al ticket T1-2026-000123)","facturasSustituidas":[{"numeroSerie":"T1-2026-000123","fechaExpedicion":"2026-10-22"}],"destinatarios":[{"nombreRazon":"CLIENTE EMPRESA SA","nif":"A28000024","domicilio":{"direccion":"Rúa do Vilar 10","codigoPostal":"15705","municipio":"Santiago de Compostela","provincia":"A Coruña"}}],"lineas":[{"descripcion":"Menú del día","cantidad":2,"precioUnitario":15,"tipoImpositivo":10},{"descripcion":"Café","cantidad":2,"precioUnitario":1.5,"tipoImpositivo":10},{"descripcion":"Botella de vino para llevar","cantidad":1,"precioUnitario":12,"tipoImpositivo":21}],"desglose":[{"claveRegimen":"01","calificacionOperacion":"S1","tipoImpositivo":10,"baseImponible":33,"cuotaRepercutida":3.3},{"claveRegimen":"01","calificacionOperacion":"S1","tipoImpositivo":21,"baseImponible":12,"cuotaRepercutida":2.52}],"baseImponible":45,"cuotaIva":5.82,"importeTotal":50.82}
El PDF de la factura completa: GET /api/v1/invoices/{invoiceId}/pdf.
Rectificativa de un ticket por diferencias (R5, tipo I)
Solo la variación, en negativo. Aquí, la devolución de un café:
{"emisorNif":"B12345674","numeroSerie":"R-2026-000007","fechaExpedicion":"2026-10-22","tipoFactura":"R5","rectificativa":{"tipo":"I","facturasRectificadas":[{"numeroSerie":"T1-2026-000123","fechaExpedicion":"2026-10-22"}],"motivo":"Devolución de un café no servido"},"nombreRazonEmisor":"RESTAURANTE EJEMPLO SL","descripcionOperacion":"Devolución parcial del ticket T1-2026-000123","lineas":[{"descripcion":"Café","cantidad":-1,"precioUnitario":1.5,"tipoImpositivo":10}],"desglose":[{"claveRegimen":"01","calificacionOperacion":"S1","tipoImpositivo":10,"baseImponible":-1.5,"cuotaRepercutida":-0.15}],"baseImponible":-1.5,"cuotaIva":-0.15,"importeTotal":-1.65}
Rectificativa de un ticket por sustitución (R5, tipo S)
Los importes correctos completos en el desglose y, en importeRectificacion, los del
ticket original:
{"emisorNif":"B12345674","numeroSerie":"R-2026-000008","fechaExpedicion":"2026-10-22","tipoFactura":"R5","rectificativa":{"tipo":"S","facturasRectificadas":[{"numeroSerie":"T1-2026-000123","fechaExpedicion":"2026-10-22"}],"importeRectificacion":{"baseRectificada":45,"cuotaRectificada":5.82},"motivo":"Se cobró un menú que no se sirvió"},"nombreRazonEmisor":"RESTAURANTE EJEMPLO SL","descripcionOperacion":"Sustituye al ticket T1-2026-000123","lineas":[{"descripcion":"Menú del día","cantidad":1,"precioUnitario":15,"tipoImpositivo":10},{"descripcion":"Café","cantidad":2,"precioUnitario":1.5,"tipoImpositivo":10},{"descripcion":"Botella de vino para llevar","cantidad":1,"precioUnitario":12,"tipoImpositivo":21}],"desglose":[{"claveRegimen":"01","calificacionOperacion":"S1","tipoImpositivo":10,"baseImponible":18,"cuotaRepercutida":1.8},{"claveRegimen":"01","calificacionOperacion":"S1","tipoImpositivo":21,"baseImponible":12,"cuotaRepercutida":2.52}],"baseImponible":30,"cuotaIva":4.32,"importeTotal":34.32}
Un ticket emitido por error (no hubo venta) no se rectifica: se anula con
POST /api/v1/invoices/{invoiceId}/anular y cuerpo {} (ver POST /api/v1/invoices/:id/anular).
Si el restaurante se queda sin conexión
La huella y el QR se generan en la API en el momento de la llamada, así que cada ticket
necesita conexión para emitirse con su QR. Según la AEAT, el registro de facturación se
genera a la vez que la factura y el ticket que se entrega tiene que llevar ya el QR: no
vale entregarlo sin QR y registrarlo después (aclaraciones a desarrolladores).
Recomendamos una conexión de respaldo en cada local.
Si tu TPV es capaz de generar él mismo los registros durante la caída (con su propio sistema
informático y su declaración responsable), puede enviárnoslos al volver la red y los remitimos sin
tocarlos: ver Registros de otro sistema.
Las caídas de la AEAT no te afectan: la API responde al momento y reenvía el registro
sola hasta que la AEAT lo acepta.
FACe: facturar a Administraciones Públicas
Facturae es el formato de factura electrónica (el XML firmado que genera
POST /invoices/:id/facturae). FACe es la plataforma del Estado donde se presentan las
facturas a los organismos públicos (Ley 25/2013). POST /invoices/:id/face genera el Facturae y
lo presenta en FACe por ti.
FACe está incluido en todos los planes y es opcional: si no facturas a la Administración no
tienes que configurar nada. Presenta facturas emitidas con Factuneo, así que requiere usar también
VeriFactu con nosotros.
1. Da de alta tu plataforma en FACe (una vez)
FACe solo acepta envíos de plataformas dadas de alta en su portal de integradores, identificadas
por su certificado. La plataforma es tuya: Factuneo presenta con ella, no con una propia.
Entra en el portal de integradores con Cl@ve o certificado: pruebasse-integradores-face.redsara.es · producciónintegradores.face.gob.es. Son dos altas
distintas: haz la de pruebas para tu clave de test y la de producción para la live.
Firma la declaración de alta como integrador. Si vas a presentar facturas de tus clientes
(software para terceros), marca «Servicios a terceros».
En Certificados, sube la parte pública de tu certificado (.cer en base64/PEM).
En Plataformas, crea una con servicio de envío y asóciale ese certificado.
2. Sube ese certificado a Factuneo
Con POST /api/v1/account/face-certificate ({ certificateBase64, password }, el .p12 con su
clave privada) o desde la tarjeta FACe del panel. Lo comprobamos con FACe en pruebas y en
producción y te decimos si lo reconoce:
no_registrado = da de alta la plataforma con ese certificado en ese entorno y vuelve a comprobarlo
con POST /api/v1/account/face-certificate/verificar. GET /api/v1/account devuelve su estado en
plataformaFace, y te avisamos antes de que caduque.
Qué certificado se usa para presentar, por orden: el de tu plataforma; si no lo has subido, el
del propio emisor (que entonces tendrá que tener su plataforma dada de alta con él); y en emisores
en modo Representado, el de representante de tu cuenta.
3. Presenta y olvídate
POST /api/v1/invoices/:id/face con las unidades DIR3 del organismo. Después, el estado se consulta
solo en FACe (cada hora mientras pueda cambiar) y cada cambio te llega por el webhook
invoice.face_status_changed: registrada, en trámite (contabilizada), pagada,
rechazada o anulada, y la respuesta a una solicitud de anulación. Si el organismo la rechaza,
además lo verás en las alertas del panel.
Emisores en modo Representado
La factura la firma el representante como tercero y Facturae exige su domicilio. Guárdalo una
vez con PATCH /api/v1/account:
{"domicilioRepresentante":{"direccion":"Calle Mayor 1","codigoPostal":"28001","municipio":"Madrid","provincia":"Madrid"}}
Incidencias conocidas del portal de FACe
Al subir el certificado, el portal falla con ficheros en binario (DER): usa el .ceren base64
(el que empieza por -----BEGIN CERTIFICATE-----).
Si al marcar «Servicios a terceros» el portal devuelve un error, puedes seguir: no es necesario
para presentar facturas.
Registros generados por otro sistema (TPV sin conexión)
Si el TPV de otro fabricante genera sus propios registros VeriFactu —por ejemplo, mientras el
local no tiene internet—, al volver la conexión puede enviárnoslos y los remitimos a la AEAT tal
cual, sin generar ni modificar nada. Es el papel de «instrumento para la remisión» que admiten las
aclaraciones de la AEAT a desarrolladores (pregunta 5).
registros: hasta 100 RegistroAlta o RegistroAnulacion, en el orden en que se
generaron y todos del mismo obligado (su NIF tiene que ser un emisor de tu cuenta).
incidencia: S si se generaron durante una incidencia (sin conexión, corte de corriente…).
Se remiten con Incidencia = S en un envío aparte.
Qué comprobamos (sin tocar el registro)
Que cumple el esquema oficial de la AEAT, que es de un emisor de tu cuenta, que no lo generó
Factuneo y que no duplica una factura ya existente (para la AEAT, la factura es NIF + serie + fecha,
venga del sistema que venga). Si falla, ese registro vuelve con su motivo (XSD_INVALIDO,
SISTEMA_PROPIO, FACTURA_DUPLICADA…) y los demás siguen adelante.
Si la huella o el encadenamiento no cuadran, lo remitimos igualmente y te avisamos
(HUELLA_NO_CUADRA, CADENA_NO_CUADRA): la AEAT los acepta con error y hay que subsanarlos desde
el sistema que los generó. Enviar dos veces el mismo registro no lo duplica.
Cada registro queda como una factura más, con origen: "EXTERNO": su estado, sus webhooks
(invoice.accepted, invoice.rejected… con origin) y la consulta a la AEAT funcionan igual. Lo
que corresponde al sistema que lo generó (su anulación, su PDF, su Facturae) responde
409 REGISTRO_EXTERNO. Cuentan para el límite de facturas de tu plan.
Lo que tiene que hacer el fabricante del TPV
Generar el registro, la huella encadenada y el ticket con QR en el momento de la venta.
Poner en los registros su bloque SistemaInformatico (su NIF, su identificador, su versión) y
un NumeroInstalacion propio en cada TPV: cada TPV lleva así su propia cadena, separada de la
de Factuneo y de la de los demás TPV.
Usar una serie propia para lo que emita sin conexión, para no repetir la de la API.
Tener su declaración responsable, que debe mencionar la versión de Factuneo que usa para remitir.
Enviarnos los registros todos y en orden en cuanto vuelva la conexión.
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.
Evento
Cuándo se emite
invoice.accepted
La AEAT aceptó la factura (con o sin avisos)
invoice.rejected
La AEAT la rechazó. Mira data.aeat
invoice.error
No se pudo enviar. El porqué, en data.reason
invoice.cancelled
La AEAT aceptó una anulación
certificate.expiring
Un certificado tuyo caduca pronto (30 días)
certificate.expired
Un certificado tuyo ya ha caducado
invoice.face_status_changed
Cambió el estado de una factura en FACe (registrada, en trámite, pagada, rechazada, anulada) o de su solicitud de anulación
Los de certificate.* solo se emiten en producción: los certificados de
sandbox son de prueba y da igual que caduquen. data.certificateType dice cuál:
EMISOR, REPRESENTANTE o FACE_PLATAFORMA.
En invoice.face_status_changed, data trae el estado nuevo y el anterior:
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.
data.origin vale EXTERNO cuando el registro lo generó otro sistema (por ejemplo, tu TPV
durante una caída de red) y solo lo remitimos; FACTUNEO en el resto.
En certificate.*, data trae certificateType (EMISOR o
REPRESENTANTE), emisorId, nif, expiresAt y daysUntilExpiry,
negativo si el certificado ya caducó.
Cabeceras
Cabecera
Contenido
X-Webhook-Signature
HMAC-SHA256 del cuerpo, en hexadecimal
X-Webhook-Id
Id del evento (el mismo id del cuerpo)
X-Webhook-Delivery
Id de esta entrega concreta. Cambia en cada reenvío
X-Webhook-Event
Nombre del evento
X-Webhook-Environment
live o test
X-Webhook-Attempt
Nº 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.
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ódigo
Significado
200
OK
201
Creado
400
Bad Request - Datos inválidos o schema incorrecto
401
Unauthorized - API Key inválida o ausente
402
Payment Required - Tope mensual de facturas por NIF alcanzado
403
Forbidden - Sin permisos para este recurso
404
Not Found - Recurso no existe
409
Conflict - Conflicto (ej: serie duplicada, NIF exists)
413
Payload Too Large - Cuerpo mayor que el permitido (1 MB; 4 MB en la subida de logos)
415
Unsupported Media Type - Envía Content-Type: application/json
422
Unprocessable Entity - Errores de validación fiscal
429
Too Many Requests - Rate limit excedido o cuota de API agotada
500
Internal Server Error - Error interno
502
Bad Gateway - La AEAT no respondió (consulta)
Códigos de error más habituales
El campo error identifica la causa de forma estable (el message puede cambiar).
Código
HTTP
Qué ha pasado
NO_EMISOR
400
La cuenta no tiene ningún emisor. Da uno de alta con POST /api/v1/emisores
EMISOR_REQUIRED
400
Tienes varios emisores: indica emisorNif en la petición
EMISOR_NOT_FOUND
404
No existe un emisor activo con ese NIF en tu cuenta
EMISOR_DUPLICADO
409
Ya existe un emisor con ese NIF en la cuenta
NIF_EN_OTRA_CUENTA
409
Ese NIF ya emite en live desde otra cuenta
FACTURA_DUPLICADA
409
Ya existe esa serie y fecha para el emisor, la generase la API u otro sistema (corrige con subsanacion: "S")
REGISTRO_EXTERNO
409
El registro lo generó otro sistema y solo lo remitimos: su anulación, su PDF o su Facturae le corresponden a ese sistema
VARIOS_OBLIGADOS
400
En POST /api/v1/registros-externos, registros de más de un obligado en la misma llamada
EMISOR_CERTIFICATE_NOT_CONFIGURED
400
El emisor está en modo PROPIO y aún no tiene certificado
ACCOUNT_REPRESENTATIVE_CERT_NOT_CONFIGURED
400
Modo REPRESENTADO sin certificado de representante en la cuenta
EMISOR_REPRESENTATION_NOT_VALID
403
El emisor no tiene representación vigente
CERTIFICATE_NIF_MISMATCH
400
El NIF del certificado no coincide con el del emisor
CERTIFICATE_EXPIRED / CERTIFICATE_INVALID
400
Certificado caducado o ilegible (¿contraseña correcta?)
FISCAL_SEMANTIC_ERROR
422
Incumple reglas fiscales de la AEAT (ver violations)
FACTURAE_MOTIVO_RECTIFICACION_REQUERIDO
400
Facturae de una rectificativa sin rectificativa.motivo
BUYER_ADDRESS_REQUIRED
400
Facturae sin domicilio del comprador (ni en la petición ni guardado en la factura)
SELLER_NAME_REQUIRED
400
Emisor persona física sin nombre y apellidos para el Facturae
FACE_YA_ENVIADA
409
La factura ya está presentada en FACe
FACE_RECHAZADA
422
FACe no ha admitido la factura (ver detalle)
FACE_PLATAFORMA_NO_REGISTRADA
422
FACe no reconoce el certificado del envío: da de alta tu plataforma con él
FACE_PLATAFORMA_NO_CONFIGURADA
422
No hay certificado con el que presentar en FACe
XSD_VALIDATION_FAILED
400
El XML no valida contra el esquema oficial
SELLER_ADDRESS_NOT_CONFIGURED
400
Falta el domicilio fiscal del emisor (obligatorio para Facturae)
INVOICE_NOT_FOUND
404
La factura no existe o no es de tu cuenta
INVOICE_NOT_REGISTERED_YET
409
Aún no la ha aceptado la AEAT: espera para anularla
INVOICE_ALREADY_CANCELLED
409
Ya existe un registro de anulación activo
INVOICE_CAP_REACHED
402
Sin suscripción de pago, alcanzado el tope de 500 facturas/NIF/mes. Con suscripción no se corta: el exceso se cobra (1,80 € por NIF y mes)
QUOTA_EXCEEDED
429
Agotadas las 10 llamadas del plan de prueba
EMAIL_NOT_VERIFIED
403
Verifica tu correo para activar las llamadas de prueba
EMISOR_QUOTA_EXCEEDED
403
El plan de prueba admite 1 NIF en live; suscríbete para más
DISPOSABLE_EMAIL
400
Correo de un servicio temporal: usa una dirección permanente
RATE_LIMIT_EXCEEDED
429
Más peticiones por minuto de las de tu cuenta (100 + 60 por NIF activo)
LOCK_TIMEOUT
409
Otra factura del mismo emisor se estaba procesando: reintenta
AEAT_CONSULTA_ERROR
502
La AEAT no respondió a la consulta
UNAUTHORIZED
401
Falta el header x-api-key o no es válido
Formato de error
Todos los errores traen error (el código estable, el que debes comprobar) y message
(texto para personas, puede cambiar). Según el caso añaden más campos: violations en
422 FISCAL_SEMANTIC_ERROR, limit en 402 INVOICE_CAP_REACHED, details o detalle
en otros. Algunos traen también statusCode, pero no todos: usa el código HTTP de la respuesta.
{"error":"FISCAL_SEMANTIC_ERROR","message":"Se han detectado violaciones de las reglas semánticas de la AEAT. Corrija los errores antes de reintentar.","violations":[{"code":"FISCAL_004","field":"baseImponible","message":"La factura de tipo F1 debe tener una base imponible mayor que cero. Valor actual: 0.00 €","severity":"ERROR"}]}
{"error":"INVOICE_CAP_REACHED","message":"Límite de 500 facturas este mes alcanzado para el NIF B12345674. Contrata un plan para seguir emitiendo.","limit":500}
Límites
Son dos límites distintos:
Límite
Valor
Alcance
Cuota del plan de prueba
10 llamadas
Total acumulado en live, no se renueva cada mes (test sin tope)
Peticiones por minuto
100 + 60 por NIF activo
Por cuenta y entorno (test y live por separado), según los emisores activos de ese entorno: con 50 restaurantes, 3.100 al minuto. Sin autenticar, 100 por IP
Al superar tu ritmo por minuto 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.