Empieza con 14 días gratis y tu primer mes completo al 50%
Sin permanencia.

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

Lo 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.

OpenAPI 3.0 · versión 1.0.0 Descargar openapi.yaml

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:

  1. POST /v1/verifactu/registros → 201 con estado: "Pendiente" y un registroId.
  2. GET /v1/verifactu/registros/{registroId} hasta que final sea true.
  3. Según el veredicto: nada que hacer, POST /anulaciones o POST /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:

  1. Completa su alta Verifactu en Kiby (credencial y declaración responsable). Sin eso, cualquier petición en producción responde 409 TENANT_NOT_READY.
  2. Activa la gema Kiby API y crea una clave en Ajustes → Claves de API.
  3. 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.com

La 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:

PrefijoCon 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.

POST/v1/verifactu/registros

Declarar 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
Ejemplo con curl (sandbox)
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
Cabeceras de la petición
CampoTipoDescripción
Campo: X-Kiby-TenantobligatorioTipo: textoDescripció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 403 TENANT_HEADER_MISMATCH.

Ejemplo: TU-NIF-O-TENANTUID

Campo: Idempotency-KeyobligatorioTipo: textoDescripción:

Identificador que eliges tú para esta operación. Obligatoria en todos los POST: sin ella, un corte de red entre tu petición y nuestra respuesta te deja sin saber si la factura se declaró, y reintentar la declararía dos veces ante la AEAT — que ya no es un error, es una subsanación.

  • Mismo contenido y misma clave → se devuelve el resultado anterior con 200.
  • Contenido distinto y misma clave → 409 IDEMPOTENCY_KEY_REUSED. No se declara nada.
  • Una clave por operación: no reutilices la del registro para anularlo.

⚠️ 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: pedido-2026-00042

