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.
Antes de empezar
Declara ante la AEAT las facturas de tu propio sistema a través de Kiby.
La numeración y el contenido son tuyos. Kiby transporta, firma con la credencial del emisor y guarda el rastro; no emite facturas de Kiby ni las guarda en su cuenta.
El ciclo son dos pasos, no uno
Al declarar recibes Pendiente: la AEAT resuelve después. 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.
Antes de integrar, lo prepara el titular de la cuenta Kiby
No es trabajo tuyo y no puedes hacerlo por él:
- Completa su alta Verifactu en Kiby (credencial y declaración responsable). Sin eso, cualquier petición en producción responde
409 TENANT_NOT_READY. - Activa la gema Kiby API y crea una clave en Ajustes → Claves de API.
- Te entrega la clave y su NIF, que es lo que va en
X-Kiby-Tenant.
Qué NO validamos
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, 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 el mismo código de serie en los dos sitios: 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.
En el sandbox, 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.
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_→ sandbox. Declara de verdad ante la AEAT, pero con el NIF de demostración de Kiby, no con el del emisor. Usa una serie propia: ese NIF lo comparten todos los integradores (ver «En el sandbox, usa una serie propia»).kiby_live_→ producción. Declara con la credencial del propio emisor, que antes tiene que haber completado su alta Verifactu en Kiby.
Autenticación con clave de API
Authorization: Bearer <clave>.
El prefijo de la clave decide el entorno, y es la única cosa que lo decide:
| Prefijo | Con qué NIF se declara |
|---|---|
Prefijo: kiby_test_ | Con qué NIF se declara: El NIF de demostración de Kiby. No es el del emisor. |
Prefijo: kiby_live_ | Con qué NIF se declara: El NIF del propio emisor. Declara de verdad en su nombre. |
⚠️ Un QR obtenido en el sandbox 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.
La respuesta trae siempre el campo entorno, también en producción: 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 en el registro de la AEAT.
/v1/verifactu/registrosDeclarar una factura
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 registroId que devuelve es tu Idempotency-Key. Guárdalo: es lo que identifica el registro para consultarlo, anularlo o subsanarlo.
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 consultar 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.
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 el permiso de facturación |
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 |
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: Ya estaba anulada |
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 |
429
RATE_LIMIT_EXCEEDED — límite por minuto, o más de 20 consultas de estado por minuto. Respeta Retry-After.
500
INTERNAL_ERROR — fallo nuestro. Reintenta con espera creciente; 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 no está configurado en este despliegue |
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
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 consultar 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.
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 el permiso de facturación |
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 |
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: Ya estaba anulada |
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 |
413
PAYLOAD_TOO_LARGE — el cuerpo supera 256 KB. Manda menos facturas por lote.
429
RATE_LIMIT_EXCEEDED — límite por minuto, o más de 20 consultas de estado por minuto. Respeta Retry-After.
500
INTERNAL_ERROR — fallo nuestro. Reintenta con espera creciente; si persiste, soporte.
503
code | Qué significa |
|---|---|
code: SANDBOX_UNAVAILABLE | Qué significa: El sandbox no está configurado en este despliegue |
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. Es lo que cierra el ciclo.
/v1/verifactu/registros/{registroId}Consultar el estado de un registro
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. Está muy por debajo del tope general de 60/min, que comparten lecturas y escrituras: un bucle de sondeo cerrado te dejaría sin presupuesto para declarar. 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-Tenantobligatorio | Tipo: texto | Descripción: NIF del emisor (o su identificador de cuenta Kiby). Es la barrera que impide declarar o consultar en nombre de otro emisor: si no resuelve a una cuenta a la que tu clave dé acceso, recibes 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.
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 el permiso de facturación |
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 |
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
RATE_LIMIT_EXCEEDED — límite por minuto, o más de 20 consultas de estado por minuto. Respeta Retry-After.
500
INTERNAL_ERROR — fallo nuestro. Reintenta con espera creciente; si persiste, soporte.
Corregir
Anular una factura registrada, o subsanar una que la AEAT rechazó.
/v1/verifactu/anulacionesAnular una factura registrada
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 consultar 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
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.
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 el permiso de facturación |
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 |
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: Ya estaba anulada |
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 |
429
RATE_LIMIT_EXCEEDED — límite por minuto, o más de 20 consultas de estado por minuto. Respeta Retry-After.
500
INTERNAL_ERROR — fallo nuestro. Reintenta con espera creciente; 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ó
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 consultar 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.
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 el permiso de facturación |
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 |
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: Ya estaba anulada |
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 |
429
RATE_LIMIT_EXCEEDED — límite por minuto, o más de 20 consultas de estado por minuto. Respeta Retry-After.
500
INTERNAL_ERROR — fallo nuestro. Reintenta con espera creciente; 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?
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 usáis Kiby, podéis probarlo 14 días gratis.
