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 |
|---|---|---|---|---|---|
| Free | Free | 10/min | 100/day | 3 | Yes |
| Dev | See pricing page | 60/min | 1,000/day | 100 | Yes |
| Pro | See pricing page | 120/min | 5,000/day | 1,000 | Yes |
| Business | See pricing page | 300/min | 20,000/day | 5,000 | Yes |
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:
| Header | Value |
|---|---|
| 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 status | code | Meaning |
|---|---|---|
| 400 | invalid_request | Missing or invalid parameter, including an unknown market id/name (see message for details) |
| 401 | unauthorized | Missing or invalid API key |
| 403 | forbidden | Valid key, but the market is outside your plan's scope (upgrade) or not ML-eligible for /score |
| 403 | demo_restricted | The 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 |
| 404 | not_found | Unknown symbol (no seasonal data), or an unknown endpoint. An unknown market is a 400, not a 404. |
| 429 | rate_limited | Too many requests - check X-RateLimit-Reset and the scope (minute|day) |
| 503 | scan_busy | The scan cache is warming up fresh data - retry shortly (a Retry-After header is included) |
| 503 | upstream_unavailable | Chart/seasonal data temporarily unavailable (upstream rate limit/outage) - retry shortly |
| 503 | service_misconfigured | Server-side auth backend misconfiguration - the API fails closed rather than authenticate incorrectly; try again shortly or contact support |
| 500 | internal | Unexpected 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
- Check
X-RateLimit-Remainingproactively and back off before hitting zero. - On 429, wait until
X-RateLimit-Resetbefore retrying. - For MCP/agents, check for
"reason": "ml_daily_limit"in the response to handle a spent daily ML allowance gracefully. - On 500, retry with exponential backoff. If the error persists, contact support.