n8n: HTTP Request to call and Code to verify
There is no official node either, and here it matters less than anywhere: n8n has a code node, and that is what you need to check a webhook's signature before trusting it.
The API is up and running. This is what there is today and what there is not
It works. The routes in this documentation are deployed and responding at https://erp.cairos.es/api/v1/. The examples on these pages can be copied and run. The full specification is at openapi.json, which is what Make and n8n import.
You create the keys yourself, from Developers in your Cairos account. Start with a cai_test_ key: it works against your real data but does not record to VeriFactu and does not send emails, so you can build your integration without dirtying an invoice series.
And what there still is NOT, said plainly: no official connector. No Shopify app, no WooCommerce plugin, no published Make or n8n module. They can be built with the API and its specification — that is what the guides in this section are for — but building them is work, and that work is not done.
If you build something with this, write to us at hola@cairos.es. We are particularly interested in whatever you find missing from the contract: that is what decides which parts get extended first.
There is no official Cairos node in n8n
It is not published and there is no date. What there is is the API — with its list of events, its OpenAPI and its webhooks — and this page, which tells you exactly what to hook up and with what code. Inside Cairos there are also ready-made recipes and a downloadable n8n flow, under API and integrations → Get started.
If your shop runs on PrestaShop, this is your route
Cairos only connects to Shopify, WooCommerce and Odoo. Not to PrestaShop yet, and it is half-finished on purpose: the VAT rate for each line does not come with the line — it has to be resolved against two other resources in their API — coupons are a separate resource applied to the order and not to the lines, and every shop calls the status that means «sold» something different, because the merchant edits those statuses. Invoicing three orders out of four and making up the VAT on the fourth is worse than saying no: the «no» is visible on day one, and VAT put in wrong shows up on the 303.
In the meantime, its orders come in through here perfectly well: PrestaShop knows how to notify an address when an order arrives, and what sits underneath is exactly the same route this page describes.
The credential, set up once
n8n has no Cairos node, and it does not need one: the HTTP Request node calls any API. What is worth setting up properly from the start is the credential, so as not to repeat the key in every node or leave it written into the flow.
You create a Header Auth credential with these two values:
Name: Authorization
Value: Bearer cai_live_TU_CLAVEFrom then on, each HTTP Request node uses that credential and never knows anything about the key again. When you rotate it, it changes in one place.
Calling Cairos
One HTTP Request node per operation. What you have to fill in is always the same:
| Node field | Value |
|---|---|
| Method | POST |
| URL | https://erp.cairos.es |
| Authentication | Generic, with the Header Auth credential above |
| Send Body | On, in JSON |
| Headers | Idempotency-Key, with the order identifier from the previous node |
And turn the node's own retry on only if you have set the idempotency key. A retry of a POST with no key is the quickest way to end up with two invoices.
The body, in JSON mode, with n8n's expressions to take the data from the previous node:
{
"contacto_id": "{{ $json.id }}",
"serie": "WEB",
"fecha_emision": "{{ $now.format('yyyy-MM-dd') }}",
"lineas": [
{
"descripcion": "{{ $('Pedido').item.json.titulo }}",
"cantidad": 1,
"precio": 180.00,
"tipo_iva": 21
}
]
}The names go exactly as they are: fecha_emision, descripcion and tipo_iva. A field that does not exist is not ignored; it returns 422 and names it in error.detalles.campos, so that is the first place to look when the node goes red.
Receiving Cairos events, which is where n8n shines
This is where n8n beats the no-code alternatives: it has a code node where you can check a webhook's signature, which is the one thing that is not negotiable when you open a public endpoint.
The setup is three nodes:
| # | Node | What it does |
|---|---|---|
| 1 | Webhook | It receives the POST from Cairos. You have to turn on the raw body option: without it the signature cannot be checked. |
| 2 | Code | It checks the signature against the subscription secret and stops if it does not match. |
| 3 | Whatever you like | Send a Slack message, write to a spreadsheet, update the order in your shop. |
const crypto = require("crypto");
const secreto = $env.CAIROS_SECRETO;
const entrada = $input.first().json;
// El cuerpo CRUDO, tal y como llegó. Si aquí usas el JSON ya
// convertido, la firma no cuadrará nunca y perderás una tarde.
const crudo = entrada.rawBody || entrada.body;
// La cabecera se llama "x-cairos-firma", en minuscula.
const cabecera = entrada.headers["x-cairos-firma"] || "";
const partes = Object.fromEntries(
cabecera.split(",").map((t) => t.trim().split("="))
);
const ahora = Math.floor(Date.now() / 1000);
if (!partes.t || Math.abs(ahora - Number(partes.t)) > 300) {
throw new Error("Suceso caducado o sin marca de tiempo");
}
const esperada = crypto
.createHmac("sha256", secreto)
.update(partes.t + "." + crudo)
.digest("hex");
if (esperada !== partes.v1) {
throw new Error("Firma no valida");
}
return [{ json: JSON.parse(crudo) }];How that signature is worked out and why the timestamp goes inside it is in webhooks. And the address the Webhook node gives you is the one you register with POST /webhooks.
Working through long lists
To read all of a year's invoices one call is not enough: the API paginates with a cursor. And here n8n makes it easy, because siguiente is already the complete URL of the next page, with your filters inside it: there is no query to reassemble.
- Ask with
?limite=200&orden=antiguo, which is the order that does not shift while you are working through it. - Take
siguientefrom the response and use it as the URL exactly as it comes. It is not the id of the last item, and it is not a cursor to be pasted by hand into?desde=: it is the complete address. - Stop when
siguientecomes back asnull, not whendatoscomes back empty.
In the HTTP Request node's own pagination that is configured with the “next URL” mode and the expression {{ $response.body.siguiente }}, and the stop condition is that field being null.
The detail on pagination, with the loop written in JavaScript and in PHP, is in errors and pagination.
Questions about n8n
Authorization and value Bearer plus the key. The nodes reference it and do not contain it, so rotating it means changing it in one place.Idempotency-Key. A retry of a POST with no key is exactly how you end up with two invoices for the same order.Build the flow and fill in the credential
The routes, the headers and the signature are the ones the server uses today. You create the key yourself from your account, and with a test one you can fire the flow without messing anything up.
HTTP Request · Header Auth · Raw body in the Webhook