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
API v1

La API de Cairos, abierta y documentada

Una API REST sobre HTTPS, con claves Bearer, JSON en los dos sentidos y webhooks firmados. Está desplegada y respondiendo, con especificación OpenAPI pública y claves que te creas tú desde tu cuenta. Esta página es el contrato entero: qué se puede hacer, cómo se autentica, cómo se pagina, qué falla y cuatro flujos de principio a fin.

Base https://erp.cairos.es/api/v1Ejemplos en curl, JavaScript y PHPEspecificación OpenAPI
POST /api/v1/facturas
Authorization Bearer cai_live_…
Idempotency-Key pedido-1042
201 Created
estado draft
numero null
total 435.60

Una factura nace en borrador y sin número, y se emite en una segunda llamada. Entre las dos hay una diferencia legal, no de estilo.

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 info@cairos.es. Nos interesa especialmente lo que te falte del contrato: es lo que decide qué se amplía primero.

Para qué existe

Cuatro cosas que la gente monta con esto

No es una API «por tener API». Estos cuatro casos son los que se han mirado cuando había que elegir entre dos diseños, y el orden es el orden en el que pesaron.

Una tienda que factura sola

El pedido pagado entra por la API y sale una factura con su número correlativo donde tiene que estar: en el ERP, no en la tienda. Con WooCommerce o Shopify, o con el conector de tiendas que ya viene dentro de Cairos si no quieres escribir nada.

Una asesoría que se baja los datos

Facturas y gastos de la cuenta de un cliente, con una clave de sólo lectura que no puede tocar nada. Una clave por empresa, sin excepción: el fallo de un guion no llega nunca a la contabilidad de otro. Más en asesorías.

Automatizar sin escribir código

Make y n8n hablan HTTP, y una API bien hecha no necesita un módulo oficial para que la llamen. Además hay una cabecera de token pensada para quien no sabe calcular un HMAC, y catálogos que rellenan los desplegables solos.

Tu propio programa por encima

Un panel para el jefe de obra, una app de campo que crea albaranes, una web de socios que registra cuotas. Se pide una clave con dos permisos y ya está: no hay que revender Cairos ni meter a nadie en la pantalla del ERP.

Lo que tienen en común los cuatro: el dato fiscal —la numeración, el registro de VeriFactu, el cálculo del IVA— se queda dentro de Cairos y no se replica fuera. Una tienda que se guarda su propio contador de facturas acaba con dos numeraciones que no cuadran, y eso no se arregla con una consulta: se arregla con rectificativas.

Primeros pasos

Empezar en tres pasos

Los tres se hacen en un rato. Si quieres el recorrido largo —contacto, factura, emisión, PDF y los fallos típicos— está en empezar en cinco minutos; esto es lo mínimo para saber si la API te sirve.

1 · Crea la clave

En tu cuenta de Cairos, en Desarrolladores. Eliges el modo y marcas los permisos, uno a uno. Empieza con una cai_test_: escribe en tus datos de verdad pero no registra en VeriFactu ni manda correos, así que puedes probar veinte veces sin ensuciar una serie de facturación.

La clave se enseña una sola vez. No la guardamos: guardamos su huella, que sirve para reconocerla y no para reconstruirla. Si la pierdes hay que revocarla y crear otra. Va en una variable de entorno o en un gestor de secretos, nunca en el código y jamás en el navegador.

2 · Comprueba que vale

El índice de la API no pide ningún permiso: basta con que la clave sea buena. Contesta a las cuatro preguntas del primer minuto —si vale, qué permisos lleva, si estás en pruebas o en real y qué hora tiene el servidor— y de paso te da la dirección de la especificación.

curl
export CAIROS_CLAVE=cai_test_TU_CLAVE

curl https://erp.cairos.es/api/v1/ \
  -H "Authorization: Bearer $CAIROS_CLAVE"
200 OK
{
  "api": "Cairos",
  "version": "1.0.0",
  "hora": "2026-08-31T09:14:02.117Z",
  "modo": "test",
  "empresa": "Grupo Bonaire SL",
  "scopes": ["contacts:write", "invoices:write", "invoices:read"],
  "limite_peticiones": { "peticiones": 120, "ventana_segundos": 60 },
  "openapi": "https://erp.cairos.es/api/v1/openapi.json"
}

Si sale 401, la clave no llega o está mal copiada. Si sale la respuesta de arriba con "scopes": [], la clave es buena y no le marcaste ningún permiso. Y el campo hora no es decorativo: cuando una firma de webhook falla sin motivo aparente, casi siempre es que el reloj de tu servidor va desviado, y compararlo con éste lo resuelve en diez segundos.

