Cairos
Facturación
Programa de facturaciónPresupuestosFacturas recurrentesGastos y proveedoresCobros y tesorería
Contabilidad e impuestos
ContabilidadModelos de HaciendaLibros registroInmovilizadoIGIC y Canarias
Operaciones
Inventario y almacenesCRMControl horarioProyectosSubvenciones y ayudas
Cumplimiento
VeriFactuTicketBAIFactura electrónicaToda la normativaSeguridad y datos
Por tipo de negocio
AutónomosPymesAsesorías y gestoríasExtranjeros en EspañaStartupsComercios y tiendas
Por sector
Hostelería y restaurantesConstrucción y reformasServicios profesionalesComercio electrónicoTodos los sectores
Por forma jurídica
AsociacionesFundacionesCooperativasClubes deportivosTodas las formas jurídicas
Cambiar de programa
ComparativasAlternativa a HoldedMigrar tus datos
Herramientas gratis
Plantilla de facturaCalculadora de IVACalculadora de IRPFTodas las herramientas
Aprender
GuíasGlosarioCalendario fiscalBlog
Desarrolladores
API y documentaciónEmpezar en cinco minutosReferencia de recursosWebhooks
Ayuda
Centro de ayudaContacto
Precios
Empieza gratis Iniciar sesión
Integración

Shopify: un webhook, un servicio en medio y una factura

Shopify no puede llamar a Cairos por su cuenta, y eso es una buena noticia: la clave de tu API no puede vivir en una tienda pública. Aquí está la pieza que va en medio, entera.

Función sin servidor en NodeFirma comprobadaSin facturar dos veces

La API está en marcha. Esto es lo que hay hoy y lo que no

Funciona. Las rutas de esta documentación están desplegadas y respondiendo en https://erp.cairos.es/api/v1/. Los ejemplos de estas páginas se pueden copiar y ejecutar. La especificación completa está en openapi.json, que es lo que importan Make y n8n.

Las claves te las creas tú, desde Desarrolladores en tu cuenta de Cairos. Empieza con una cai_test_: trabaja contra tus datos de verdad pero no registra en VeriFactu ni envía correos, así que puedes montar tu integración sin ensuciar una serie de facturación.

Y lo que todavía NO hay, dicho sin adornos: ningún conector oficial. Ni app de Shopify, ni plugin de WooCommerce, ni módulo publicado en Make o n8n. Con la API y su especificación se pueden construir —para eso están las guías de esta sección— pero construirlos es trabajo, y ese trabajo no está hecho.

Si montas algo con esto, escríbenos a hola@cairos.es. Nos interesa especialmente lo que te falte del contrato: es lo que decide qué se amplía primero.

No hay app de Cairos en Shopify

No está publicada en su tienda de aplicaciones y no hay fecha para que lo esté. Lo que hay es la API y esta página, que te dice exactamente qué enganchar y con qué código. Es menos cómodo que instalar algo y es lo que hay hoy; decirlo así te ahorra buscar un conector que no existe.

Por qué hace falta algo en medio

Shopify no puede llamar a Cairos por su cuenta. No hay un sitio donde pegar una clave de API ajena y decirle «cuando se pague un pedido, manda este JSON»: lo que Shopify sabe hacer es avisar por webhook a una dirección tuya.

Así que el montaje son tres piezas y no dos:

PiezaQué hace
ShopifyManda un webhook orders/paid a tu dirección, firmado con el secreto de tu aplicación.
Un servicio tuyoComprueba la firma de Shopify, traduce el pedido y llama a Cairos con tu clave. Cabe en una función sin servidor de veinte líneas.
CairosCrea el contacto, la factura, la emite y registra el cobro.

La pieza del medio es también donde vive tu clave de Cairos, y eso es exactamente lo que se quiere: una clave de API nunca puede estar en la tienda, porque la tienda es pública.

Si prefieres no montar nada tú, esa pieza del medio la pueden hacer Make o n8n: son justo eso, un sitio donde recibir un webhook y lanzar una llamada HTTP.

El NIF, otra vez

Igual que en WooCommerce y por el mismo motivo: el proceso de compra de Shopify es internacional y no pregunta el NIF. Hay que añadirlo, con un campo del proceso de compra o con un atributo del carrito, y llega en el pedido como propiedad o como atributo.

Y una cosa más que se olvida: los pedidos de Shopify se editan después de creados. Si tu servicio factura al recibir orders/paid y alguien edita el pedido media hora más tarde, la factura ya está emitida y no se puede tocar. Lo correcto entonces es una rectificativa, no intentar cuadrar la factura con el pedido.

El servicio de en medio

Dos comprobaciones antes de tocar nada, y las dos son obligatorias: que el aviso viene de Shopify de verdad, y que ese pedido no está facturado ya.

Función sin 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;

  const contacto = await cairos("/contactos", {
    nombre: pedido.billing_address.company || pedido.billing_address.name,
    nif: (pedido.note_attributes || [])
      .find((a) => a.name === "nif")?.value,
    email: pedido.email,
    tipo: "cliente",
  }, llave + "-contacto");

  const lineas = pedido.line_items.map((l) => ({
    concepto: l.title,
    cantidad: l.quantity,
    precio: Number(l.price),
    iva: 21,
  }));

  const factura = await cairos("/facturas", {
    contacto_id: contacto.id,
    serie: "WEB",
    fecha: pedido.created_at.slice(0, 10),
    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),
    medio: "tarjeta",
  }, llave + "-cobro");

  // Responder rápido: Shopify reintenta si tardas.
  return Response.json({ numero: emitida.numero });
}

Fíjate en que la llave de idempotencia sale del identificador del pedido de Shopify. Ésa es la pieza que hace que un reintento de Shopify —que los hay, y son normales— no acabe en una segunda factura.

Dos firmas distintas, y conviene no mezclarlas

En este montaje hay dos comprobaciones de firma y no son la misma:

Shopify → tu servicioCairos → tu servicio
CabeceraX-Shopify-Hmac-Sha256Cairos-Firma
AlgoritmoHMAC-SHA256 del cuerpo crudoHMAC-SHA256 de la marca de tiempo y el cuerpo crudo
Formatobase64hexadecimal, con t= y v1=
SecretoEl de tu aplicación de ShopifyEl de la suscripción de webhook de Cairos

Sólo necesitas la segunda si además te suscribes a los sucesos de Cairos, que es lo razonable si quieres devolver el número de factura al pedido. Está en webhooks.

Lo único que comparten es el consejo: cuerpo crudo y comparación en tiempo constante. Todo lo demás cambia.

Preguntas sobre Shopify

No. Lo que hay es la API y esta guía. Hace falta un pequeño servicio en medio, que puede ser una función sin servidor tuya o un escenario de Make o de n8n.
Porque Shopify no tiene dónde guardar la clave de una API ajena ni cómo montar la llamada. Lo que sabe hacer es avisar por webhook a una dirección tuya, y ahí es donde va la clave.
orders/paid. Facturar en orders/create significa facturar pedidos que quizá no lleguen a pagarse.
Con un campo del proceso de compra o un atributo del carrito. Llega en el pedido y desde ahí viaja a nif del contacto. Sin NIF sólo se puede emitir factura simplificada.
La factura ya está emitida y no se toca: hace falta una rectificativa, que hoy se emite desde Cairos a mano. Un pedido editable y una serie de facturación son dos cosas con reglas distintas, y las de la factura no las ponemos nosotros.

Monta la pieza de en medio y déjala esperando

El código está escrito contra el contrato definitivo. Cuando la API abra, sólo hay que poner la clave.

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

Soporte