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.
Las cuotas
Enlace a esta sección: Las cuotas| Cuota | Límite | Se aplica a |
|---|---|---|
| Global | 120 peticiones por minuto, ventana fija | Todas las rutas, contadas por clave de API. |
| Disponibilidad | 30 peticiones por minuto, ventana fija | POST /api, POST /api y POST /api, que comparten una cuota. Cuenta además de la global, porque son las consultas más costosas: cada una de estas peticiones también usa una de las 120 por minuto. |
- Los límites son los mismos para todos los planes.
- Un 401 y un 403
insufficient_scopeno consumen cuota; las demás respuestas de una clave autenticada sí. - OAuth tiene sus propios límites, por dirección IP: 5 peticiones por minuto para registrar un cliente y 120 para el endpoint de tokens. Consulta OAuth.
- Si falla el almacén que cuenta las peticiones, la API responde 500
internal_erroren lugar de saltarse el límite: reintenta con espera creciente. - La cuota pertenece a la clave, no al proceso: cada worker y cada servidor que usa la misma clave gasta las mismas 120.
- Cuenta con que las llamadas de disponibilidad cuentan contra ambas cuotas.
- Una consulta de disponibilidad por lotes admite como máximo 100 elementos. Agrupa los elementos en una sola llamada en lugar de hacer muchas.
- Las herramientas MCP se ejecutan con la misma clave y los mismos límites. Consulta Límites de MCP.
Cabeceras
Enlace a esta sección: CabecerasToda respuesta que consumió cuota, que es toda respuesta salvo un 401 y un 403 insufficient_scope, correcta o no, lleva las cabeceras de límite de peticiones del IETF. Reflejan la más ajustada de las cuotas que consumió la petición.
| Cabecera | Significado |
|---|---|
RateLimit-Limit | Peticiones permitidas por minuto en esa cuota. |
RateLimit-Remaining | Peticiones restantes en la ventana actual de un minuto. |
RateLimit-Reset | Segundos hasta que se reinicie la ventana. |
Retry-After | Solo en el 429: segundos hasta la siguiente ventana. El problema del 429 lo repite en retryAfter. |
Cuando superas un límite
Enlace a esta sección: Cuando superas un límiteLa respuesta es 429 rate_limited, un documento de problema con Retry-After y las cabeceras RateLimit-*, que también indican qué cuota se ha agotado. El cuerpo lleva la misma espera en retryAfter (segundos), y detail también la indica. Por MCP no hay cabeceras: el problema va en el resultado de la herramienta, con retryAfter, y el estado de la cuota está en meta..
HTTP/1.1 429 Too Many Requests
Content-Type: application/problem+json
Retry-After: 23
RateLimit-Limit: 120
RateLimit-Remaining: 0
RateLimit-Reset: 23
X-Request-Id: req_5f1c2f4e3a5b4f6d8e7a0b1c2d3e4f5aDeja de enviar peticiones con esa cuota
Espera
Retry-AftersegundosReintenta la misma llamada con la misma Idempotency-Key
La misma clave evita que se duplique una escritura reintentada si un intento anterior sí llegó.
Espera más tiempo entre intentos si sigue ocurriendo
Aumenta la espera de forma exponencial y añade jitter, para que varios workers no reintenten al unísono, y baja tu ritmo estable.
El helper de referencia en Errores ya hace esto.
Presupuesta tus llamadas
Enlace a esta sección: Presupuesta tus llamadas- Lee en páginas grandes. Con el tamaño máximo de página de 500, 10,000 registros se leen en 20 peticiones; una lectura de detalle por registro serían 10,000 peticiones, más de 80 minutos a 120 por minuto.
- No vuelvas a leer lo que no ha cambiado. En las listas que declaran
updatedFrom, filtra por tiempo y lee solo el delta: consulta Sincronizar datos. - Guarda en caché los datos que cambian poco, como la cuenta y el catálogo.
- Deja margen. Apunta al 80 por ciento del límite en régimen estable, para que una tormenta de reintentos o un segundo worker no te hagan pasarte.
- Comparte un limitador entre los workers que usan una clave, o da a cada integración su propia clave.
- Llama a las comprobaciones de disponibilidad desde tu servidor, no una vez por cada visitante de una página web. Guarda sus respuestas en caché durante poco tiempo, agrupa hasta 100 elementos por consulta y, cuando recibas un 429, muestra una alternativa como una petición de presupuesto en lugar de un error.
Leer los límites desde la API
Enlace a esta sección: Leer los límites desde la APICada operación publica sus cuotas en la extensión x-rateLimit del documento OpenAPI, y los límites de la clave que usas están en rateLimits, devuelto por GET /api.
{ "global": { "perMinute": 120 }, "availability": { "perMinute": 30 } }Una operación fuera del grupo de disponibilidad lista solo global. Las herramientas pueden leer estas extensiones para dimensionar sus colas.
Un cliente que respeta las cabeceras
Enlace a esta sección: Un cliente que respeta las cabecerasEste limitador mantiene un cubo para la cuota global y otro para las consultas de disponibilidad; una consulta de disponibilidad espera en ambos y cuenta contra ambos. Espera cuando RateLimit-Remaining llega a cero hasta que ha pasado RateLimit-Reset, serializa las peticiones de un cubo y, ante un 429, duerme durante Retry-After antes de volver a intentarlo. No reintenta escrituras de forma segura por sí solo: combínalo con el helper de la guía de Errores, que conserva la Idempotency-Key.
// limiter.mjs: spaces calls out using the RateLimit-* headers. Node 22+.
const BASE = process.env.FILMO_BASE_URL ?? "https://app.filmo.io";
const AVAILABILITY = ["/api/v1/availability/check", "/api/v1/staff/availability", "/api/v1/vehicles/availability"];
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
class Bucket {
remaining = Infinity; // from RateLimit-Remaining
resetAt = 0; // when the window resets, from RateLimit-Reset
tail = Promise.resolve(); // one request at a time per bucket
async wait() {
const pause = this.resetAt - Date.now();
if (this.remaining <= 0 && pause > 0) await sleep(pause);
}
update(headers) {
const remaining = Number(headers.get("ratelimit-remaining"));
const reset = Number(headers.get("ratelimit-reset"));
if (!headers.has("ratelimit-remaining") || Number.isNaN(remaining) || Number.isNaN(reset)) return;
this.remaining = remaining;
this.resetAt = Date.now() + reset * 1000;
}
}
// The key has 120/min for everything. The three availability checks also count against
// their own 30/min quota, so they consume both buckets.
const buckets = { global: new Bucket(), availability: new Bucket() };
export function filmoCall(method, path, body) {
const isAvailability = AVAILABILITY.includes(path);
const used = isAvailability ? [buckets.global, buckets.availability] : [buckets.global];
const bucket = buckets.global;
const run = Promise.all(used.map((b) => b.tail)).then(async () => {
for (let attempt = 0; ; attempt++) {
for (const b of used) await b.wait();
const res = await fetch(BASE + path, {
method,
headers: {
Authorization: `Bearer ${process.env.FILMO_API_KEY}`,
...(body ? { "Content-Type": "application/json" } : {}),
},
body: body ? JSON.stringify(body) : undefined,
});
// The headers describe the tighter quota. For an availability check that is
// usually its own; the global bucket just counts the call it consumed.
if (isAvailability) {
buckets.availability.update(res.headers);
buckets.global.remaining -= 1;
} else {
bucket.update(res.headers);
}
if (res.status !== 429 || attempt >= 5) return res;
// Over the limit: wait for Retry-After (seconds), then try the same call again.
await sleep((Number(res.headers.get("retry-after")) || 1) * 1000);
}
});
for (const b of used) b.tail = run.then(() => {}, () => {});
return run;
}
// Example: 300 reads in a row never trigger a 429.
for (let i = 0; i < 300; i++) {
const res = await filmoCall("GET", "/api/v1/orders?limit=1");
if (!res.ok) throw new Error(String(res.status));
}# limiter.py: spaces calls out using the RateLimit-* headers. Python 3 with requests.
import os
import threading
import time
from contextlib import ExitStack
import requests
BASE = os.environ.get("FILMO_BASE_URL", "https://app.filmo.io")
AVAILABILITY = {"/api/v1/availability/check", "/api/v1/staff/availability", "/api/v1/vehicles/availability"}
class Bucket:
def __init__(self):
self.remaining = float("inf") # from RateLimit-Remaining
self.reset_at = 0.0 # when the window resets, from RateLimit-Reset
self.lock = threading.Lock() # one request at a time per bucket
def wait(self):
pause = self.reset_at - time.time()
if self.remaining <= 0 and pause > 0:
time.sleep(pause)
def update(self, headers):
try:
self.remaining = int(headers["RateLimit-Remaining"])
self.reset_at = time.time() + int(headers["RateLimit-Reset"])
except (KeyError, ValueError):
pass
# The key has 120/min for everything. The three availability checks also count against
# their own 30/min quota, so they consume both buckets.
BUCKETS = {"global": Bucket(), "availability": Bucket()}
def filmo_call(method, path, body=None):
is_availability = path.split("?")[0] in AVAILABILITY
used = [BUCKETS["global"], BUCKETS["availability"]] if is_availability else [BUCKETS["global"]]
with ExitStack() as stack:
for b in used:
stack.enter_context(b.lock)
for attempt in range(6):
for b in used:
b.wait()
res = requests.request(
method,
BASE + path,
json=body,
headers={"Authorization": f"Bearer {os.environ['FILMO_API_KEY']}"},
timeout=30,
)
# The headers describe the tighter quota. For an availability check that is
# usually its own; the global bucket just counts the call it consumed.
if is_availability:
BUCKETS["availability"].update(res.headers)
BUCKETS["global"].remaining -= 1
else:
BUCKETS["global"].update(res.headers)
if res.status_code != 429 or attempt == 5:
return res
# Over the limit: wait for Retry-After (seconds), then try the same call again.
time.sleep(float(res.headers.get("Retry-After") or 1))
# Example: 300 reads in a row never trigger a 429.
for _ in range(300):
filmo_call("GET", "/api/v1/orders?limit=1").raise_for_status()Centro de ayuda: Límites de peticiones explicados.
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
Paginación
Las listas devuelven una página cada vez. Pide una página con
limity, mientraspagination.seahasMore true, vuelve a llamar con elnextCursorque te dieron.Guías
Sincronizar datos
Filmo v1 no envía webhooks, así que mantienes otro sistema sincronizado consultando periódicamente: lee lo que cambió en ventanas de tiempo semiabiertas y ejecuta un barrido aparte para encontrar lo que se eliminó. No sigas el cursor de paginación para decidir qué cambió o qué desapareció.
Guías
Límites y contexto
El catálogo es grande. Elige permisos para que el cliente liste solo las herramientas que necesita y pide páginas pequeñas.
¿Te ha resultado útil esta página?