Skip to content

Error Handling

The API uses RFC 9457 problem details for all error responses, providing machine-readable error codes alongside human-readable messages.

{
"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"
}
StatusMeaningCommon cause
400Bad RequestInvalid query parameter, malformed JSON
401UnauthorizedMissing or expired token
403ForbiddenValid auth, insufficient tier or scope
404Not FoundResource doesn’t exist
409ConflictDuplicate resource (e.g., webhook URL already subscribed)
422Unprocessable EntityValid JSON but invalid values
429Too Many RequestsRate limit exceeded
500Internal Server ErrorServer-side issue — retry with backoff

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"
}

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');
}