Error Handling
The API uses RFC 9457 problem details for all error responses, providing machine-readable error codes alongside human-readable messages.
Error response format
Section titled “Error response format”{ "type": "https://nbaproplab.com/errors/not-found", "title": "Resource not found", "status": 404, "detail": "Pick with ID 99999 does not exist", "instance": "/api/v1/data/picks/99999"}Status codes
Section titled “Status codes”| Status | Meaning | Common cause |
|---|---|---|
400 | Bad Request | Invalid query parameter, malformed JSON |
401 | Unauthorized | Missing or expired token |
403 | Forbidden | Valid auth, insufficient tier or scope |
404 | Not Found | Resource doesn’t exist |
409 | Conflict | Duplicate resource (e.g., webhook URL already subscribed) |
422 | Unprocessable Entity | Valid JSON but invalid values |
429 | Too Many Requests | Rate limit exceeded |
500 | Internal Server Error | Server-side issue — retry with backoff |
Handling 403 upgrade responses
Section titled “Handling 403 upgrade responses”When a Free/Pro user hits a Sharp-only endpoint, the 403 includes upgrade info:
{ "type": "https://nbaproplab.com/errors/tier-required", "title": "Subscription upgrade required", "status": 403, "detail": "This endpoint requires a Sharp subscription", "requiredTier": "Sharp", "upgradeUrl": "https://nbaproplab.com/pricing?highlight=Sharp"}Retry strategy
Section titled “Retry strategy”For transient errors (5xx, network timeouts), use exponential backoff:
async function fetchWithRetry(url: string, opts: RequestInit, maxRetries = 3) { for (let attempt = 0; attempt <= maxRetries; attempt++) { const res = await fetch(url, opts); if (res.ok) return res; if (res.status < 500 && res.status !== 429) throw new Error(`HTTP ${res.status}`);
const delay = Math.min(1000 * 2 ** attempt, 30000); await new Promise(r => setTimeout(r, delay)); } throw new Error('Max retries exceeded');}