Error Codes Reference
Guide to the OilPriceAPI error responses, their meanings, and how to handle them.
Error Response Formats
OilPriceAPI does not use a single error envelope. There are three distinct shapes depending on what went wrong. Handle each explicitly.
| Situation | HTTP Status | Body |
|---|---|---|
| Missing or invalid API key | 401 | JSON with a top-level error object |
| Invalid commodity code | 400 | JSON with status: "fail" and an error under data |
| Unknown route / endpoint | 404 | Empty body (text/html), no JSON |
401 — Missing or invalid API key
Both a missing key and an invalid key return the same shape with a single code, UNAUTHORIZED. There is no top-level status field and no meta block.
{
"error": {
"code": "UNAUTHORIZED",
"message": "Missing or invalid API key. Include header: Authorization: Token YOUR_API_KEY",
"status": 401,
"request_id": "fa0e389f-ca86-47b2-b167-2b4645a2fb8b",
"docs": "https://docs.oilpriceapi.com#UNAUTHORIZED",
"signup_url": "https://www.oilpriceapi.com/auth/signup",
"demo_endpoint": "/v1/demo/prices"
}
}
The error code lives at body.error.code and the request_id (useful for support) at body.error.request_id.
How to fix:
curl "https://api.oilpriceapi.com/v1/prices/latest?by_code=WTI_USD" \
-H "Authorization: Token YOUR_API_KEY" # Add or correct this header
- Verify the key in your dashboard.
- Check for typos or extra whitespace.
- Both
Token YOUR_API_KEYandBearer YOUR_API_KEYare accepted;Tokenis the recommended form.
400 — Invalid commodity code
When you request a commodity code that doesn't exist, the response has a top-level status: "fail" and the error details are nested under data.
{
"status": "fail",
"data": {
"error": "invalid_code",
"message": "Code 'NOT_REAL' not found.",
"suggestions": [],
"invalid_codes": ["NOT_REAL"],
"all_codes_url": "https://www.oilpriceapi.com/v1/prices/codes"
}
}
Here the error string is at body.data.error (value "invalid_code"), and the offending codes are in body.data.invalid_codes.
How to fix:
# Use a valid commodity code. Fetch the full list from /v1/commodities.
import requests
headers = {'Authorization': 'Token YOUR_API_KEY'}
codes = requests.get(
'https://api.oilpriceapi.com/v1/commodities',
headers=headers,
).json()['data']
valid = {c['code'] for c in codes}
assert 'WTI_USD' in valid
The
all_codes_urlfield in the response points at/v1/prices/codes, which is not currently a working endpoint. For the authoritative list of commodity codes, useGET /v1/commoditiesinstead.
404 — Unknown commodity (details endpoint)
Requesting a specific commodity that doesn't exist — e.g. GET /v1/commodities/NOT_REAL — returns HTTP 404 with a JSON body whose code is COMMODITY_NOT_FOUND:
{
"status": "fail",
"error": {
"code": "COMMODITY_NOT_FOUND",
"message": "Commodity not found",
"commodity_code": "NOT_REAL"
}
}
Note this is a different shape from the 400 invalid_code returned by /v1/prices/latest?by_code=… — the error is under body.error (not body.data).
404 — Unknown route
Requesting a path the API doesn't recognize (a route that doesn't exist at all) returns HTTP 404 with an empty body and Content-Type: text/html. There is no JSON and no request_id, so error-handling code must not attempt to parse JSON or read a request ID from an unknown-route 404.
How to fix:
- Check the endpoint path against the documentation.
- Verify the API version prefix (
/v1). - Remove trailing slashes.
HTTP Status Codes
| Status | Meaning | Body |
|---|---|---|
200 | Success | JSON payload |
400 | Bad Request (e.g. invalid commodity code) | JSON, status: "fail" |
401 | Unauthorized — missing or invalid API key | JSON, top-level error object |
404 | Not Found — unknown route | Empty (text/html) |
429 | Too Many Requests — quota exceeded | — |
500 | Internal Server Error | — |
Error Handling Best Practices
1. Branch on status code, not a single envelope
Because the body shape differs by status, inspect the HTTP status first and only then reach for the matching field.
import requests
def handle_response(response):
"""Return parsed data on success, or raise with a useful message."""
if response.status_code == 200:
return response.json()
if response.status_code == 401:
# Top-level `error` object. request_id is available for support.
body = response.json()
err = body.get('error', {})
raise APIError(
f"Unauthorized ({err.get('code')}): {err.get('message')} "
f"[request_id={err.get('request_id')}]"
)
if response.status_code == 400:
# `status: "fail"` with details nested under `data`.
body = response.json()
data = body.get('data', {})
raise APIError(
f"Bad request ({data.get('error')}): {data.get('message')} "
f"invalid_codes={data.get('invalid_codes')}"
)
if response.status_code == 404:
# An unknown route has an EMPTY body; the commodity-details endpoint
# returns JSON (COMMODITY_NOT_FOUND). Parse defensively.
try:
body = response.json()
err = body.get('error', {})
raise APIError(f"Not found ({err.get('code', '404')}): {err.get('message', response.url)}")
except ValueError:
raise APIError('Not found (404): check the endpoint path and /v1 prefix')
if response.status_code == 429:
# Quota exceeded. Wait for the monthly reset or upgrade your plan.
raise APIError('Quota exceeded (429)')
response.raise_for_status()
2. Look up the right field per shape
There is no universal data.error.code path. Use the correct access path for each status:
| Status | Access path | Example value |
|---|---|---|
401 | body.error.code | "UNAUTHORIZED" |
401 | body.error.request_id | "fa0e389f-…" |
400 | body.data.error | "invalid_code" |
400 | body.data.invalid_codes | ["NOT_REAL"] |
404 | (no body) — check the status code only | — |
// JavaScript: dispatch on status, then read the right field.
async function parseError(response) {
const status = response.status;
if (status === 404) {
// Empty body — nothing to parse.
return { status, message: "Not found — check the endpoint path" };
}
const body = await response.json();
if (status === 401) {
return {
status,
code: body.error.code, // "UNAUTHORIZED"
message: body.error.message,
requestId: body.error.request_id, // present on 401 only
};
}
if (status === 400) {
return {
status,
code: body.data.error, // "invalid_code"
message: body.data.message,
invalidCodes: body.data.invalid_codes,
};
}
return { status, message: `Unexpected status ${status}` };
}
3. Retry logic
Retry transient failures (network errors, 429, 5xx) with backoff. Do not retry 400 or 401 — those require a fix on your side.
async function apiRequest(url, options, maxRetries = 3) {
let lastError;
for (let i = 0; i < maxRetries; i++) {
try {
const response = await fetch(url, options);
if (response.ok) {
return await response.json();
}
// Client errors (except 429) are not retryable.
if (
response.status >= 400 &&
response.status < 500 &&
response.status !== 429
) {
lastError = await parseError(response);
throw new Error(lastError.message);
}
// 429 and 5xx: wait, then retry.
const retryAfter = Number(response.headers.get("Retry-After")) || i + 1;
await new Promise((resolve) => setTimeout(resolve, retryAfter * 1000));
} catch (err) {
lastError = err;
await new Promise((resolve) => setTimeout(resolve, (i + 1) * 1000));
}
}
throw lastError;
}
Rate-Limit & Usage Headers
Successful (200) responses carry headers describing your monthly quota. These track the monthly request allowance, not a per-minute window.
| Header | Description |
|---|---|
X-RateLimit-Limit | Your monthly request limit |
X-RateLimit-Remaining | Requests remaining this month |
X-RateLimit-Used | Requests used this month |
X-RateLimit-Reset | Epoch (seconds) when the monthly quota resets |
X-RateLimit-Tier | Your plan tier |
X-Request-Id | Unique request identifier |
X-Cache | Cache status for the response |
response = requests.get(url, headers=headers)
remaining = int(response.headers.get('X-RateLimit-Remaining', 0))
if remaining < 100:
logging.warning(f"Only {remaining} requests left this month")
Support
For persistent errors:
- Email: support@oilpriceapi.com
- Include the
request_idfrom the response when available. Note that401responses carry arequest_id(atbody.error.request_id), but404responses have an empty body and therefore norequest_id— in that case include the exact URL you requested instead.