Nordlet

← Documentación / Glosario

¿Qué es la idempotencia? Por qué la necesitan las API de contabilidad y pagos

Cómo una solicitud reintentada crea una factura duplicada, qué garantiza una clave de idempotencia, los modos de fallo que debe gestionar y cómo implementarla correctamente.

Una operación es idempotente si realizarla dos veces produce el mismo efecto que realizarla una sola vez. Leer un registro es naturalmente idempotente. Crear una factura no lo es; si la ejecutas dos veces, obtienes dos facturas, dos cuentas por cobrar y un cliente preguntando por qué se le ha cobrado por duplicado.

Ese margen importa porque las redes fallan de una manera específica y molesta: la solicitud se procesa con éxito y la respuesta se pierde. El cliente detecta un tiempo de espera agotado (timeout) y no puede distinguir entre "nunca llegó" y "llegó, funcionó, pero la respuesta se perdió". Su única opción aparentemente segura es reintentar, y el reintento es precisamente lo que crea el duplicado.

Por qué la contabilidad empeora las cosas

En la mayoría de los sistemas, un duplicado es una molestia. En un libro mayor (ledger), es un problema acumulativo:

  • Una factura duplicada consume un número de una secuencia sin huecos, por lo que eliminarla no es una opción; la inmutabilidad implica que corregirla requiere una nota de crédito, que es un documento que su cliente verá.
  • El duplicado registra asientos contables, por lo que los ingresos, el IVA y las cuentas por cobrar quedan sobreestimados hasta que alguien se percata.
  • Entra en la declaración de IVA y, en Lituania, en los registros i.SAF.
  • Si desencadenó un pago, el dinero realmente se movió.

"Simplemente buscar duplicados" tampoco funciona, ya que dos facturas genuinamente separadas para el mismo cliente por el mismo importe en el mismo día son perfectamente normales. El sistema no puede deducir la intención a partir del contenido; el cliente debe declararla.

Cómo funcionan las claves de idempotencia

El cliente genera una clave única por operación lógica y la envía junto con la solicitud:

POST /v1/sales/invoices/create
Idempotency-Key: 8f14e45f-ea1b-4c2b-9f3a-2d7c1e0b5a91

A partir de ahí, el servidor garantiza lo siguiente:

  1. Primera solicitud con esta clave: se procesa con normalidad y el resultado se almacena vinculado a la clave.
  2. Reintento con la misma clave y la misma carga útil (payload): no se vuelve a ejecutar; se devuelve la respuesta almacenada.
  3. Misma clave, carga útil diferente: se rechaza. La clave es una declaración de que se trata de la misma operación; un cuerpo diferente significa que no lo es, y devolver silenciosamente el resultado anterior sería peor que un error.
  4. Reintento mientras la original sigue en ejecución: se rechaza con un error reintentable en lugar de ejecutarse de forma concurrente.

El punto 4 es el que las implementaciones ingenuas pasan por alto. Un cliente cuyo tiempo de espera agota a los 5 segundos y reintenta a los 6 chocará contra su propia solicitud en curso; sin un control de concurrencia (concurrency guard), ambas avanzan y se produce el duplicado que el mecanismo pretendía evitar.

Generación correcta de claves

La clave debe identificar la operación, no el intento:

  • Correcto: un UUID generado una sola vez cuando el usuario hace clic en "emitir factura", reutilizado en cada reintento de ese clic.
  • Correcto: una clave determinista derivada de su propio dominio: order-1041-invoice.
  • Incorrecto: un UUID nuevo por cada intento HTTP. Cada reintento obtiene una clave nueva, por lo que cada reintento es una operación nueva y no habrá implementado nada.
  • Incorrecto: un hash de la carga útil. Dos operaciones legítimamente idénticas chocarán entre sí y la segunda será descartada en silencio.

Las claves necesitan un período de retención, el tiempo suficiente para cubrir ventanas de reintento realistas y caídas del cliente, pero no de forma indefinida.

Lo que no resuelve