Cuerpo
Ejemplo del cuerpo (JSON)
{
  "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
Campos del cuerpo
CampoTipoDescripción
Campo: serieobligatorioTipo: textoDescripción:

Tu serie. Kiby no la valida ni la asigna.

Campo: numeroobligatorioTipo: textoDescripción:

Tu número. Kiby no comprueba la correlatividad.

Campo: fecha_expedicionobligatorioTipo: textoDescripción:

YYYY-MM-DD, y tiene que ser una fecha real: 2026-02-31 se rechaza.

Formato date

Campo: tipo_facturaTipo: textoDescripción:

Catálogo de la AEAT. F1 ordinaria, F2 simplificada, F3 sustitutiva de simplificadas, R1–R5 rectificativas. Se omite para una factura ordinaria.

F2 tiene dos particularidades: puede ir sin destinatario, y no puede alcanzar 3000 € (RD 1619/2012, art. 4 — «inferior a 3.000 euros», así que 3000,00 exactos ya no vale). Por encima hay que emitir F1, que sí identifica al destinatario. Tampoco admite importes negativos: para rectificar una simplificada se emite una R5.

⚠️ 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 F2 es responsabilidad del emisor: nosotros no conocemos ni su sector ni la operación, así que rechazamos lo que es imposible pero no podemos validar lo que es correcto.

Valores: F1, F2, F3, R1, R2, R3, R4, R5 · Por defecto: F1

Campo: descripcionobligatorioTipo: textoDescripción:

Máximo 500 caracteres

Campo: destinatarioTipo: objetoDescripción:

Obligatorio salvo en F2. Si lo mandas —también en una simplificada, donde mejora la trazabilidad— tiene que ir completo: la AEAT valida la pareja NIF + nombre, así que medio destinatario no es menos información, es un rechazo.

Campo: destinatario.nifobligatorioTipo: textoDescripció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 VERIFACTU_REJECTED.

Campo: destinatario.nombreobligatorioTipo: textoDescripción:
Campo: desgloseobligatorioTipo: listaDescripció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 400 TOO_MANY_LINES.

Entre 1 y 12 elementos

Campo: desglose[].base_imponibleobligatorioTipo: númeroDescripción:

Formato double

Campo: desglose[].tipo_impositivoobligatorioTipo: númeroDescripción:

Porcentaje, no fracción. 21 para el 21 %. No puede ser negativo.

Campo: desglose[].cuota_repercutidaobligatorioTipo: númeroDescripción:

Formato double

Campo: desglose[].tipo_recargo_equivalenciaTipo: númeroDescripció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_equivalenciaTipo: númeroDescripción:

Formato double

Campo: importe_totalobligatorioTipo: númeroDescripció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 double

Respuestas

200

Ya estaba declarada. Dos casos, y en los dos la respuesta es la del registro que ya existe:

  • Reintento reconocido: misma Idempotency-Key y 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.

Respuesta 200 (JSON)
{
  "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
Campos de la respuesta 200
CampoTipoDescripción
Campo: registroIdTipo: textoDescripción:

Coincide con tu Idempotency-Key. Guárdalo.

Campo: entornoTipo: textoDescripción:

Viaja siempre, también en producción.

Valores: test, prod

Campo: estadoTipo: textoDescripció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. Pendiente es lo normal al crear.

Campo: uuidTipo: textoDescripción:
Campo: urlTipo: textoDescripción:

Página de verificación de la AEAT.

Campo: qrTipo: textoDescripción:

PNG en base64 del QR de verificación.

201

Registro creado y enviado.

Respuesta 201 (JSON)
{
  "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
Campos de la respuesta 201
CampoTipoDescripción
Campo: registroIdTipo: textoDescripción:

Coincide con tu Idempotency-Key. Guárdalo.

Campo: entornoTipo: textoDescripción:

Viaja siempre, también en producción.

Valores: test, prod

Campo: estadoTipo: textoDescripció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. Pendiente es lo normal al crear.

Campo: uuidTipo: textoDescripción:
Campo: urlTipo: textoDescripción:

Página de verificación de la AEAT.

Campo: qrTipo: textoDescripción:

PNG en base64 del QR de verificación.

Errores

Todos los errores llegan con la misma forma:

Forma de un error (JSON)
{
  "error": {
    "code": "INVALID_BODY",
    "message": "string",
    "details": {
      "clave": "string"
    }
  }
}

400

Tu petición. Reintentar no sirve: corrígela.

codeCuándo
code: MISSING_HEADERCuándo: Falta X-Kiby-Tenant o Idempotency-Key
code: INVALID_IDEMPOTENCY_KEYCuándo: La cabecera está, pero con caracteres que no admite
code: INVALID_BODYCuándo: El cuerpo no es un objeto, o un campo tiene tipo o formato equivocado, o el desglose no cuadra
code: UNKNOWN_FIELDCuándo: Un campo que no existe en el contrato
code: FORBIDDEN_FIELDCuándo: Un campo que existe pero lo fija el servidor
code: INVALID_LINECuándo: Una línea del desglose está mal
code: TOO_MANY_LINESCuándo: Más de 12 líneas de desglose
code: FIELD_NOT_AMENDABLECuándo: Intentas subsanar algo que exige rectificativa
code: TOO_MANY_REGISTROSCuándo: Más de 50 facturas en un lote
code: DUPLICATE_REGISTRO_IDCuándo: El mismo registroId dos veces en un lote
code: DUPLICATE_INVOICE_NUMBERCuándo: Dos facturas del lote con la misma serie y número
code: VERIFACTU_REJECTEDCuá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

codeCuándo
code: TENANT_HEADER_MISMATCHCuándo: X-Kiby-Tenant no resuelve a una cuenta a la que tu clave dé acceso
code: INSUFFICIENT_SCOPECuándo: La clave no tiene el permiso de facturación
code: IP_NOT_ALLOWEDCuándo: La clave tiene lista de IPs y la tuya no está
code: FORBIDDENCuá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.

codeQué significa
code: IDEMPOTENCY_KEY_REUSEDQué significa: Esa clave ya se usó para otra factura distinta
code: TENANT_NOT_READYQué 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_DECLAREDQué significa: Se anotó pero no llegó a salir. Reinténtalo por el endpoint por el que lo enviaste
code: REGISTRO_EN_OTRO_ENDPOINTQué 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_UNCONFIRMEDQué 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_CANCELLABLEQué significa: La AEAT no tiene registrada esa factura: no hay nada que anular. Corresponde subsanar
code: REGISTRO_ALREADY_CANCELLEDQué significa: Ya estaba anulada
code: REGISTRO_NOT_AMENDABLEQué significa: Su estado no admite subsanación
code: REGISTRO_PAYLOAD_INCOMPLETEQué significa: Al registro le faltan datos para construir la baja. Contacta con soporte
code: REGISTRO_ESTADO_INCONSISTENTEQué significa: Anomalía en un dato que solo escribimos nosotros. Contacta con soporte
code: VERIFACTU_REQUEST_IN_PROGRESSQué 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

codeQué significa
code: SANDBOX_UNAVAILABLEQué significa: El sandbox no está configurado en este despliegue
code: GATEWAY_MISCONFIGUREDQué significa: Configuración nuestra incoherente. No es tu petición: contacta con soporte
code: SERVICE_UNAVAILABLEQué significa: Solo dentro de un lote, en una factura: no pudimos prepararla. Reenvíala tal cual en otro lote
POST/v1/verifactu/lotes

Declarar 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 registroId ya se usó para otra factura (409 IDEMPOTENCY_KEY_REUSED, con la lista en details.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
statuscodeQué hacer
status: 201code: —Qué hacer: Declarada ahora
status: 200code: —Qué hacer: Ya estaba declarada: es lo guardado
status: 400code: VERIFACTU_REJECTEDQué 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: 409code: IDEMPOTENCY_KEY_REUSEDQué hacer: Ese registroId se usó para otra factura mientras procesábamos el lote
status: 409code: VERIFACTU_REQUEST_IN_PROGRESSQué hacer: Otro lote la está enviando ahora. Consulta su estado en unos segundos
status: 409code: REGISTRO_EN_OTRO_ENDPOINTQué hacer: Entró por /registros sin resultado: reinténtala por ahí
status: 409code: VERIFACTU_UNCONFIRMEDQué hacer: Un lote anterior quedó sin confirmar. No la reenvíes: soporte
status: 502code: VERIFACTU_UPSTREAM_ERRORQué hacer: No llegó a enviarse. Reenvíala tal cual en otro lote
status: 502code: VERIFACTU_UNCONFIRMEDQué hacer: Se envió y no hubo respuesta fiable: puede estar en la AEAT. No la reenvíes: soporte
status: 503code: SERVICE_UNAVAILABLEQué hacer: No pudimos prepararla. Reenvíala tal cual en otro lote
Petición
Ejemplo con curl (sandbox)
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
Cabeceras de la petición
CampoTipoDescripción
Campo: X-Kiby-TenantobligatorioTipo: textoDescripció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 403 TENANT_HEADER_MISMATCH.

Ejemplo: TU-NIF-O-TENANTUID

Campo: Idempotency-KeyobligatorioTipo: textoDescripción:

Identificador que eliges tú para esta operación. Obligatoria en todos los POST: sin ella, un corte de red entre tu petición y nuestra respuesta te deja sin saber si la factura se declaró, y reintentar la declararía dos veces ante la AEAT — que ya no es un error, es una subsanación.

  • Mismo contenido y misma clave → se devuelve el resultado anterior con 200.
  • Contenido distinto y misma clave → 409 IDEMPOTENCY_KEY_REUSED. No se declara nada.
  • Una clave por operación: no reutilices la del registro para anularlo.

⚠️ 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: pedido-2026-00042

Cuerpo
Ejemplo del cuerpo (JSON)
{
  "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
Campos del cuerpo
CampoTipoDescripción
Campo: registrosobligatorioTipo: listaDescripción:

Cada elemento es el cuerpo de POST /registros más su registroId. Ni el registroId ni la pareja serie + número pueden repetirse dentro del lote.

Entre 1 y 50 elementos

Campo: registros[].registroIdobligatorioTipo: textoDescripción:

Identifica ESTA factura para siempre. Mismas reglas que la Idempotency-Key.

Campo: registros[].serieobligatorioTipo: textoDescripción:

Tu serie. Kiby no la valida ni la asigna.

Campo: registros[].numeroobligatorioTipo: textoDescripción:

Tu número. Kiby no comprueba la correlatividad.

Campo: registros[].fecha_expedicionobligatorioTipo: textoDescripción:

YYYY-MM-DD, y tiene que ser una fecha real: 2026-02-31 se rechaza.

Formato date

Campo: registros[].tipo_facturaTipo: textoDescripción:

Catálogo de la AEAT. F1 ordinaria, F2 simplificada, F3 sustitutiva de simplificadas, R1–R5 rectificativas. Se omite para una factura ordinaria.

F2 tiene dos particularidades: puede ir sin destinatario, y no puede alcanzar 3000 € (RD 1619/2012, art. 4 — «inferior a 3.000 euros», así que 3000,00 exactos ya no vale). Por encima hay que emitir F1, que sí identifica al destinatario. Tampoco admite importes negativos: para rectificar una simplificada se emite una R5.

⚠️ 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 F2 es responsabilidad del emisor: nosotros no conocemos ni su sector ni la operación, así que rechazamos lo que es imposible pero no podemos validar lo que es correcto.

Valores: F1, F2, F3, R1, R2, R3, R4, R5 · Por defecto: F1

Campo: registros[].descripcionobligatorioTipo: textoDescripción:

Máximo 500 caracteres

Campo: registros[].destinatarioTipo: objetoDescripción:

Obligatorio salvo en F2. Si lo mandas —también en una simplificada, donde mejora la trazabilidad— tiene que ir completo: la AEAT valida la pareja NIF + nombre, así que medio destinatario no es menos información, es un rechazo.

Campo: registros[].destinatario.nifobligatorioTipo: textoDescripció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 VERIFACTU_REJECTED.

Campo: registros[].destinatario.nombreobligatorioTipo: textoDescripción:
Campo: registros[].desgloseobligatorioTipo: listaDescripció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 400 TOO_MANY_LINES.

Entre 1 y 12 elementos

Campo: registros[].desglose[].base_imponibleobligatorioTipo: númeroDescripción:

Formato double

Campo: registros[].desglose[].tipo_impositivoobligatorioTipo: númeroDescripción:

Porcentaje, no fracción. 21 para el 21 %. No puede ser negativo.

Campo: registros[].desglose[].cuota_repercutidaobligatorioTipo: númeroDescripción:

Formato double

Campo: registros[].desglose[].tipo_recargo_equivalenciaTipo: númeroDescripció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_equivalenciaTipo: númeroDescripción:

Formato double

Campo: registros[].importe_totalobligatorioTipo: númeroDescripció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 double

Respuestas

207

Lote procesado. Mira el status de cada factura.

Respuesta 207 (JSON)
{
  "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
Campos de la respuesta 207
CampoTipoDescripción
Campo: entornoTipo: textoDescripción:

Valores: test, prod

Campo: resultadosTipo: listaDescripción:

Uno por factura, en el mismo orden que las enviaste.

Campo: resultados[].registroIdobligatorioTipo: textoDescripción:
Campo: resultados[].statusobligatorioTipo: enteroDescripción:

El que te habría dado POST /registros para esta factura.

Campo: resultados[].estadoTipo: textoDescripción:
Campo: resultados[].uuidTipo: textoDescripción:
Campo: resultados[].urlTipo: textoDescripción:
Campo: resultados[].qrTipo: textoDescripción:
Campo: resultados[].errorTipo: objetoDescripción:
Campo: resultados[].error.codeTipo: textoDescripción:
Campo: resultados[].error.messageTipo: textoDescripción:
Campo: resultados[].error.detailsTipo: objetoDescripción:
Errores

Todos los errores llegan con la misma forma:

Forma de un error (JSON)
{
  "error": {
    "code": "INVALID_BODY",
    "message": "string",
    "details": {
      "clave": "string"
    }
  }
}

400

Tu petición. Reintentar no sirve: corrígela.

codeCuándo
code: MISSING_HEADERCuándo: Falta X-Kiby-Tenant o Idempotency-Key
code: INVALID_IDEMPOTENCY_KEYCuándo: La cabecera está, pero con caracteres que no admite
code: INVALID_BODYCuándo: El cuerpo no es un objeto, o un campo tiene tipo o formato equivocado, o el desglose no cuadra
code: UNKNOWN_FIELDCuándo: Un campo que no existe en el contrato
code: FORBIDDEN_FIELDCuándo: Un campo que existe pero lo fija el servidor
code: INVALID_LINECuándo: Una línea del desglose está mal
code: TOO_MANY_LINESCuándo: Más de 12 líneas de desglose
code: FIELD_NOT_AMENDABLECuándo: Intentas subsanar algo que exige rectificativa
code: TOO_MANY_REGISTROSCuándo: Más de 50 facturas en un lote
code: DUPLICATE_REGISTRO_IDCuándo: El mismo registroId dos veces en un lote
code: DUPLICATE_INVOICE_NUMBERCuándo: Dos facturas del lote con la misma serie y número
code: VERIFACTU_REJECTEDCuá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

codeCuándo
code: TENANT_HEADER_MISMATCHCuándo: X-Kiby-Tenant no resuelve a una cuenta a la que tu clave dé acceso
code: INSUFFICIENT_SCOPECuándo: La clave no tiene el permiso de facturación
code: IP_NOT_ALLOWEDCuándo: La clave tiene lista de IPs y la tuya no está
code: FORBIDDENCuá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.

codeQué significa
code: IDEMPOTENCY_KEY_REUSEDQué significa: Esa clave ya se usó para otra factura distinta
code: TENANT_NOT_READYQué 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_DECLAREDQué significa: Se anotó pero no llegó a salir. Reinténtalo por el endpoint por el que lo enviaste
code: REGISTRO_EN_OTRO_ENDPOINTQué 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_UNCONFIRMEDQué 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_CANCELLABLEQué significa: La AEAT no tiene registrada esa factura: no hay nada que anular. Corresponde subsanar
code: REGISTRO_ALREADY_CANCELLEDQué significa: Ya estaba anulada
code: REGISTRO_NOT_AMENDABLEQué significa: Su estado no admite subsanación
code: REGISTRO_PAYLOAD_INCOMPLETEQué significa: Al registro le faltan datos para construir la baja. Contacta con soporte
code: REGISTRO_ESTADO_INCONSISTENTEQué significa: Anomalía en un dato que solo escribimos nosotros. Contacta con soporte
code: VERIFACTU_REQUEST_IN_PROGRESSQué 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

codeQué significa
code: SANDBOX_UNAVAILABLEQué significa: El sandbox no está configurado en este despliegue
code: GATEWAY_MISCONFIGUREDQué significa: Configuración nuestra incoherente. No es tu petición: contacta con soporte
code: SERVICE_UNAVAILABLEQué 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.

GET/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 trae Retry-After con 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
Ejemplo con curl (sandbox)
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
Cabeceras de la petición
CampoTipoDescripción
Campo: X-Kiby-TenantobligatorioTipo: textoDescripció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 403 TENANT_HEADER_MISMATCH.

Ejemplo: TU-NIF-O-TENANTUID

Parámetros de la ruta
Parámetros de la ruta
CampoTipoDescripción
Campo: registroIdobligatorioTipo: textoDescripción:

El registroId que devolvió el 201 al declarar (tu Idempotency-Key).

Ejemplo: pedido-2026-00042

Respuestas

200

Estado actual del registro.

Respuesta 200 (JSON)
{
  "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
Campos de la respuesta 200
CampoTipoDescripción
Campo: registroIdTipo: textoDescripción:
Campo: entornoTipo: textoDescripción:

El entorno con el que se declaró, no con el que estás preguntando.

Valores: test, prod

Campo: estadoTipo: textoDescripción:

El literal del servicio de envío a la AEAT.

Campo: estadoNormalizadoTipo: textoDescripción:

Nuestro vocabulario canónico, para que no tengas que mantener tu propia tabla de sinónimos: Correcta, Incorrecta, Pendiente, Enviada, Anulada, No registrada, Aceptada con errores, Duplicado…

Campo: finalTipo: sí/noDescripción:

true = el estado ya no va a cambiar solo; deja de preguntar. false = sigue en curso y la respuesta trae Retry-After.

⚠️ Incorrecta es final. Que no cambie solo no significa que esté aceptada: significa que te toca actuar.

Campo: uuidTipo: textoDescripción:
Campo: urlTipo: textoDescripción:
Campo: qrTipo: textoDescripción:
Campo: declaradoEnTipo: textoDescripción:

Cuándo se anotó la declaración.

Formato date-time

Campo: actualizadoEnTipo: textoDescripción:

Cuándo cambió por última vez. Te dice lo reciente que es el veredicto.

Formato date-time

Campo: ultimaSubsanacionTipo: objetoDescripción:

Solo si hubo alguna. Saber que fue rechazada es lo que te dice que hay que volver a subsanar.

Puede ser null

Campo: ultimaSubsanacion.subsanacionIdTipo: textoDescripción:

Puede ser null

Campo: ultimaSubsanacion.estadoTipo: textoDescripción:

Puede ser null

Campo: ultimaSubsanacion.rechazadaTipo: sí/noDescripción:
Errores

Todos los errores llegan con la misma forma:

Forma de un error (JSON)
{
  "error": {
    "code": "INVALID_BODY",
    "message": "string",
    "details": {
      "clave": "string"
    }
  }
}

400

Tu petición. Reintentar no sirve: corrígela.

codeCuándo
code: MISSING_HEADERCuándo: Falta X-Kiby-Tenant o Idempotency-Key
code: INVALID_IDEMPOTENCY_KEYCuándo: La cabecera está, pero con caracteres que no admite
code: INVALID_BODYCuándo: El cuerpo no es un objeto, o un campo tiene tipo o formato equivocado, o el desglose no cuadra
code: UNKNOWN_FIELDCuándo: Un campo que no existe en el contrato
code: FORBIDDEN_FIELDCuándo: Un campo que existe pero lo fija el servidor
code: INVALID_LINECuándo: Una línea del desglose está mal
code: TOO_MANY_LINESCuándo: Más de 12 líneas de desglose
code: FIELD_NOT_AMENDABLECuándo: Intentas subsanar algo que exige rectificativa
code: TOO_MANY_REGISTROSCuándo: Más de 50 facturas en un lote
code: DUPLICATE_REGISTRO_IDCuándo: El mismo registroId dos veces en un lote
code: DUPLICATE_INVOICE_NUMBERCuándo: Dos facturas del lote con la misma serie y número
code: VERIFACTU_REJECTEDCuá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

codeCuándo
code: TENANT_HEADER_MISMATCHCuándo: X-Kiby-Tenant no resuelve a una cuenta a la que tu clave dé acceso
code: INSUFFICIENT_SCOPECuándo: La clave no tiene el permiso de facturación
code: IP_NOT_ALLOWEDCuándo: La clave tiene lista de IPs y la tuya no está
code: FORBIDDENCuá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

codeQué significa
code: REGISTRO_NOT_DECLAREDQué 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_UNCONFIRMEDQué 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ó.

POST/v1/verifactu/anulaciones

Anular 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
Ejemplo con curl (sandbox)
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
Cabeceras de la petición
CampoTipoDescripción
Campo: X-Kiby-TenantobligatorioTipo: textoDescripció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 403 TENANT_HEADER_MISMATCH.

Ejemplo: TU-NIF-O-TENANTUID

Campo: Idempotency-KeyobligatorioTipo: textoDescripción:

Identificador que eliges tú para esta operación. Obligatoria en todos los POST: sin ella, un corte de red entre tu petición y nuestra respuesta te deja sin saber si la factura se declaró, y reintentar la declararía dos veces ante la AEAT — que ya no es un error, es una subsanación.

  • Mismo contenido y misma clave → se devuelve el resultado anterior con 200.
  • Contenido distinto y misma clave → 409 IDEMPOTENCY_KEY_REUSED. No se declara nada.
  • Una clave por operación: no reutilices la del registro para anularlo.

⚠️ 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: pedido-2026-00042

Cuerpo
Ejemplo del cuerpo (JSON)
{
  "registroId": "pedido-2026-00042"
}
Ver los 1 campos del cuerpo
Campos del cuerpo
CampoTipoDescripción
Campo: registroIdobligatorioTipo: textoDescripción:

El que devolvió el 201 al declarar.

Respuestas

201

Anulación enviada.

Respuesta 201 (JSON)
{
  "anulacionId": "string",
  "registroId": "string",
  "entorno": "test",
  "estado": "Pendiente",
  "uuid": "string"
}
Ver los campos de la respuesta 201
Campos de la respuesta 201
CampoTipoDescripción
Campo: anulacionIdTipo: textoDescripción:
Campo: registroIdTipo: textoDescripción:
Campo: entornoTipo: textoDescripción:

Valores: test, prod

Campo: estadoTipo: textoDescripción:
Campo: uuidTipo: textoDescripción:
Errores

Todos los errores llegan con la misma forma:

Forma de un error (JSON)
{
  "error": {
    "code": "INVALID_BODY",
    "message": "string",
    "details": {
      "clave": "string"
    }
  }
}

400

Tu petición. Reintentar no sirve: corrígela.

codeCuándo
code: MISSING_HEADERCuándo: Falta X-Kiby-Tenant o Idempotency-Key
code: INVALID_IDEMPOTENCY_KEYCuándo: La cabecera está, pero con caracteres que no admite
code: INVALID_BODYCuándo: El cuerpo no es un objeto, o un campo tiene tipo o formato equivocado, o el desglose no cuadra
code: UNKNOWN_FIELDCuándo: Un campo que no existe en el contrato
code: FORBIDDEN_FIELDCuándo: Un campo que existe pero lo fija el servidor
code: INVALID_LINECuándo: Una línea del desglose está mal
code: TOO_MANY_LINESCuándo: Más de 12 líneas de desglose
code: FIELD_NOT_AMENDABLECuándo: Intentas subsanar algo que exige rectificativa
code: TOO_MANY_REGISTROSCuándo: Más de 50 facturas en un lote
code: DUPLICATE_REGISTRO_IDCuándo: El mismo registroId dos veces en un lote
code: DUPLICATE_INVOICE_NUMBERCuándo: Dos facturas del lote con la misma serie y número
code: VERIFACTU_REJECTEDCuá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

codeCuándo
code: TENANT_HEADER_MISMATCHCuándo: X-Kiby-Tenant no resuelve a una cuenta a la que tu clave dé acceso
code: INSUFFICIENT_SCOPECuándo: La clave no tiene el permiso de facturación
code: IP_NOT_ALLOWEDCuándo: La clave tiene lista de IPs y la tuya no está
code: FORBIDDENCuá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.

codeQué significa
code: IDEMPOTENCY_KEY_REUSEDQué significa: Esa clave ya se usó para otra factura distinta
code: TENANT_NOT_READYQué 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_DECLAREDQué significa: Se anotó pero no llegó a salir. Reinténtalo por el endpoint por el que lo enviaste
code: REGISTRO_EN_OTRO_ENDPOINTQué 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_UNCONFIRMEDQué 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_CANCELLABLEQué significa: La AEAT no tiene registrada esa factura: no hay nada que anular. Corresponde subsanar
code: REGISTRO_ALREADY_CANCELLEDQué significa: Ya estaba anulada
code: REGISTRO_NOT_AMENDABLEQué significa: Su estado no admite subsanación
code: REGISTRO_PAYLOAD_INCOMPLETEQué significa: Al registro le faltan datos para construir la baja. Contacta con soporte
code: REGISTRO_ESTADO_INCONSISTENTEQué significa: Anomalía en un dato que solo escribimos nosotros. Contacta con soporte
code: VERIFACTU_REQUEST_IN_PROGRESSQué 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.

POST/v1/verifactu/subsanaciones

Corregir 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
Ejemplo con curl (sandbox)
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
Cabeceras de la petición
CampoTipoDescripción
Campo: X-Kiby-TenantobligatorioTipo: textoDescripció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 403 TENANT_HEADER_MISMATCH.

Ejemplo: TU-NIF-O-TENANTUID

Campo: Idempotency-KeyobligatorioTipo: textoDescripción:

Identificador que eliges tú para esta operación. Obligatoria en todos los POST: sin ella, un corte de red entre tu petición y nuestra respuesta te deja sin saber si la factura se declaró, y reintentar la declararía dos veces ante la AEAT — que ya no es un error, es una subsanación.

  • Mismo contenido y misma clave → se devuelve el resultado anterior con 200.
  • Contenido distinto y misma clave → 409 IDEMPOTENCY_KEY_REUSED. No se declara nada.
  • Una clave por operación: no reutilices la del registro para anularlo.

⚠️ 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: pedido-2026-00042

Cuerpo
Ejemplo del cuerpo (JSON)
{
  "registroId": "pedido-2026-00042",
  "descripcion": "Servicios de consultoría — septiembre 2026"
}
Ver los 2 campos del cuerpo
Campos del cuerpo
CampoTipoDescripción
Campo: registroIdobligatorioTipo: textoDescripción:
Campo: descripcionTipo: textoDescripción:

Máximo 500 caracteres

Respuestas

201

Subsanación enviada.

Respuesta 201 (JSON)
{
  "subsanacionId": "string",
  "registroId": "string",
  "entorno": "test",
  "estado": "Pendiente",
  "rechazoPrevio": "N",
  "uuid": "string",
  "url": "string",
  "qr": "string"
}
Ver los campos de la respuesta 201
Campos de la respuesta 201
CampoTipoDescripción
Campo: subsanacionIdTipo: textoDescripción:
Campo: registroIdTipo: textoDescripción:
Campo: entornoTipo: textoDescripción:

Valores: test, prod

Campo: estadoTipo: textoDescripción:
Campo: rechazoPrevioTipo: textoDescripción:

Lo que se declaró a la AEAT en tu nombre, derivado por nosotros. N = el registro constaba, X = el alta original fue rechazada, S = una subsanación anterior fue rechazada.

Valores: N, X, S

Campo: uuidTipo: textoDescripción:
Campo: urlTipo: textoDescripción:
Campo: qrTipo: textoDescripción:
Errores

Todos los errores llegan con la misma forma:

Forma de un error (JSON)
{
  "error": {
    "code": "INVALID_BODY",
    "message": "string",
    "details": {
      "clave": "string"
    }
  }
}

400

Tu petición. Reintentar no sirve: corrígela.

codeCuándo
code: MISSING_HEADERCuándo: Falta X-Kiby-Tenant o Idempotency-Key
code: INVALID_IDEMPOTENCY_KEYCuándo: La cabecera está, pero con caracteres que no admite
code: INVALID_BODYCuándo: El cuerpo no es un objeto, o un campo tiene tipo o formato equivocado, o el desglose no cuadra
code: UNKNOWN_FIELDCuándo: Un campo que no existe en el contrato
code: FORBIDDEN_FIELDCuándo: Un campo que existe pero lo fija el servidor
code: INVALID_LINECuándo: Una línea del desglose está mal
code: TOO_MANY_LINESCuándo: Más de 12 líneas de desglose
code: FIELD_NOT_AMENDABLECuándo: Intentas subsanar algo que exige rectificativa
code: TOO_MANY_REGISTROSCuándo: Más de 50 facturas en un lote
code: DUPLICATE_REGISTRO_IDCuándo: El mismo registroId dos veces en un lote
code: DUPLICATE_INVOICE_NUMBERCuándo: Dos facturas del lote con la misma serie y número
code: VERIFACTU_REJECTEDCuá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

codeCuándo
code: TENANT_HEADER_MISMATCHCuándo: X-Kiby-Tenant no resuelve a una cuenta a la que tu clave dé acceso
code: INSUFFICIENT_SCOPECuándo: La clave no tiene el permiso de facturación
code: IP_NOT_ALLOWEDCuándo: La clave tiene lista de IPs y la tuya no está
code: FORBIDDENCuá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.

codeQué significa
code: IDEMPOTENCY_KEY_REUSEDQué significa: Esa clave ya se usó para otra factura distinta
code: TENANT_NOT_READYQué 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_DECLAREDQué significa: Se anotó pero no llegó a salir. Reinténtalo por el endpoint por el que lo enviaste
code: REGISTRO_EN_OTRO_ENDPOINTQué 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_UNCONFIRMEDQué 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_CANCELLABLEQué significa: La AEAT no tiene registrada esa factura: no hay nada que anular. Corresponde subsanar
code: REGISTRO_ALREADY_CANCELLEDQué significa: Ya estaba anulada
code: REGISTRO_NOT_AMENDABLEQué significa: Su estado no admite subsanación
code: REGISTRO_PAYLOAD_INCOMPLETEQué significa: Al registro le faltan datos para construir la baja. Contacta con soporte
code: REGISTRO_ESTADO_INCONSISTENTEQué significa: Anomalía en un dato que solo escribimos nosotros. Contacta con soporte
code: VERIFACTU_REQUEST_IN_PROGRESSQué 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.

Solicita una demo gratis

Tipo de negocio: *
¿Qué quieres gestionar con Kiby?
¿Cuándo prefieres que te llamemos?