The error envelope
HTTP/1.1 429 Too Many Requests
Retry-After: 60
Content-Type: application/json
{ "error": { "code": "RATE_LIMIT_EXCEEDED", "message": "Too many requests. Try again after the current minute." } }Every code and what to do
| Status | error.code | Action |
|---|---|---|
| 400 | invalid_query | Fix the request. Messages name the problem, for example limit out of 1–200, duplicate parameter, or cursor and after both sent. |
| 401 | INVALID_API_KEY | Missing, malformed, or revoked key. Do not retry until the key is fixed. |
| 403 | ACCOUNT_RESTRICTED | Account-level restriction. Contact support; retrying will not help. |
| 429 | RATE_LIMIT_EXCEEDED | Per-minute limit. Wait for Retry-After, then continue. |
| 429 | MONTHLY_QUOTA_EXCEEDED | Monthly allowance used. Stop until the next UTC month or change plan. |
| 503 | service_unavailable | Temporary. Retry with backoff. No demo data is substituted. |
Code casing differs between the authentication layer and the data layer, so compare exact strings. Successful authenticated responses include X-RateLimit-Remaining with the monthly requests left.
How limits are counted
| Plan | Requests per month | Requests per minute |
|---|---|---|
| Free | 1,000 | 10 |
| Developer | 100,000 | 60 |
| Production | 1,000,000 | 300 |
Both limits are per account, shared across every key. The month is the UTC calendar month; the minute is the UTC calendar minute. A request is counted once its key authenticates, before parameter validation, so 400 and 503 responses still use allowance. The public demo endpoints are not metered.
A retry policy that respects the codes
async function withRetry(call, { maxAttempts = 4 } = {}) {
for (let attempt = 1; ; attempt++) {
try {
return await call();
} catch (err) {
const retryable = err.code === "RATE_LIMIT_EXCEEDED" || err.status === 503;
if (!retryable || attempt >= maxAttempts) throw err; // 400/401/403/monthly quota: stop
const retryAfter = Number(err.retryAfter);
const base = Number.isFinite(retryAfter) && retryAfter > 0 ? retryAfter * 1000 : 2 ** attempt * 1000;
await new Promise((r) => setTimeout(r, base + Math.random() * 1000)); // jitter
}
}
}This expects errors shaped like the ApiError in the JavaScript guide. It caps attempts, adds jitter so several workers do not retry in lockstep, and never retries a monthly-quota or authentication failure.
Spend fewer requests
- Use limit=200 when you read whole boards.
- Filter by league and stat on the server instead of downloading everything.
- Poll /changes with a saved cursor rather than re-reading every projection.
- Cache shared responses on your backend; never fetch per page view.
- Validate parameters locally before sending, since rejected requests still count.
Frequently asked questions
Is the per-minute limit a sliding window?
No. It counts requests within each UTC calendar minute. A 429 RATE_LIMIT_EXCEEDED response includes Retry-After: 60 as a safe upper bound.
Do invalid requests count toward my quota?
Yes, if the key is valid. Usage is recorded at authentication, before query parameters are validated, so a 400 still consumes a request.
References
Current API documentation ↗Plans and request limits ↗MDN: Retry-After ↗MDN: 429 Too Many Requests ↗PrizePicks API is an independent data service. It is not affiliated with or endorsed by PrizePicks. Observations, completeness, and freshness are separate claims; consult source status before relying on a stored line.