Skip to content

Rate limits

Tango enforces rate limits to keep the API fast and reliable for everyone. Limits apply at the account level: requests made via API keys and OAuth2 tokens for the same account all draw from the same quotas.

Where to see your limits and usage

How rate limits work (burst + daily)

Most plans have more than one rate limit window:

  • Burst: short window (e.g. per minute) that protects the API from sudden spikes
  • Daily: fixed window that resets at midnight UTC, caps total volume per day

You may be “fine” on daily usage but still hit burst limits (or vice versa).

Rate limit headers

Every /api/* response includes rate limit headers.

Overall headers (most restrictive window)

These headers summarize the most restrictive window (the one you’re closest to hitting):

  • X-RateLimit-Limit: total requests allowed for that window
  • X-RateLimit-Remaining: requests remaining in that window
  • X-RateLimit-Reset: seconds until reset for that window

Per-window headers (daily, burst, etc.)

For each configured window (commonly Daily and Burst), you’ll also see:

  • X-RateLimit-Daily-Limit, X-RateLimit-Daily-Remaining, X-RateLimit-Daily-Reset
  • X-RateLimit-Burst-Limit, X-RateLimit-Burst-Remaining, X-RateLimit-Burst-Reset

Each *-Reset value is seconds until that specific window resets.

Quick header check (curl)

curl -s -D - -o /dev/null \
  -H "X-API-KEY: your-api-key-here" \
  "https://tango.makegov.com/api/contracts/?limit=1"

Example response headers:

X-RateLimit-Daily-Limit: 2400
X-RateLimit-Daily-Remaining: 2350
X-RateLimit-Daily-Reset: 86400
X-RateLimit-Burst-Limit: 100
X-RateLimit-Burst-Remaining: 95
X-RateLimit-Burst-Reset: 45
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 95
X-RateLimit-Reset: 45
X-Execution-Time: 0.045s

What happens when you exceed a limit (HTTP 429)

When you hit a rate limit, Tango responds with HTTP 429 and a JSON body like:

{
  "detail": "Rate limit exceeded for burst. Please try again in 45 seconds.",
  "wait_in_seconds": 45
}
  • Stop retrying immediately after a 429.
  • Sleep for at least wait_in_seconds (preferred) or X-RateLimit-Reset.
  • Then retry with exponential backoff + jitter to avoid a thundering herd.

Python example

import random
import time
import httpx

url = "https://tango.makegov.com/api/contracts/?limit=1"
headers = {"X-API-KEY": "your-api-key-here"}

backoff = 1.0
for _ in range(10):
    r = httpx.get(url, headers=headers)
    if r.status_code != 429:
        r.raise_for_status()
        break

    body = r.json()
    wait = body.get("wait_in_seconds")
    if wait is not None:
        time.sleep(float(wait))
        continue

    reset = r.headers.get("X-RateLimit-Reset")
    if reset is not None:
        time.sleep(float(reset))
        continue

    time.sleep(backoff + random.random())
    backoff = min(backoff * 2, 60)

JavaScript example

This example assumes fetch is available (modern browsers or Node 18+).

(async () => {
  const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

  const url = "https://tango.makegov.com/api/contracts/?limit=1";
  const headers = { "X-API-KEY": "your-api-key-here" };

  let backoffMs = 1000;
  for (let i = 0; i < 10; i++) {
    const r = await fetch(url, { headers });
    if (r.status !== 429) {
      if (!r.ok) throw new Error(`HTTP ${r.status}`);
      break;
    }

    const body = await r.json();
    if (body.wait_in_seconds != null) {
      await sleep(body.wait_in_seconds * 1000);
      continue;
    }

    const reset = r.headers.get("X-RateLimit-Reset");
    if (reset != null) {
      await sleep(Number(reset) * 1000);
      continue;
    }

    await sleep(backoffMs + Math.random() * 250);
    backoffMs = Math.min(backoffMs * 2, 60_000);
  }
})().catch((err) => {
  console.error("Request failed:", err);
});

Reduce calls (and avoid limits) in practice

  • Use response shaping (shape=) to avoid extra “follow-up” requests. See the Response Shaping Guide.
  • Paginate responsibly; avoid re-fetching the same pages repeatedly.
  • Cache hot lookups on your side when appropriate (e.g. “entity by UEI”).
  • Prefer webhooks for event-driven updates instead of polling where possible.