# Rate limits

Limits apply to calls that start work. Reading the model list is limited per address; polling never counts against your plan.

## Starting generations

Each `POST /v1/generations` counts against three limits at once:

| Account | Per key, per minute | Per account, per minute | Per account, per day |
| - | - | - | - |
| No plan | 20 | 40 | 2,500 |
| Any paid plan | 60 | 120 | 10,000 |
| Paid plan with $100 or more of top-up credit | 120 | 240 | 25,000 |

- A top-up balance of $100 or more moves an account up one row, with or without a plan.
- Minutes are fixed windows; the day resets at midnight UTC.
- A retry that returns an existing generation (same `Idempotency-Key`) does not count.

## Viral video library

Calls to the [viral video library](https://docs.vidlab.ai/viral-videos) also count against the library's own limits, per account per minute: 20 searches (the trending feed included), 40 full-video reads and 30 saves.

## What never counts

- Polling and reading: `GET /v1/generations/{id}`, the `/v1/jobs/...` aliases, `/content` and `GET /v1/generations`.
- Requests refused before they reach the limiter (bad input, unknown model).

One safety ceiling still applies to reads: 1,200 a minute per key (20 a second), across `GET /v1/generations/{id}`, the `/v1/jobs/...` aliases, `/content`, `GET /v1/generations` and `GET /v1/balance`. Past it, reads answer `429 read_rate_limited` with `Retry-After`. Polling each generation every few seconds stays far below it; to watch many at once, read them together with `GET /v1/generations`.

## Public endpoints

Counted per IP address, per minute:

| Endpoints | Limit |
| - | - |
| `GET /v1/models`, `GET /v1/models/{id}`, `GET /v1/status`, `GET /v1/open/catalog`, `GET /v1/balance` | 120 |
| `POST /v1/estimate`, `POST /v1/open/estimate` | 60 |
| `POST /v1/generations/quote` | 120 |

Past the limit they answer `429 rate_limited` with `Retry-After`. Model list answers can be cached for 60 seconds (status for 30), so you rarely need to ask more often.

## The 429 answer

A submit over its limit answers `429` with the limit that was hit, what was counted, and when it resets:

```json
{
  "error": {
    "message": "Request limit reached for this API key (61/60 requests this minute on the Base plan). Resets at 2026-11-01T18:05:00.000Z. Job status/result polling never counts against rate limits.",
    "type": "rate_limit_error",
    "code": "request_per_minute"
  },
  "limits": { "per_minute": 60, "per_user_per_minute": 120, "per_day": 10000 },
  "observed": { "minute_count": 61, "user_minute_count": 61, "day_count": 812 },
  "plan": "base",
  "reset_at": "2026-11-01T18:05:00.000Z"
}
```

Wait the number of seconds in the `Retry-After` header, then retry with the same `Idempotency-Key`.

## Idempotency

Send an `Idempotency-Key` header on every `POST /v1/generations`. If the answer is lost (a timeout, a dropped connection), send the exact same request again with the same key:

- The first call's generation comes back, with its current status. Nothing is started, held or charged a second time.
- A key belongs to your account and stays tied to the first generation it made. A later call with the same key returns that generation even if the body is different, so use a new key for new work.
- Two calls with the same key at the same moment still make only one generation.
- Format: 8 to 100 characters of letters, digits and `_ : . -`. A UUID works well. Anything else answers `400 invalid_idempotency_key`.

```bash cURL
curl -X POST https://vidlab.ai/v1/generations \
  -H "Authorization: Bearer $VIDLAB_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 6f1c2a9e-3b4d-4e8f-9a0b-1c2d3e4f5a6b" \
  -d '{"model": "gpt-image-2", "input": {"prompt": "A red sneaker on white"}}'
```

```js JavaScript
import { randomUUID } from "node:crypto";

const key = randomUUID(); // keep it with the work item, reuse it on every retry
async function start(body, tries = 4) {
  for (let i = 0; i < tries; i++) {
    const res = await fetch("https://vidlab.ai/v1/generations", {
      method: "POST",
      headers: {
        Authorization: `Bearer ${process.env.VIDLAB_API_KEY}`,
        "Content-Type": "application/json",
        "Idempotency-Key": key,
      },
      body: JSON.stringify(body),
    }).catch(() => null);
    if (res && res.ok) return (await res.json()).data;
    if (res && res.status < 500 && res.status !== 429) throw new Error((await res.json()).error.message);
    const wait = Number(res?.headers.get("retry-after")) || 2 ** i;
    await new Promise((r) => setTimeout(r, wait * 1000));
  }
  throw new Error("VidLab did not answer");
}
```

```python Python
import os, time, uuid, requests

key = str(uuid.uuid4())  # keep it with the work item, reuse it on every retry

def start(body, tries=4):
    for i in range(tries):
        try:
            r = requests.post(
                "https://vidlab.ai/v1/generations",
                headers={
                    "Authorization": f"Bearer {os.environ['VIDLAB_API_KEY']}",
                    "Idempotency-Key": key,
                },
                json=body,
                timeout=30,
            )
        except requests.RequestException:
            r = None
        if r is not None and r.ok:
            return r.json()["data"]
        if r is not None and r.status_code < 500 and r.status_code != 429:
            raise RuntimeError(r.json()["error"]["message"])
        wait = int(r.headers.get("retry-after", 0)) if r is not None else 0
        time.sleep(wait or 2 ** i)
    raise RuntimeError("VidLab did not answer")
```
