Cairos
Invoicing
Invoicing softwareQuotesRecurring invoicesExpenses and suppliersReceipts and cash flow
Accounting and tax
AccountingAEAT tax formsRecord booksFixed assetsIGIC and the Canary Islands
Operations
Inventory and warehousesCRMTime trackingProjectsGrants and funding
Compliance
VeriFactuTicketBAIElectronic invoicingAll the regulationsSecurity and data
By type of business
Self-employedSmall businessesAccountants and tax advisersForeigners in SpainStartupsRetail and shops
By sector
Hospitality and restaurantsConstruction and renovationProfessional servicesE-commerceAll sectors
By legal structure
AssociationsFoundationsCooperativesSports clubsAll legal structures
Switching software
ComparisonsAn alternative to HoldedMigrating your data
Free tools
Invoice templateVAT calculatorIRPF calculatorAll the tools
Learn
GuidesGlossaryTax calendarBlog
Developers
API and documentationGet started in five minutesResource referenceWebhooks
Help
Help centreContact
Pricing
Start for free Log in
Automation

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.

Header Auth credentialThe signature checked in a Code nodeCursor pagination

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:

Header Auth credential
Name:  Authorization
Value: Bearer cai_live_TU_CLAVE

From 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 fieldValue
MethodPOST
URLhttps://erp.cairos.es/api/v1/facturas
AuthenticationGeneric, with the Header Auth credential above
Send BodyOn, in JSON
HeadersIdempotency-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:

Body · JSON
{
  "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:

#NodeWhat it does
1WebhookIt receives the POST from Cairos. You have to turn on the raw body option: without it the signature cannot be checked.
2CodeIt checks the signature against the subscription secret and stops if it does not match.
3Whatever you likeSend a Slack message, write to a spreadsheet, update the order in your shop.
Code node · check the signature
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 siguiente from 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 siguiente comes back as null, not when datos comes 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

No. You use the HTTP Request node with a Header Auth credential. For everything this documentation describes, that is enough.
With a Header Auth credential: name 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.
Yes, and it is where n8n does better than the no-code alternatives: a Webhook node with the raw body option turned on, and a Code node that checks the HMAC signature before going any further.
Almost always because you are signing against the JSON already parsed into an object and serialised again, which is not identical to the one that arrived. You have to turn on the raw body in the Webhook node and sign against that.
Yes, but only if you send Idempotency-Key. A retry of a POST with no key is exactly how you end up with two invoices for the same order.
Yes, and today it is the way to do it. Cairos connects on its own to Shopify, WooCommerce and Odoo; not yet to PrestaShop, because its API does not let you safely work out the VAT rate on each line or the coupons. With PrestaShop's order webhook coming into the Webhook node and the route this page describes, the order ends up as an issued invoice just the same.
Yes, and you download it from Cairos itself: API and integrations → Get started. It comes with the nodes and the connections already made, and the key is not inside the file: when you import it, n8n asks you to create the credential and that is where you paste it, once.

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

Support