3 · Escribe algo

La primera escritura sensata es un contacto, porque es lo que va a pedir después la factura. upsert hace de una vez lo que si no son tres pasos: buscar por NIF, mirar si hay resultados y crear si no los hay. Esa bifurcación es donde se equivoca todo el mundo, y equivocarse ahí llena la cartera de clientes duplicados que luego arrastran facturas y el 347.

curl
curl -X POST https://erp.cairos.es/api/v1/contactos/upsert \
  -H "Authorization: Bearer $CAIROS_CLAVE" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: alta-cliente-1042" \
  -d '{
    "nif": "B17654321",
    "nombre": "Grupo Bonaire SL",
    "email": "admin@bonaire.example",
    "tipo": "customer"
  }'

Vuelve un 201 con creado: true si lo ha dado de alta, o un 200 con creado: false si ya estaba. El estado dice lo mismo que el campo, para que valgan tanto los clientes que miran el código como los que miran el cuerpo. Y sólo toca los campos que mandas: lo que no viene se queda como estaba, que es justo lo que hace falta cuando la tienda sabe el nombre y el correo pero no el IBAN ni los días de pago.

Esa cabecera de arriba no es opcional de hecho

Idempotency-Key es lo que hace que reintentar sea seguro. Aquí todavía no duele —un contacto duplicado se borra— pero es el mismo hábito que dos pasos más adelante impide emitir dos veces la misma factura, y eso ya no se borra. Está entero en idempotencia.

Autenticación

Claves y permisos

Una cabecera y una lista de permisos. Ni cookies, ni OAuth, ni sesión que caduque a mitad de una importación.

Toda llamada lleva Authorization: Bearer con la clave. Sólo HTTPS: una petición por HTTP no se redirige, se rechaza, porque una redirección con la clave dentro es la clave viajando en claro una vez.

Hay dos clases de clave y la diferencia no es la que espera casi todo el mundo:

cai_live_cai_test_
DatosLos tuyosLos mismos, no una copia aparte
Registro en VeriFactuSí, encadenado y sin vuelta atrásNo
Correos al clienteSíNo
Para qué sirveProducciónMontar la integración sin ensuciar una serie

No hay una base de datos de pruebas: lo que crees con una clave cai_test_ aparece en tu cuenta y lo tendrás que limpiar tú. Es un modo, no un servidor distinto, y conviene saberlo antes de lanzar un bucle. El detalle está en autenticación y scopes.

La regla que sorprende: escribir no implica leer

Los permisos van por recurso y por operación: invoices:read y invoices:write son dos, y tener el segundo no te da el primero. Una clave con invoices:write emite facturas y no puede listarlas.

Va al revés de lo que hace casi todo el mundo, y es deliberado. La integración de una tienda sólo necesita escribir; el día que esa clave se filtre —un repositorio público, un registro de errores, un becario— quien la tenga podrá meterte facturas de broma, que se rectifican, y no podrá descargarse tu cartera de clientes ni tu facturación, que no se deshace. Quien necesite las dos cosas pide las dos, y lo hace a sabiendas.

Un permiso que falta da 403 sin_permiso, y el error dice cuál falta y cuáles tienes. Una clave mala o ausente da 401 no_autenticado. No se confunden nunca: si te sale 403, la clave es buena.

Los permisos, por área

La raya se pone donde cambia lo que se ve, no donde cambia la pantalla. Las comisiones enseñan cuánto cobra cada comercial y las horas del equipo son datos de personas: ninguna de las dos cosas la quiere una clave hecha para meter pedidos de una tienda.

ÁreaPermisosQué abre
Ventas y cobroinvoices:read · invoices:write · payments:*Facturas, presupuestos, albaranes, proformas y suscripciones —son la misma tabla— más los cobros y pagos. Un permiso por los cuatro documentos y no cuatro permisos: separarlos no protegería nada distinto y obligaría a pedirlos todos para la misma integración.
Comprasexpenses:read · expenses:writeGastos con su IVA soportado, su retención y su categoría, y los pedidos a proveedor con su recepción de mercancía.
Ficheros maestroscontacts:* · products:*Clientes, proveedores y catálogo. Los dos con upsert, que es lo que usa toda integración.
Tiendas conectadasstores:read · stores:writeVer a qué plataforma está enganchada cada tienda y lanzar su sincronización. Las credenciales de la tienda no salen nunca: una clave de Cairos no es una llave de tu Shopify.
Operacionescrm:* · inventory:* · pricing:* · projects:* · assets:readOportunidades y tareas, almacenes, existencias, traspasos y lotes, tarifas y precios de proveedor, proyectos y sus costes y el inmovilizado, que es de leer y nada más.
Datos de personascommissions:read · members:* · timetracking:readComisiones, socios y cuotas, y las horas del equipo. Van aparte porque son lo más sensible que hay aquí dentro.
Avisoswebhooks:manageDar de alta, modificar y borrar webhooks, y reintentar un envío.

