Cairos
Facturació
Programa de facturacióPressupostosFactures recurrentsDespeses i proveïdorsCobraments i tresoreria
Comptabilitat i impostos
ComptabilitatModels d'HisendaLlibres registreImmobilitzatIGIC i Canàries
Operacions
Inventari i magatzemsCRMControl horariProjectesSubvencions i ajuts
Compliment
VeriFactuTicketBAIFactura electrònicaTota la normativaSeguretat i dades
Per tipus de negoci
AutònomsPimesAssessories i gestoriesEstrangers a EspanyaStartupsComerços i botigues
Per sector
Hostaleria i restaurantsConstrucció i reformesServeis professionalsComerç electrònicTots els sectors
Per forma jurídica
AssociacionsFundacionsCooperativesClubs esportiusTotes les formes jurídiques
Canviar de programa
ComparativesAlternativa a HoldedMigrar les teves dades
Eines gratis
Plantilla de facturaCalculadora d'IVACalculadora d'IRPFTotes les eines
Aprendre
GuiesGlossariCalendari fiscalBlog
Desenvolupadors
API i documentacióComença en cinc minutsReferència de recursosWebhooks
Ajuda
Centre d'ajudaContacte
Preus
Comença gratis Inicia la sessió
Integració

Shopify: un webhook, un servei al mig i una factura

Shopify no pot cridar Cairos pel seu compte, i això és una bona notícia: la clau de la teva API no pot viure en una botiga pública. Aquí hi ha la peça que va al mig, sencera.

Funció sense servidor en NodeSignatura comprovadaSense facturar dues vegades

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.

Ja no cal programar això

Cairos connecta amb Shopify des de dins del programa: encens el mòdul de botigues en línia, enganxes les credencials que et dóna la teva botiga i cada comanda es converteix en factura amb la teva sèrie, el teu IVA i la teva numeració. No cal instal·lar cap app de la seva botiga d'aplicacions: la connexió es fa des de Cairos amb el testimoni d'una app privada de la teva pròpia botiga, que tu crees i tu revoques.

Aquesta guia es queda per a qui prefereixi muntar-s'ho a la seva manera, tingui un flux que no encaixi, o vulgui saber què fa el connector per dins abans de fiar-se'n.

Per què cal alguna cosa al mig

Shopify no pot cridar Cairos pel seu compte. No hi ha un lloc on enganxar una clau d'API aliena i dir-li «quan es pagui una comanda, envia aquest JSON»: el que Shopify sap fer és avisar per webhook una adreça teva.

Així que el muntatge són tres peces i no dues:

PeçaQuè fa
ShopifyEnvia un webhook orders/paid a la teva adreça, signat amb el secret de la teva aplicació.
Un servei teuComprova la signatura de Shopify, tradueix la comanda i crida Cairos amb la teva clau. Cap en una funció sense servidor de vint línies.
CairosCrea el contacte, la factura, l'emet i registra el cobrament.

La peça del mig és també on viu la teva clau de Cairos, i això és exactament el que es vol: una clau d'API mai no pot estar a la botiga, perquè la botiga és pública.

Si prefereixes no muntar res tu, aquesta peça del mig la poden fer Make o n8n: són just això, un lloc on rebre un webhook i llançar una crida HTTP.

El NIF, un altre cop

Igual que a WooCommerce i pel mateix motiu: el procés de compra de Shopify és internacional i no pregunta el NIF. Cal afegir-lo, amb un camp del procés de compra o amb un atribut del carretó, i arriba a la comanda com a propietat o com a atribut.

I una cosa més que s'oblida: les comandes de Shopify s'editen després de creades. Si el teu servei factura en rebre orders/paid i algú edita la comanda mitja hora més tard, la factura ja està emesa i no es pot tocar. El correcte llavors és una rectificativa, no intentar quadrar la factura amb la comanda.

El servei del mig

Dues comprovacions abans de tocar res, i totes dues són obligatòries: que l'avís ve de Shopify de debò, i que aquella comanda no està ja facturada.

Funció sense servidor, en Node
import crypto from "node:crypto";

const BASE = "https://erp.cairos.es/api/v1";

