prizepicksapi

Errors & limits

Handle PrizePicks API errors and rate limits.

Errors come back as JSON with a stable code. Branch on that code, not on message text, and treat a monthly quota very differently from a per-minute limit.

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

Statuserror.codeAction
400invalid_queryFix the request. Messages name the problem, for example limit out of 1–200, duplicate parameter, or cursor and after both sent.
401INVALID_API_KEYMissing, malformed, or revoked key. Do not retry until the key is fixed.
403ACCOUNT_RESTRICTEDAccount-level restriction. Contact support; retrying will not help.
429RATE_LIMIT_EXCEEDEDPer-minute limit. Wait for Retry-After, then continue.
429MONTHLY_QUOTA_EXCEEDEDMonthly allowance used. Stop until the next UTC month or change plan.
503service_unavailableTemporary. 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

PlanRequests per monthRequests per minute
Free1,00010
Developer100,00060
Production1,000,000300

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.