La lista completa, con la descripción de cada permiso, sale de la propia API: GET /api/v1/ te devuelve los de tu clave y openapi.json los declara todos en x-scopes. No la copiamos aquí porque crece cada semana.

El mapa

Los recursos, por área

Lo que hay hoy, agrupado por lo que resuelve. Las rutas exactas y el esquema campo a campo salen de la especificación, que es lo único que no se queda viejo.

ÁreaQué hayDónde está el detalle
OrganizaciónTus datos fiscales: NIF, domicilio, régimen de IVA, tipo por defecto, recargo de equivalencia. Sólo lectura.Referencia
ContactosClientes y proveedores, con NIF-IVA intracomunitario y días de pago. Lectura, alta, edición y upsert por NIF, correo o referencia.Referencia
ProductosCatálogo con precio, tipo de IVA, unidad, código de barras, existencias y aviso de mínimo. También con upsert, aquí por SKU.Referencia
FacturasAlta en borrador o emitida de una vez, emisión aparte y PDF. Los importes se calculan en el servidor, también si mandas los precios con el IVA dentro.Referencia
Presupuestos, albaranes y proformasCada uno con ruta propia y con lo suyo: aceptar y rechazar un presupuesto, y convertir cualquiera de los tres en factura, que nace en borrador. Antes esto sólo se podía desde la pantalla.Presupuestos
SuscripcionesLas facturas recurrentes: qué se cobra cada periodo, cuándo toca la siguiente, y pausar, reanudar o emitir ahora sin esperar a la fecha.Facturas recurrentes
GastosBase, tipo de IVA, retención al proveedor y clase de bien o servicio, que es la clave del 349.Referencia
Pedidos de compraLo que le pides a un proveedor, su estado y la recepción de mercancía línea a línea, que es lo que va bajando lo que falta por llegar y lo que mete las unidades en el almacén.Inventario
CobrosCobros totales o parciales imputados a una factura, con su forma de pago. El estado de la factura se mueve solo.Referencia
CRMOportunidades con sus etapas y sus motivos de pérdida —moverlas, ganarlas, perderlas y convertirlas— y las tareas de seguimiento, que se completan y se aplazan.CRM
InventarioAlmacenes, existencias por producto y almacén, el histórico de movimientos, ajustes y recuentos, traspasos entre almacenes y lotes con su caducidad.Inventario
Tarifas y preciosTarifas de venta con sus escalas por cantidad, precios de proveedor, y la pregunta que de verdad se hace una tienda: qué precio le toca a este cliente por este artículo, resuelta con la misma cascada que aplica el editor de facturas.Especificación
ProyectosProyectos o actividades, con lo que llevan gastado, lo imputado por el otro lado y la desviación sobre su presupuesto.Proyectos
ComisionesReglas, devengos y liquidaciones. Sólo lectura: cuánto ha comisionado cada comercial se consulta, no se escribe desde fuera.Especificación
Socios, cuotas y donativosEl libro de socios de una entidad, las cuotas que se les giran y los donativos que después salen en el certificado del donante.Asociaciones
Control horarioPersonas, jornadas y el resumen de horas de un periodo. Sólo lectura, y a propósito: el dato vive en ficheo.app, con el que Cairos se enlaza como quien entra con una cuenta de otro sitio —tú autorizas desde ficheo, sale una clave de sólo lectura y la revocas desde allí cuando quieras—. Duplicar aquí los marcajes sería crear una segunda verdad sobre la jornada de un trabajador, y de esas discusiones no se sale.Control horario
InmovilizadoLos bienes, su cuadro de amortización año a año y la foto a cierre de ejercicio. Sólo lectura.Inmovilizado
Tiendas conectadasLas que ya están enganchadas al ERP —Shopify, WooCommerce y Odoo—, cómo les va la sincronización, cuál ha dejado de traer pedidos y cómo lanzar una pasada ahora mismo. Las credenciales de la tienda no salen nunca.Integraciones
InformesLos totales de un periodo sumados en el servidor, con el cambio de divisa congelado de cada documento. Existe para no sumar mal desde fuera.Flujo del resumen
Catálogos y sucesosListas para rellenar desplegables sin escribirlas a fuego, y la ficha de cada suceso de webhook con un cuerpo de ejemplo.Webhooks
WebhooksAlta, cambio y baja, historial de envíos, reintento manual y un botón de prueba que dispara un aviso de mentira contra tu servidor.Webhooks

