Guías
Concurrencia y bloqueo optimista
Cada lectura de detalle devuelve un ETag. Envíalo de vuelta en la cabecera If-Match de 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.
ETag e If-Match
Enlace a esta sección: ETag e If-MatchGET /api/v1 /<recurso> /{id} 200con unETagfuertePATCH/PUT/DELETE/ acción conIf-Match: <ETag>La comparación y la escritura se ejecutan bajo un bloqueo por recurso.
200: nada cambió desde que lo leísteSi el recurso cambió entretanto
412 precondition_failed, antes de hacer nada: vuelve a leerlo y reintenta409 conflict(lock_timeout) si la escritura esperó más de 5 s al bloqueo: reintentaIf-Match: *solo exige que el recurso exista. Sin la cabecera, la escritura siempre se ejecuta.
El ETag es una etiqueta fuerte: un hash de la representación, así que cambia siempre que cambia algo que puedes ver en el registro. El flujo tiene cuatro pasos.
Leer el registro
Llama a la lectura de detalle (por ejemplo
GET /api) y conserva la cabecera de respuesta/v1 /contacts /{contactId} ETag.Cámbialo en tu código
Calcula los nuevos valores a partir de lo que has leído.
Escribir con If-Match
Envía el PATCH, PUT, DELETE o la acción (como
POST /api) con/v1 /orders /{orderId} /reserve If-Match: <the ETag you read>.Gestionar la respuesta
Si tiene éxito, una operación que devuelve el registro te da su nuevo
ETagen la respuesta. Con un 412precondition_failed, no se escribió nada: lee el registro de nuevo, rehaz el cambio y reintenta.
La comparación y la escritura se ejecutan bajo un bloqueo por recurso, así que dos escrituras concurrentes con la misma etiqueta terminan en 200 y 412, exactamente como si hubieran llegado una tras otra. Una escritura que espera más de cinco segundos a ese bloqueo recibe 409 conflict con lock_timeout en errors[0]., y se puede reintentar.
- Sin
If-Matchla escritura siempre se realiza: gana la última escritura. If-Match: *solo exige que el recurso exista. No protege frente a cambios concurrentes.- Las expansiones que pides en una lectura, como el historial de etapas de una oportunidad, no forman parte de la etiqueta. La etiqueta de una lectura expandida sigue coincidiendo.
BASE="${FILMO_BASE_URL:-https://app.filmo.io}"
# CONTACT_ID: the id of a contact in your account
# 1. Read the contact and keep its ETag.
ETAG=$(curl -sS -D - -o contact.json "$BASE/api/v1/contacts/$CONTACT_ID" \
-H "Authorization: Bearer $FILMO_API_KEY" | awk 'tolower($1)=="etag:" {print $2}' | tr -d '\r')
# 2. Write with If-Match. HTTP 412 means someone changed the contact since step 1.
curl -sS -w '\nHTTP %{http_code}\n' -X PATCH "$BASE/api/v1/contacts/$CONTACT_ID" \
-H "Authorization: Bearer $FILMO_API_KEY" \
-H "Content-Type: application/json" \
-H "If-Match: $ETAG" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"position":"Rental manager"}'Qué operaciones aceptan If-Match
Enlace a esta sección: Qué operaciones aceptan If-MatchLo aceptan las escrituras sobre un recurso que tiene lectura de detalle. La tabla enumera todas las operaciones que declaran la cabecera.
Las escrituras sobre recursos que aún no tienen lectura de detalle no aceptan If-Match: etiquetas, notas, indisponibilidades, colecciones, asignaciones del planificador y ejecuciones de workflows. Tampoco lo acepta el lote de devoluciones por línea, porque abarca varios pedidos. En una creación (un POST a una colección) la cabecera no es una precondición: solo If-Match: * pasa, y cualquier otro valor responde 412. En esos casos, protégete de los duplicados con una clave de idempotencia y lee antes de escribir.
Qué significa cada respuesta
Enlace a esta sección: Qué significa cada respuesta| Respuesta | Significado | Qué hacer |
|---|---|---|
412 precondition_failed | El ETag que enviaste ya no coincide. No se escribió nada. | Lee el registro de nuevo, rehaz el cambio sobre la versión nueva y reintenta. |
409 conflict | errors[0]. dice por qué. lock_timeout: la escritura esperó más de cinco segundos al bloqueo por recurso. in_progress: una petición con el mismo Idempotency-Key sigue en ejecución. invalid_state: el estado del registro prohíbe la operación. | Reintenta lock_timeout e in_progress con espera. Para invalid_state, lee el registro y comprueba su estado; esperar no ayuda. Consulta las decisiones de reintento en la guía de errores. |
404 not_found | El registro no existe o queda fuera del aislamiento de tu clave. | Comprueba el id. |
Un bucle de leer-modificar-escribir
Enlace a esta sección: Un bucle de leer-modificar-escribirLas listas en un cuerpo PATCH suelen reemplazar la lista entera. Este bucle añade un número de teléfono a un contacto con PATCH /api: lee el contacto, añade el número, escribe con If-Match y vuelve a empezar ante un 412 o ante un 409 lock_timeout. Pon en CONTACT_ID un contacto de tu cuenta.
import { randomUUID } from "node:crypto";
const base = process.env.FILMO_BASE_URL ?? "https://app.filmo.io";
const auth = { Authorization: `Bearer ${process.env.FILMO_API_KEY}` };
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
// Adds a phone number without overwriting a change someone else made in between.
export async function addPhone(contactId, number) {
const url = base + "/api/v1/contacts/" + contactId;
for (let attempt = 0; attempt < 5; attempt++) {
const read = await fetch(url, { headers: auth });
if (!read.ok) throw new Error("Read failed: HTTP " + read.status);
const etag = read.headers.get("ETag");
const { data } = await read.json();
if (data.phoneNumbers.some((p) => p.number === number)) return data;
// phoneNumbers replaces the whole list, so send the existing entries back too.
const phoneNumbers = data.phoneNumbers.map((p) => ({
number: p.number,
isPrimary: p.isPrimary,
...(p.label ? { label: p.label } : {}),
...(p.countryCode ? { countryCode: p.countryCode } : {}),
}));
phoneNumbers.push({ number, isPrimary: false });
const write = await fetch(url, {
method: "PATCH",
headers: {
...auth,
"Content-Type": "application/json",
"If-Match": etag,
// A new key per attempt: each attempt sends a different body.
"Idempotency-Key": randomUUID(),
},
body: JSON.stringify({ phoneNumbers }),
});
if (write.ok) return (await write.json()).data;
const failure = await write.text();
const cause = write.status === 409 ? JSON.parse(failure).errors?.[0]?.code : null;
// 412: the contact changed since the read. 409 "lock_timeout": another write held the lock for 5 seconds.
if (write.status === 412 || cause === "lock_timeout") {
await sleep(100 * 2 ** attempt);
continue;
}
throw new Error("Write failed: HTTP " + write.status + " " + failure);
}
throw new Error("Too many concurrent changes; try again later");
}
const contact = await addPhone(process.env.CONTACT_ID, "+34 600 000 000");
console.log(contact.phoneNumbers.length, "phone numbers");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 add_phone(contact_id, number):
"""Adds a phone number without overwriting a change made in between."""
url = BASE + "/api/v1/contacts/" + contact_id
for attempt in range(5):
read = requests.get(url, headers=AUTH, timeout=30)
read.raise_for_status()
etag = read.headers["ETag"]
data = read.json()["data"]
if any(p["number"] == number for p in data["phoneNumbers"]):
return data
# phoneNumbers replaces the whole list, so send the existing entries back too.
phone_numbers = []
for p in data["phoneNumbers"]:
entry = {"number": p["number"], "isPrimary": p["isPrimary"]}
if p.get("label"):
entry["label"] = p["label"]
if p.get("countryCode"):
entry["countryCode"] = p["countryCode"]
phone_numbers.append(entry)
phone_numbers.append({"number": number, "isPrimary": False})
write = requests.patch(
url,
json={"phoneNumbers": phone_numbers},
headers={
**AUTH,
"If-Match": etag,
# A new key per attempt: each attempt sends a different body.
"Idempotency-Key": str(uuid.uuid4()),
},
timeout=30,
)
if write.ok:
return write.json()["data"]
# 412: the contact changed since the read. 409 "lock_timeout": another write held the lock for 5 seconds.
cause = None
if write.status_code == 409:
cause = ((write.json().get("errors") or [{}])[0]).get("code")
if write.status_code == 412 or cause == "lock_timeout":
time.sleep(0.1 * 2**attempt)
continue
raise RuntimeError(f"Write failed: HTTP {write.status_code} {write.text}")
raise RuntimeError("Too many concurrent changes; try again later")
contact = add_phone(os.environ["CONTACT_ID"], "+34 600 000 000")
print(len(contact["phoneNumbers"]), "phone numbers")Lecturas condicionales
Enlace a esta sección: Lecturas condicionalesLas lecturas de detalle, y GET /api, también aceptan If-None-Match. Envía el ETag de la copia que ya tienes: si sigue vigente, la respuesta es 304 sin cuerpo y con la cabecera ETag, y tu copia está al día. Si el registro cambió, recibes el 200 completo con su nuevo ETag.
BASE="${FILMO_BASE_URL:-https://app.filmo.io}"
# CONTACT_ID: the id of a contact in your account
# ETAG: the ETag header of an earlier read of that contact, for example
ETAG=$(curl -sS -D - -o /dev/null "$BASE/api/v1/contacts/$CONTACT_ID" \
-H "Authorization: Bearer $FILMO_API_KEY" | awk 'tolower($1)=="etag:" {print $2}' | tr -d '\r')
# Send the ETag you already have. HTTP 304 means your copy is current.
curl -sS -o /dev/null -w 'HTTP %{http_code}\n' "$BASE/api/v1/contacts/$CONTACT_ID" \
-H "Authorization: Bearer $FILMO_API_KEY" \
-H "If-None-Match: $ETAG"Un 304 ahorra ancho de banda, no cuota: cuenta con que computa contra tu límite, como las demás respuestas de una clave autenticada (consulta Límites de peticiones).
Las herramientas de detalle devuelven la etiqueta en meta.. Pásala como ifMatch en una escritura sobre ese recurso para obtener el mismo comportamiento 412. Omítela para escribir sin condiciones.
Páginas relacionadas
Enlace a esta sección: Páginas relacionadasGuías
Idempotencia y reintentos seguros
Envía una cabecera
Idempotency-Keycon 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.Guí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
Respuestas
Las respuestas correctas envuelven su contenido en
data; las listas añadenpagination; algunas operaciones añadenmeta; los errores son problemas RFC 9457. Las respuestas de la API comparten un mismo conjunto de cabeceras.
¿Te ha resultado útil esta página?