Guías
Idempotencia y reintentos seguros
Envía una cabecera Idempotency-Key con cada escritura y reutiliza el mismo valor cuando reintentes esa operación: Filmo devuelve el resultado almacenado del primer intento en lugar de escribir dos veces.
Un timeout de red no te dice si la petición llegó. Reintentar una creación a ciegas puede crear el registro dos veces; no reintentar puede dejarlo sin crear. Tú eliges la clave, así que puedes reintentar tantas veces como necesites y obtener exactamente un efecto.
Cómo funciona la clave
Enlace a esta sección: Cómo funciona la clavePOSTconIdempotency-Key: KUna clave por operación de negocio, en el primer intento y en cada reintento de esa misma operación.
Ejecuta y guarda la respuesta
El reintento, dentro de 24 h
Misma clave
K, mismo cuerpoLa respuesta guardada,
Idempotent-Replay: true, sin ejecutar nada otra vezTambién cuando el primer intento falló después de confirmar la escritura: ese error se guarda y se repite.
Misma clave
K, cuerpo distinto422 idempotency_key_reused, el manejador no se ejecutaMisma clave
Kmientras el primer intento sigue en curso409 conflict(errors[0].:code in_progress): espera y reintentaPasadas 24 h la clave caduca: es una petición nueva.
Pon un solo valor en la cabecera Idempotency-Key (hasta 255 caracteres; sirve un UUID) en el primer intento y en cada reintento de esa misma operación. Una clave identifica una operación, nunca un lote de ellas.
| Situación | Qué hace Filmo | Lo que haces |
|---|---|---|
| Misma clave, mismo cuerpo, en 24 horas | Devuelve la respuesta guardada del primer intento sin ejecutar nada de nuevo, con la cabecera Idempotent-Replay: true. | Trátala como el resultado de la operación. Esta es la respuesta que buscabas. |
| Misma clave, cuerpo distinto | Responde 422 idempotency_key_reused. El handler nunca se ejecuta. El método, la ruta y el cuerpo en bruto forman parte de la huella de la primera petición, así que reordenar las claves JSON o enviar la misma clave a otra ruta también responde 422. | Corrige el cliente: reutilizar una clave para otra cosa casi siempre es un error. Usa una clave nueva solo para una operación realmente distinta. |
| Misma clave mientras el primer intento sigue en curso | Responde 409 conflict, con in_progress en errors[0].. | Espera y reintenta con la misma clave. No envíes un segundo intento en paralelo. |
| El primer intento falló antes de escribir nada | Libera la clave. El reintento se ejecuta de nuevo. | Reintenta con la misma clave. |
| El primer intento falló después de confirmarse la escritura (no se pudo construir la respuesta) | Guarda ese error y lo reproduce como cualquier otra respuesta. El reintento no escribe dos veces. | Deja de reintentar y lee el recurso para ver en qué estado está. |
| Más de 24 horas después | La clave ha caducado y la petición es nueva. | Si necesitas una garantía de más de 24 horas, usa otra clave y comprueba antes por tu lado. |
Qué operaciones lo aceptan
Enlace a esta sección: Qué operaciones lo aceptanSolo las operaciones que escriben: 93 de ellas, que cubren toda creación, actualización, borrado y acción. Las lecturas ignoran la cabecera. También los POST de consulta por cuerpo, como POST /api y POST /api: son POST porque la pregunta no cabe en una cadena de consulta, no porque cambien nada. Cada operación que acepta la clave declara la cabecera en su propio contrato, así que un cliente generado ya la conoce.
Una respuesta repetida no es una segunda ejecución, así que no emite eventos: un Idempotency-Key repetido no vuelve a iniciar los workflows de la cuenta. Aun así, la clave no convierte todas las operaciones en no-ops. No cuentes con que archivar de nuevo sea un no-op: archivar un pedido o un proyecto que ya está archivado, con una clave nueva, vuelve a mover su updatedAt y vuelve a disparar el evento de archivado, aunque la descripción de la operación prometa que no hay ningún efecto adicional. Comprueba el estado antes de archivar dos veces.
Cuatro operaciones que necesitan tu propia clave
Enlace a esta sección: Cuatro operaciones que necesitan tu propia claveEn la mayoría de las escrituras, reintentar sin clave es solo una molestia. En estas cuatro duplica algo que no se puede deshacer, así que envía siempre una clave que hayas generado y guardado, y reenvía la misma si reintentas.
| Operación | Qué duplica un reintento a ciegas |
|---|---|
POST /api | La devolución: el mismo material vuelve dos veces al conjunto. |
POST /api | El stock: newStockReceived registra inventario nuevo cada vez. |
POST /api | El pago: se registra dos veces. |
POST /api | La ejecución: se encola una segunda ejecución en lugar de devolver la primera. |
Generar y guardar claves
Enlace a esta sección: Generar y guardar claves- Genera un UUID por cada operación de negocio (por ejemplo, uno por cada registro que vayas a crear), no uno por cada intento HTTP.
- Guarda la clave junto a tu propio registro antes del primer intento. Si tu proceso se cae entre la petición y la respuesta, la siguiente ejecución debe reutilizar la misma clave.
- Reutiliza la clave en cada reintento de esa operación, con exactamente el mismo cuerpo. Un reintento con el cuerpo editado es un 422.
- No generes nunca la clave dentro del bucle de reintentos.
- Recuerda la ventana de 24 horas. Una cola que pueda reintentar pasado un día debe leer antes el recurso en lugar de fiarse de la clave.
Crear un registro con reintentos seguros
Enlace a esta sección: Crear un registro con reintentos segurosEste ejemplo crea un contacto y reintenta ante fallos de red, 409, 429, 500 y 502, 503 o 504 (un fallo de pasarela sin documento de problema, tratado como un 500) con la misma clave, espera Retry-After cuando la respuesta lo lleva, y se detiene cuando vuelve un 500 marcado Idempotent-Replay: true o un 409 notifica invalid_state.
BASE="${FILMO_BASE_URL:-https://app.filmo.io}"
KEY="$(uuidgen)" # store this with your own record BEFORE the first attempt
for attempt in 1 2 3 4; do
rm -f headers.txt
status=$(curl -sS -o response.json -D headers.txt -w '%{http_code}' \
-X POST "$BASE/api/v1/contacts" \
-H "Authorization: Bearer $FILMO_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $KEY" \
-d '{"name":"Acme Rentals","contactType":"company","role":"client","email":"hello@example.com"}') || status=000
case "$status" in
2??) break ;;
500)
if grep -qi '^idempotent-replay: true' headers.txt; then
echo "Stored 500: stop and read the resource before doing anything else" >&2
exit 1
fi ;;
409)
# in_progress or lock_timeout: wait and retry. invalid_state never clears by waiting.
if grep -q '"invalid_state"' response.json; then
echo "Conflict with the state of the resource: read it before doing anything else" >&2
cat response.json >&2
exit 1
fi ;;
000|429|502|503|504) ;; # 502, 503, 504 without a problem document: treat like a 500, same key
*) echo "Not retryable: HTTP $status" >&2; cat response.json >&2; exit 1 ;;
esac
wait=$(grep -i '^retry-after:' headers.txt 2>/dev/null | tr -dc '0-9')
sleep "${wait:-$((2 ** attempt))}"
done
case "$status" in
2??) cat response.json ;;
*) echo "Gave up (last status $status): read the resource before retrying" >&2; exit 1 ;;
esacimport { randomUUID } from "node:crypto";
const base = process.env.FILMO_BASE_URL ?? "https://app.filmo.io";
const sleep = (seconds) => new Promise((resolve) => setTimeout(resolve, seconds * 1000));
// `key` is one UUID per business operation, stored with your own record
// before the first attempt and reused on every retry of that operation.
export async function createContact(body, key) {
for (let attempt = 0; attempt < 4; attempt++) {
let res;
try {
res = await fetch(base + "/api/v1/contacts", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.FILMO_API_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": key,
},
body: JSON.stringify(body),
});
} catch {
await sleep(2 ** attempt); // network failure: retry with the SAME key
continue;
}
if (res.ok) {
const replayed = res.headers.get("Idempotent-Replay") === "true";
return { contact: (await res.json()).data, replayed };
}
if (res.status === 500 && res.headers.get("Idempotent-Replay") === "true") {
throw new Error("Stored 500: stop and read the resource before doing anything else");
}
if (res.status === 409) {
const problem = await res.json();
// "in_progress" and "lock_timeout" clear by waiting; "invalid_state" never does.
if (problem.errors?.[0]?.code === "invalid_state") {
throw new Error("Conflict with the state of the resource: " + problem.detail);
}
} else if (![429, 500, 502, 503, 504].includes(res.status)) {
throw new Error("Not retryable: HTTP " + res.status + " " + (await res.text()));
}
await sleep(Number(res.headers.get("Retry-After")) || 2 ** attempt);
}
throw new Error("Gave up: read the resource before retrying");
}
const { contact, replayed } = await createContact(
{ name: "Acme Rentals", contactType: "company", role: "client", email: "hello@example.com" },
randomUUID(),
);
console.log(contact.id, replayed ? "(replayed)" : "(created)");import os
import time
import uuid
import requests
BASE = os.environ.get("FILMO_BASE_URL", "https://app.filmo.io")
AUTH = {"Authorization": f"Bearer {os.environ['FILMO_API_KEY']}"}
def create_contact(body, key):
"""key: one UUID per business operation, stored before the first attempt."""
for attempt in range(4):
try:
res = requests.post(
BASE + "/api/v1/contacts",
json=body,
headers={**AUTH, "Idempotency-Key": key},
timeout=30,
)
except requests.RequestException:
time.sleep(2**attempt) # network failure: retry with the SAME key
continue
if res.ok:
return res.json()["data"], res.headers.get("Idempotent-Replay") == "true"
if res.status_code == 500 and res.headers.get("Idempotent-Replay") == "true":
raise RuntimeError("Stored 500: stop and read the resource before doing anything else")
if res.status_code == 409:
# "in_progress" and "lock_timeout" clear by waiting; "invalid_state" never does.
cause = ((res.json().get("errors") or [{}])[0]).get("code")
if cause == "invalid_state":
raise RuntimeError("Conflict with the state of the resource: " + res.json()["detail"])
elif res.status_code not in (429, 500, 502, 503, 504):
raise RuntimeError(f"Not retryable: HTTP {res.status_code} {res.text}")
time.sleep(float(res.headers.get("Retry-After", 2**attempt)))
raise RuntimeError("Gave up: read the resource before retrying")
contact, replayed = create_contact(
{"name": "Acme Rentals", "contactType": "company", "role": "client", "email": "hello@example.com"},
str(uuid.uuid4()),
)
print(contact["id"], "(replayed)" if replayed else "(created)")El 500 que se reproduce durante 24 horas
Enlace a esta sección: El 500 que se reproduce durante 24 horasUn 500 puede ocurrir antes de la escritura o después. Si ocurrió después de confirmar la escritura, Filmo guarda ese 500 y lo repite durante 24 horas para esa clave. Reintentar con espera exponencial devuelve entonces el mismo 500 cada vez, y el registro ya existe.
La señal es la cabecera Idempotent-Replay: true en un 500. Cuando la veas, deja de reintentar y lee el recurso. Si el reintento de un 500 vuelve sin esa cabecera, el primer intento falló antes de escribir y el reintento se ha ejecutado de verdad.
Para leer el recurso tras una creación fallida, búscalo con una lista filtrada por una ventana de creación o por tu propia referencia, o léelo por id si ya lo tienes. No lo vuelvas a crear nunca con una clave nueva antes de haber buscado.
Un 500 en una escritura sin clave
Sin una Idempotency-Key, nada protege una escritura reintentada de crear un duplicado. Lee antes de reintentar.
Tabla de decisión de reintentos
Enlace a esta sección: Tabla de decisión de reintentosUna regla por código de estado no basta: el mismo 409 puede significar espera y reintenta o no reintentes nunca, y un 500 puede repetirse durante 24 horas. La tabla de decisión, con el ayudante de reintentos de referencia, está en Errores. Para los casos que dependen de la clave, en resumen:
- 409
conflictconin_progressenerrors[0].: tu clave sigue en ejecución. Espera y reintenta con la misma clave.code - 409
conflictconlock_timeout: otra escritura condicional retuvo el recurso durante más de cinco segundos. Reintenta con espera creciente. - 409
conflictconinvalid_state: el estado del recurso prohíbe la operación. No reintentes; lee el recurso. - 500
internal_errorconIdempotent-Replay: true: detente y lee el recurso. - 422
idempotency_key_reused: un error del cliente. Usa una clave nueva solo para una operación realmente distinta.
Las lecturas no necesitan clave. Reintenta libremente, dentro de los límites descritos en la guía de límites de peticiones.
Las herramientas de escritura aceptan un argumento idempotencyKey con las mismas reglas. Si lo omites, nada protege un reintento, salvo en cuatro herramientas en las que el servidor genera una clave y la devuelve en meta., tanto en caso de éxito como de error; reenvía ese valor cuando reintentes. Aun así, no puede proteger una llamada cuya respuesta nunca te llegó, así que para filmo_orders_line_items_return, filmo_purchase_orders_receive, filmo_treasury_payments_create y filmo_workflows_run (las cuatro que generan una) envían siempre tu propio UUID, y envían uno en cualquier otra herramienta de escritura también.
Donde REST envía Idempotent-Replay: true, el resultado de una herramienta lleva meta. cuando es la respuesta guardada de una llamada anterior con la misma clave. Si una escritura falla con internal_error y el resultado es una repetición, detente y lee el recurso, igual que con un 500 guardado por REST.
Páginas relacionadas
Enlace a esta sección: Páginas relacionadasGuías
Errores
Los errores son documentos de problema RFC 9457. Ramifica según el
codeestable, nunca según el texto, y usa la tabla de reintentos para decidir si volver a intentarlo.Guías
Límites de peticiones
Cada clave de API tiene 120 peticiones por minuto, y las tres comprobaciones de disponibilidad comparten su propia cuota de 30 por minuto. Al pasar un límite, la API responde 429 con
Retry-After.Guías
Concurrencia y bloqueo optimista
Cada lectura de detalle devuelve un
ETag. Envíalo de vuelta en la cabeceraIf-Matchde una escritura y Filmo rechaza la escritura con 412 si el registro ha cambiado desde que lo leíste, de modo que dos integraciones no puedan sobrescribirse en silencio.Guías
Lista de comprobación de salida a producción
Repasa estas ocho comprobaciones antes de que tu integración funcione con datos de producción. Cada una remite a la guía que la explica.
¿Te ha resultado útil esta página?