Rate Limits & Errors

Per-tier limits, retry headers, error codes, and the upgrade stub.

Rate Limits & Errors

Limits are enforced per account, not per API key. Multiple keys under one account share the same limits - keys are for attribution and rotation, not for multiplying quota. Exceeding the limit returns a 429 Too Many Requests response with retry headers.

Per-tier limits

Tier Price Rate (per min) Rate (per day) Max results (/opportunities) ML access
FreeFree10/min100/day3Yes
DevSee pricing page60/min1,000/day100Yes
ProSee pricing page120/min5,000/day1,000Yes
BusinessSee pricing page300/min20,000/day5,000Yes

The max results column is the tier cap for the limit parameter on GET /v1/opportunities. Requesting more than the cap returns up to the cap without error.

Rate limit headers

Every response includes these headers:

HeaderValue
X-RateLimit-Limit Your tier's per-minute request limit
X-RateLimit-Remaining Requests remaining in the current minute window
X-RateLimit-Reset Unix timestamp (UTC) when the current window resets

Example headers on a 429 response:

HTTP/1.1 429 Too Many Requests
X-RateLimit-Limit:     10
X-RateLimit-Remaining: 0
X-RateLimit-Reset:     1748384460
Content-Type:          application/json

{
  "error": {
    "code":    "rate_limited",
    "message": "You have exceeded 10 requests per minute. Retry after 2026-05-27T14:01:00Z."
  }
}

Error codes

HTTP statuscodeMeaning
400invalid_requestMissing or invalid parameter, including an unknown market id/name (see message for details)
401unauthorizedMissing or invalid API key
403forbiddenValid key, but the market is outside your plan's scope (upgrade) or not ML-eligible for /score
403demo_restrictedThe public demo token was used outside its allowlist (a symbol other than the 5 demo tickers, or a bulk/enumeration endpoint) - create a free key for full access
404not_foundUnknown symbol (no seasonal data), or an unknown endpoint. An unknown market is a 400, not a 404.
429rate_limitedToo many requests - check X-RateLimit-Reset and the scope (minute|day)
503scan_busyThe scan cache is warming up fresh data - retry shortly (a Retry-After header is included)
503upstream_unavailableChart/seasonal data temporarily unavailable (upstream rate limit/outage) - retry shortly
503service_misconfiguredServer-side auth backend misconfiguration - the API fails closed rather than authenticate incorrectly; try again shortly or contact support
500internalUnexpected server error - try again; report if persistent

ML daily limit stub

When the daily ML allowance is already fully spent, POST /v1/score returns HTTP 200 with a limit stub rather than an error. This lets MCP agents degrade gracefully. The stub includes ml_remaining_today: 0 so callers can check it without parsing the message.

{
  "requires":          "upgrade",
  "reason":            "ml_daily_limit",
  "message":           "Daily ML limit reached (5/day on your plan). Upgrade for unlimited ML scoring.",
  "upgrade_url":       "https://tradewave.ai/account/api/keys",
  "ml_remaining_today": 0
}

For MCP/agents, check for "reason": "ml_daily_limit" to distinguish a spent daily allowance from other upgrade prompts.

Error envelope

All error responses (4xx, 5xx) follow this shape:

{
  "error": {
    "code":    "string",
    "message": "string"
  }
}

Best practices