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.
{ "id": "whe_3k8m2q9x7c4v1b6n", "url": "https://hooks.example.com/dotwick", "secret": null, "secretAlreadyShown": true}Reintentar con seguridad
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étodo | Ruta | Scope | Para qué sirve |
|---|---|---|---|
| POST | /v1/codes | codes:write | Crea un códigoidempotente |
| POST | /v1/codes/move | codes:write | Mueve códigos a una carpetaidempotente |
| POST | /v1/codes/move-domain | codes:write | Mueve códigos a otro dominioidempotente |
| POST | /v1/codes/tags | codes:write | Añade o quita etiquetas en varios códigosidempotente |
| POST | /v1/codes/{id}/duplicate | codes:write | Duplica un códigoidempotente |
| POST | /v1/codes/bulk | codes:write | Crea hasta 500 códigos en una sola tandaidempotente |
| POST | /v1/bulk-imports/{id}/cancel | codes:write | Cancela una tanda en cola o en cursoidempotente |
| POST | /v1/bulk-imports/{id}/retry | codes:write | Reintenta las filas fallidas de una tandaidempotente |
| POST | /v1/analytics/exports | analytics:read | Escaneos en CSV, preparados en segundo planoidempotente |
| POST | /v1/reports | analytics:read | Genera un informe de estadísticas en PDF o CSVidempotente |
| POST | /v1/exports | codes:read | Exporta muchos códigos en un solo ZIPidempotente |
| POST | /v1/tags | tags:write | Crea una etiquetaidempotente |
| POST | /v1/folders | folders:write | Crea una carpetaidempotente |
| POST | /v1/templates | templates:write | Crea una plantillaidempotente |
| POST | /v1/webhooks | webhooks:write | Registra un endpoint y los tipos de eventoidempotente |
| POST | /v1/webhooks/{id}/test | webhooks:write | Envía un evento webhook.testidempotente |
| POST | /v1/webhooks/{id}/deliveries/{deliveryId}/redeliver | webhooks:write | Reenvía una entregaidempotente |
| POST | /v1/webhooks/{id}/recover | webhooks:write | Reenvía las entregas fallidasidempotente |
| POST | /v1/hooks | webhooks:write | Suscribe un hook a un eventoidempotente |
| POST | /v1/cards | cards:write | Crea una tarjeta de visitaidempotente |
| POST | /v1/cards/{id}/deactivate | cards:write | Desactiva una tarjeta de visitaidempotente |
| POST | /v1/conversions | conversions:write | Registra una conversiónidempotente |
| POST | /v1/nfc/tags | codes:write | Registra una etiqueta NFC en un códigoidempotente |