Cada área pide su permiso, y son los de la tabla de arriba. Lo que no vas a encontrar en esta página es cuántas rutas u operaciones hay dentro de cada una: eso lo cuenta la especificación, aquí abajo, y no envejece.

Cuántas rutas hay: cuéntalas tú

Aquí no vas a encontrar el número, y no es un descuido. Un recuento en una página de documentación envejece en silencio: la versión anterior de esta página decía «18 rutas» y hacía tiempo que no era verdad. Y hoy sería peor todavía, porque las áreas de arriba han multiplicado por tres el mapa en un solo día. Lo que describe esta tabla son áreas, que es lo que se mantiene; contar es un segundo contra la especificación, que se genera del propio código:

curl
# Cuántas rutas hay ahora mismo
curl -s https://erp.cairos.es/api/v1/openapi.json \
  | jq '.paths | keys | length'

# Y cuáles son, en orden
curl -s https://erp.cairos.es/api/v1/openapi.json \
  | jq -r '.paths | keys[]'

La especificación es OpenAPI 3.1, no pide clave para descargarse y es lo que importan Make y n8n para montar los pasos solos. También es la que le das a un generador de clientes si quieres tu propio SDK: sale actualizado y no lo mantiene nadie.

El contrato

Las reglas que valen para todas las rutas

Cuatro cosas que no cambian de un recurso a otro. Si las montas bien una vez, no vuelves a tocarlas.

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

No existe ?page=2. Una lista se recorre con ?limite= —50 por defecto, 200 de tope— y el campo siguiente que viene en la respuesta, que ya es la URL entera de la página que sigue, con los filtros que llevabas puestos. Cuando llega a null, se acabó.

Los números de página pierden filas y repiten otras, y no en un caso raro: es lo normal. Si mientras recorres cuatro mil contactos alguien da de alta uno, todo se desplaza una posición y la última fila de la primera página sale también la primera de la segunda. Con un borrado, al revés: una fila se cuela entre dos páginas y no sale en ninguna. Quien importa acaba con duplicados y con huecos sin saber por qué.

JavaScript
// Recorrer una lista entera, sea del tamaño que sea.
// "antiguo" va sobre una fecha que no se mueve: es el orden
// que hay que usar para el histórico.
let url = "https://erp.cairos.es/api/v1/facturas" +
          "?limite=200&orden=antiguo";

while (url) {
  const r = await fetch(url, {
    headers: { Authorization: "Bearer " + CLAVE },
  });
  const pagina = await r.json();
  for (const factura of pagina.datos) {
    // lo tuyo
  }
  url = pagina.siguiente;   // ya viene con los filtros dentro
}

Hay un tercer orden, orden=modificado, y existe para los disparadores por sondeo de Make, Zapier y n8n: pone lo último que ha cambiado arriba, no lo último que se creó. Tiene un coste que hay que decir: el orden se mueve mientras paginas, así que sirve para mirar las últimas y no para recorrer el histórico.

Errores: siempre el mismo sobre

Un fallo llega con el código HTTP que le toca y con este cuerpo, siempre igual, venga de donde venga:

422 Unprocessable Content
{
  "error": {
    "codigo": "datos_invalidos",
    "mensaje": "La línea 2 no tiene precio.",
    "detalles": {
      "campos": [
        { "campo": "lineas[1].precio", "motivo": "es obligatorio" }
      ]
    }
  }
}

Los códigos son un puñado corto y estable —no_autenticado, sin_permiso, no_encontrado, datos_invalidos, conflicto, demasiadas_peticiones y error_interno— y sólo el 422 te dice dónde mirar, con el índice de la línea que falla dentro del nombre del campo. Qué hacer con cada uno, en errores.

El límite de peticiones

120 peticiones por minuto y por clave, en ventana deslizante. No es un contador que se pone a cero al dar la hora: con eso se podrían meter 120 a las 10:00:59 y otras 120 a las 10:01:00, que es justo el pico que se quiere evitar.

Da de sobra para lo que se hace de verdad: una sincronización nocturna gasta unas tres llamadas por pedido, así que son unos cuarenta pedidos por minuto. Y es lo bastante bajo para que una integración en bucle no deje sin respuesta al resto de clientes, que no es una hipótesis: es lo que hace cualquier integración con un fallo de paginación.

Las cabeceras RateLimit-* van en todas las respuestas, no sólo en el rechazo —Retry-After, ésa sí, sólo en el 429—. Un cliente bien hecho mira RateLimit-Remaining y se frena solo antes de comerse el 429.

