API para desarrolladores
API Verifactu: declara las facturas de tu sistema a través de Kiby
Conecta tu programa con la AEAT sin montar Verifactu por tu cuenta. La numeración y el contenido son tuyos: Kiby firma con la credencial del emisor, lo envía y guarda el rastro.
- OpenAPI 3.0
- REST y JSON
- Sandbox y producción
POST /v1/verifactu/registros
Authorization: Bearer kiby_test_••••••••
X-Kiby-Tenant: TU-NIF
Idempotency-Key: pedido-2026-00042
Content-Type: application/json
→ 201 · estado: "Pendiente"
consulta hasta que final sea trueLo esencial
Tres cosas que conviene saber antes de la primera llamada
01
El prefijo de la clave decide el entorno
Autenticación con Authorization: Bearer. Una clave kiby_test_ declara en el sandbox con el NIF de demostración de Kiby. Una kiby_live_ declara de verdad con el NIF del emisor.
02
Declarar son dos pasos, no uno
Al declarar recibes Pendiente: la AEAT resuelve después. Consulta el registro hasta que final sea true; si no, no te enterarás de los rechazos.
03
Cada POST lleva su Idempotency-Key
Si se corta la red, reintenta con la misma clave y no declararás la factura dos veces. Una clave por operación: no reutilices la del registro para anularlo.
Referencia
Todos los endpoints, respuestas y errores
Sale directamente del contrato OpenAPI de la pasarela, así que siempre está al día. Puedes importarlo en Postman, Insomnia o un generador de cliente. La guía de «Antes de empezar» es común a las dos APIs de Kiby; lo que cuenta de las facturas de la cuenta lo tienes en la API de Kiby.
Antes de empezar
Dos productos con la misma clave, la misma URL y el mismo formato de error:
| Producto | Rutas | Qué hace |
|---|---|---|
| Producto: API de facturación | Rutas: /v1/invoices, /v1/tenants | Qué hace: Crea facturas en Kiby: Kiby numera, calcula los totales, guarda la factura en la cuenta, apunta el cobro si se lo pides y la declara a Verifactu si la cuenta lo tiene activo. También permite consultarlas, incluidas las hechas a mano en Kiby. |
| Producto: Pasarela Verifactu | Rutas: /v1/verifactu/* | Qué hace: Declara ante la AEAT las facturas de tu propio sistema. La numeración y el contenido son tuyos; Kiby transporta, firma con la credencial del emisor y guarda el rastro. No crea facturas en Kiby. |
Permisos de la clave
Cada ruta exige un permiso. La clave los recibe al crearla en Ajustes → Claves de API; si le falta el de la ruta, la respuesta es 403 INSUFFICIENT_SCOPE y no se hace nada más.
| Permiso | Nombre en Ajustes | Rutas |
|---|---|---|
Permiso: invoices:write | Nombre en Ajustes: Crear facturas en Kiby | Rutas: POST /v1/invoices |
Permiso: invoices:read | Nombre en Ajustes: Consultar facturas de Kiby | Rutas: GET /v1/invoices, GET /v1/invoices/{id}, GET /v1/tenants |
Permiso: verifactu:write | Nombre en Ajustes: Pasarela Verifactu | Rutas: Todo /v1/verifactu/*, incluida la consulta GET /v1/verifactu/registros/{registroId} |
Son independientes a propósito: invoices:write no abre la pasarela y verifactu:write no deja crear facturas en Kiby. A un sistema que solo declara sus propias facturas no hace falta darle acceso a la numeración de Kiby. Cada operación indica su permiso en su descripción y en la extensión x-kiby-scope.
⚠️ Claves de la pasarela creadas antes de que existiera verifactu:write. Hasta ahora la pasarela se abría con invoices:write. Esas claves reciben verifactu:write automáticamente en la migración que Kiby ejecuta antes de publicar este cambio: no tienes que crear otra. Si aun así una llamada a /v1/verifactu/* te responde 403 INSUFFICIENT_SCOPE, avisa a soporte.
Pruebas: kiby_test_ no hace lo mismo en los dos productos
El entorno lo decide el prefijo de la clave, no la URL: kiby_test_ es pruebas y kiby_live_ es producción. Pero «pruebas» significa cosas distintas en cada producto, y confundirlas es caro:
En la pasarela Verifactu, una clave kiby_test_ declara DE VERDAD ante el entorno de pruebas de la AEAT, pero con el NIF de demostración de Kiby, no con el del emisor. Ese NIF lo comparten todos los integradores (ver «En el sandbox de la pasarela, usa una serie propia»), y el QR que devuelve verifica un registro de Kiby: no vale como justificante fiscal de nadie.
En la API de facturación, una clave kiby_test_ NO declara nada a Verifactu. POST /v1/invoices ejecuta el mismo código que en producción —misma validación, mismos totales con la configuración real de la cuenta (serie, IRPF, régimen del cliente), mismo control del orden de fechas contra la serie real— pero:
- la factura no entra en los libros de la cuenta: se guarda aparte, con su propia numeración (un contador de pruebas por serie y año, que no consume números reales);
- no se declara:
verifactu.estadoes siempreOmitidoy la respuesta llevasandbox: true; - no se apunta cobro ni se generan vencimientos: con
paid: truela factura saleCOBRADO, pero sin bloquecobro; - el cliente se busca en los contactos reales sin tocarlos y después entre los de pruebas; si no está en ninguno, se crea solo entre los de pruebas (con los mismos datos obligatorios que en producción);
- todo se borra solo a los 90 días: la factura, los clientes de pruebas y el registro de idempotencia;
- las consultas con
kiby_test_solo ven las facturas de pruebas, y conkiby_live_solo las reales; - exige lo mismo que producción (suscripción al corriente y gema activa) y tiene su propio cupo.
Antes de integrar, lo prepara el titular de la cuenta Kiby
No es trabajo tuyo y no puedes hacerlo por él:
- Para la API de facturación: activa la gema API de facturación (sin ella,
403 MODULE_NOT_ACTIVE) y tiene al menos una serie de facturas configurada. Si además tiene Verifactu configurado, el alta tiene que estar terminada (si no,409 TENANT_NOT_READY). - Para la pasarela: completa su alta Verifactu en Kiby (credencial y declaración responsable). Sin eso, cualquier petición en producción responde
409 TENANT_NOT_READY. - Crea una clave en Ajustes → Claves de API con solo los permisos que necesitas y te la entrega junto con su NIF, que es lo que va en
X-Kiby-Tenant.
Las dos cosas exigen que la cuenta esté al corriente de pago (si no, 402).
Qué NO validamos en la pasarela
Tu numeración, tu correlatividad y si el destinatario existe. No somos tu sistema de facturación, y fingir que lo comprobamos sería peor que no hacerlo. La numeración correlativa por serie es una obligación del emisor (RD 1619/2012, art. 6.1.a).
⚠️ Si el emisor además factura dentro de Kiby (a mano o con POST /v1/invoices), los dos canales escriben en la cadena del mismo NIF. Es legal y habitual repartir la facturación en series separadas, pero no reutilicéis en la pasarela un código de serie que use Kiby: cada canal lleva su propio contador y ninguno ve al otro. Ojo con la A, que es la serie que Kiby crea por defecto en toda cuenta nueva.
Límites de uso
Se cuentan por cuenta, no por clave: crear más claves no multiplica nada. Cada producto tiene su cupo mensual, y las facturas de pruebas el suyo:
| Plan | Pasarela (al mes) | Facturación (al mes) | Facturación de pruebas (al mes) | Por minuto |
|---|---|---|---|---|
| Plan: Freelance | Pasarela (al mes): 1.000 | Facturación (al mes): 500 | Facturación de pruebas (al mes): 2.500 | Por minuto: 30 |
| Plan: Pro | Pasarela (al mes): 5.000 | Facturación (al mes): 2.500 | Facturación de pruebas (al mes): 12.500 | Por minuto: 60 |
| Plan: Business Basic | Pasarela (al mes): 20.000 | Facturación (al mes): 10.000 | Facturación de pruebas (al mes): 50.000 | Por minuto: 120 |
| Plan: Business Pro | Pasarela (al mes): 60.000 | Facturación (al mes): 30.000 | Facturación de pruebas (al mes): 150.000 | Por minuto: 240 |
| Plan: Expansion | Pasarela (al mes): 200.000 | Facturación (al mes): 100.000 | Facturación de pruebas (al mes): 500.000 | Por minuto: 600 |
| Plan: Enterprise | Pasarela (al mes): ilimitadas | Facturación (al mes): ilimitadas | Facturación de pruebas (al mes): ilimitadas | Por minuto: 600 |
- Al mes cuenta cada petición que sale bien (2xx), consultas incluidas; los errores no gastan cupo. Un lote de la pasarela cuenta una, lleve las facturas que lleve. La pasarela con clave
kiby_test_cuenta en el cupo de la pasarela. Si la cuenta no está al corriente de pago se aplica el tramo Freelance. - Lo que gastas lo ves en cada respuesta, en
X-RateLimit-Limit,X-RateLimit-RemainingyX-RateLimit-Reset(segundos Unix del inicio del mes siguiente, hora de Madrid). Con cupo ilimitado,X-RateLimit-Limit: unlimitedy sin las otras dos. Viajan también en las respuestas de error, salvo en las que se cortan antes de mirar el cupo (credencial, permiso o límite por minuto). - Por minuto cuenta toda petición, salga bien o mal: un bucle con un cuerpo mal formado también lo gasta. Es un único cupo por cuenta para la pasarela y la facturación real; las facturas de pruebas tienen el suyo. Además hay dos topes propios, por debajo del de tu plan: 20 consultas de estado por minuto en la pasarela y 30 consultas de facturas por minuto (
GET /v1/invoices,/v1/invoices/{id}y/v1/tenants). - Si agotas el cupo del mes de la API de facturación, se corta todo (crear y consultar) hasta el mes siguiente o hasta mejorar de plan.
- Si agotas el cupo del mes de la pasarela, no se pueden declarar facturas nuevas, pero consultar, anular y subsanar siguen funcionando: nunca te quedas sin poder corregir una factura que la AEAT ha rechazado.
- Avisamos al titular de la cuenta por correo al llegar al 80, al 90 y al 100 % del cupo.
En el sandbox de la pasarela, usa una serie propia
Con una clave kiby_test_ todos los integradores declaran con el mismo NIF de demostración de Kiby, así que comparten numeración. La AEAT rechaza una factura cuya serie, número y fecha ya consten para ese NIF: si otro integrador probó A / 1 el mismo día que tú, tu registro saldrá rechazado como duplicado aunque tu integración esté bien.
Para que no te pase, usa en el sandbox una serie que solo uses tú —por ejemplo con el nombre de tu empresa: ACME-PRUEBAS— y números que no reutilices. Con la misma serie y número en fechas distintas no hay choque (medido en el entorno de pruebas). En producción esto no aplica: declaras con el NIF del emisor, que es solo suyo. Y en la API de facturación tampoco: allí el número lo pone Kiby.
Errores
Todos tienen la misma forma, { "error": { "code", "message", "details"? } }, vengan de donde vengan. Ramifica por code, nunca por el texto de message: los mensajes se reescriben para que sean más claros y no son un contrato. Los códigos sí.
Versionado
/v1 es estable. Cambios compatibles (pueden llegar sin aviso): campos nuevos en las respuestas, códigos de error nuevos dentro de una familia HTTP ya existente, mensajes reescritos. Diseña tu cliente para tolerarlos: ignora los campos que no conozcas y trata un code desconocido según su familia HTTP.
Cambios incompatibles —quitar o renombrar un campo, cambiar el tipo de uno existente, endurecer una validación— no se hacen sobre /v1: irían a una versión nueva, con las dos conviviendo y un plazo de migración anunciado por correo a los titulares de claves activas.
Entornos: sandbox y producción
Sandbox
https://api.gokiby.comLa misma URL para probar y para producción: el entorno lo elige tu clave (kiby_test_ o kiby_live_). Qué hace cada una en cada producto está en la introducción, en «Pruebas».
Autenticación con clave de API
Authorization: Bearer <clave>. Nunca en la URL: una clave en un parámetro (?api_key=) se rechaza con 401 aunque también venga en la cabecera, porque ya ha quedado en logs y proxies. Rótala.
Cada ruta pide un permiso (ver la tabla de la introducción); si la clave no lo tiene, 403 INSUFFICIENT_SCOPE.
El prefijo de la clave decide el entorno, y es la única cosa que lo decide:
| Prefijo | Pasarela Verifactu | API de facturación |
|---|---|---|
Prefijo: kiby_test_ | Pasarela Verifactu: Declara de verdad en el entorno de pruebas de la AEAT, con el NIF de demostración de Kiby | API de facturación: No declara. La factura se guarda aparte, numera aparte y se borra a los 90 días |
Prefijo: kiby_live_ | Pasarela Verifactu: Declara con el NIF del propio emisor, en su nombre | API de facturación: Factura real en los libros del emisor, declarada si tiene Verifactu |
⚠️ Un QR obtenido en el sandbox de la pasarela verifica un registro declarado con el NIF de Kiby, no con el del emisor. Sirve como prueba técnica y no vale como justificante fiscal de nadie. No lo pongas en un PDF que vaya a un cliente real.
En la pasarela la respuesta trae siempre el campo entorno, y en la API de facturación una factura de pruebas trae sandbox: true: un modo de pruebas silencioso es el peor fallo posible de este producto y queremos que se note en cada llamada.
Declarar
Dar de alta una factura de tu sistema en el registro de la AEAT.
/v1/verifactu/registrosDeclarar una factura
Permiso: verifactu:write.
Envía tu factura al registro de la AEAT. serie y numero los pones tú: aquí Kiby no numera.
La respuesta trae estado: "Pendiente" casi siempre: significa que el servicio de envío a la AEAT la tiene y la AEAT todavía no ha resuelto. Consulta el estado para saber el veredicto. El ciclo completo es:
POST /v1/verifactu/registros→201conestado: "Pendiente"y unregistroId.GET /v1/verifactu/registros/{registroId}hasta quefinalseatrue.- Según el veredicto: nada que hacer,
POST /anulacionesoPOST /subsanaciones.
Si te quedas en el paso 1 creyendo que ya está declarado, no vas a enterarte de los rechazos — y un rechazo de la AEAT significa que esa factura no consta.
El registroId que devuelve es tu Idempotency-Key. Guárdalo: es lo que identifica el registro para consultarlo, anularlo o subsanarlo.
Con una clave kiby_test_ declara de verdad en el entorno de pruebas de la AEAT con el NIF de demostración de Kiby: usa una serie propia (ver la introducción).
Petición
curl -X POST "https://api.gokiby.com/v1/verifactu/registros" \
-H "Authorization: Bearer kiby_test_…" \
-H "X-Kiby-Tenant: TU-NIF-O-TENANTUID" \
-H "Idempotency-Key: pedido-2026-00042" \
-H "Content-Type: application/json" \
-d '{
"serie": "A",
"numero": "2026-001",
"fecha_expedicion": "2026-09-24",
"tipo_factura": "F1",
"descripcion": "Servicios de consultoría",
"destinatario": {
"nif": "B75777847",
"nombre": "Cliente SL"
},
"desglose": [
{
"base_imponible": 100.0,
"tipo_impositivo": 21,
"cuota_repercutida": 21.0,
"tipo_recargo_equivalencia": 5.2,
"cuota_recargo_equivalencia": 5.2
}
],
"importe_total": 121.0
}'Autenticación: Authorization: Bearer <clave>. Con una clave kiby_test_ va al sandbox.
Cabeceras
| Campo | Tipo | Descripción |
|---|---|---|
Campo: X-Kiby-Tenantobligatorio | Tipo: texto | Descripción: NIF del emisor (o su identificador de cuenta Kiby). Es la barrera que impide declarar o facturar en nombre de otro emisor: si no resuelve a una cuenta a la que tu clave dé acceso, recibes Ejemplo: |
Campo: Idempotency-Keyobligatorio | Tipo: texto | Descripción: Identificador que eliges tú para esta operación. Obligatoria en todos los
⚠️ Genérala ANTES del primer intento y consérvala en los reintentos. El error clásico es generarla dentro del bucle de reintento: cada intento lleva una clave distinta, la deduplicación no puede reconocer nada y acabas con la misma factura declarada varias veces. Ejemplo: |
Cuerpo
{
"serie": "A",
"numero": "2026-001",
"fecha_expedicion": "2026-09-24",
"tipo_factura": "F1",
"descripcion": "Servicios de consultoría",
"destinatario": {
"nif": "B75777847",
"nombre": "Cliente SL"
},
"desglose": [
{
"base_imponible": 100.0,
"tipo_impositivo": 21,
"cuota_repercutida": 21.0,
"tipo_recargo_equivalencia": 5.2,
"cuota_recargo_equivalencia": 5.2
}
],
"importe_total": 121.0
}Ver los 15 campos del cuerpo
| Campo | Tipo | Descripción |
|---|---|---|
Campo: serieobligatorio | Tipo: texto | Descripción: Tu serie. Kiby no la valida ni la asigna. |
Campo: numeroobligatorio | Tipo: texto | Descripción: Tu número. Kiby no comprueba la correlatividad. |
Campo: fecha_expedicionobligatorio | Tipo: texto | Descripción:
Formato |
Campo: tipo_factura | Tipo: texto | Descripción: Catálogo de la AEAT.
⚠️ Los 3000 € son un tope técnico nuestro, no un permiso. Ese límite solo aplica a las actividades que enumera el art. 4; fuera de ellas el máximo es bastante menor, y hay operaciones donde no cabe simplificada en absoluto. Además, un empresario destinatario puede exigir factura completa. Elegir Valores: |
Campo: descripcionobligatorio | Tipo: texto | Descripción: Máximo 500 caracteres |
Campo: destinatario | Tipo: objeto | Descripción: Obligatorio salvo en |
Campo: destinatario.nifobligatorio | Tipo: texto | Descripción: ⚠️ La AEAT comprueba que exista en su censo. Nosotros no lo validamos —es tu factura— pero un NIF inventado hace que el registro se rechace con |
Campo: destinatario.nombreobligatorio | Tipo: texto | Descripción: |
Campo: desgloseobligatorio | Tipo: lista | Descripción: Máximo 12 líneas, y es el límite del registro en la AEAT, no nuestro. El desglose va agrupado por tipo impositivo, no por concepto: una factura de cuarenta líneas al 21 % es una línea de desglose. Si te pasas, recibes Entre 1 y 12 elementos |
Campo: desglose[].base_imponibleobligatorio | Tipo: número | Descripción: Formato |
Campo: desglose[].tipo_impositivoobligatorio | Tipo: número | Descripción: Porcentaje, no fracción. |
Campo: desglose[].cuota_repercutidaobligatorio | Tipo: número | Descripción: Formato |
Campo: desglose[].tipo_recargo_equivalencia | Tipo: número | Descripción: Solo para recargo de equivalencia, y los dos campos van juntos o ninguno: mandarlos «por si acaso» cambiaría el régimen fiscal del registro. |
Campo: desglose[].cuota_recargo_equivalencia | Tipo: número | Descripción: Formato |
Campo: importe_totalobligatorio | Tipo: número | Descripción: Tiene que cuadrar con la suma de bases + cuotas repercutidas + recargos, con una tolerancia de 0,01 € para absorber redondeos. Es el error de integración que más se cuela, y por eso se comprueba antes de enviar nada: una vez registrado en la AEAT ya no es un error, es una subsanación. Formato |
Respuestas
200
Ya estaba declarada. Dos casos, y en los dos la respuesta es la del registro que ya existe:
- Reintento reconocido: misma
Idempotency-Keyy mismo cuerpo. - Carrera: mandaste varias a la vez con la misma clave. Solo una responde
201; las demás,200.
El uuid es el mismo en todas, así que no hay una segunda factura en la cadena de la AEAT. Si cuentas registros creados, cuenta los 201.
{
"registroId": "pedido-2026-00042",
"entorno": "test",
"estado": "Pendiente",
"uuid": "b018ced3-1f2a-4c7b-9d3e-8a1b2c3d4e5f",
"url": "string",
"qr": "string"
}Ver los campos de la respuesta 200
| Campo | Tipo | Descripción |
|---|---|---|
Campo: registroId | Tipo: texto | Descripción: Coincide con tu |
Campo: entorno | Tipo: texto | Descripción: Viaja siempre, también en producción. Valores: |
Campo: estado | Tipo: texto | Descripción: El literal del servicio de envío a la AEAT, no una traducción nuestra: esto es una pasarela y debes ver lo que dice quien declara. |
Campo: uuid | Tipo: texto | Descripción: |
Campo: url | Tipo: texto | Descripción: Página de verificación de la AEAT. |
Campo: qr | Tipo: texto | Descripción: PNG en base64 del QR de verificación. |
201
Registro creado y enviado.
{
"registroId": "pedido-2026-00042",
"entorno": "test",
"estado": "Pendiente",
"uuid": "b018ced3-1f2a-4c7b-9d3e-8a1b2c3d4e5f",
"url": "string",
"qr": "string"
}Ver los campos de la respuesta 201
| Campo | Tipo | Descripción |
|---|---|---|
Campo: registroId | Tipo: texto | Descripción: Coincide con tu |
Campo: entorno | Tipo: texto | Descripción: Viaja siempre, también en producción. Valores: |
Campo: estado | Tipo: texto | Descripción: El literal del servicio de envío a la AEAT, no una traducción nuestra: esto es una pasarela y debes ver lo que dice quien declara. |
Campo: uuid | Tipo: texto | Descripción: |
Campo: url | Tipo: texto | Descripción: Página de verificación de la AEAT. |
Campo: qr | Tipo: texto | Descripción: PNG en base64 del QR de verificación. |
Errores
Todos los errores llegan con la misma forma:
{
"error": {
"code": "INVALID_BODY",
"message": "string",
"details": {
"clave": "string"
}
}
}400
Tu petición. Reintentar no sirve: corrígela.
code | Cuándo |
|---|---|
code: MISSING_HEADER | Cuándo: Falta X-Kiby-Tenant o Idempotency-Key |
code: INVALID_IDEMPOTENCY_KEY | Cuándo: La cabecera está, pero con caracteres que no admite |
code: INVALID_BODY | Cuándo: El cuerpo no es un objeto, o un campo tiene tipo o formato equivocado, o el desglose no cuadra |
code: UNKNOWN_FIELD | Cuándo: Un campo que no existe en el contrato |
code: FORBIDDEN_FIELD | Cuándo: Un campo que existe pero lo fija el servidor |
code: INVALID_LINE | Cuándo: Una línea del desglose está mal |
code: TOO_MANY_LINES | Cuándo: Más de 12 líneas de desglose |
code: FIELD_NOT_AMENDABLE | Cuándo: Intentas subsanar algo que exige rectificativa |
code: TOO_MANY_REGISTROS | Cuándo: Más de 50 facturas en un lote |
code: DUPLICATE_REGISTRO_ID | Cuándo: El mismo registroId dos veces en un lote |
code: DUPLICATE_INVOICE_NUMBER | Cuándo: Dos facturas del lote con la misma serie y número |
code: VERIFACTU_REJECTED | Cuándo: El servicio de envío rechazó los datos. No se envió nada a la AEAT. Su código estable va en details.verifacti_codigo |
401
UNAUTHORIZED — clave ausente, mal formada, revocada o caducada, o mandada en la URL (?api_key=), que se rechaza siempre: a esas alturas ya está en logs ajenos. Rótala.
402
La cuenta Kiby del emisor no está al corriente de pago. Lo resuelve el titular de la cuenta, no tú. Solo lo devuelven las rutas que DECLARAN (/registros y /lotes), también con clave kiby_test_. Anular, subsanar y consultar siguen funcionando aunque la cuenta deje de pagar, para que siempre puedas corregir lo ya declarado. No se ha leído, anotado ni enviado nada.
code | Cuándo |
|---|---|
code: TENANT_TRIAL | Cuándo: La cuenta está en periodo de prueba. Declarar es para cuentas con suscripción |
code: TENANT_SUBSCRIPTION_INACTIVE | Cuándo: La suscripción está impagada o cancelada. Vuelve a funcionar en menos de un minuto tras regularizar el pago |
403
code | Cuándo |
|---|---|
code: TENANT_HEADER_MISMATCH | Cuándo: X-Kiby-Tenant no resuelve a una cuenta a la que tu clave dé acceso |
code: INSUFFICIENT_SCOPE | Cuándo: La clave no tiene verifactu:write. Si es una clave de la pasarela creada antes de que existiera ese permiso, avisa a soporte |
code: IP_NOT_ALLOWED | Cuándo: La clave tiene lista de IPs y la tuya no está |
code: FORBIDDEN | Cuándo: Denegado por un motivo que no encaja en los anteriores. Si te llega, dínoslo: significa que hay un caso que deberíamos estar nombrando |
code: TENANT_NOT_FOUND | Cuándo: Solo al declarar (/registros, /lotes): la cuenta Kiby de la clave ya no existe |
409
Mira el estado del registro antes de insistir. Reintentar tal cual no lo va a arreglar.
code | Qué significa |
|---|---|
code: IDEMPOTENCY_KEY_REUSED | Qué significa: Esa clave ya se usó para otra factura distinta |
code: TENANT_NOT_READY | Qué significa: El alta Verifactu del emisor no está operativa. Lo resuelve el titular de la cuenta Kiby, no tú. El estado concreto va en details.verifactu_status |
code: REGISTRO_NOT_DECLARED | Qué significa: Se anotó pero no llegó a salir. Reinténtalo por el endpoint por el que lo enviaste |
code: REGISTRO_EN_OTRO_ENDPOINT | Qué significa: Ese registro entró por el otro endpoint de alta (/registros o /lotes) y no consta su resultado. Reinténtalo por aquel: cruzarlos podría declararlo dos veces |
code: VERIFACTU_UNCONFIRMED | Qué significa: Se envió en un lote sin respuesta fiable: puede estar ya en la AEAT. No lo reenvíes; contacta con soporte |
code: REGISTRO_NOT_CANCELLABLE | Qué significa: La AEAT no tiene registrada esa factura: no hay nada que anular. Corresponde subsanar |
code: REGISTRO_ALREADY_CANCELLED | Qué significa: Al subsanar: está anulada. Al anular: ya tiene una anulación con otro contenido (contacta con soporte) |
code: REGISTRO_NOT_AMENDABLE | Qué significa: Su estado no admite subsanación |
code: REGISTRO_PAYLOAD_INCOMPLETE | Qué significa: Al registro le faltan datos para construir la baja. Contacta con soporte |
code: REGISTRO_ESTADO_INCONSISTENTE | Qué significa: Anomalía en un dato que solo escribimos nosotros. Contacta con soporte |
code: VERIFACTU_REQUEST_IN_PROGRESS | Qué significa: La misma operación está en curso (también: un lote está enviando ese registro). Espera y consulta el estado |
Sobre TENANT_NOT_READY, dos valores de details.verifactu_status que conviene distinguir: VERIFACTU_STATUS_NIF_PENDING es transitorio y se resuelve solo en unos minutos; VERIFACTU_STATUS_PENDING_REPRESENTATION no se arregla reintentando jamás — falta que el titular firme la declaración responsable.
413
PAYLOAD_TOO_LARGE — el cuerpo supera 256 KB. En un lote, manda menos facturas por llamada.
429
Respeta siempre Retry-After.
code | Qué significa |
|---|---|
code: RATE_LIMIT_EXCEEDED | Qué significa: Has superado el límite por minuto de tu plan (cuenta toda petición, salga bien o mal), o más de 20 consultas de estado por minuto. Espera los segundos de Retry-After |
code: MONTHLY_QUOTA_EXCEEDED | Qué significa: Has agotado el cupo del mes de la pasarela. Solo lo reciben las peticiones que declaran (POST /registros y POST /lotes): consultar, anular y subsanar siguen funcionando. Retry-After apunta al inicio del mes siguiente; para seguir antes, mejora de plan |
500
INTERNAL_ERROR — fallo nuestro. Reintenta con espera creciente (con la misma Idempotency-Key si era un POST); si persiste, soporte.
502
VERIFACTU_UPSTREAM_ERROR — no hemos podido hablar con el servicio de envío a la AEAT, o nos ha respondido algo que no sabemos interpretar.
⚠️ Reintenta con la MISMA Idempotency-Key. Aquí no se sabe si la declaración llegó o no, y esa clave es lo único que impide declararla dos veces.
503
code | Qué significa |
|---|---|
code: SANDBOX_UNAVAILABLE | Qué significa: El sandbox de la pasarela no está disponible ahora. Nunca se cae a la credencial real: preferimos no declarar a declarar de verdad algo que creías una prueba |
code: GATEWAY_MISCONFIGURED | Qué significa: Configuración nuestra incoherente. No es tu petición: contacta con soporte |
code: SERVICE_UNAVAILABLE | Qué significa: Solo dentro de un lote, en una factura: no pudimos prepararla. Reenvíala tal cual en otro lote |
/v1/verifactu/lotesDeclarar hasta 50 facturas en una llamada
Permiso: verifactu:write.
Lo mismo que POST /registros, de 50 en 50. Sirve para no gastar una llamada por factura cuando cierras el mes con cientos.
Cada factura lleva su registroId, que hace lo que en /registros hace la Idempotency-Key: es el identificador con el que la consultas, la anulas o la subsanas, con los endpoints de siempre. Una vez declarada, una factura del lote es exactamente igual que una declarada suelta.
Máximo 50 por llamada. Si tienes más, repártelas en varias llamadas.
La respuesta es siempre 207
Cuando el lote se procesa, la respuesta es 207 aunque todas salgan bien: cada factura trae su propio status, el que te habría dado POST /registros (201, 200, 400, 409, 502…). Mira el de cada una. En un lote lo normal es que alguna falle, y un rechazo no afecta a las demás.
El lote entero se rechaza con un 4xx solo si:
- el cuerpo es inválido (todo o nada: una factura mal y no se declara ninguna; el mensaje dice cuál),
- o algún
registroIdya se usó para otra factura (409 IDEMPOTENCY_KEY_REUSED, con la lista endetails.registroIds).
Reintentos
Reenviar el mismo lote es seguro: las que ya están declaradas vuelven con 200 y lo que ya tenían, sin volver a enviarse; solo se envían las que faltan.
⚠️ Una factura se reintenta por el endpoint por el que entró. Si la mandaste en un lote y no se declaró, reinténtala en otro lote; si la mandaste por /registros, por /registros. Por el otro lado recibirás 409 REGISTRO_EN_OTRO_ENDPOINT. No es burocracia: cada endpoint identifica el envío de forma distinta ante el servicio de envío a la AEAT, y cruzarlos podría declarar la factura dos veces.
⚠️ VERIFACTU_UNCONFIRMED no se reintenta. Significa que se envió y no hubo respuesta fiable: puede estar ya en la AEAT. Queda bloqueada para no duplicarla. Contacta con soporte.
La cabecera Idempotency-Key es obligatoria como en todo POST, pero aquí no es lo que evita duplicados —eso lo hacen los registroId—: identifica la petición para poder seguirla.
Códigos por factura
status | code | Qué hacer |
|---|---|---|
status: 201 | code: — | Qué hacer: Declarada ahora |
status: 200 | code: — | Qué hacer: Ya estaba declarada: es lo guardado |
status: 400 | code: VERIFACTU_REJECTED | Qué hacer: El servicio de envío rechazó sus datos; no se creó nada. Corrígela y envíala con otro registroId (el mismo con otro contenido es IDEMPOTENCY_KEY_REUSED) |
status: 409 | code: IDEMPOTENCY_KEY_REUSED | Qué hacer: Ese registroId se usó para otra factura mientras procesábamos el lote |
status: 409 | code: VERIFACTU_REQUEST_IN_PROGRESS | Qué hacer: Otro lote la está enviando ahora. Consulta su estado en unos segundos |
status: 409 | code: REGISTRO_EN_OTRO_ENDPOINT | Qué hacer: Entró por /registros sin resultado: reinténtala por ahí |
status: 409 | code: VERIFACTU_UNCONFIRMED | Qué hacer: Un lote anterior quedó sin confirmar. No la reenvíes: soporte |
status: 502 | code: VERIFACTU_UPSTREAM_ERROR | Qué hacer: No llegó a enviarse. Reenvíala tal cual en otro lote |
status: 502 | code: VERIFACTU_UNCONFIRMED | Qué hacer: Se envió y no hubo respuesta fiable: puede estar en la AEAT. No la reenvíes: soporte |
status: 503 | code: SERVICE_UNAVAILABLE | Qué hacer: No pudimos prepararla. Reenvíala tal cual en otro lote |
Petición
curl -X POST "https://api.gokiby.com/v1/verifactu/lotes" \
-H "Authorization: Bearer kiby_test_…" \
-H "X-Kiby-Tenant: TU-NIF-O-TENANTUID" \
-H "Idempotency-Key: pedido-2026-00042" \
-H "Content-Type: application/json" \
-d '{
"registros": [
{
"registroId": "pedido-2026-00147",
"serie": "A",
"numero": "2026-001",
"fecha_expedicion": "2026-09-24",
"tipo_factura": "F1",
"descripcion": "Servicios de consultoría",
"destinatario": {
"nif": "B75777847",
"nombre": "Cliente SL"
},
"desglose": [
{
"base_imponible": 100.0,
"tipo_impositivo": 21,
"cuota_repercutida": 21.0,
"tipo_recargo_equivalencia": 5.2,
"cuota_recargo_equivalencia": 5.2
}
],
"importe_total": 121.0
}
]
}'Autenticación: Authorization: Bearer <clave>. Con una clave kiby_test_ va al sandbox.
Cabeceras
| Campo | Tipo | Descripción |
|---|---|---|
Campo: X-Kiby-Tenantobligatorio | Tipo: texto | Descripción: NIF del emisor (o su identificador de cuenta Kiby). Es la barrera que impide declarar o facturar en nombre de otro emisor: si no resuelve a una cuenta a la que tu clave dé acceso, recibes Ejemplo: |
Campo: Idempotency-Keyobligatorio | Tipo: texto | Descripción: Identificador que eliges tú para esta operación. Obligatoria en todos los
⚠️ Genérala ANTES del primer intento y consérvala en los reintentos. El error clásico es generarla dentro del bucle de reintento: cada intento lleva una clave distinta, la deduplicación no puede reconocer nada y acabas con la misma factura declarada varias veces. Ejemplo: |
Cuerpo
{
"registros": [
{
"registroId": "pedido-2026-00147",
"serie": "A",
"numero": "2026-001",
"fecha_expedicion": "2026-09-24",
"tipo_factura": "F1",
"descripcion": "Servicios de consultoría",
"destinatario": {
"nif": "B75777847",
"nombre": "Cliente SL"
},
"desglose": [
{
"base_imponible": 100.0,
"tipo_impositivo": 21,
"cuota_repercutida": 21.0,
"tipo_recargo_equivalencia": 5.2,
"cuota_recargo_equivalencia": 5.2
}
],
"importe_total": 121.0
}
]
}Ver los 17 campos del cuerpo
| Campo | Tipo | Descripción |
|---|---|---|
Campo: registrosobligatorio | Tipo: lista | Descripción: Cada elemento es el cuerpo de Entre 1 y 50 elementos |
Campo: registros[].registroIdobligatorio | Tipo: texto | Descripción: Identifica ESTA factura para siempre. Mismas reglas que la |
Campo: registros[].serieobligatorio | Tipo: texto | Descripción: Tu serie. Kiby no la valida ni la asigna. |
Campo: registros[].numeroobligatorio | Tipo: texto | Descripción: Tu número. Kiby no comprueba la correlatividad. |
Campo: registros[].fecha_expedicionobligatorio | Tipo: texto | Descripción:
Formato |
Campo: registros[].tipo_factura | Tipo: texto | Descripción: Catálogo de la AEAT.
⚠️ Los 3000 € son un tope técnico nuestro, no un permiso. Ese límite solo aplica a las actividades que enumera el art. 4; fuera de ellas el máximo es bastante menor, y hay operaciones donde no cabe simplificada en absoluto. Además, un empresario destinatario puede exigir factura completa. Elegir Valores: |
Campo: registros[].descripcionobligatorio | Tipo: texto | Descripción: Máximo 500 caracteres |
Campo: registros[].destinatario | Tipo: objeto | Descripción: Obligatorio salvo en |
Campo: registros[].destinatario.nifobligatorio | Tipo: texto | Descripción: ⚠️ La AEAT comprueba que exista en su censo. Nosotros no lo validamos —es tu factura— pero un NIF inventado hace que el registro se rechace con |
Campo: registros[].destinatario.nombreobligatorio | Tipo: texto | Descripción: |
Campo: registros[].desgloseobligatorio | Tipo: lista | Descripción: Máximo 12 líneas, y es el límite del registro en la AEAT, no nuestro. El desglose va agrupado por tipo impositivo, no por concepto: una factura de cuarenta líneas al 21 % es una línea de desglose. Si te pasas, recibes Entre 1 y 12 elementos |
Campo: registros[].desglose[].base_imponibleobligatorio | Tipo: número | Descripción: Formato |
Campo: registros[].desglose[].tipo_impositivoobligatorio | Tipo: número | Descripción: Porcentaje, no fracción. |
Campo: registros[].desglose[].cuota_repercutidaobligatorio | Tipo: número | Descripción: Formato |
Campo: registros[].desglose[].tipo_recargo_equivalencia | Tipo: número | Descripción: Solo para recargo de equivalencia, y los dos campos van juntos o ninguno: mandarlos «por si acaso» cambiaría el régimen fiscal del registro. |
Campo: registros[].desglose[].cuota_recargo_equivalencia | Tipo: número | Descripción: Formato |
Campo: registros[].importe_totalobligatorio | Tipo: número | Descripción: Tiene que cuadrar con la suma de bases + cuotas repercutidas + recargos, con una tolerancia de 0,01 € para absorber redondeos. Es el error de integración que más se cuela, y por eso se comprueba antes de enviar nada: una vez registrado en la AEAT ya no es un error, es una subsanación. Formato |
Respuestas
207
Lote procesado. Mira el status de cada factura.
{
"entorno": "test",
"resultados": [
{
"registroId": "pedido-2026-00147",
"status": 201,
"estado": "Pendiente",
"uuid": "string",
"url": "string",
"qr": "string",
"error": {
"code": "VERIFACTU_REJECTED",
"message": "string",
"details": {
"clave": "string"
}
}
}
]
}Ver los campos de la respuesta 207
| Campo | Tipo | Descripción |
|---|---|---|
Campo: entorno | Tipo: texto | Descripción: Valores: |
Campo: resultados | Tipo: lista | Descripción: Uno por factura, en el mismo orden que las enviaste. |
Campo: resultados[].registroIdobligatorio | Tipo: texto | Descripción: |
Campo: resultados[].statusobligatorio | Tipo: entero | Descripción: El que te habría dado |
Campo: resultados[].estado | Tipo: texto | Descripción: |
Campo: resultados[].uuid | Tipo: texto | Descripción: |
Campo: resultados[].url | Tipo: texto | Descripción: |
Campo: resultados[].qr | Tipo: texto | Descripción: |
Campo: resultados[].error | Tipo: objeto | Descripción: |
Campo: resultados[].error.code | Tipo: texto | Descripción: |
Campo: resultados[].error.message | Tipo: texto | Descripción: |
Campo: resultados[].error.details | Tipo: objeto | Descripción: |
Errores
Todos los errores llegan con la misma forma:
{
"error": {
"code": "INVALID_BODY",
"message": "string",
"details": {
"clave": "string"
}
}
}400
Tu petición. Reintentar no sirve: corrígela.
code | Cuándo |
|---|---|
code: MISSING_HEADER | Cuándo: Falta X-Kiby-Tenant o Idempotency-Key |
code: INVALID_IDEMPOTENCY_KEY | Cuándo: La cabecera está, pero con caracteres que no admite |
code: INVALID_BODY | Cuándo: El cuerpo no es un objeto, o un campo tiene tipo o formato equivocado, o el desglose no cuadra |
code: UNKNOWN_FIELD | Cuándo: Un campo que no existe en el contrato |
code: FORBIDDEN_FIELD | Cuándo: Un campo que existe pero lo fija el servidor |
code: INVALID_LINE | Cuándo: Una línea del desglose está mal |
code: TOO_MANY_LINES | Cuándo: Más de 12 líneas de desglose |
code: FIELD_NOT_AMENDABLE | Cuándo: Intentas subsanar algo que exige rectificativa |
code: TOO_MANY_REGISTROS | Cuándo: Más de 50 facturas en un lote |
code: DUPLICATE_REGISTRO_ID | Cuándo: El mismo registroId dos veces en un lote |
code: DUPLICATE_INVOICE_NUMBER | Cuándo: Dos facturas del lote con la misma serie y número |
code: VERIFACTU_REJECTED | Cuándo: El servicio de envío rechazó los datos. No se envió nada a la AEAT. Su código estable va en details.verifacti_codigo |
401
UNAUTHORIZED — clave ausente, mal formada, revocada o caducada, o mandada en la URL (?api_key=), que se rechaza siempre: a esas alturas ya está en logs ajenos. Rótala.
402
La cuenta Kiby del emisor no está al corriente de pago. Lo resuelve el titular de la cuenta, no tú. Solo lo devuelven las rutas que DECLARAN (/registros y /lotes), también con clave kiby_test_. Anular, subsanar y consultar siguen funcionando aunque la cuenta deje de pagar, para que siempre puedas corregir lo ya declarado. No se ha leído, anotado ni enviado nada.
code | Cuándo |
|---|---|
code: TENANT_TRIAL | Cuándo: La cuenta está en periodo de prueba. Declarar es para cuentas con suscripción |
code: TENANT_SUBSCRIPTION_INACTIVE | Cuándo: La suscripción está impagada o cancelada. Vuelve a funcionar en menos de un minuto tras regularizar el pago |
403
code | Cuándo |
|---|---|
code: TENANT_HEADER_MISMATCH | Cuándo: X-Kiby-Tenant no resuelve a una cuenta a la que tu clave dé acceso |
code: INSUFFICIENT_SCOPE | Cuándo: La clave no tiene verifactu:write. Si es una clave de la pasarela creada antes de que existiera ese permiso, avisa a soporte |
code: IP_NOT_ALLOWED | Cuándo: La clave tiene lista de IPs y la tuya no está |
code: FORBIDDEN | Cuándo: Denegado por un motivo que no encaja en los anteriores. Si te llega, dínoslo: significa que hay un caso que deberíamos estar nombrando |
code: TENANT_NOT_FOUND | Cuándo: Solo al declarar (/registros, /lotes): la cuenta Kiby de la clave ya no existe |
409
Mira el estado del registro antes de insistir. Reintentar tal cual no lo va a arreglar.
code | Qué significa |
|---|---|
code: IDEMPOTENCY_KEY_REUSED | Qué significa: Esa clave ya se usó para otra factura distinta |
code: TENANT_NOT_READY | Qué significa: El alta Verifactu del emisor no está operativa. Lo resuelve el titular de la cuenta Kiby, no tú. El estado concreto va en details.verifactu_status |
code: REGISTRO_NOT_DECLARED | Qué significa: Se anotó pero no llegó a salir. Reinténtalo por el endpoint por el que lo enviaste |
code: REGISTRO_EN_OTRO_ENDPOINT | Qué significa: Ese registro entró por el otro endpoint de alta (/registros o /lotes) y no consta su resultado. Reinténtalo por aquel: cruzarlos podría declararlo dos veces |
code: VERIFACTU_UNCONFIRMED | Qué significa: Se envió en un lote sin respuesta fiable: puede estar ya en la AEAT. No lo reenvíes; contacta con soporte |
code: REGISTRO_NOT_CANCELLABLE | Qué significa: La AEAT no tiene registrada esa factura: no hay nada que anular. Corresponde subsanar |
code: REGISTRO_ALREADY_CANCELLED | Qué significa: Al subsanar: está anulada. Al anular: ya tiene una anulación con otro contenido (contacta con soporte) |
code: REGISTRO_NOT_AMENDABLE | Qué significa: Su estado no admite subsanación |
code: REGISTRO_PAYLOAD_INCOMPLETE | Qué significa: Al registro le faltan datos para construir la baja. Contacta con soporte |
code: REGISTRO_ESTADO_INCONSISTENTE | Qué significa: Anomalía en un dato que solo escribimos nosotros. Contacta con soporte |
code: VERIFACTU_REQUEST_IN_PROGRESS | Qué significa: La misma operación está en curso (también: un lote está enviando ese registro). Espera y consulta el estado |
Sobre TENANT_NOT_READY, dos valores de details.verifactu_status que conviene distinguir: VERIFACTU_STATUS_NIF_PENDING es transitorio y se resuelve solo en unos minutos; VERIFACTU_STATUS_PENDING_REPRESENTATION no se arregla reintentando jamás — falta que el titular firme la declaración responsable.
413
PAYLOAD_TOO_LARGE — el cuerpo supera 256 KB. En un lote, manda menos facturas por llamada.
429
Respeta siempre Retry-After.
code | Qué significa |
|---|---|
code: RATE_LIMIT_EXCEEDED | Qué significa: Has superado el límite por minuto de tu plan (cuenta toda petición, salga bien o mal), o más de 20 consultas de estado por minuto. Espera los segundos de Retry-After |
code: MONTHLY_QUOTA_EXCEEDED | Qué significa: Has agotado el cupo del mes de la pasarela. Solo lo reciben las peticiones que declaran (POST /registros y POST /lotes): consultar, anular y subsanar siguen funcionando. Retry-After apunta al inicio del mes siguiente; para seguir antes, mejora de plan |
500
INTERNAL_ERROR — fallo nuestro. Reintenta con espera creciente (con la misma Idempotency-Key si era un POST); si persiste, soporte.
503
code | Qué significa |
|---|---|
code: SANDBOX_UNAVAILABLE | Qué significa: El sandbox de la pasarela no está disponible ahora. Nunca se cae a la credencial real: preferimos no declarar a declarar de verdad algo que creías una prueba |
code: GATEWAY_MISCONFIGURED | Qué significa: Configuración nuestra incoherente. No es tu petición: contacta con soporte |
code: SERVICE_UNAVAILABLE | Qué significa: Solo dentro de un lote, en una factura: no pudimos prepararla. Reenvíala tal cual en otro lote |
Consultar
Saber qué ha decidido la AEAT sobre un registro. Es lo que cierra el ciclo.
/v1/verifactu/registros/{registroId}Consultar el estado de un registro
Permiso: verifactu:write — el de la pasarela, aunque sea una consulta: quien declara puede leer lo que declaró. invoices:read es para las facturas de Kiby y no abre esta ruta.
El campo que importa es final.
final: false→ la AEAT todavía no ha resuelto. La respuesta traeRetry-Aftercon los segundos que sugerimos esperar.final: true→ el estado ya no cambia. Deja de preguntar por ese registro.
⚠️ Trampa habitual: Incorrecta también es final. No se arregla esperando, se subsana. Si tratas «definitivo» como sinónimo de «aceptado», te quedas esperando para siempre algo que te toca corregir a ti.
No lleva Idempotency-Key: una consulta no puede duplicar nada.
Máximo 20 consultas por minuto por emisor. Es un tope propio de esta consulta, por debajo del límite por minuto de tu plan, que comparten lecturas y escrituras: un bucle de sondeo cerrado te dejaría sin presupuesto para declarar. Y cada consulta que sale bien gasta cupo mensual. Respeta final y no te acercarás a ninguno de los dos.
Petición
curl -X GET "https://api.gokiby.com/v1/verifactu/registros/pedido-2026-00042" \
-H "Authorization: Bearer kiby_test_…" \
-H "X-Kiby-Tenant: TU-NIF-O-TENANTUID"Autenticación: Authorization: Bearer <clave>. Con una clave kiby_test_ va al sandbox.
Cabeceras
| Campo | Tipo | Descripción |
|---|---|---|
Campo: X-Kiby-Tenant | Tipo: texto | Descripción: NIF del emisor (o su identificador de cuenta Kiby). En las consultas puede omitirse si la clave da acceso a un solo emisor (hoy, siempre). Si va, tiene que corresponder a la clave: si no, Ejemplo: |
Parámetros de la ruta
| Campo | Tipo | Descripción |
|---|---|---|
Campo: registroIdobligatorio | Tipo: texto | Descripción: El Ejemplo: |
Respuestas
200
Estado actual del registro.
{
"registroId": "pedido-2026-00042",
"entorno": "test",
"estado": "Correcto",
"estadoNormalizado": "Correcta",
"final": true,
"uuid": "string",
"url": "string",
"qr": "string",
"declaradoEn": "2026-01-01T09:00:00Z",
"actualizadoEn": "2026-01-01T09:00:00Z",
"ultimaSubsanacion": {
"subsanacionId": "string",
"estado": "string",
"rechazada": true
}
}Ver los campos de la respuesta 200
| Campo | Tipo | Descripción |
|---|---|---|
Campo: registroId | Tipo: texto | Descripción: |
Campo: entorno | Tipo: texto | Descripción: El entorno con el que se declaró, no con el que estás preguntando. Valores: |
Campo: estado | Tipo: texto | Descripción: El literal del servicio de envío a la AEAT. |
Campo: estadoNormalizado | Tipo: texto | Descripción: Nuestro vocabulario canónico, para que no tengas que mantener tu propia tabla de sinónimos: |
Campo: final | Tipo: sí/no | Descripción:
⚠️ |
Campo: uuid | Tipo: texto | Descripción: |
Campo: url | Tipo: texto | Descripción: |
Campo: qr | Tipo: texto | Descripción: |
Campo: declaradoEn | Tipo: texto | Descripción: Cuándo se anotó la declaración. Formato |
Campo: actualizadoEn | Tipo: texto | Descripción: Cuándo cambió por última vez. Te dice lo reciente que es el veredicto. Formato |
Campo: ultimaSubsanacion | Tipo: objeto | Descripción: Solo si hubo alguna. Saber que fue rechazada es lo que te dice que hay que volver a subsanar. Puede ser |
Campo: ultimaSubsanacion.subsanacionId | Tipo: texto | Descripción: Puede ser |
Campo: ultimaSubsanacion.estado | Tipo: texto | Descripción: Puede ser |
Campo: ultimaSubsanacion.rechazada | Tipo: sí/no | Descripción: |
Errores
Todos los errores llegan con la misma forma:
{
"error": {
"code": "INVALID_BODY",
"message": "string",
"details": {
"clave": "string"
}
}
}400
Tu petición. Reintentar no sirve: corrígela.
code | Cuándo |
|---|---|
code: MISSING_HEADER | Cuándo: Falta X-Kiby-Tenant o Idempotency-Key |
code: INVALID_IDEMPOTENCY_KEY | Cuándo: La cabecera está, pero con caracteres que no admite |
code: INVALID_BODY | Cuándo: El cuerpo no es un objeto, o un campo tiene tipo o formato equivocado, o el desglose no cuadra |
code: UNKNOWN_FIELD | Cuándo: Un campo que no existe en el contrato |
code: FORBIDDEN_FIELD | Cuándo: Un campo que existe pero lo fija el servidor |
code: INVALID_LINE | Cuándo: Una línea del desglose está mal |
code: TOO_MANY_LINES | Cuándo: Más de 12 líneas de desglose |
code: FIELD_NOT_AMENDABLE | Cuándo: Intentas subsanar algo que exige rectificativa |
code: TOO_MANY_REGISTROS | Cuándo: Más de 50 facturas en un lote |
code: DUPLICATE_REGISTRO_ID | Cuándo: El mismo registroId dos veces en un lote |
code: DUPLICATE_INVOICE_NUMBER | Cuándo: Dos facturas del lote con la misma serie y número |
code: VERIFACTU_REJECTED | Cuándo: El servicio de envío rechazó los datos. No se envió nada a la AEAT. Su código estable va en details.verifacti_codigo |
401
UNAUTHORIZED — clave ausente, mal formada, revocada o caducada, o mandada en la URL (?api_key=), que se rechaza siempre: a esas alturas ya está en logs ajenos. Rótala.
403
code | Cuándo |
|---|---|
code: TENANT_HEADER_MISMATCH | Cuándo: X-Kiby-Tenant no resuelve a una cuenta a la que tu clave dé acceso |
code: INSUFFICIENT_SCOPE | Cuándo: La clave no tiene verifactu:write. Si es una clave de la pasarela creada antes de que existiera ese permiso, avisa a soporte |
code: IP_NOT_ALLOWED | Cuándo: La clave tiene lista de IPs y la tuya no está |
code: FORBIDDEN | Cuándo: Denegado por un motivo que no encaja en los anteriores. Si te llega, dínoslo: significa que hay un caso que deberíamos estar nombrando |
code: TENANT_NOT_FOUND | Cuándo: Solo al declarar (/registros, /lotes): la cuenta Kiby de la clave ya no existe |
404
NOT_FOUND — ese registroId no existe en este entorno. Si estabas usando una clave kiby_test_ y el registro se declaró con una kiby_live_ (o al revés), no lo vas a encontrar: son dos espacios separados a propósito.
409
code | Qué significa |
|---|---|
code: REGISTRO_NOT_DECLARED | Qué significa: Se anotó pero no llegó a salir hacia la AEAT. Reinténtalo por el mismo endpoint por el que lo enviaste: POST /registros con la misma Idempotency-Key, o POST /lotes con el mismo registroId. El mensaje te dice cuál |
code: VERIFACTU_UNCONFIRMED | Qué significa: Se envió en un lote y no hubo respuesta fiable: puede estar ya en la AEAT. No lo reenvíes; contacta con soporte |
429
Respeta siempre Retry-After.
code | Qué significa |
|---|---|
code: RATE_LIMIT_EXCEEDED | Qué significa: Has superado el límite por minuto de tu plan (cuenta toda petición, salga bien o mal), o más de 20 consultas de estado por minuto. Espera los segundos de Retry-After |
code: MONTHLY_QUOTA_EXCEEDED | Qué significa: Has agotado el cupo del mes de la pasarela. Solo lo reciben las peticiones que declaran (POST /registros y POST /lotes): consultar, anular y subsanar siguen funcionando. Retry-After apunta al inicio del mes siguiente; para seguir antes, mejora de plan |
500
INTERNAL_ERROR — fallo nuestro. Reintenta con espera creciente (con la misma Idempotency-Key si era un POST); si persiste, soporte.
Corregir
Anular una factura registrada, o subsanar una que la AEAT rechazó.
/v1/verifactu/anulacionesAnular una factura registrada
Permiso: verifactu:write.
Da de baja ante la AEAT una factura que sí consta.
Se anula por registroId, no por serie + número + fecha. No es una comodidad: en el sandbox todos los clientes declaran con el mismo NIF de demostración, así que aceptar serie y número permitiría tumbar la factura de otro acertando una combinación tan común como A/1.
⚠️ Anular no es corregir. Si la factura está bien registrada y quieres cambiarle algo, lo que corresponde es una rectificativa (R1–R5), no una baja seguida de un alta.
⚠️ Una factura que la AEAT marcó Incorrecta o No registrada no se puede anular: no consta, así que no hay nada que dar de baja. Eso se subsana. Recibirás 409 REGISTRO_NOT_CANCELLABLE.
Petición
curl -X POST "https://api.gokiby.com/v1/verifactu/anulaciones" \
-H "Authorization: Bearer kiby_test_…" \
-H "X-Kiby-Tenant: TU-NIF-O-TENANTUID" \
-H "Idempotency-Key: pedido-2026-00042" \
-H "Content-Type: application/json" \
-d '{
"registroId": "pedido-2026-00042"
}'Autenticación: Authorization: Bearer <clave>. Con una clave kiby_test_ va al sandbox.
Cabeceras
| Campo | Tipo | Descripción |
|---|---|---|
Campo: X-Kiby-Tenantobligatorio | Tipo: texto | Descripción: NIF del emisor (o su identificador de cuenta Kiby). Es la barrera que impide declarar o facturar en nombre de otro emisor: si no resuelve a una cuenta a la que tu clave dé acceso, recibes Ejemplo: |
Campo: Idempotency-Keyobligatorio | Tipo: texto | Descripción: Identificador que eliges tú para esta operación. Obligatoria en todos los
⚠️ Genérala ANTES del primer intento y consérvala en los reintentos. El error clásico es generarla dentro del bucle de reintento: cada intento lleva una clave distinta, la deduplicación no puede reconocer nada y acabas con la misma factura declarada varias veces. Ejemplo: |
Cuerpo
{
"registroId": "pedido-2026-00042"
}Ver los 1 campos del cuerpo
| Campo | Tipo | Descripción |
|---|---|---|
Campo: registroIdobligatorio | Tipo: texto | Descripción: El que devolvió el |
Respuestas
200
Ya estaba anulada. Pedir anular un registro ya anulado devuelve la anulación que hubo, no un error: es lo que significa la petición, «asegúrate de que este registro está anulado».
{
"anulacionId": "string",
"registroId": "string",
"entorno": "test",
"estado": "Pendiente",
"uuid": "string"
}Ver los campos de la respuesta 200
| Campo | Tipo | Descripción |
|---|---|---|
Campo: anulacionId | Tipo: texto | Descripción: |
Campo: registroId | Tipo: texto | Descripción: |
Campo: entorno | Tipo: texto | Descripción: Valores: |
Campo: estado | Tipo: texto | Descripción: |
Campo: uuid | Tipo: texto | Descripción: |
201
Anulación enviada.
{
"anulacionId": "string",
"registroId": "string",
"entorno": "test",
"estado": "Pendiente",
"uuid": "string"
}Ver los campos de la respuesta 201
| Campo | Tipo | Descripción |
|---|---|---|
Campo: anulacionId | Tipo: texto | Descripción: |
Campo: registroId | Tipo: texto | Descripción: |
Campo: entorno | Tipo: texto | Descripción: Valores: |
Campo: estado | Tipo: texto | Descripción: |
Campo: uuid | Tipo: texto | Descripción: |
Errores
Todos los errores llegan con la misma forma:
{
"error": {
"code": "INVALID_BODY",
"message": "string",
"details": {
"clave": "string"
}
}
}400
Tu petición. Reintentar no sirve: corrígela.
code | Cuándo |
|---|---|
code: MISSING_HEADER | Cuándo: Falta X-Kiby-Tenant o Idempotency-Key |
code: INVALID_IDEMPOTENCY_KEY | Cuándo: La cabecera está, pero con caracteres que no admite |
code: INVALID_BODY | Cuándo: El cuerpo no es un objeto, o un campo tiene tipo o formato equivocado, o el desglose no cuadra |
code: UNKNOWN_FIELD | Cuándo: Un campo que no existe en el contrato |
code: FORBIDDEN_FIELD | Cuándo: Un campo que existe pero lo fija el servidor |
code: INVALID_LINE | Cuándo: Una línea del desglose está mal |
code: TOO_MANY_LINES | Cuándo: Más de 12 líneas de desglose |
code: FIELD_NOT_AMENDABLE | Cuándo: Intentas subsanar algo que exige rectificativa |
code: TOO_MANY_REGISTROS | Cuándo: Más de 50 facturas en un lote |
code: DUPLICATE_REGISTRO_ID | Cuándo: El mismo registroId dos veces en un lote |
code: DUPLICATE_INVOICE_NUMBER | Cuándo: Dos facturas del lote con la misma serie y número |
code: VERIFACTU_REJECTED | Cuándo: El servicio de envío rechazó los datos. No se envió nada a la AEAT. Su código estable va en details.verifacti_codigo |
401
UNAUTHORIZED — clave ausente, mal formada, revocada o caducada, o mandada en la URL (?api_key=), que se rechaza siempre: a esas alturas ya está en logs ajenos. Rótala.
403
code | Cuándo |
|---|---|
code: TENANT_HEADER_MISMATCH | Cuándo: X-Kiby-Tenant no resuelve a una cuenta a la que tu clave dé acceso |
code: INSUFFICIENT_SCOPE | Cuándo: La clave no tiene verifactu:write. Si es una clave de la pasarela creada antes de que existiera ese permiso, avisa a soporte |
code: IP_NOT_ALLOWED | Cuándo: La clave tiene lista de IPs y la tuya no está |
code: FORBIDDEN | Cuándo: Denegado por un motivo que no encaja en los anteriores. Si te llega, dínoslo: significa que hay un caso que deberíamos estar nombrando |
code: TENANT_NOT_FOUND | Cuándo: Solo al declarar (/registros, /lotes): la cuenta Kiby de la clave ya no existe |
404
NOT_FOUND — ese registroId no existe en este entorno. Si estabas usando una clave kiby_test_ y el registro se declaró con una kiby_live_ (o al revés), no lo vas a encontrar: son dos espacios separados a propósito.
409
Mira el estado del registro antes de insistir. Reintentar tal cual no lo va a arreglar.
code | Qué significa |
|---|---|
code: IDEMPOTENCY_KEY_REUSED | Qué significa: Esa clave ya se usó para otra factura distinta |
code: TENANT_NOT_READY | Qué significa: El alta Verifactu del emisor no está operativa. Lo resuelve el titular de la cuenta Kiby, no tú. El estado concreto va en details.verifactu_status |
code: REGISTRO_NOT_DECLARED | Qué significa: Se anotó pero no llegó a salir. Reinténtalo por el endpoint por el que lo enviaste |
code: REGISTRO_EN_OTRO_ENDPOINT | Qué significa: Ese registro entró por el otro endpoint de alta (/registros o /lotes) y no consta su resultado. Reinténtalo por aquel: cruzarlos podría declararlo dos veces |
code: VERIFACTU_UNCONFIRMED | Qué significa: Se envió en un lote sin respuesta fiable: puede estar ya en la AEAT. No lo reenvíes; contacta con soporte |
code: REGISTRO_NOT_CANCELLABLE | Qué significa: La AEAT no tiene registrada esa factura: no hay nada que anular. Corresponde subsanar |
code: REGISTRO_ALREADY_CANCELLED | Qué significa: Al subsanar: está anulada. Al anular: ya tiene una anulación con otro contenido (contacta con soporte) |
code: REGISTRO_NOT_AMENDABLE | Qué significa: Su estado no admite subsanación |
code: REGISTRO_PAYLOAD_INCOMPLETE | Qué significa: Al registro le faltan datos para construir la baja. Contacta con soporte |
code: REGISTRO_ESTADO_INCONSISTENTE | Qué significa: Anomalía en un dato que solo escribimos nosotros. Contacta con soporte |
code: VERIFACTU_REQUEST_IN_PROGRESS | Qué significa: La misma operación está en curso (también: un lote está enviando ese registro). Espera y consulta el estado |
Sobre TENANT_NOT_READY, dos valores de details.verifactu_status que conviene distinguir: VERIFACTU_STATUS_NIF_PENDING es transitorio y se resuelve solo en unos minutos; VERIFACTU_STATUS_PENDING_REPRESENTATION no se arregla reintentando jamás — falta que el titular firme la declaración responsable.
429
Respeta siempre Retry-After.
code | Qué significa |
|---|---|
code: RATE_LIMIT_EXCEEDED | Qué significa: Has superado el límite por minuto de tu plan (cuenta toda petición, salga bien o mal), o más de 20 consultas de estado por minuto. Espera los segundos de Retry-After |
code: MONTHLY_QUOTA_EXCEEDED | Qué significa: Has agotado el cupo del mes de la pasarela. Solo lo reciben las peticiones que declaran (POST /registros y POST /lotes): consultar, anular y subsanar siguen funcionando. Retry-After apunta al inicio del mes siguiente; para seguir antes, mejora de plan |
500
INTERNAL_ERROR — fallo nuestro. Reintenta con espera creciente (con la misma Idempotency-Key si era un POST); si persiste, soporte.
502
VERIFACTU_UPSTREAM_ERROR — no hemos podido hablar con el servicio de envío a la AEAT, o nos ha respondido algo que no sabemos interpretar.
⚠️ Reintenta con la MISMA Idempotency-Key. Aquí no se sabe si la declaración llegó o no, y esa clave es lo único que impide declararla dos veces.
/v1/verifactu/subsanacionesCorregir una factura que la AEAT rechazó
Permiso: verifactu:write.
Es la salida de los estados que la AEAT marca como subsanables: Incorrecta, No registrada y Aceptada con errores. Los dos primeros no se pueden anular porque no constan.
Lo único que puede cambiar hoy es descripcion. Los importes, la serie, el número y el destinatario NO son subsanables: para eso está la rectificativa. Intentarlo devuelve 400 FIELD_NOT_AMENDABLE diciendo qué hacer en su lugar.
⚠️ No envíes rechazo_previo. La AEAT exige declarar qué fue lo rechazado —nada, el alta original o una subsanación anterior— y lo derivamos nosotros del historial que guardamos. Tú no puedes saberlo, y un valor equivocado declara un hecho falso a la AEAT. Te lo devolvemos en la respuesta para que veas qué se declaró en tu nombre.
Un mismo registro admite varias subsanaciones: si la AEAT rechaza la corrección, se vuelve a subsanar. Consulta el estado para ver ultimaSubsanacion.rechazada.
Petición
curl -X POST "https://api.gokiby.com/v1/verifactu/subsanaciones" \
-H "Authorization: Bearer kiby_test_…" \
-H "X-Kiby-Tenant: TU-NIF-O-TENANTUID" \
-H "Idempotency-Key: pedido-2026-00042" \
-H "Content-Type: application/json" \
-d '{
"registroId": "pedido-2026-00042",
"descripcion": "Servicios de consultoría — septiembre 2026"
}'Autenticación: Authorization: Bearer <clave>. Con una clave kiby_test_ va al sandbox.
Cabeceras
| Campo | Tipo | Descripción |
|---|---|---|
Campo: X-Kiby-Tenantobligatorio | Tipo: texto | Descripción: NIF del emisor (o su identificador de cuenta Kiby). Es la barrera que impide declarar o facturar en nombre de otro emisor: si no resuelve a una cuenta a la que tu clave dé acceso, recibes Ejemplo: |
Campo: Idempotency-Keyobligatorio | Tipo: texto | Descripción: Identificador que eliges tú para esta operación. Obligatoria en todos los
⚠️ Genérala ANTES del primer intento y consérvala en los reintentos. El error clásico es generarla dentro del bucle de reintento: cada intento lleva una clave distinta, la deduplicación no puede reconocer nada y acabas con la misma factura declarada varias veces. Ejemplo: |
Cuerpo
{
"registroId": "pedido-2026-00042",
"descripcion": "Servicios de consultoría — septiembre 2026"
}Ver los 2 campos del cuerpo
| Campo | Tipo | Descripción |
|---|---|---|
Campo: registroIdobligatorio | Tipo: texto | Descripción: |
Campo: descripcion | Tipo: texto | Descripción: Máximo 500 caracteres |
Respuestas
201
Subsanación enviada.
{
"subsanacionId": "string",
"registroId": "string",
"entorno": "test",
"estado": "Pendiente",
"rechazoPrevio": "N",
"uuid": "string",
"url": "string",
"qr": "string"
}Ver los campos de la respuesta 201
| Campo | Tipo | Descripción |
|---|---|---|
Campo: subsanacionId | Tipo: texto | Descripción: |
Campo: registroId | Tipo: texto | Descripción: |
Campo: entorno | Tipo: texto | Descripción: Valores: |
Campo: estado | Tipo: texto | Descripción: |
Campo: rechazoPrevio | Tipo: texto | Descripción: Lo que se declaró a la AEAT en tu nombre, derivado por nosotros. Valores: |
Campo: uuid | Tipo: texto | Descripción: |
Campo: url | Tipo: texto | Descripción: |
Campo: qr | Tipo: texto | Descripción: |
Errores
Todos los errores llegan con la misma forma:
{
"error": {
"code": "INVALID_BODY",
"message": "string",
"details": {
"clave": "string"
}
}
}400
Tu petición. Reintentar no sirve: corrígela.
code | Cuándo |
|---|---|
code: MISSING_HEADER | Cuándo: Falta X-Kiby-Tenant o Idempotency-Key |
code: INVALID_IDEMPOTENCY_KEY | Cuándo: La cabecera está, pero con caracteres que no admite |
code: INVALID_BODY | Cuándo: El cuerpo no es un objeto, o un campo tiene tipo o formato equivocado, o el desglose no cuadra |
code: UNKNOWN_FIELD | Cuándo: Un campo que no existe en el contrato |
code: FORBIDDEN_FIELD | Cuándo: Un campo que existe pero lo fija el servidor |
code: INVALID_LINE | Cuándo: Una línea del desglose está mal |
code: TOO_MANY_LINES | Cuándo: Más de 12 líneas de desglose |
code: FIELD_NOT_AMENDABLE | Cuándo: Intentas subsanar algo que exige rectificativa |
code: TOO_MANY_REGISTROS | Cuándo: Más de 50 facturas en un lote |
code: DUPLICATE_REGISTRO_ID | Cuándo: El mismo registroId dos veces en un lote |
code: DUPLICATE_INVOICE_NUMBER | Cuándo: Dos facturas del lote con la misma serie y número |
code: VERIFACTU_REJECTED | Cuándo: El servicio de envío rechazó los datos. No se envió nada a la AEAT. Su código estable va en details.verifacti_codigo |
401
UNAUTHORIZED — clave ausente, mal formada, revocada o caducada, o mandada en la URL (?api_key=), que se rechaza siempre: a esas alturas ya está en logs ajenos. Rótala.
403
code | Cuándo |
|---|---|
code: TENANT_HEADER_MISMATCH | Cuándo: X-Kiby-Tenant no resuelve a una cuenta a la que tu clave dé acceso |
code: INSUFFICIENT_SCOPE | Cuándo: La clave no tiene verifactu:write. Si es una clave de la pasarela creada antes de que existiera ese permiso, avisa a soporte |
code: IP_NOT_ALLOWED | Cuándo: La clave tiene lista de IPs y la tuya no está |
code: FORBIDDEN | Cuándo: Denegado por un motivo que no encaja en los anteriores. Si te llega, dínoslo: significa que hay un caso que deberíamos estar nombrando |
code: TENANT_NOT_FOUND | Cuándo: Solo al declarar (/registros, /lotes): la cuenta Kiby de la clave ya no existe |
404
NOT_FOUND — ese registroId no existe en este entorno. Si estabas usando una clave kiby_test_ y el registro se declaró con una kiby_live_ (o al revés), no lo vas a encontrar: son dos espacios separados a propósito.
409
Mira el estado del registro antes de insistir. Reintentar tal cual no lo va a arreglar.
code | Qué significa |
|---|---|
code: IDEMPOTENCY_KEY_REUSED | Qué significa: Esa clave ya se usó para otra factura distinta |
code: TENANT_NOT_READY | Qué significa: El alta Verifactu del emisor no está operativa. Lo resuelve el titular de la cuenta Kiby, no tú. El estado concreto va en details.verifactu_status |
code: REGISTRO_NOT_DECLARED | Qué significa: Se anotó pero no llegó a salir. Reinténtalo por el endpoint por el que lo enviaste |
code: REGISTRO_EN_OTRO_ENDPOINT | Qué significa: Ese registro entró por el otro endpoint de alta (/registros o /lotes) y no consta su resultado. Reinténtalo por aquel: cruzarlos podría declararlo dos veces |
code: VERIFACTU_UNCONFIRMED | Qué significa: Se envió en un lote sin respuesta fiable: puede estar ya en la AEAT. No lo reenvíes; contacta con soporte |
code: REGISTRO_NOT_CANCELLABLE | Qué significa: La AEAT no tiene registrada esa factura: no hay nada que anular. Corresponde subsanar |
code: REGISTRO_ALREADY_CANCELLED | Qué significa: Al subsanar: está anulada. Al anular: ya tiene una anulación con otro contenido (contacta con soporte) |
code: REGISTRO_NOT_AMENDABLE | Qué significa: Su estado no admite subsanación |
code: REGISTRO_PAYLOAD_INCOMPLETE | Qué significa: Al registro le faltan datos para construir la baja. Contacta con soporte |
code: REGISTRO_ESTADO_INCONSISTENTE | Qué significa: Anomalía en un dato que solo escribimos nosotros. Contacta con soporte |
code: VERIFACTU_REQUEST_IN_PROGRESS | Qué significa: La misma operación está en curso (también: un lote está enviando ese registro). Espera y consulta el estado |
Sobre TENANT_NOT_READY, dos valores de details.verifactu_status que conviene distinguir: VERIFACTU_STATUS_NIF_PENDING es transitorio y se resuelve solo en unos minutos; VERIFACTU_STATUS_PENDING_REPRESENTATION no se arregla reintentando jamás — falta que el titular firme la declaración responsable.
429
Respeta siempre Retry-After.
code | Qué significa |
|---|---|
code: RATE_LIMIT_EXCEEDED | Qué significa: Has superado el límite por minuto de tu plan (cuenta toda petición, salga bien o mal), o más de 20 consultas de estado por minuto. Espera los segundos de Retry-After |
code: MONTHLY_QUOTA_EXCEEDED | Qué significa: Has agotado el cupo del mes de la pasarela. Solo lo reciben las peticiones que declaran (POST /registros y POST /lotes): consultar, anular y subsanar siguen funcionando. Retry-After apunta al inicio del mes siguiente; para seguir antes, mejora de plan |
500
INTERNAL_ERROR — fallo nuestro. Reintenta con espera creciente (con la misma Idempotency-Key si era un POST); si persiste, soporte.
502
VERIFACTU_UPSTREAM_ERROR — no hemos podido hablar con el servicio de envío a la AEAT, o nos ha respondido algo que no sabemos interpretar.
⚠️ Reintenta con la MISMA Idempotency-Key. Aquí no se sabe si la declaración llegó o no, y esa clave es lo único que impide declararla dos veces.
Para empezar
¿Quieres integrar tu software con Verifactu?
Tu programa sigue facturando como hasta ahora: no hace falta facturar con Kiby. Para tener la clave, el titular de la cuenta Kiby completa su alta de Verifactu, activa la gema Kiby API y crea la clave en Ajustes → Claves de API. Si todavía no tienes cuenta, puedes probarla 14 días gratis.
¿Ya facturas con Kiby y quieres conectar tu cuenta con otras herramientas? Lo tuyo es la API de Kiby.
