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.

CuotaLímiteSe aplica a
Global120 peticiones por minuto, ventana fijaTodas las rutas, contadas por clave de API.
Disponibilidad30 peticiones por minuto, ventana fijaPOST /api/v1/availability/check, POST /api/v1/staff/availability y POST /api/v1/vehicles/availability, 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_scope no 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_error en 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.

Toda 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.

CabeceraSignificado
RateLimit-LimitPeticiones permitidas por minuto en esa cuota.
RateLimit-RemainingPeticiones restantes en la ventana actual de un minuto.
RateLimit-ResetSegundos hasta que se reinicie la ventana.
Retry-AfterSolo en el 429: segundos hasta la siguiente ventana. El problema del 429 lo repite en retryAfter.

La 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.rateLimit.

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_5f1c2f4e3a5b4f6d8e7a0b1c2d3e4f5a
  1. Deja de enviar peticiones con esa cuota

  2. Espera Retry-After segundos

  3. Reintenta la misma llamada con la misma Idempotency-Key

    La misma clave evita que se duplique una escritura reintentada si un intento anterior sí llegó.

  4. 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.

  • 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.

Cada 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/v1/account.

x-rateLimit en una comprobación de disponibilidad
{ "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.

Este 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));
}

Centro de ayuda: Límites de peticiones explicados.

  • Guías

    Errores

    Los errores son documentos de problema RFC 9457. Ramifica según el code estable, 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 limit y, mientras pagination.hasMore sea true, vuelve a llamar con el nextCursor que 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?