Skip to main content

Documentation Index

Fetch the complete documentation index at: https://teardowns.aero/docs/llms.txt

Use this file to discover all available pages before exploring further.

The public API rate-limits only key-authenticated traffic. The web UI (JWT) is never throttled by our limiter. So your integration’s traffic budget is independent of what your users do in the web app.

The four buckets

BucketScopeLimitWhat it protects
Aper API key, JSON endpoints600 req/min (10/s sustained, burst 60)Runaway loops, retry storms.
Bper source IP, failed auth on /public/v1/*30 fails/minBrute-force keyspace scanning.
Cper source IP, /org/api-keys mint endpoint10 req/minAbuse of the issuance UI.
Dper API key, upload endpoints60 req/min (burst 5)Supabase Storage egress. 60 × 50 MB = 3 GB/min cap.
Each bucket is independent a request only consumes from the one(s) it falls into. A regular JSON request consumes from A; a multipart upload consumes from D; a failed auth consumes from B (regardless of which endpoint).

What happens on overflow

Response:
HTTP/1.1 429 Too Many Requests
Retry-After: 30
Content-Type: application/json

{
  "detail": {
    "error_code": "too_many_requests",
    "message": "Rate limit exceeded for this API key.",
    "limit": "600 per 1 minute"
  }
}
The Retry-After header is in seconds. Wait at least that long before retrying. Treat the value as authoritative our backend recalculates it from the actual window state.

Designing your ERP around the limits

A few patterns that work in practice:
Mint a second key just for the batch process. Each key has its own 600 rpm bucket. A nightly bulk-import script with its own key doesn’t impair the day-to-day key.
On any 5xx or 429, retry with exponential backoff. Capped at, say, 5 retries with a base of 1 second and a max of 60. Always respect Retry-After when present it’s better than your backoff math.
import time, requests
def post_with_retry(url, **kw):
    for attempt in range(5):
        r = requests.post(url, **kw)
        if r.status_code == 429:
            wait = int(r.headers.get('Retry-After', 2 ** attempt))
            time.sleep(wait)
            continue
        if r.status_code >= 500:
            time.sleep(2 ** attempt)
            continue
        return r
    r.raise_for_status()
Bucket A allows bursts up to 60 req. If you’ve been idle, you can fire 60 in a second. After that the leaky bucket refills at 10/s. So a for loop firing 100 requests will burst the first 60, then receive 429s for the next 40 until enough time has passed.Cleaner: just paced the loop at one request per 100 ms. Predictable and never trips the limiter.
Bucket D is 60/min with a tighter burst of 5. Parallel uploads in excess of 5 will start receiving 429s. Cap your upload concurrency at ~5 and queue the rest. The math:
  • 5 simultaneous uploads, each 30 seconds → cycle every ~30 s.
  • In one minute you can clear ~10 uploads at 5x concurrency.
  • 60 rpm bucket gives you headroom for 60 uploads in a minute, provided you don’t spike the burst.
There are no public-API webhooks today, so partners sometimes poll. For a status-check loop:
  • Poll every 5 minutes for slow-moving state (e.g., counter-offers).
  • Poll every 30 seconds for state that should change quickly.
  • Never poll faster than 1/sec.
All of these stay well within the 10/s sustained rate of bucket A.

Visibility

  • Every 429 includes a Retry-After header. That’s your primary signal.
  • Every failed auth (401s on /public/v1/*) consumes bucket B. If your ERP is hammering with a wrong key, you’ll start getting 429s on top of the 401s. Fix the key.
  • Mint and rotation calls hit bucket C. A misbehaving CI job that re-mints keys in a loop will trip this within a minute.

What’s NOT rate-limited

  • Anything authenticated with a JWT. Web app, internal scripts that use a Supabase JWT, etc. Out of scope for this limiter.
  • Successful requests by an org’s other keys. Bucket A is per individual key. If your org has three keys, each gets its own 600 rpm.

When the platform behavior changes

The numbers above are the production defaults. We may raise the limits for specific partners on request email support@teardowns.aero with the integration name and a sense of the traffic you expect. We’re unlikely to lower the limits without notice; if we ever did, you’d hear about it via the changelog and via direct email if you’re an active integration.

Implementation note

We use slowapi (a FastAPI rate-limit middleware) with a custom key-func that reads the bearer token directly from the Authorization header. UI traffic without a tdao_live_ prefix is exempt. Counts are held in-memory per process with N worker processes the effective limit per key per minute is N × the configured value (worst case). For the 600 rpm default that’s not a meaningful difference in practice.