Cairos
Facturación
Programa de facturaciónPresupuestosFacturas recurrentesGastos y proveedoresCobros y tesorería
Contabilidad e impuestos
ContabilidadModelos de HaciendaLibros registroInmovilizadoIGIC y Canarias
Operaciones
Inventario y almacenesCRMControl horarioProyectosSubvenciones y ayudas
Cumplimiento
VeriFactuTicketBAIFactura electrónicaToda la normativaSeguridad y datos
Por tipo de negocio
AutónomosPymesAsesorías y gestoríasExtranjeros en EspañaStartupsComercios y tiendas
Por sector
Hostelería y restaurantesConstrucción y reformasServicios profesionalesComercio electrónicoTodos los sectores
Por forma jurídica
AsociacionesFundacionesCooperativasClubes deportivosTodas las formas jurídicas
Cambiar de programa
ComparativasAlternativa a HoldedMigrar tus datos
Herramientas gratis
Plantilla de facturaCalculadora de IVACalculadora de IRPFTodas las herramientas
Aprender
GuíasGlosarioCalendario fiscalBlog
Desarrolladores
API y documentaciónEmpezar en cinco minutosReferencia de recursosWebhooks
Ayuda
Centro de ayudaContacto
Precios
Empieza gratis Iniciar sesión
Errores y límites

Siete códigos, y sólo dos se reintentan

Todos los errores llegan en el mismo sobre y con un código de una lista cerrada. Lo que hay que saber de cada uno no es qué significa, sino si tiene sentido volver a intentarlo.

Formato único de errorPaginación por cursorReintento con espera creciente

La API está en marcha. Esto es lo que hay hoy y lo que no

Funciona. Las rutas de esta documentación están desplegadas y respondiendo en https://erp.cairos.es/api/v1/. Los ejemplos de estas páginas se pueden copiar y ejecutar. La especificación completa está en openapi.json, que es lo que importan Make y n8n.

Las claves te las creas tú, desde Desarrolladores en tu cuenta de Cairos. Empieza con una cai_test_: trabaja contra tus datos de verdad pero no registra en VeriFactu ni envía correos, así que puedes montar tu integración sin ensuciar una serie de facturación.

Y lo que todavía NO hay, dicho sin adornos: ningún conector oficial. Ni app de Shopify, ni plugin de WooCommerce, ni módulo publicado en Make o n8n. Con la API y su especificación se pueden construir —para eso están las guías de esta sección— pero construirlos es trabajo, y ese trabajo no está hecho.

Si montas algo con esto, escríbenos a hola@cairos.es. Nos interesa especialmente lo que te falte del contrato: es lo que decide qué se amplía primero.

Un solo sobre para todos los errores

Cualquier respuesta que no sea un 2xx trae exactamente esta forma. Sin excepciones, sin páginas HTML de error y sin un formato distinto según el recurso.

404 Not Found
{
  "error": {
    "codigo": "no_encontrado",
    "mensaje": "No existe una factura con ese identificador.",
    "detalles": null
  }
}
  • codigo es para tu programa. Es una lista cerrada de siete valores y no va a crecer sin avisar: es lo que puedes meter en un switch.
  • mensaje es para una persona. Está en español y puede cambiar de redacción sin previo aviso, así que no lo uses para decidir nada: nunca compares el texto de un mensaje.
  • detalles es null casi siempre. Cuando no lo es, trae el desglose de qué campo falla y por qué.

Los siete códigos, y qué hacer con cada uno

La columna que importa es la tercera. Un error del que no sabes si reintentar es un error que acaba en una tarea manual a las once de la noche.

HTTPcodigo¿Reintentar?Qué significa
401no_autenticadoNo.No ha llegado clave, o no vale, o está revocada. Reintentar da lo mismo.
403sin_permisoNo.La clave vale pero le falta el scope. Hace falta una clave con más permisos, no otra llamada.
404no_encontradoNo.Ese identificador no existe en tu organización. Ojo: también sale si el recurso todavía no está abierto en tu cuenta.
409conflictoNo, sin cambiar algo.El estado no permite lo que pides: emitir una factura ya emitida, cobrar de más, reutilizar una llave de idempotencia con otro cuerpo.
422datos_invalidosNo, sin corregir.El JSON llegó y algo de dentro no vale. detalles dice qué campo.
429demasiadas_peticiones, esperando.Has ido demasiado rápido. Espera y vuelve, con espera creciente.
500error_interno, con cuidado.Se ha roto algo por nuestro lado. Reintenta con espera creciente y con Idempotency-Key, o duplicarás.

Regla corta: los 4xx son cosa tuya y reintentarlos sin cambiar nada sólo gasta cuota; el 429 y el 500 son los dos únicos que se reintentan.

El 422, que es el único que te dice dónde mirar

Cuando la validación falla, detalles trae un objeto con el camino del campo y el motivo. El camino usa puntos e índices, así que señala también dentro de una lista de líneas:

422 Unprocessable Entity
{
  "error": {
    "codigo": "datos_invalidos",
    "mensaje": "La factura tiene dos campos que no son válidos.",
    "detalles": {
      "contacto_id": "No existe ningún contacto con ese identificador.",
      "lineas.0.iva": "Tipo de IVA no permitido: 19."
    }
  }
}

Enséñaselo a quien esté usando tu integración en vez de tragártelo. La mitad de los 422 son un dato mal escrito en el otro sistema, y quien puede arreglarlo es quien lo escribió, no tú.

Paginación por cursor, no por número de página

Todas las listas se recorren igual, con dos parámetros:

