De zero a una factura emesa, en sis crides
Sense SDK, sense instal·lar res i sense llegir-se la referència sencera. Sis crides que pots copiar, enganxar i executar ara mateix: l'API està desplegada i la clau te la crees tu des del teu compte. Comença amb una de proves i no embrutaràs cap sèrie de facturació.
L'API està en marxa. Això és el que hi ha avui i el que no
Funciona. Les rutes d'aquesta documentació estan desplegades i responent a https://erp.cairos.es/api/v1/. Els exemples d'aquestes pàgines es poden copiar i executar. L'especificació completa és a openapi.json, que és el que importen Make i n8n.
Les claus te les crees tu, des de Desenvolupadors al teu compte de Cairos. Comença amb una cai_test_: treballa contra les teves dades de veritat però no registra a VeriFactu ni envia correus, així que pots muntar la teva integració sense embrutar una sèrie de facturació.
I el que encara NO hi ha, dit sense adorns: cap connector oficial. Ni app de Shopify, ni plugin de WooCommerce, ni mòdul publicat a Make o n8n. Amb l'API i la seva especificació es poden construir —per això hi ha les guies d'aquesta secció— però construir-los és feina, i aquesta feina no està feta.
Si muntes alguna cosa amb això, escriu-nos a hola@cairos.es. Ens interessa especialment el que et falti del contracte: és el que decideix què s'amplia primer.
El que faràs
Tres blocs. El primer es fa al teu compte, en un minut; els altres dos són codi.
Crear la clau
A Desenvolupadors, dins del teu compte. Comença per una de proves, i apunta-te-la: s'ensenya una sola vegada.
Comprovar que arriba
Una crida a /organizacion, que no demana cap permís. Si respon, la resta és costa avall.
Facturar
Contacte, factura en esborrany, emissió i PDF. Quatre crides més i ja està fet.
1 · Crea la clau
Al teu compte de Cairos, a Desenvolupadors. No cal demanar-la per correu ni esperar que ningú contesti: tries el mode, marques els permisos un a un i es genera a l'instant. Comença amb una de proves, que comença per cai_test_.
Marca els permisos que faràs servir en aquesta pàgina i cap més: contacts:write per al pas 3, i invoices:read i invoices:write per als passos 4, 5 i 6. Si te'n falta un, l'error ho diu pel seu nom i s'arregla creant una altra clau.
La clau s'ensenya una sola vegada. No la guardem: guardem la seva empremta, que serveix per reconèixer-la però no per reconstruir-la. Si la perds no te la podem recuperar; s'ha de revocar i crear-ne una altra. Guarda-la on guardis les contrasenyes del teu servidor —una variable d'entorn, un gestor de secrets— i no al codi.
Què fa diferent la clau de proves
Treballa contra les mateixes dades que la de producció: el que creïs amb ella apareix al teu compte. El que no fa és registrar a VeriFactu ni enviar correus. Això és exactament el que et permet desenvolupar sense embrutar una sèrie de facturació de debò. No és una còpia de la teva empresa en un servidor a part, i convé tenir-ho clar abans de llançar un bucle de proves.
2 · La primera crida, que confirma que tot està bé
GET /organizacion retorna les dades fiscals de la teva empresa. És la crida més barata que hi ha i no demana cap scope: si la teva clau val, respon.
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));Quan surti un 200 amb el nom de la teva empresa a dins, hauràs acabat la part 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"
}Els identificadors són així de lletjos a propòsit: són cuid, opacs i sense prefix. No els interpretis ni intentis deduir de quin recurs són —guarda'ls tal qual i envia'ls on toqui—, perquè el dia que deixin de tenir aquesta forma el teu codi no se n'hauria d'assabentar.
I si surt un 401, la clau no ha arribat bé. Els dos motius són gairebé sempre el mateix: falta la paraula Bearer al davant, o la variable d'entorn és buida i estàs enviant la capçalera sense res al darrere.
3 · Crea un contacte
Una factura necessita a qui la fas. Amb 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);Torna un 201 amb el contacte creat i el seu id. Guarda-te'l: és el que demanarà la factura.
Fixa't en la capçalera Idempotency-Key. Aquí encara no fa mal —un contacte duplicat s'esborra— però és el mateix hàbit que d'aquí a dos passos evita que emetis dues factures de la mateixa comanda. Està explicat sencer a idempotència.
4 · Crea la factura, que neix en esborrany
Amb invoices:write. La factura es crea sense número i en estat 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 i referencia vénen a null, i això és correcte: el número no existeix fins que s'emet. Els totals, en canvi, ja vénen calculats, així que aquí és on es comprova que el que has enviat quadra amb el que esperaves cobrar.
Fixa't en estado: val draft, en anglès. Els recursos i els camps d'aquesta API són en castellà, però uns quants valors d'enumerat es van quedar en anglès —draft, sent, paid, overdue— i aquí es queden, perquè traduir-los trencaria qualsevol integració escrita fins avui.
5 · Emet-la, que és el pas que no es desfà
Ara sí. En emetre, la factura pren el següent número de la seva sèrie i es genera el registre de VeriFactu encadenat amb l'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
}
}Dues coses d'aquesta resposta que convé mirar bé. numero és un enter, no un text: el «F27-0042» que s'ensenya imprès és referencia, que ajunta sèrie i número. I l'estat passa a sent, que és el mateix en què la deixa el botó «Marca enviada» de la pantalla.
Si tornes a cridar /emitir sobre una factura ja emesa, no surt un error: surt la mateixa factura amb el seu mateix número, i un 200 en comptes del 201. És a propòsit, i és el que permet reintentar sense arriscar un salt en la numeració quan no saps si la primera crida va arribar.
Amb una clau de proves, «verifactu» ve amb registrada: false
El bloc arriba igual —no desapareix—, però diu la veritat: una clau cai_test_ no registra a VeriFactu ni envia correus, així que huella i registrada_en vénen a null. Tota la resta —el número de la sèrie, els totals, el PDF— funciona igual. I aquest camp és just el que s'ha de mirar el dia que passis a producció, per comprovar que ara sí que es registra.
6 · I descarrega't el PDF
GET /facturas/{id}/pdf no retorna JSON: retorna el fitxer, amb el seu codi QR i la seva llegenda ja impresos.
curl https://erp.cairos.es/api/v1/facturas/clx3k2p9y0001qz8h5e6f7g8h/pdf \
-H "Authorization: Bearer $CAIROS_CLAVE" \
-o factura.pdfI ja està: sis crides, i al final una factura emesa, numerada i en PDF. La resta de la documentació és detall sobre això.
El que falla en els dos primers minuts
Gairebé sempre és una d'aquestes cinc coses, i cap no és interessant. Per això val la pena tenir-les juntes:
| El que veus | El que passa de debò |
|---|---|
401 no_autenticado | Falta la paraula Bearer davant de la clau, o la variable d'entorn és buida i envies la capçalera amb res a dins. Comprova això segon abans de donar la clau per perduda. |
403 sin_permiso | La clau és vàlida però li falta el scope d'aquella crida. I compte amb el parany: tenir invoices:write no et deixa llegir factures. Són dos permisos diferents. |
422 datos_invalidos | El JSON va arribar, però alguna cosa de dins no val. error.detalles et diu quin camp i per què; mira-ho abans de tocar res. |
409 conflicto | Estàs intentant una cosa que l'estat del document no permet: emetre una factura ja emesa, o reutilitzar una Idempotency-Key amb un cos diferent. |
| El servidor no respon JSON | Sol ser el Content-Type. Si envies cos, ha d'anar application/json: sense ell, el cos arriba i no es llegeix. |
Els set codis d'error, amb el que s'ha de fer amb cadascun, són a errors i límits.
I ara, per on continuo?
Depèn del que estiguis muntant, i sincerament només hi ha tres camins:
- Crearàs factures des d'un altre sistema. Llegeix idempotència abans que res. És la pàgina més avorrida d'aquesta secció i la que evita l'únic error d'aquesta API que no s'arregla esborrant.
- Necessites assabentar-te del que passa a Cairos. Llavors el teu són els webhooks, no consultar l'API cada cinc minuts.
- Llegiràs dades i poca cosa més. Mira la paginació per cursor, perquè una llista de factures d'un any no cap en una resposta.
I si el que tens al davant és una botiga o una eina d'automatització, hi ha pàgina pròpia: WooCommerce, Shopify, Make i n8n.
Dubtes del primer dia
DELETE que ho arregli. Està explicat a VeriFactu.precio és el preu unitari sense impostos i tipo_iva és el tipus en percentatge. La resposta porta base_imponible, cuota_iva i total ja calculats, que és el que convé comparar amb el que esperaves. Si véns d'una botiga al públic, on el preu que existeix és el de l'etiqueta, envia precios_con_iva: true i Cairos desglossa la base cap enrere en cèntims sencers, amb el mateix motor que fa servir el connector de botigues.Sis crides, quinze minuts amb calma
Crea la clau de proves, enganxa el primer curl i continua fins al PDF. I si muntes alguna cosa amb això, explica-nos-ho: el que li falta a la gent del contracte és el que decideix què s'amplia primer.
Clau de proves · Les mateixes dades, sense VeriFactu ni correus