Cairos
Facturación
Programa de facturaciónOrzamentosFacturas recorrentesGastos e provedoresCobros e tesouraría
Contabilidade e impostos
ContabilidadeModelos de FacendaLibros rexistroInmobilizadoIGIC e Canarias
Operacións
Inventario e almacénsCRMControl horarioProxectosSubvencións e axudas
Cumprimento
VeriFactuTicketBAIFactura electrónicaToda a normativaSeguridade e datos
Por tipo de negocio
AutónomosPemesAsesorías e xestoríasEstranxeiros en EspañaStartupsComercios e tendas
Por sector
Hostalaría e restaurantesConstrución e reformasServizos profesionaisComercio electrónicoTodos os sectores
Por forma xurídica
AsociaciónsFundaciónsCooperativasClubs deportivosTodas as formas xurídicas
Cambiar de programa
ComparativasAlternativa a HoldedMigrar os teus datos
Ferramentas gratis
Modelo de facturaCalculadora de IVECalculadora de IRPFTodas as ferramentas
Aprender
GuíasGlosarioCalendario fiscalBlog
Desenvolvedores
API e documentaciónComezar en cinco minutosReferencia de recursosWebhooks
Axuda
Centro de axudaContacto
Prezos
Comeza gratis Iniciar sesión
Integración

Shopify: un webhook, un servizo no medio e unha factura

Shopify non pode chamar a Cairos pola súa conta, e iso é unha boa noticia: a clave da túa API non pode vivir nunha tenda pública. Aquí está a peza que vai no medio, enteira.

Función sen servidor en NodeSinatura comprobadaSen facturar dúas veces

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.

Xa non fai falta programar isto

Cairos conecta con Shopify desde dentro do programa: acendes o módulo de tendas en liña, pegas as credenciais que che dá a túa tenda e cada pedido convértese en factura coa túa serie, o teu IVE e a túa numeración. Non hai que instalar ningunha app da súa tenda de aplicacións: a conexión faise desde Cairos co token dunha app privada da túa propia tenda, que ti creas e ti revogas.

Esta guía queda para quen prefira montalo á súa maneira, teña un fluxo que non encaixe, ou queira saber que fai o conector por dentro antes de fiarse del.

Por que fai falta algo no medio

Shopify non pode chamar a Cairos pola súa conta. Non hai un sitio onde pegar unha clave de API allea e dicirlle «cando se pague un pedido, manda este JSON»: o que Shopify sabe facer é avisar por webhook a un enderezo teu.

Así que a montaxe son tres pezas e non dúas:

PezaQue fai
ShopifyManda un webhook orders/paid ao teu enderezo, asinado co segredo da túa aplicación.
Un servizo teuComproba a sinatura de Shopify, traduce o pedido e chama a Cairos coa túa clave. Cabe nunha función sen servidor de vinte liñas.
CairosCrea o contacto, a factura, emítea e rexistra o cobro.

A peza do medio é tamén onde vive a túa clave de Cairos, e iso é exactamente o que se quere: unha clave de API nunca pode estar na tenda, porque a tenda é pública.

Se prefires non montar nada ti, esa peza do medio pódena facer Make ou n8n: son xusto iso, un sitio onde recibir un webhook e lanzar unha chamada HTTP.

O NIF, outra vez

Igual ca en WooCommerce e polo mesmo motivo: o proceso de compra de Shopify é internacional e non pregunta o NIF. Hai que engadilo, cun campo do proceso de compra ou cun atributo do carriño, e chega no pedido como propiedade ou como atributo.

E unha cousa máis que se esquece: os pedidos de Shopify edítanse despois de creados. Se o teu servizo factura ao recibir orders/paid e alguén edita o pedido media hora máis tarde, a factura xa está emitida e non se pode tocar. O correcto entón é unha rectificativa, non intentar cadrar a factura co pedido.

O servizo do medio

Dúas comprobacións antes de tocar nada, e as dúas son obrigatorias: que o aviso vén de Shopify de verdade, e que ese pedido non está facturado xa.

Función sen 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 });
}

Fíxate en que a chave de idempotencia sae do identificador do pedido de Shopify. Esa é a peza que fai que un reintento de Shopify —que os hai, e son normais— non acabe nunha segunda factura.

Dúas sinaturas distintas, e convén non mesturalas

Nesta montaxe hai dúas comprobacións de sinatura e non son a mesma:

Shopify → o teu servizoCairos → o teu servizo
CabeceiraX-Shopify-Hmac-Sha256x-cairos-firma, en minúscula
AlgoritmoHMAC-SHA256 do corpo cruHMAC-SHA256 da marca de tempo e do corpo cru
Formatobase64hexadecimal, con t= e v1=
SegredoO da túa aplicación de ShopifyO da subscrición de webhook de Cairos

Só necesitas a segunda se ademais te subscribes aos sucesos de Cairos, que é o razoable se queres devolver o número de factura ao pedido. Está en webhooks.

O único que comparten é o consello: corpo cru e comparación en tempo constante. Todo o demais cambia.

Preguntas sobre Shopify

Non. O que hai é a API e esta guía. Fai falta un pequeno servizo no medio, que pode ser unha función sen servidor túa ou un escenario de Make ou de n8n.
Porque Shopify non ten onde gardar a clave dunha API allea nin como montar a chamada. O que sabe facer é avisar por webhook a un enderezo teu, e aí é onde vai a clave.
orders/paid. Facturar en orders/create significa facturar pedidos que quizais non cheguen a pagarse.
Cun campo do proceso de compra ou cun atributo do carriño. Chega no pedido e desde aí viaxa a nif do contacto. Sen NIF só se pode emitir factura simplificada.
A factura xa está emitida e non se toca: fai falta unha rectificativa, que hoxe se emite desde Cairos a man. Un pedido editable e unha serie de facturación son dúas cousas con regras distintas, e as da factura non as poñemos nós.

Monta a peza do medio e próbaa hoxe

O código está escrito contra os campos que o servidor acepta de verdade. O único que lle falta é a túa clave, e esa créala ti nun minuto.

orders/paid · Sinatura comprobada · Idempotency-Key por pedido

Soporte