Idempotencia: la cabecera que evita facturar dos veces

Todo lo que crea algo admite Idempotency-Key. Mandas una llave tuya —el identificador del pedido, no un aleatorio— y si esa misma petición llega dos veces, la segunda recibe la respuesta de la primera en vez de crear otra cosa. Misma llave con un cuerpo distinto es un 409 conflicto: eso no es un reintento, es un fallo tuyo, y decírtelo es mejor que crear dos. Las llaves viven 24 horas.

Importa porque una factura emitida no se borra. Se rectifica, que es lo que exige el reglamento de facturación, así que una factura de más no es un registro sobrante: es una rectificativa, un asiento y una llamada a tu cliente. La cabecera es una línea. El porqué, largo, está en idempotencia.

Un caso que conviene saberse: POST /facturas/{id}/emitir es idempotente sin cabecera. Emitir una factura ya emitida devuelve la misma factura con su mismo número —200 en vez de 201—, no un error ni un número nuevo. Es la única respuesta que no arriesga un salto en la numeración cuando quien reintenta no sabe si la primera llegó.

Avisos salientes

Webhooks: que te avisen en vez de preguntar

Sondear cada cinco minutos gasta llamadas, llega tarde y se salta cosas. Un webhook es una petición nuestra a una URL tuya, en cuanto pasa.

Los sucesos

Hay avisos de facturas (creada, emitida, cobrada, anulada), de presupuestos —aceptado y también rechazado, que es la única respuesta del cliente que antes no llegaba nunca a la automatización—, de albaranes y de pedidos de compra, de suscripciones cuando emiten su factura del periodo, de cobros, de contactos (alta, cambio, baja), de productos —incluido el de existencias por debajo del mínimo, que salta al cruzar el umbral y no en cada venta posterior—, de gastos, de oportunidades del CRM (creada, movida de etapa, ganada, perdida), de socios y donativos, y de pedidos importados de una tienda conectada, que va aparte de «factura creada» para que quien sólo mira las ventas de la tienda no tenga que filtrar todas las facturas de la empresa.

La lista exacta, con la explicación y un cuerpo de ejemplo de cada uno, se pide a la API: GET /api/v1/eventos. Es un endpoint y no un párrafo por un motivo práctico: n8n y Zapier lo leen para rellenar el desplegable de «¿qué suceso?» y para enseñar campos de ejemplo antes de que haya llegado ningún aviso de verdad.

La firma, que es lo único que no es opcional

Cada envío lleva una cabecera x-cairos-firma con esta forma:

HTTP
POST /tu/extremo HTTP/1.1
x-cairos-firma: t=1756284862,v1=9f8c4b1e…
x-cairos-suceso: invoice.issued
x-cairos-envio: env_a1b2c3
x-cairos-intento: 1

Se firma con HMAC-SHA256 la cadena marca.cuerpo, no el cuerpo a secas: con la marca de tiempo dentro de lo firmado, una petición capturada hoy no sirve mañana. La tolerancia es de cinco minutos —ancha para dos relojes sin sincronizar, estrecha para que una repetición no cuele— y la comparación se hace en tiempo constante, porque comparar dos hexadecimales con === filtra por lo que tarda cuántos caracteres has acertado.

Un extremo público que no comprueba la firma no es un extremo: es una puerta. Cualquiera que adivine tu URL puede meterte facturas en tu propio sistema. El código para comprobarla, en tres lenguajes, está en webhooks.

Y si tu herramienta no sabe calcular un HMAC

Hay una alternativa honesta, pensada para Make, Zapier y n8n sin nodos de código: al dar de alta el webhook se pide modo_aviso: "token" y entonces llega también una cabecera x-cairos-token con el secreto en claro, que sólo hay que comparar con lo que guardaste. Es más débil —el secreto viaja en cada petición y no caduca— y por eso no sustituye a nada: la cabecera de firma sigue yendo igual, y el día que puedas comprobarla cambias el modo con un PATCH.

Reintentos y duplicados

Si tu servidor no contesta con un 2xx, se reintenta seis veces con esperas que crecen: un minuto, cinco, media hora, dos horas, seis y un día. El último intento cae más de un día después del primero, que es lo que hace falta para que alguien despliegue el arreglo un lunes por la mañana. Tras quince fallos encadenados el webhook se desactiva solo y hay que volver a activarlo a mano: a esas alturas no es un bache, es que esa URL ya no existe.

Como consecuencia, puedes recibir el mismo aviso dos veces: si tardaste en contestar y ya lo habías procesado, nuestro reintento llega igual. La cabecera x-cairos-envio es el identificador del envío y los reintentos repiten el mismo valor, así que guardarlo y descartar los repetidos es todo lo que hace falta.

