De cero a unha factura emitida, en seis chamadas
Sen SDK, sen instalar nada e sen ler a referencia enteira. Seis chamadas que podes copiar, pegar e executar agora mesmo: a API está despregada e a clave créala ti desde a túa conta. Comeza cunha de probas e non enlixarás ningunha serie de facturación.
A API está en marcha. Isto é o que hai hoxe e o que non
Funciona. As rutas desta documentación están despregadas e respondendo en https://erp.cairos.es/api/v1/. Os exemplos destas páxinas pódense copiar e executar. A especificación completa está en openapi.json, que é o que importan Make e n8n.
As claves créalas ti, desde Desenvolvedores na túa conta de Cairos. Comeza cunha cai_test_: traballa contra os teus datos de verdade pero non rexistra en VeriFactu nin envía correos, así que podes montar a túa integración sen enlixar unha serie de facturación.
E o que aínda NON hai, dito sen adornos: ningún conector oficial. Nin app de Shopify, nin plugin de WooCommerce, nin módulo publicado en Make ou n8n. Coa API e a súa especificación pódense construír —para iso están as guías desta sección— pero construílos é traballo, e ese traballo non está feito.
Se montas algo con isto, escríbenos a hola@cairos.es. Interésanos especialmente o que che falte do contrato: é o que decide que se amplía primeiro.
O que vas facer
Tres bloques. O primeiro faise na túa conta, nun minuto; os outros dous son código.
Crear a clave
En Desenvolvedores, dentro da túa conta. Comeza por unha de probas, e apúntaa: amósase unha soa vez.
Comprobar que chega
Unha chamada a /organizacion, que non pide ningún permiso. Se responde, o resto é costa abaixo.
Facturar
Contacto, factura en borrador, emisión e PDF. Catro chamadas máis e xa está feito.
1 · Crea a clave
Na túa conta de Cairos, en Desenvolvedores. Non hai que pedila por correo nin agardar a que ninguén conteste: escolles o modo, marcas os permisos un a un e xérase no momento. Comeza cunha de probas, que empeza por cai_test_.
Marca os permisos que vas usar nesta páxina e ningún máis: contacts:write para o paso 3, e invoices:read e invoices:write para os pasos 4, 5 e 6. Se che falta un, o erro dío polo seu nome e arránxase creando outra clave.
A clave amósase unha soa vez. Non a gardamos: gardamos a súa pegada, que serve para recoñecela pero non para reconstruíla. Se a perdes non cha podemos recuperar; hai que revogala e crear outra. Gárdaa onde gardes os contrasinais do teu servidor —unha variable de contorno, un xestor de segredos— e non no código.
Que fai distinta a clave de probas
Traballa contra os mesmos datos ca a de produción: o que crees con ela aparece na túa conta. O que non fai é rexistrar en VeriFactu nin enviar correos. Iso é exactamente o que che permite desenvolver sen enlixar unha serie de facturación de verdade. Non é unha copia da túa empresa nun servidor aparte, e convén telo claro antes de lanzar un bucle de probas.
2 · A primeira chamada, que confirma que todo está ben
GET /organizacion devolve os datos fiscais da túa empresa. É a chamada máis barata que hai e non pide ningún scope: se a túa clave vale, responde.
export CAIROS_CLAVE=cai_test_TU_CLAVE
curl https://erp.cairos.es/api/v1/organizacion \
-H "Authorization: Bearer $CAIROS_CLAVE"const CLAVE = process.env.CAIROS_CLAVE;
const BASE = "https://erp.cairos.es/api/v1";
const r = await fetch(BASE + "/organizacion", {
headers: { Authorization: "Bearer " + CLAVE },
});
const datos = await r.json();
console.log(r.status, datos);<?php
$clave = getenv("CAIROS_CLAVE");
$base = "https://erp.cairos.es/api/v1";
$ch = curl_init($base . "/organizacion");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"Authorization: Bearer " . $clave,
"Accept: application/json",
]);
$cuerpo = curl_exec($ch);
$estado = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
var_dump($estado, json_decode($cuerpo, true));Cando saia un 200 co nome da túa empresa dentro, terás rematado a parte difícil.
{
"id": "clx3k2p9y0000qz8h1a2b3c4d",
"nombre": "Talleres Miralles SL",
"razon_social": "Talleres Miralles, S.L.",
"nif": "B12345674",
"ciudad": "Girona",
"provincia": "Girona",
"moneda": "EUR",
"region_fiscal": "peninsula",
"tipo_iva_defecto": 21,
"recargo_equivalencia": false,
"plan_contable": "pgc"
}Os identificadores son así de feos a propósito: son cuid, opacos e sen prefixo. Non os interpretes nin intentes deducir de que recurso son —gárdaos tal cal e mándaos onde toque—, porque o día que deixen de ter esta forma o teu código non debería enterarse.
E se sae un 401, a clave non chegou ben. Os dous motivos son case sempre o mesmo: falta a palabra Bearer diante, ou a variable de contorno está baleira e estás mandando a cabeceira sen nada detrás.
3 · Crea un contacto
Unha factura necesita a quen lla fas. Con contacts:write:
curl -X POST https://erp.cairos.es/api/v1/contactos \
-H "Authorization: Bearer $CAIROS_CLAVE" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: alta-cliente-1042" \
-d '{
"nombre": "Grupo Bonaire SL",
"nif": "B17654321",
"email": "admin@bonaire.example",
"tipo": "customer"
}'const r = await fetch(BASE + "/contactos", {
method: "POST",
headers: {
Authorization: "Bearer " + CLAVE,
"Content-Type": "application/json",
"Idempotency-Key": "alta-cliente-1042",
},
body: JSON.stringify({
nombre: "Grupo Bonaire SL",
nif: "B17654321",
email: "admin@bonaire.example",
tipo: "customer",
}),
});
const contacto = await r.json();<?php
$datos = [
"nombre" => "Grupo Bonaire SL",
"nif" => "B17654321",
"email" => "admin@bonaire.example",
"tipo" => "customer",
];
$ch = curl_init($base . "/contactos");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($datos));
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"Authorization: Bearer " . $clave,
"Content-Type: application/json",
"Idempotency-Key: alta-cliente-1042",
]);
$contacto = json_decode(curl_exec($ch), true);
curl_close($ch);Volve un 201 co contacto creado e o seu id. Gárdao: é o que vai pedir a factura.
Fíxate na cabeceira Idempotency-Key. Aquí aínda non doe —un contacto duplicado bórrase— pero é o mesmo hábito que dentro de dous pasos evita que emitas dúas facturas do mesmo pedido. Está contado enteiro en idempotencia.
4 · Crea a factura, que nace en borrador
Con invoices:write. A factura créase sen número e en estado borrador:
curl -X POST https://erp.cairos.es/api/v1/facturas \
-H "Authorization: Bearer $CAIROS_CLAVE" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: pedido-1042" \
-d '{
"contacto_id": "clx3k2p9y0000qz8h1a2b3c4d",
"serie": "F27",
"fecha_emision": "2027-03-02",
"lineas": [
{
"descripcion": "Revision anual",
"cantidad": 2,
"precio": 180.00,
"tipo_iva": 21
}
]
}'const r = await fetch(BASE + "/facturas", {
method: "POST",
headers: {
Authorization: "Bearer " + CLAVE,
"Content-Type": "application/json",
"Idempotency-Key": "pedido-1042",
},
body: JSON.stringify({
contacto_id: contacto.id,
serie: "F27",
fecha_emision: "2027-03-02",
lineas: [
{
descripcion: "Revision anual",
cantidad: 2,
precio: 180.0,
tipo_iva: 21,
},
],
}),
});
const factura = await r.json();<?php
$factura = [
"contacto_id" => $contacto["id"],
"serie" => "F27",
"fecha_emision" => "2027-03-02",
"lineas" => [
[
"descripcion" => "Revision anual",
"cantidad" => 2,
"precio" => 180.00,
"tipo_iva" => 21,
],
],
];
$ch = curl_init($base . "/facturas");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($factura));
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"Authorization: Bearer " . $clave,
"Content-Type: application/json",
"Idempotency-Key: pedido-1042",
]);
$creada = json_decode(curl_exec($ch), true);
curl_close($ch);{
"id": "clx3k2p9y0001qz8h5e6f7g8h",
"serie": "F27",
"numero": null,
"referencia": null,
"tipo_documento": "invoice",
"estado": "draft",
"fecha_emision": "2027-03-02",
"fecha_vencimiento": null,
"base_imponible": 360.00,
"cuota_iva": 75.60,
"cuota_irpf": 0.00,
"total": 435.60,
"cobrado": 0.00,
"pendiente": 435.60
}numero e referencia veñen a null, e iso é correcto: o número non existe ata que se emite. Os totais, en cambio, xa veñen calculados, así que aquí é onde se comproba que o que mandaches cadra co que esperabas cobrar.
Fíxate en estado: vale draft, en inglés. Os recursos e os campos desta API están en castelán, pero uns cantos valores de enumerado quedaron en inglés —draft, sent, paid, overdue— e aí quedan, porque traducilos rompería toda integración escrita ata hoxe.
5 · Emítea, que é o paso que non se desfai
Agora si. Ao emitir, a factura toma o seguinte número da súa serie e xérase o rexistro de VeriFactu encadeado co anterior.
curl -X POST \
https://erp.cairos.es/api/v1/facturas/clx3k2p9y0001qz8h5e6f7g8h/emitir \
-H "Authorization: Bearer $CAIROS_CLAVE" \
-H "Idempotency-Key: emitir-pedido-1042"const r = await fetch(BASE + "/facturas/" + factura.id + "/emitir", {
method: "POST",
headers: {
Authorization: "Bearer " + CLAVE,
"Idempotency-Key": "emitir-pedido-1042",
},
});
const emitida = await r.json();<?php
$url = $base . "/facturas/" . $creada["id"] . "/emitir";
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"Authorization: Bearer " . $clave,
"Idempotency-Key: emitir-pedido-1042",
]);
$emitida = json_decode(curl_exec($ch), true);
curl_close($ch);{
"id": "clx3k2p9y0001qz8h5e6f7g8h",
"serie": "F27",
"numero": 42,
"referencia": "F27-0042",
"estado": "sent",
"total": 435.60,
"pendiente": 435.60,
"verifactu": {
"registrada": true,
"huella": "7f3c1a…d904",
"registrada_en": "2027-03-02T10:41:08.517Z",
"aeat": null
}
}Dúas cousas desa resposta que convén mirar ben. numero é un enteiro, non un texto: o «F27-0042» que se amosa impreso é referencia, que xunta serie e número. E o estado pasa a sent, que é o mesmo en que a deixa o botón «Marcar enviada» da pantalla.
Se volves chamar a /emitir sobre unha factura xa emitida, non sae un erro: sae a mesma factura co seu mesmo número, e un 200 en vez do 201. É a propósito, e é o que permite reintentar sen arriscar un salto na numeración cando non sabes se a primeira chamada chegou.
Cunha clave de probas, «verifactu» vén con registrada: false
O bloque chega igual —non desaparece—, pero di a verdade: unha clave cai_test_ non rexistra en VeriFactu nin manda correos, así que huella e registrada_en veñen a null. Todo o demais —o número da serie, os totais, o PDF— funciona igual. E ese campo é xusto o que hai que mirar o día que pases a produción, para comprobar que agora si se rexistra.
6 · E descarga o PDF
GET /facturas/{id}/pdf non devolve JSON: devolve o ficheiro, co seu código QR e a súa lenda xa impresos.
curl https://erp.cairos.es/api/v1/facturas/clx3k2p9y0001qz8h5e6f7g8h/pdf \
-H "Authorization: Bearer $CAIROS_CLAVE" \
-o factura.pdfE xa está: seis chamadas, e ao final unha factura emitida, numerada e en PDF. O resto da documentación é detalle sobre isto.
O que falla nos dous primeiros minutos
Case sempre é unha destas cinco cousas, e ningunha é interesante. Por iso paga a pena telas xuntas:
| O que ves | O que pasa de verdade |
|---|---|
401 no_autenticado | Falta a palabra Bearer diante da clave, ou a variable de contorno está baleira e mandas a cabeceira con nada dentro. Comproba o segundo antes de dar por perdida a clave. |
403 sin_permiso | A clave é válida pero fáltalle o scope desa chamada. E ollo coa trampa: ter invoices:write non che deixa ler facturas. Son dous permisos distintos. |
422 datos_invalidos | O JSON chegou, pero algo dentro non vale. error.detalles dicho que campo e por que; mírao antes de tocar nada. |
409 conflicto | Estás intentando algo que o estado do documento non permite: emitir unha factura xa emitida, ou reutilizar unha Idempotency-Key cun corpo distinto. |
| O servidor non responde JSON | Adoita ser o Content-Type. Se mandas corpo, ten que ir application/json: sen el, o corpo chega e non se le. |
Os sete códigos de erro, co que hai que facer con cada un, están en erros e límites.
E agora, por onde sigo?
Depende do que esteas montando, e sinceramente só hai tres camiños:
- Vas crear facturas desde outro sistema. Le idempotencia antes ca nada. É a páxina máis aburrida desta sección e a que evita o único erro desta API que non se arranxa borrando.
- Necesitas enterarte do que pasa en Cairos. Entón o teu son os webhooks, non consultar a API cada cinco minutos.
- Vas ler datos e pouco máis. Mira a paxinación por cursor, porque unha lista de facturas dun ano non cabe nunha resposta.
E se o que tes diante é unha tenda ou unha ferramenta de automatización, hai páxina propia: WooCommerce, Shopify, Make e n8n.
Dúbidas do primeiro día
DELETE que o arranxe. Está explicado en VeriFactu.precio é o prezo unitario sen impostos e tipo_iva é o tipo en porcentaxe. A resposta trae base_imponible, cuota_iva e total xa calculados, que é o que convén comparar co que esperabas. Se vés dunha tenda ao público, onde o prezo que existe é o da etiqueta, manda precios_con_iva: true e Cairos detalla a base cara a atrás en céntimos enteiros, co mesmo motor que usa o conector de tendas.Seis chamadas, quince minutos con calma
Crea a clave de probas, pega o primeiro curl e segue ata o PDF. E se montas algo con isto, cóntanolo: o que lle falta á xente do contrato é o que decide que se amplía primeiro.
Clave de probas · Mesmos datos, sen VeriFactu nin correos