bichito
API

Rate limits

How the public bichito endpoints throttle requests, and what your client should do when it gets a 429.

The public surface of the bichito API is rate-limited so a single bad actor (or a buggy client) can't bring everything down. The limits are intentionally generous for legitimate traffic and tight enough to discourage abuse.

The limits

EndpointLimitBucketWhy
POST /api/v1/auth/signup5 / hourclient IPStops automated mass-registration.
POST /api/v1/auth/login10 / minuteclient IPAnti-spray from a single host.
POST /api/v1/auth/login20 / houremail submittedAnti credential-stuffing against a specific account, even from rotating IPs.
POST /api/v1/bugs60 / minuteX-API-Key valueCaps damage from a leaked widget key.

Both login limits apply at the same time — the stricter one wins. So a single host that knows a real email can try at most 10 passwords/minute against it, and after 20 attempts in an hour the account is frozen for that hour regardless of where the attempts came from.

What clients see when limited

When a request is denied, the API returns:

  • HTTP 429 Too Many Requests
  • A JSON body explaining which limit fired:
{
  "detail": "Rate limit exceeded: 10 per 1 minute"
}
  • A Retry-After header with the number of seconds the client should wait before retrying:
HTTP/1.1 429 Too Many Requests
Retry-After: 60
content-type: application/json

Handling 429 in your client

A robust client should:

  1. Stop hammering. Don't retry immediately.
  2. Read Retry-After and back off for at least that many seconds. Apply jitter so clients don't all retry at the same instant.
  3. Surface a clear message to the user — for the widget, "we received too many reports lately, please try again in a minute" is fine.

Pseudocode:

async function postWithBackoff(url: string, body: unknown) {
  const res = await fetch(url, {
    method: "POST",
    headers: { "content-type": "application/json" },
    body: JSON.stringify(body),
  });
  if (res.status === 429) {
    const retry = Number(res.headers.get("retry-after") ?? "60");
    const jitter = Math.random() * 1000;
    await new Promise((r) => setTimeout(r, retry * 1000 + jitter));
    return postWithBackoff(url, body);
  }
  return res;
}

Need higher limits?

The widget limit (60 reports / minute / API key) covers anything a normal app will ever produce. If you have a legitimate reason to exceed it — a high-traffic incident response tool, a public widget reused across many sites under one key — get in touch and we'll raise the cap on a per-account basis.

Implementation notes

  • Backed by slowapi with the in-memory storage by default. When the API is deployed across multiple workers we switch to a Redis URI via the RATE_LIMIT_STORAGE_URI environment variable; clients don't notice the change.
  • Per-IP buckets honor the first hop of X-Forwarded-For when the API runs behind a proxy, so the limit is keyed off the real client and not the proxy.
  • Quota limits (the 100-reports/month free plan cap) are separate from rate limits; you can hit either independently.

On this page