La idempotencia protege contra la ejecución duplicada de la misma solicitud. No protege frente a:

  • Dos usuarios diferentes creando la misma factura desde sesiones distintas; eso es un problema de reglas de negocio, resuelto mediante restricciones de unicidad como una factura por pedido.
  • Duplicados lógicos con claves diferentes.
  • Falta de determinismo dentro de la operación; si el controlador genera una marca de tiempo (timestamp) o un número, la respuesta almacenada captura el primero, que es el comportamiento deseado pero conviene tenerlo presente.

Se complementa con las restricciones de base de datos, en lugar de sustituirlas. El libro mayor (ledger) de Nordlet utiliza ambas: las claves de idempotencia detienen las solicitudes reintentadas, mientras que los índices únicos detienen lotes de liquidación duplicados por ID de pago, facturas de compra duplicadas por proveedor y número de documento, y ejecuciones de depreciación duplicadas por activo y período.

Cómo lo implementa Nordlet

Envíe una cabecera Idempotency-Key en cualquier solicitud que modifique estados (una cadena de texto que no esté vacía y tenga un máximo de 255 caracteres).

La primera solicitud con una clave dada se ejecuta y su resultado se almacena. Un reintento con la misma clave y la misma carga útil (payload) devuelve la respuesta almacenada sin volver a ejecutarse, por lo que la factura se crea exactamente una vez sin importar cuántas veces se repita la llamada.

Dos modos de fallo son explícitos en lugar de silenciosos:

  • Misma clave, carga útil diferente → rechazada: "Idempotency-Key was already used with a different request payload." Esto detecta el caso genuinamente peligroso en el que un cliente reutiliza una clave mientras cambia el importe.
  • Misma clave, la original sigue en curso → rechazada como reintentable: "A request with this Idempotency-Key is still being processed, retry shortly." Se evita la doble ejecución concurrente.

Las claves se delimitan por empresa (company), de modo que dos inquilinos (tenants) que generen el mismo UUID nunca entran en conflicto.

La garantía complementaria funciona en sentido contrario. La entrega de webhooks es de al menos una vez (at-least-once), lo que significa que su extremo (endpoint) recibirá ocasionalmente el mismo evento dos veces, tras un tiempo de espera agotado donde la entrega realmente se completó, o en una reexpedición. Cada evento incluye un ID, y su controlador debe registrar los ID procesados y omitir las repeticiones. La idempotencia es una propiedad que necesitan ambos lados de una integración; la guía de convenciones de API aborda la parte de las solicitudes en detalle.

FAQ

¿Qué es una clave de idempotencia?

Un identificador generado por el cliente que se envía con una solicitud y permite al servidor reconocer un reintento de la misma operación lógica. La primera solicitud se ejecuta; los reintentos con la misma clave devuelven el resultado almacenado en lugar de volver a ejecutarse.

¿Por qué las API de contabilidad necesitan idempotencia?

Porque una escritura duplicada crea un documento real con consecuencias reales: una segunda factura que consume un número secuencial, se registra en el libro mayor y entra en una declaración de IVA. A diferencia de una lectura duplicada, no se puede simplemente eliminar; debe corregirse con una nota de crédito.

¿Debo generar una clave nueva para cada reintento?

No, eso arruina el mecanismo por completo. Genere una clave por operación lógica y reutilícela en cada reintento de esa operación. Una clave nueva por intento significa que cada intento se trata como una operación nueva.

¿Qué sucede si reutilizo una clave con una carga útil diferente?

La solicitud es rechazada. La clave afirma que se trata de la misma operación, por lo que un cuerpo modificado indica que algo va mal, muy probablemente un error de reutilización de claves en el cliente. Devolver el resultado anterior en silencio sería peor.

¿Impide la idempotencia todos los duplicados?

No. Impide la ejecución duplicada de la misma solicitud. Dos usuarios diferentes que crean de forma independiente la misma factura han realizado dos solicitudes distintas, lo cual constituye un problema de reglas de negocio para las restricciones de unicidad, y no algo que una clave de idempotencia pueda detectar.