De principio a fin

Cuatro flujos completos, en orden

Ninguna referencia de rutas te dice en qué orden llamarlas. Esto sí, con la trampa de cada uno escrita al lado.

1 · Pedido de la tienda → contacto → factura → cobro

El caso más común con diferencia. Se dispara desde el webhook de «pedido pagado» de tu tienda, y son cinco llamadas.

HTTP
# 1 · El comprador: se busca por NIF y se crea si no está
POST /contactos/upsert
     Idempotency-Key: pedido-1042-contacto
     { "nif": "B17654321", "nombre": "Grupo Bonaire SL",
       "email": "compras@bonaire.example", "tipo": "customer" }
# -> 201 creado:true   ·   200 creado:false si ya existía

# 2 · La factura, todavía borrador y sin número
POST /facturas
     Idempotency-Key: pedido-1042
     { "contacto_id": "...", "serie": "TIENDA",
       "precios_con_iva": true,
       "lineas": [ { "descripcion": "Camiseta talla M",
                     "cantidad": 2, "precio": 24.20,
                     "tipo_iva": 21 } ] }
# -> 201 estado:"draft"  numero:null

# 3 · Emitir: aquí se reserva el número y se registra en VeriFactu
POST /facturas/{id}/emitir
# -> 201 si la emites tú   ·   200 si ya estaba emitida

# 4 · El cobro que ya hizo la pasarela
POST /cobros
     Idempotency-Key: pedido-1042-cobro
     { "factura_id": "...", "importe": 48.40, "metodo": "card" }

# 5 · El PDF, para adjuntarlo a tu propio correo
GET  /facturas/{id}/pdf

La trampa está en el paso 2. El precio que existe en una tienda al público es el de la etiqueta —12,10 €—, que es lo que cobró la pasarela y lo que aparece en el extracto del banco; la base imponible no es un dato de la tienda, es una consecuencia. Por eso está precios_con_iva: sin él hay que desglosar a mano, y ese desglose se escribe mal casi siempre —se calcula hacia delante en vez de hacia atrás—, con lo que la factura acaba uno o dos céntimos por debajo del cobro y el descuadre se acumula hasta que el 303 no cuadra con la caja.

Y la Idempotency-Key es el número de pedido, siempre el mismo. Si la tienda reintenta su webhook, no sale una segunda factura. El enganche escrito está en WooCommerce y en Shopify; si no quieres escribir nada, el conector de tiendas del propio ERP hace estos cinco pasos solo.

2 · Formulario de la web → oportunidad → presupuesto aceptado

Un formulario de contacto que no se queda en un correo. Todo esto ya tiene ruta: el CRM y los presupuestos se abrieron con sus acciones, así que el recorrido entero —incluido aceptar el presupuesto, que hasta ahora sólo se podía desde la pantalla— se hace desde fuera.

PasoLlamadaQué te llevas
1POST /contactos/upsert con buscar_por: "email"El id del contacto. Si esa persona ya te escribió el mes pasado, no se duplica.
2POST /oportunidades · crm:writeLa oportunidad colgada de ese contacto, con su titulo, su importe y su etapa. Las etapas válidas se piden antes a GET /oportunidades/etapas: son las de esa empresa, no una lista fija.
3POST /tareas · crm:writeEl «llamar el martes» que hace que la oportunidad no se muera sola. Va con oportunidad_id y con responsable_id: sin dueño, una tarea no la hace nadie.
4POST /oportunidades/{id}/convertirEl presupuesto, con las líneas de la oportunidad y la tarifa que le toque a ese cliente ya aplicada.
5POST /presupuestos/{id}/aceptar, o el webhook quote.acceptedEl «sí» del cliente. Y con POST /presupuestos/{id}/convertir sale la factura, en borrador y sin número, lista para emitir.

La trampa aquí es el paso 1: si en vez de upsert se encadena «buscar / si hay resultados / si no crear», el día que la búsqueda devuelva una lista vacía y nadie mire, se crea un contacto por cada formulario. Y una nota sobre el paso 5: perder también avisa —quote.rejected—, que es lo que impide que el seguimiento siga persiguiendo algo ya cerrado. Ver el módulo de CRM.

3 · Cada lunes a las ocho, el resumen de ventas

Una sola llamada, y ésa es la gracia.

HTTP
GET /informes/ventas?desde=2026-08-24&hasta=2026-08-30

# -> { "facturas": 37, "base_imponible": 18420.00,
#      "cuota_iva": 3868.20, "total": 22288.20,
#      "cobrado": 14105.60, "pendiente": 8182.60,
#      "ticket_medio": 602.38, "divisa": "EUR",
#      "por_serie": [...], "por_estado": [...] }

