Una cabecera que evita dos facturas por el mismo pedido
Si la red se corta antes de la respuesta, quien llama no sabe si la factura se creó. Reintentar sin llave produce dos números correlativos por un pedido, y eso en una serie de facturación no se arregla borrando.
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.
El problema, contado como pasa de verdad
Tu tienda recibe el pedido 1042. Tu código llama a Cairos para crear la factura y emitirla. Y entonces se cae la red.
Fíjate en lo que sabes y en lo que no. Sabes que mandaste la petición. No sabes si llegó. No sabes si se procesó. Lo único que tienes es un tiempo de espera agotado, que es exactamente la misma respuesta tanto si la factura se creó como si no.
Y aquí es donde empieza el problema de verdad, porque la reacción natural es reintentar. Si la primera llamada sí había llegado, ahora tienes dos facturas, con dos números correlativos, por el mismo pedido.
Diagrama de secuencia en dos carriles: a la izquierda «tu sistema», a la derecha «Cairos». Flecha de ida POST /facturas que llega; Cairos crea la factura F27/0042; la flecha de vuelta 201 se corta a mitad con una marca de error. Debajo, el reintento sin llave crea F27/0043 duplicada, y con llave devuelve la misma F27/0042. Estilo de línea, colores del sistema, radio 0.
Por qué eso no se arregla borrando
En casi cualquier otra API, dos registros duplicados se borran y no ha pasado nada. En una serie de facturación, no.
Los números de una serie son correlativos y sin huecos: es lo que exige el reglamento de facturación y lo que comprueba cualquiera que mire tus libros. Si has emitido la F27/0042 y la F27/0043 por el mismo pedido y borras la segunda, no te queda una serie limpia: te queda una serie con un hueco, y un hueco en una numeración correlativa es exactamente la señal que busca una inspección.
Lo que hay que hacer entonces es lo correcto, y es trabajo: emitir una factura rectificativa que anule la duplicada, dejando constancia de por qué. Con VeriFactu hay además un registro de anulación encadenado, porque la norma no permite que una factura desaparezca sin dejar rastro.
Reintentar sin llave
- Dos facturas con dos números por un pedido.
- Tu cliente recibe dos facturas y llama.
- No se arregla borrando: hay que rectificar y justificar.
- El IVA repercutido del trimestre sale de más hasta que lo corrijas.
- Y si el reintento fue automático, puede haber pasado cien veces.
Reintentar con llave
- La segunda llamada devuelve la misma factura, con su mismo número.
- Tu código no tiene que distinguir «creado» de «ya estaba creado».
- Un tiempo de espera agotado deja de ser un problema: repites y ya.
- Vale igual para el
500y para el proceso que se reinicia a medias. - Y no cuesta nada: es una cabecera.
Cómo se usa: una cabecera y nada más
En cualquier llamada que cree algo, añade Idempotency-Key con un valor tuyo:
curl -X POST https://erp.cairos.es/api/v1/facturas \
-H "Authorization: Bearer $CAIROS_CLAVE" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: pedido-1042" \
-d '{ "contacto_id": "con_7Qb3xK", "serie": "F27" }'// La llave sale del pedido, no de un aleatorio: tiene que ser
// la misma en el intento y en el reintento.
const llave = "pedido-" + pedido.id;
const r = await fetch(BASE + "/facturas", {
method: "POST",
headers: {
Authorization: "Bearer " + CLAVE,
"Content-Type": "application/json",
"Idempotency-Key": llave,
},
body: JSON.stringify(cuerpo),
});<?php
$llave = "pedido-" . $pedido->get_id();
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"Authorization: Bearer " . $clave,
"Content-Type: application/json",
"Idempotency-Key: " . $llave,
]);Lo que hace el servidor con eso:
- Si es la primera vez que ve esa llave, hace el trabajo y guarda la respuesta.
- Si ya la había visto con el mismo cuerpo, no repite nada: devuelve la respuesta guardada, con el mismo identificador y el mismo número de factura.
- Si ya la había visto con un cuerpo distinto, devuelve
409 conflicto. Eso no es un fastidio: es la API avisándote de que estás reutilizando una llave para dos cosas diferentes, que es un fallo de tu lado.
Qué llave poner, que es lo único que hay que pensar
La regla es una sola y todo lo demás se deduce de ella: la llave tiene que ser la misma en el intento y en el reintento, y distinta para dos operaciones distintas.
| Llave | ¿Sirve? | Por qué |
|---|---|---|
pedido-1042 | Sí | Sale del identificador del pedido en tu sistema. Al reintentar sale la misma sin tener que guardarla en ninguna parte. |
emitir-pedido-1042 | Sí | Crear la factura y emitirla son dos operaciones: dos llaves. Con la misma para las dos, la segunda llamada daría 409. |
wc-1042-2027 | Sí | Si tienes dos tiendas que numeran pedidos por su cuenta, mete de dónde viene: dos pedidos 1042 distintos no pueden compartir llave. |
| Un UUID aleatorio por intento | No | Es el error clásico. Si el reintento genera otra llave, la API no puede saber que es el mismo trabajo, y duplicas igual que sin cabecera. |
| La marca de tiempo | No | Cambia entre el intento y el reintento. Mismo caso que el anterior. |
factura | No | Demasiado genérica: la segunda factura del día chocaría con la primera y saldría un 409 que no significa nada. |
Si tienes que guardar la llave en algún sitio para poder reintentar, elige otra: la buena es la que se calcula a partir de algo que ya tienes.
Un UUID aleatorio sí vale, pero sólo si lo generas una vez y lo guardas junto al pedido antes de llamar, y lo reutilizas en los reintentos. Que es más trabajo que usar el identificador del pedido y no da nada a cambio.
Cuánto dura una llave
Las llaves se recuerdan durante veinticuatro horas. Pasado ese plazo, la misma llave vuelve a considerarse nueva y crearía otra factura.
Es tiempo de sobra para lo que la cabecera resuelve —un corte de red, un reintento automático, un proceso que se reinicia—, y no es un mecanismo para no duplicar nunca. Si lo que necesitas es garantizar que un pedido de hace tres semanas no se vuelva a facturar, eso no lo arregla la idempotencia: lo arregla guardar en tu sistema el identificador de la factura que creaste y mirarlo antes de llamar. Las dos cosas se complementan y hacen falta las dos.
El patrón completo, en tres líneas
Uno: antes de llamar, mira si ya guardaste el identificador de factura de ese pedido. Si lo tienes, no llames. Dos: si no lo tienes, llama con Idempotency-Key derivada del pedido. Tres: guarda el identificador que te devuelve antes de hacer nada más. Con eso, ni un corte de red ni un reproceso a mano duplican nada.
Dónde vale y dónde no hace falta
Mándala en todo lo que cree algo: POST /contactos, POST /productos, POST /facturas, POST /facturas/{id}/emitir, POST /gastos y POST /cobros. Emitir y registrar un cobro son las dos donde más duele el duplicado.
No hace falta en las lecturas. Un GET se puede repetir cien veces sin consecuencias; ésa es la definición de idempotente y por eso no lleva cabecera.
Y en un PATCH tampoco es necesaria, porque mandar dos veces el mismo cambio deja el recurso igual. Puedes mandarla si te simplifica el código, y no estorba.
La forma corta de acordarse: si la operación consume un número de serie o mueve dinero, lleva llave.
Preguntas sobre idempotencia
pedido-1042. Lo que no vale es un aleatorio nuevo en cada intento, porque entonces el reintento parece otra operación y duplica igual.Es una cabecera. Ponla desde el primer día
Añadirla cuando la integración ya está en marcha es fácil. Explicarle a un cliente por qué tiene dos facturas del mismo pedido, no.
Idempotency-Key · En POST · Derivada del pedido