ParámetroQué hace
limiteCuántos elementos por respuesta. 50 por defecto, 200 de tope. Pedir más de 200 no es un error: te dan 200.
desdeEl cursor. Se pone el valor de siguiente de la respuesta anterior.
GET /facturas?limite=2
{
  "datos": [
    { "id": "fac_5Np2wD", "numero": "F27/0042", "total": 435.60 },
    { "id": "fac_4Mq1vC", "numero": "F27/0041", "total": 1210.00 }
  ],
  "total": 1284,
  "siguiente": "fac_4Mq1vC"
}

total es cuántos hay en total, para poder enseñar un progreso. siguiente viene a null cuando ya no queda nada, y ésa —y no que datos venga vacío— es la condición con la que se termina el bucle.

async function todas(recurso) {
  const salida = [];
  let desde = null;

  do {
    const url = new URL(BASE + recurso);
    url.searchParams.set("limite", "200");
    if (desde) url.searchParams.set("desde", desde);

    const r = await fetch(url, {
      headers: { Authorization: "Bearer " + CLAVE },
    });
    if (!r.ok) throw new Error("Cairos respondio " + r.status);

    const pagina = await r.json();
    salida.push(...pagina.datos);
    desde = pagina.siguiente;
  } while (desde);

  return salida;
}

Por qué cursor y no ?pagina=7

Porque los datos se mueven mientras los lees. Con números de página, una factura nueva creada a mitad del recorrido desplaza todo un puesto: acabas leyendo dos veces un elemento y saltándote otro, y no te enteras. El cursor apunta a un sitio fijo de la lista, así que lo que llega después no descoloca lo que ya has leído.

Límites: lo que hay y lo que no vamos a inventar

Hay un límite de peticiones por clave, y al pasarlo vuelve un 429 demasiadas_peticiones. Eso es firme.

La cifra exacta no está publicada todavía, y no la vamos a poner aquí a ojo. Es el único dato de toda esta sección que no puedes comprobar tú mismo hasta que te explota en producción, así que un número inventado sería peor que ninguno. Cuando esté fijada, se escribe aquí.

Lo que sí se puede decir es cómo se escribe un cliente que no depende de esa cifra, que es como hay que escribirlo de todos modos:

  • Reintenta el 429 con espera creciente. Un segundo, dos, cuatro, ocho, y ríndete a los cinco intentos.
  • Añade un poco de azar a la espera. Si cinco procesos tuyos reintentan al mismo tiempo, vuelven a chocar todos a la vez. Un margen aleatorio los separa.
  • No hagas un bucle apretado. Para volcar mil facturas, limite=200 y cinco llamadas es mejor que mil llamadas de una en una, y encima es más rápido.
  • Escucha en vez de preguntar. Consultar cada minuto por si hay algo nuevo es lo que agota cualquier límite. Para eso están los webhooks.
Reintento con espera creciente
async function conReintento(peticion, intentos = 5) {
  for (let i = 0; i < intentos; i++) {
    const r = await peticion();

    // Sólo estos dos se reintentan. Un 4xx reintentado
    // gasta cuota y falla exactamente igual.
    if (r.status !== 429 && r.status < 500) return r;

    const espera = Math.pow(2, i) * 1000 + Math.random() * 400;
    await new Promise((listo) => setTimeout(listo, espera));
  }
  throw new Error("Cairos no responde despues de varios intentos");
}

Reintentar sin llave de idempotencia es cómo se duplican las facturas

Un reintento de una escritura es exactamente el caso para el que existe Idempotency-Key. Si vas a reintentar un POST, mándala; si no, el primer 500 que te encuentres puede haber creado ya la factura que estás a punto de crear otra vez. Está contado entero en idempotencia.

Y cuando el error es nuestro

Un 500 error_interno significa que se ha roto algo por nuestro lado. Tres cosas que conviene hacer, en este orden:

  1. Reintenta con espera creciente y con llave de idempotencia. Muchos 500 son pasajeros.
  2. Si insiste, para. Un proceso que reintenta indefinidamente contra un servidor caído no ayuda a nadie, y menos a que se levante antes.
  3. Cuéntanoslo. A hola@cairos.es, con la hora aproximada y qué llamabas. Con eso se encuentra en el registro; sin eso, casi nunca.

Y lo que no hay que hacer: dar la operación por hecha o por no hecha. Un 500 no dice si el trabajo se hizo. Lo que lo dice es consultar el recurso, o haber mandado una llave de idempotencia y repetir la llamada.

Preguntas sobre errores y límites

Siete: no_autenticado (401), sin_permiso (403), no_encontrado (404), conflicto (409), datos_invalidos (422), demasiadas_peticiones (429) y error_interno (500). Es una lista cerrada y siempre llegan con el mismo formato.
No. El mensaje está pensado para que lo lea una persona y puede cambiar de redacción. Lo que no cambia es codigo: es lo que tiene que mirar tu programa.
Hay límite y devuelve 429 demasiadas_peticiones, pero la cifra todavía no está publicada y no la vamos a poner a ojo. Escribe el cliente con reintento y espera creciente y no dependerás de ella.
Con ?limite=200 y el cursor ?desde=, repitiendo hasta que siguiente venga a null. No hay números de página: con datos que se mueven, saltarse un elemento sería cuestión de tiempo.
Reintentar con espera creciente y con Idempotency-Key, y si insiste, parar y avisarnos. Lo que no hay que hacer es suponer si la operación se hizo o no: un 500 no lo dice.

Un cliente que trata bien los errores no da guerra en un año

Mira el código y no el mensaje, reintenta sólo el 429 y el 500, y manda siempre la llave de idempotencia cuando escribas.

Siete códigos · Cursor, no páginas · Espera creciente

Soporte