La receta obvia es otra: GET /facturas?estado=paid&limite=200 y sumar. Está mal de tres maneras y las tres son silenciosas. Se queda corta pasadas 200 facturas, porque el escenario coge la primera página y no sigue el cursor, y un número más bajo del esperado parece una mala semana. estado=paid no son las ventas, es lo cobrado: una factura a 30 días emitida el jueves no está pagada el lunes. Y si hay facturas en otra moneda, sumar euros con dólares da un número que no es dinero de ninguna moneda.

El informe suma en el servidor, sobre el periodo entero, con el cambio congelado de cada documento —el del devengo, que es el que manda el artículo 79.Once de la Ley del IVA—, deja fuera los borradores porque un borrador no es una venta, y va por fecha de emisión, que es donde lo va a contar Hacienda. Además contesta por separado a lo facturado y a lo cobrado, que son dos preguntas distintas.

4 · Sincronizar el stock en los dos sentidos

Dos medias sincronizaciones que hay que montar por separado.

De la tienda a Cairos: POST /productos/upsert, con el SKU como llave. Actualiza lo que mandas y no vacía lo que no mandas, así que un catálogo que sólo conoce precio y existencias no destroza la ficha del producto en cada pasada.

De Cairos a la tienda: escucha los avisos de producto modificado y de existencias bajo mínimo. Y si tu herramienta no recibe webhooks, sondea con GET /productos?orden=modificado, que es el orden que existe justo para eso.

El bucle, que es el fallo clásico de toda sincronización a dos bandas

Tu receptor escribe en Cairos, Cairos emite «producto modificado», tu receptor lo recibe y escribe en la tienda, la tienda avisa, y vuelta a empezar. Se rompe marcando el origen —una etiqueta, un campo de referencia— y descartando el eco propio antes de escribir. Y compensa hacerlo el primer día: un bucle de sincronización se come el límite de peticiones en un par de minutos y se nota como «la API va lenta», no como lo que es.

Sin adornos

Lo que esta API no hace

Es la sección que hace creíble el resto. Preferimos que lo descartes hoy a que lo descubras a mitad de la integración.

Lo que no hayQué hacer en su lugar
Conectores publicados en Make, Zapier o n8n. No busques «Cairos» en su directorio de aplicaciones: no está.Las tres llaman a cualquier API por HTTP, y para eso están las guías de Make y n8n. Además hay una cabecera de token para quien no puede calcular un HMAC, y openapi.json para que monten los pasos solos —con ?version=3.0 si tu herramienta todavía no digiere OpenAPI 3.1, que es el caso de Make—.
Plugin de WooCommerce ni app de Shopify en sus tiendas de extensiones.Dentro del ERP sí hay un conector de tiendas —Shopify, WooCommerce y Odoo— que trae los pedidos sin escribir código. Y si prefieres tu propio enganche, el código está escrito en WooCommerce y Shopify.
Conector de PrestaShop. Está a medias a propósito y no hay fecha.El IVA de cada línea y los cupones de PrestaShop no se pueden deducir con seguridad desde su API, y un tipo de IVA inventado en una factura no es un fallo de presentación: es una declaración mal hecha. Mientras tanto, sus pedidos entran por la API con Make, Zapier o n8n.
SDK oficial en PHP, JavaScript ni Python.Un generador de clientes contra openapi.json te saca el tuyo, y actualizado. Es lo que haríamos nosotros.
OAuth ni «conectar con Cairos» para aplicaciones de terceros.Una clave por organización, creada por su dueño desde su cuenta. Si montas un producto para varios clientes, cada uno te da la suya.
Una clave que vea varias empresas.Una asesoría con diez clientes maneja diez claves. Es lo que impide que un fallo de tu guion escriba en la contabilidad de otro.
Borrar una factura emitida. No hay DELETE, por mucho que lo busques.Se rectifica, que es lo que exige el reglamento de facturación. Por eso pesa tanto la idempotencia.
Presentar modelos ante la AEAT desde la API.Cairos calcula el 303, el 130, el 347 y los demás y te los deja listos; la presentación se hace en la sede electrónica con tu certificado. Está contado en modelos.
Un entorno de pruebas aparte.Las claves cai_test_ escriben en tus datos reales sin registrar en VeriFactu ni mandar correos. Es un modo, no un servidor distinto.
Websockets ni streaming.Los avisos salientes son webhooks firmados, con reintentos. Para lo entrante, peticiones normales.
Nombres en inglés.Los recursos y los campos están en castellano. Sólo quedan en inglés algunos valores de enumerado —draft, paid, goods—, y se quedan porque cambiarlos rompería lo ya escrito.

