*API REST › 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

| 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/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`](https://docs.filmo.io/es/rest-api/errors/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](https://docs.filmo.io/es/rest-api/guides/oauth#rate-limits).
- Si falla el almacén que cuenta las peticiones, la API responde 500 [`internal_error`](https://docs.filmo.io/es/rest-api/errors/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](https://docs.filmo.io/es/mcp/guides/limits-and-context).

## Cabeceras

Toda respuesta que consumió cuota, que es toda respuesta salvo un 401 y un 403 [`insufficient_scope`](https://docs.filmo.io/es/rest-api/errors/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

La respuesta es 429 [`rate_limited`](https://docs.filmo.io/es/rest-api/errors/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
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](https://docs.filmo.io/es/rest-api/guides/errors#retry-helper) ya hace esto.

## 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](https://docs.filmo.io/es/rest-api/guides/syncing-data).
- 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

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`](https://docs.filmo.io/es/rest-api/objects/account#response-rateLimits), devuelto por [`GET /api/v1/account`](https://docs.filmo.io/es/rest-api/endpoint-reference/account/get-the-account).

*x-rateLimit en una comprobación de disponibilidad*

```json
{ "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

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

*Un limitador guiado por las cabeceras*

**Node**

```javascript
// 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));
}
```

**Python**

```python
# 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](https://www.filmo.io/es/help/reference/integraciones-y-api/limites-de-uso-explicados).

---

URL canónica: https://docs.filmo.io/es/rest-api/guides/rate-limits  
Última actualización: 2026-10-09