Saltar al contenido

Idempotencia

Cómo funciona

Elige una clave única por operación (un UUID, o un id propio como un número de pedido) y envíala como Idempotency-Key. Durante 24 horas, la misma clave en la misma ruta devuelve de nuevo el primer resultado, con Idempotent-Replayed: true, en lugar de repetir el trabajo.

  • La misma clave con un cuerpo distinto responde 422 idempotency_conflict.
  • La misma clave mientras la primera solicitud sigue en curso responde 409 conflict: espera y reintenta.
  • Una solicitud que falló no se guarda, así que reintentarla con la misma clave hace el trabajo.
  • Las claves se limitan al espacio de trabajo, a quien llama, a la ruta y a su path. Quien llama es la clave de API (o, en la app, el miembro con sesión iniciada): dos claves de API que envían la misma Idempotency-Key nunca se repiten las respuestas entre sí, y una clave de API renovada es un nuevo llamante. La misma clave en las rutas de dos códigos distintos tampoco colisiona.
  • PATCH y DELETE no llevan clave: repetirlos deja el mismo estado.

Las repeticiones nunca llevan un secreto

La respuesta guardada nunca incluye un secreto. Un POST que devuelve uno (registrar un endpoint de webhook, y crear o renovar una clave de API en la app) lleva el secreto solo en la primera respuesta, con secretAlreadyShown: false. Una repetición dentro de las 24 horas responde con el mismo objeto con secret: null y secretAlreadyShown: true.

Si se perdió la primera respuesta, el objeto sigue existiendo (su id está en la repetición). Rota el secreto del endpoint en la app (o elimina el endpoint y regístralo de nuevo), o renueva la clave de API, para obtener un secreto nuevo.

Repetición de POST /v1/webhooks (extracto)
{  "id": "whe_3k8m2q9x7c4v1b6n",  "url": "https://hooks.example.com/dotwick",  "secret": null,  "secretAlreadyShown": true}

Reintentar con seguridad

ejemploPOST /v1/codes
import { randomUUID } from "node:crypto";// one key per logical operation, reused on every retry of itconst idempotencyKey = randomUUID();for (let attempt = 1; attempt <= 5; attempt++) {  const res = await fetch("https://api.dotwick.com/v1/codes", {    method: "POST",    headers: {      Authorization: `Bearer ${process.env.DOTWICK_API_KEY}`,      "Content-Type": "application/json",      "Idempotency-Key": idempotencyKey,    },    body: JSON.stringify({ name: "Spring menu", content: { type: "url", url: "https://example.com/menu" } }),  });  if (res.ok) break; // a replay answers the first result with Idempotent-Replayed: true  if (res.status !== 429 && res.status < 500) throw new Error((await res.json()).error.message);  const wait = Number(res.headers.get("Retry-After") ?? 2 ** attempt);  await new Promise((r) => setTimeout(r, wait * 1000));}

Rutas que aceptan una clave

Todos los POST excepto la subida multipart POST /v1/assets.

MétodoRutaScopePara qué sirve
POST/v1/codescodes:writeCrea un códigoidempotente
POST/v1/codes/movecodes:writeMueve códigos a una carpetaidempotente
POST/v1/codes/move-domaincodes:writeMueve códigos a otro dominioidempotente
POST/v1/codes/tagscodes:writeAñade o quita etiquetas en varios códigosidempotente
POST/v1/codes/{id}/duplicatecodes:writeDuplica un códigoidempotente
POST/v1/codes/bulkcodes:writeCrea hasta 500 códigos en una sola tandaidempotente
POST/v1/bulk-imports/{id}/cancelcodes:writeCancela una tanda en cola o en cursoidempotente
POST/v1/bulk-imports/{id}/retrycodes:writeReintenta las filas fallidas de una tandaidempotente
POST/v1/analytics/exportsanalytics:readEscaneos en CSV, preparados en segundo planoidempotente
POST/v1/reportsanalytics:readGenera un informe de estadísticas en PDF o CSVidempotente
POST/v1/exportscodes:readExporta muchos códigos en un solo ZIPidempotente
POST/v1/tagstags:writeCrea una etiquetaidempotente
POST/v1/foldersfolders:writeCrea una carpetaidempotente
POST/v1/templatestemplates:writeCrea una plantillaidempotente
POST/v1/webhookswebhooks:writeRegistra un endpoint y los tipos de eventoidempotente
POST/v1/webhooks/{id}/testwebhooks:writeEnvía un evento webhook.testidempotente
POST/v1/webhooks/{id}/deliveries/{deliveryId}/redeliverwebhooks:writeReenvía una entregaidempotente
POST/v1/webhooks/{id}/recoverwebhooks:writeReenvía las entregas fallidasidempotente
POST/v1/hookswebhooks:writeSuscribe un hook a un eventoidempotente
POST/v1/cardscards:writeCrea una tarjeta de visitaidempotente
POST/v1/cards/{id}/deactivatecards:writeDesactiva una tarjeta de visitaidempotente
POST/v1/conversionsconversions:writeRegistra una conversiónidempotente
POST/v1/nfc/tagscodes:writeRegistra una etiqueta NFC en un códigoidempotente