Si algo de esta lista es justo lo que te hace falta, dilo: info@cairos.es. Lo que le falta a la gente del contrato es lo que decide qué se amplía primero, y no hay una lista mejor que ésa.

La API está en español

Los recursos, los parámetros, los nombres de campo y los códigos de error están en castellano: /facturas y no /invoices, limite y no limit, no_encontrado y no not_found. Cairos es un ERP español y no mantiene un segundo juego de nombres en inglés. Lo decimos arriba porque quien integra puede no hablar español, y enterarse a mitad del trabajo es peor que saberlo antes de empezar. La aplicación sí está traducida al inglés, al catalán, al gallego y al euskera; lo que no se traduce son los nombres de los recursos de la API, porque cambiarlos rompería toda integración escrita hasta hoy.

¿Vas a montar algo? Cuéntanoslo

Nos interesa sobre todo lo que te falte del contrato: es lo que decide qué área se abre primero. Y si lo que quieres es que lo montemos nosotros, también se puede hablar.

Preguntas sobre la API

Sí. Está desplegada y respondiendo en https://erp.cairos.es/api/v1/, con especificación OpenAPI pública, y las claves te las creas tú desde Desarrolladores en tu cuenta. Lo que no hay es un conector ya montado: ni plugin de WooCommerce, ni app de Shopify, ni módulo publicado en Make o en n8n. La API está; el conector lo construyes tú o lo construimos contigo.
No ponemos el número en esta página a propósito, porque cambia casi cada semana y una cifra vieja hace más daño que ninguna. La cuenta exacta de hoy sale de la propia API: descárgate openapi.json y cuenta las claves de paths; el comando entero está entre los recursos. Lo que sí es estable es el reparto por áreas, que es lo que no cambia aunque entren rutas nuevas.
Nada aparte de tu plan. No hay un «plan API», ni precio por llamada, ni un tramo de peticiones que se factura. Lo que incluye cada plan está en precios.
Hay claves cai_test_, que es algo distinto y conviene entenderlo antes de empezar: trabajan contra los mismos datos que las de producción, pero no registran en VeriFactu ni envían correos. Sirven para desarrollar sin ensuciar una serie de facturación de verdad. No son una base de datos aparte: lo que crees con una clave de pruebas lo verás en tu cuenta. Está explicado en autenticación.
No, y es lo que más sorprende de esta API. invoices:write deja crear y emitir facturas, y no deja listarlas: para eso hace falta además invoices:read. Va al revés de lo que espera casi todo el mundo y es deliberado. Una integración de una tienda sólo necesita escribir; el día que le roben la clave, quien la tenga no puede descargarse tu cartera de clientes ni tu facturación.
No, y no lo vamos a insinuar. Lo que hay es la especificación en GET /api/v1/openapi.json, con la que un generador de clientes te saca el tuyo en el lenguaje que uses y actualizado. Los ejemplos de estas páginas son curl, fetch y cURL de PHP a pelo, sin una sola dependencia.
En el repositorio de WordPress y en la tienda de aplicaciones de Shopify, no: no busques lo que no existe. Hay dos cosas distintas que sí existen y suelen resolver el problema. Una, el conector de tiendas que viene dentro del ERP, que se configura desde la pantalla y trae los pedidos solo. Otra, las guías de WooCommerce y Shopify, con el código del enganche escrito para quien prefiera montárselo a su manera.
No, y no es una limitación técnica: es que una clave de la API en el JavaScript de una página pública la puede leer cualquiera que abra el inspector, y esa clave escribe en tu contabilidad. Las llamadas se hacen desde tu servidor, desde una función sin servidor o desde la herramienta de automatización. En el navegador no se pone nunca.
No. Los recursos, los parámetros y los códigos de error están en castellano, igual que la aplicación: /facturas y no /invoices, limite y no limit. Lo único que sigue en inglés son algunos valores de enumerado —draft, paid, goods, transfer—, porque cambiarlos rompería toda integración escrita hasta hoy.
Es uno de los tres usos para los que se diseñó. Cada organización tiene sus propias claves, así que una asesoría acaba con una clave por cliente: no existe una clave que vea varias empresas a la vez, y eso es una propiedad del diseño, no una carencia. Lo demás, en Cairos para asesorías.

El contrato está cerrado y la API responde

Rutas, permisos, errores y firma no van a cambiar debajo de ti: lo que crece son las áreas nuevas, y eso sólo añade. Crea la clave y prueba la primera llamada.

API v1 · JSON sobre HTTPS · Webhooks firmados · Especificación OpenAPI

Soporte