// 1 · La firma de Shopify: HMAC-SHA256 en base64 del cuerpo
//     crudo, con el secreto de tu aplicación. Cuerpo CRUDO:
//     si lo reserializas, no cuadra.
function deShopify(crudo, firma, secreto) {
  const esperada = crypto
    .createHmac("sha256", secreto)
    .update(crudo, "utf8")
    .digest("base64");
  const a = Buffer.from(esperada);
  const b = Buffer.from(String(firma));
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

async function cairos(ruta, cuerpo, llave) {
  const cabeceras = {
    Authorization: "Bearer " + process.env.CAIROS_CLAVE,
    "Content-Type": "application/json",
  };
  if (llave) cabeceras["Idempotency-Key"] = llave;

  const r = await fetch(BASE + ruta, {
    method: "POST",
    headers: cabeceras,
    body: cuerpo ? JSON.stringify(cuerpo) : undefined,
  });

  const datos = await r.json();
  if (!r.ok) {
    throw new Error("Cairos " + r.status + ": " + datos.error.codigo);
  }
  return datos;
}

export async function handler(peticion) {
  const crudo = await peticion.text();

  if (!deShopify(crudo, peticion.headers.get("x-shopify-hmac-sha256"),
                 process.env.SHOPIFY_SECRETO)) {
    return new Response("Firma no valida", { status: 401 });
  }

  const pedido = JSON.parse(crudo);
  const llave = "shopify-" + pedido.id;

  // "tipo" es un enumerado en ingles: customer o supplier.
  const contacto = await cairos("/contactos/upsert", {
    nombre: pedido.billing_address.company || pedido.billing_address.name,
    nif: (pedido.note_attributes || [])
      .find((a) => a.name === "nif")?.value,
    email: pedido.email,
    tipo: "customer",
    buscar_por: "email",
  }, llave + "-contacto");

  // El texto de la linea es "descripcion" y el impuesto
  // "tipo_iva". "concepto" e "iva" no existen: 422.
  const lineas = pedido.line_items.map((l) => ({
    descripcion: l.title,
    cantidad: l.quantity,
    precio: Number(l.price),
    tipo_iva: 21,
  }));

  const factura = await cairos("/facturas", {
    contacto_id: contacto.id,
    serie: "WEB",
    fecha_emision: pedido.created_at.slice(0, 10),
    // El precio de Shopify es el del escaparate, con el IVA
    // dentro: que lo desglose el servidor y no un nodo tuyo.
    precios_con_iva: true,
    lineas,
  }, llave);

  const emitida = await cairos(
    "/facturas/" + factura.id + "/emitir",
    null,
    llave + "-emitir"
  );

  await cairos("/cobros", {
    factura_id: factura.id,
    fecha: pedido.created_at.slice(0, 10),
    importe: Number(pedido.total_price),
    metodo: "card",
    notas: "Shopify · pedido " + pedido.name,
  }, llave + "-cobro");

  // Responder rápido: Shopify reintenta si tardas.
  // "referencia" es lo que se imprime; "numero" es un entero.
  return Response.json({ referencia: emitida.referencia });
}

Fixa't que la clau d'idempotència surt de l'identificador de la comanda de Shopify. Aquesta és la peça que fa que un reintent de Shopify —que n'hi ha, i són normals— no acabi en una segona factura.

Dues signatures diferents, i convé no barrejar-les

En aquest muntatge hi ha dues comprovacions de signatura i no són la mateixa:

Shopify → el teu serveiCairos → el teu servei
CapçaleraX-Shopify-Hmac-Sha256x-cairos-firma, en minúscula
AlgorismeHMAC-SHA256 del cos cruHMAC-SHA256 de la marca de temps i el cos cru
Formatbase64hexadecimal, amb t= i v1=
SecretEl de la teva aplicació de ShopifyEl de la subscripció de webhook de Cairos

Només necessites la segona si a més et subscrius als esdeveniments de Cairos, que és el raonable si vols retornar el número de factura a la comanda. És a webhooks.

L'única cosa que comparteixen és el consell: cos cru i comparació en temps constant. Tota la resta canvia.

Preguntes sobre Shopify

No. El que hi ha és l'API i aquesta guia. Cal un petit servei al mig, que pot ser una funció sense servidor teva o un escenari de Make o de n8n.
Perquè Shopify no té on desar la clau d'una API aliena ni com muntar la crida. El que sap fer és avisar per webhook una adreça teva, i aquí és on va la clau.
orders/paid. Facturar a orders/create vol dir facturar comandes que potser no arribin a pagar-se.
Amb un camp del procés de compra o un atribut del carretó. Arriba a la comanda i des d'aquí viatja a nif del contacte. Sense NIF només es pot emetre factura simplificada.
La factura ja està emesa i no es toca: cal una rectificativa, que avui s'emet des de Cairos a mà. Una comanda editable i una sèrie de facturació són dues coses amb regles diferents, i les de la factura no les posem nosaltres.

Munta la peça del mig i prova-la avui

El codi està escrit contra els camps que el servidor accepta de debò. L'única cosa que li falta és la teva clau, i aquesta te la crees tu en un minut.

orders/paid · Signatura comprovada · Idempotency-Key per comanda

Suport