Error Handling
The OpenAlex API uses standard HTTP status codes to indicate success or failure.
HTTP Status Codes
| Code | Meaning | What to Do |
|---|---|---|
200 | Success | Request completed successfully |
301 | Moved Permanently | Entity was merged; follow the redirect |
400 | Bad Request | Check your filter syntax or parameters |
403 | Forbidden | You don’t have access to this resource. (A collection you can’t read returns 404, not 403.) |
404 | Not Found | Entity doesn’t exist |
429 | Too Many Requests | Rate limit or daily credit budget exceeded; slow down or wait for the reset |
500 | Server Error | Temporary issue; retry with backoff |
Error Response Format
Error responses include a message explaining what went wrong:
{
"error": "Invalid filter",
"message": "Unknown filter field: author_name. Did you mean: authorships.author.id?"
}
Common Errors
Invalid Filter Syntax
400 Bad Request
"Invalid filter: publication_year:abc"
Fix: Ensure values match expected types. publication_year expects an integer.
Unknown Filter Field
400 Bad Request
"Unknown filter field: author_name"
Fix: Use the correct filter field. For authors, use authorships.author.id with an ID, not a name. See Resolve IDs.
Rate Limit Exceeded
429 Too Many Requests
"Rate limit exceeded"
Fix:
- Check rate limit headers to see your remaining allowance
- Add delays between requests
- Use
per_page=100to reduce total requests - Consider a paid plan for higher limits
Entity Not Found
404 Not Found
"Work W9999999999 not found"
Fix: Verify the ID exists. The entity may have been merged—check if you’re being redirected.
Retry Logic
Implement exponential backoff for transient errors:
import time
import requests
def fetch_with_retry(url, max_retries=5):
for attempt in range(max_retries):
try:
response = requests.get(url, timeout=30)
if response.status_code == 200:
return response.json()
if response.status_code == 429:
# Rate limited - wait longer
wait_time = 2 ** attempt
time.sleep(wait_time)
continue
if response.status_code >= 500:
# Server error - retry
wait_time = 2 ** attempt
time.sleep(wait_time)
continue
# Client error - don't retry
response.raise_for_status()
except requests.exceptions.Timeout:
if attempt < max_retries - 1:
time.sleep(2 ** attempt)
else:
raise
raise Exception(f"Failed after {max_retries} retries")
Rate Limit Headers
Every response includes headers showing your current status:
X-RateLimit-Limit: 10000
X-RateLimit-Remaining: 8766
X-RateLimit-Credits-Used: 1
X-RateLimit-Reset: 43200
Error responses are free. Any 4xx or 5xx response costs 0 credits, and its X-RateLimit-Credits-Used header reports 0. A typo in a filter or search does not use up your daily budget.
An OQL query with a calculate step, a split by a list, bins or conditions, or a filter on its
groups is priced from its plan (see Example costs).
If it costs more than you have left today, it is refused before it runs, with HTTP 429 and
"error": "not_enough_credits". The body names the price (cost, step by step) and how to fix
it: run it after the daily reset, add prepaid usage, or narrow
the query (fewer listed searches). The refusal itself is free.
Use these to:
- Monitor your usage
- Pause before hitting limits
- Calculate when limits reset (seconds until midnight UTC)
Best Practices
- Always set timeouts — Use 30-second timeouts to avoid hanging requests
- Implement backoff — Don’t retry immediately; wait 1s, 2s, 4s, etc.
- Check headers — Monitor
X-RateLimit-Remainingto avoid hitting limits - Log errors — Record failures for debugging
- Use bulk endpoints — Batch requests to reduce total API calls
Last updated
View as Markdown