Appearance
Idempotencia
En toda operación que crea o muta datos (POST, PATCH, PUT) puedes mandar el header Idempotency-Key. Si por cualquier motivo (red intermitente, doble-click, reintento de un job) el mismo request llega dos veces, te devolvemos la respuesta de la primera vez en lugar de duplicar.
Cómo usarla
Genera un identificador único por intento conceptual de la operación. Mándalo en el header:
Idempotency-Key: f1a2b3c4-d5e6-7890-abcd-ef0123456789Recomendación: usa un UUID v4 o un hash del payload + timestamp.
Reglas
- La key vive 24 horas desde el primer uso. Después de eso, la cache se libera y la misma key puede reusarse para un request distinto.
- La cache está scopada por empresa: la misma key entre dos cuentas distintas no colisiona.
- Si reusas una key con un body idéntico, te devolvemos la respuesta cacheada y agregamos el header
Idempotent-Replayed: true. - Si reusas una key con un body distinto, recibes
422 idempotency_key_reused. Cambia la key o iguala el body. - Solo cacheamos respuestas con status 2xx. Si el primer intento dio
422o500, no hay caché y un retry pasa por el flujo normal. - El body máximo idempotency-keyed es 100 KB. Bodies más grandes recibirán
413 idempotency_body_too_largey debes quitar el header.
Ejemplo
javascript
const idempotencyKey = crypto.randomUUID()
async function createLead(payload) {
return fetch('https://developers.fi-nova.com/api/v1/leads', {
method: 'POST',
headers: {
'Authorization': `Bearer ${SECRET}`,
'Content-Type': 'application/json',
'Idempotency-Key': idempotencyKey,
},
body: JSON.stringify({ data: payload }),
})
}
// Primer intento (red flaky) — falla en la red, pero el server ya creó el lead.
await createLead({ name: 'Laura' }).catch(() => null)
// Reintento. Misma key, mismo body. NO duplicamos — devolvemos la respuesta original.
const res = await createLead({ name: 'Laura' })
res.headers.get('Idempotent-Replayed') // → 'true'Por qué no idempotencia automática
Podríamos hashear el body y deduplicar implícitamente, pero eso sería frágil — dos requests con el mismo contenido pero distinto intent conceptual (ej. dos formularios enviados por personas distintas con coincidentemente el mismo email) se colapsarían a uno. Pedir explícitamente la key te deja el control.
Mejores prácticas
- Genera la key del lado del cliente, no del servidor que despacha. Si tu frontend hace doble-click, ambas requests llevan la misma key — perfecto. Si tu lambda regenera la key en cada invocación, perdiste la idempotencia.
- Persiste la key con el form data mientras el usuario llena el formulario. Así, si refresca, la próxima envío con el mismo contenido sigue siendo el mismo request.
- No reuses keys entre operaciones distintas.
create-lead-abcno debe usarse luego paracreate-customer-xyz.
¿GETs?
Los GET son ya idempotentes por definición HTTP — no necesitas el header. Lo ignoramos si lo mandas.
Idempotency-Key no basta para el dinero
La cabecera guarda la respuesta original 24 horas. Eso cubre el reintento inmediato: el doble clic, la red que se cae a mitad, el reintento automático de tu cliente HTTP.
No cubre el otro caso, que es el que duele:
El
POSTllegó y creó la venta. La respuesta se perdió por el camino. Tu proceso murió antes de anotar nada. Tres horas después, un barrido de reconciliación ve una orden sin venta en Finova y la vuelve a registrar.
Ahí la clave ya caducó, o el nuevo proceso genera una distinta, y el resultado es un movimiento contable duplicado: dinero contado dos veces, sin forma de distinguir la copia del original.
metadata.external_ref
Para eso está. En los recursos que mueven dinero — ingresos, ventas y recurrencias — mandas tu propia referencia dentro de metadata:
json
{ "data": { "…": "…", "metadata": { "external_ref": "ord_123" } } }Lleva un índice único por empresa. Si repites el alta con la misma referencia, recibes 200 con el recurso que ya existía en lugar de un segundo.
Idempotency-Key | metadata.external_ref | |
|---|---|---|
| Dónde va | cabecera | cuerpo, dentro de metadata |
| Cuánto dura | 24 h | para siempre |
| Qué compara | el cuerpo entero | sólo la referencia |
| Dónde se decide | caché compartida | índice único en la base |
| Aplica a | cualquier POST/PUT | ingresos, ventas, recurrencias |
El 200 en vez de 201 es la señal de «ya estaba». No es un error: para ti, «lo acabo de crear» y «ya existía» son el mismo desenlace bueno, y devolverte un 409 sólo te obligaría a escribir el camino de recuperación que este campo ya es.
Úsalos juntos
Idempotency-Key para el reintento inmediato, external_ref para el que llega tarde. No se estorban.
Elige bien la referencia
Tiene que ser estable y derivable de tus datos, no aleatoria: el reintento tiene que poder calcular la misma. Un SecureRandom.uuid nuevo en cada intento no protege de nada.
| Bien | Mal |
|---|---|
ord_123 (tu id de orden) | uuid generado en cada intento |
sub_abc:2026-10 (tu suscripción + periodo) | la marca de tiempo del intento |