Handling Responses
Learn how to parse and work with API responses from OilPriceAPI.
Response Format
All API responses follow a consistent JSON structure:
Success Response
A single-commodity request returns a flat object in data. A multi-commodity or historical request returns data.prices (an array) plus a data.metadata block:
{
"status": "success",
"data": {
"prices": [
{
"code": "WTI_USD",
"price": 68.58,
"formatted": "$68.58",
"currency": "USD",
"created_at": "2026-07-03T13:43:01.099Z"
}
],
"metadata": {
"request_id": "6390b34903a8d3f0",
"timestamp": "2026-07-03T13:47:37Z",
"version": "v1"
}
}
}
Error Response
Errors do not share a single envelope — the shape depends on the status code (see Error Codes for the full reference):
// 401 Unauthorized — missing or invalid API key
{
"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"
}
}
// 400 Bad Request — unknown commodity code
{
"status": "fail",
"data": {
"error": "invalid_code",
"message": "Code 'NOT_REAL' not found.",
"invalid_codes": ["NOT_REAL"]
}
}
A
404 Not Foundreturns an empty body (no JSON), so check the HTTP status before parsing.
Response Status Codes
| Status Code | Description |
|---|---|
| 200 OK | Request successful |
| 400 Bad Request | Invalid parameters or request format |
| 401 Unauthorized | Invalid or missing API key |
| 404 Not Found | Resource not found |
| 429 Too Many Requests | Rate limit exceeded |
| 500 Internal Server Error | Server error (rare) |
Parsing Price Data
Single Commodity Response
// Response from /v1/prices/latest?by_code=WTI_USD
{
"status": "success",
"data": {
"price": 74.52,
"formatted": "$74.52",
"currency": "USD",
"code": "WTI_USD",
"created_at": "2025-12-29T15:30:00.000Z",
"type": "spot_price",
"source": "market_reporting",
"metadata": {
"source": "market_reporting",
"source_description": "Business Insider - Most accurate source matching ICE exchange prices"
}
}
}
// Parsing in JavaScript
const response = await fetch(url, options);
const result = await response.json();
if (result.status === 'success') {
const { price, formatted, code, created_at } = result.data;
console.log(`${code}: ${formatted}`); // "WTI_USD: $74.52"
console.log(`Updated: ${new Date(created_at).toLocaleString()}`);
}
Multiple Commodities Response
When requesting multiple commodities, data.prices is an array of price objects (one per commodity):
# Response from /v1/prices/latest?by_code=WTI_USD,BRENT_CRUDE_USD
response = {
"status": "success",
"data": {
"prices": [
{
"price": 68.58,
"formatted": "$68.58",
"currency": "USD",
"code": "WTI_USD",
"created_at": "2026-07-03T13:43:01.099Z",
"type": "spot_price"
},
{
"price": 71.98,
"formatted": "$71.98",
"currency": "USD",
"code": "BRENT_CRUDE_USD",
"created_at": "2026-07-03T13:43:01.099Z",
"type": "spot_price"
}
],
"metadata": {
"request_id": "6390b34903a8d3f0",
"timestamp": "2026-07-03T13:47:37Z",
"version": "v1"
}
}
}
# Parsing in Python
import requests
response = requests.get(url, headers=headers)
data = response.json()
if data['status'] == 'success':
# data['data']['prices'] is a list - iterate or key it by code
for price_data in data['data']['prices']:
print(f"{price_data['code']}: {price_data['formatted']}")
Working with Historical Data
Historical Response Structure
Historical endpoints return the price points as an array under data.prices:
{
"status": "success",
"data": {
"prices": [
{
"price": 67.84,
"formatted": "$67.84",
"currency": "USD",
"code": "WTI_USD",
"created_at": "2026-07-02T13:54:43.160Z",
"type": "spot_price"
},
{
"price": 67.61,
"formatted": "$67.61",
"currency": "USD",
"code": "WTI_USD",
"created_at": "2026-07-02T13:44:41.010Z",
"type": "spot_price"
},
{
"price": 67.42,
"formatted": "$67.42",
"currency": "USD",
"code": "WTI_USD",
"created_at": "2026-07-02T13:34:38.920Z",
"type": "spot_price"
}
]
}
}
Processing Historical Data
// JavaScript - Calculate average price
function calculateAverage(historicalData) {
const prices = historicalData.data.prices; // data.prices is an array
const sum = prices.reduce((acc, item) => acc + item.price, 0);
return sum / prices.length;
}
// Find price trends
function analyzeTrend(historicalData) {
const prices = historicalData.data.prices;
const firstPrice = prices[prices.length - 1].price; // oldest
const lastPrice = prices[0].price; // most recent
const change = lastPrice - firstPrice;
const changePercent = (change / firstPrice) * 100;
return {
trend: change > 0 ? "upward" : "downward",
changeAmount: change.toFixed(2),
changePercent: changePercent.toFixed(2),
};
}
# Python - Create DataFrame for analysis
import pandas as pd
import requests
def process_historical_data(response_data):
"""Convert API response to pandas DataFrame"""
prices = response_data['data']['prices'] # data.prices is an array
df = pd.DataFrame(prices)
df['created_at'] = pd.to_datetime(df['created_at'])
df.set_index('created_at', inplace=True)
# Calculate moving averages
df['ma_5'] = df['price'].rolling(window=5).mean()
df['ma_10'] = df['price'].rolling(window=10).mean()
return df
# Usage
response = requests.get(url, headers=headers)
data = response.json()
df = process_historical_data(data)
print(df.describe())
Error Handling
Handling Errors
Because the error shape depends on the status code, branch on the HTTP status rather than a single error.code field. A 404 has no body, so guard the JSON parse:
async function makeAPICall(url, options) {
try {
const response = await fetch(url, options);
if (response.status === 401) {
// { "error": { "code": "UNAUTHORIZED", "message": ..., "request_id": ... } }
const body = await response.json();
console.error(`Auth failed (${body.error.code}):`, body.error.message);
console.error("Include header: Authorization: Token YOUR_API_KEY");
return null;
}
if (response.status === 400) {
// { "status": "fail", "data": { "error": "invalid_code", "invalid_codes": [...] } }
const body = await response.json();
console.error("Bad request:", body.data.error, body.data.message);
// For a bad commodity code, fetch the valid list from GET /v1/commodities
return null;
}
if (response.status === 429) {
const retryAfter = response.headers.get("Retry-After") || 60;
console.warn(`Rate limited. Retry after ${retryAfter}s.`);
return null;
}
if (response.status === 404) {
// 404 returns an empty body — do not parse JSON
console.error("Not found:", url);
return null;
}
return await response.json();
} catch (err) {
console.error("Network error:", err);
return null;
}
}
Retry Logic
import time
from typing import Optional, Dict, Any
class APIClient:
def __init__(self, api_key: str, max_retries: int = 3):
self.api_key = api_key
self.max_retries = max_retries
def make_request(self, endpoint: str, params: Dict[str, Any]) -> Optional[Dict]:
"""Make API request with automatic retry on failure"""
for attempt in range(self.max_retries):
try:
response = requests.get(
f"https://api.oilpriceapi.com/v1{endpoint}",
params=params,
headers={'Authorization': f'Token {self.api_key}'},
timeout=10
)
if response.status_code == 429:
# Rate limited - wait before retry
retry_after = int(response.headers.get('Retry-After', 60))
if attempt < self.max_retries - 1:
time.sleep(retry_after)
continue
response.raise_for_status()
return response.json()
except requests.exceptions.RequestException as e:
if attempt == self.max_retries - 1:
print(f"Failed after {self.max_retries} attempts: {e}")
return None
# Exponential backoff
wait_time = 2 ** attempt
time.sleep(wait_time)
return None
Response Headers
Important headers included in responses:
X-RateLimit-Limit: 10000 # Monthly request quota for your plan
X-RateLimit-Remaining: 9876 # Requests remaining this month
X-RateLimit-Used: 124 # Requests used this month
X-RateLimit-Reset: 1785542399 # Unix timestamp when the monthly quota resets
X-RateLimit-Tier: developer # Your current plan tier
X-Request-Id: 8c1fc2b1-b8e2-46ae-bd18-b6916d526e7a # Request ID — include it in support tickets
These headers track your monthly quota, not a per-minute window. Provide the
X-Request-Idwhen contacting support.
Monitoring Usage
class UsageMonitor {
constructor() {
this.usage = {
limit: null,
remaining: null,
resetAt: null,
};
}
updateFromHeaders(headers) {
this.usage.limit = parseInt(headers.get("x-ratelimit-limit"));
this.usage.remaining = parseInt(headers.get("x-ratelimit-remaining"));
this.usage.resetAt = new Date(
parseInt(headers.get("x-ratelimit-reset")) * 1000,
);
this.checkUsage();
}
checkUsage() {
const percentUsed =
((this.usage.limit - this.usage.remaining) / this.usage.limit) * 100;
if (percentUsed > 90) {
console.warn(`API usage at ${percentUsed.toFixed(1)}%`);
this.sendUsageAlert();
}
}
getDaysUntilReset() {
const now = new Date();
const msUntilReset = this.usage.resetAt - now;
return Math.ceil(msUntilReset / (1000 * 60 * 60 * 24));
}
}
Data Transformation
Format for Display
function formatPriceDisplay(priceData) {
const formatter = new Intl.NumberFormat("en-US", {
style: "currency",
currency: priceData.currency,
minimumFractionDigits: 2,
maximumFractionDigits: 2,
});
const pct = priceData.changes?.["24h"]?.percent ?? 0;
return {
formatted: priceData.formatted, // API returns a pre-formatted string, e.g. "$68.58"
unit: priceData.unit,
change: pct > 0 ? "↑" : "↓",
changeColor: pct > 0 ? "green" : "red",
changeText: `${pct > 0 ? "+" : ""}${pct}%`,
};
}
// Usage — find the commodity in the prices array
const wti = result.data.prices.find((p) => p.code === "WTI_USD");
const display = formatPriceDisplay(wti);
console.log(`${display.formatted}/${display.unit} ${display.changeText}`);
// Output: $68.58/barrel +1.06%
Export to CSV
import csv
from datetime import datetime
def export_to_csv(historical_data, filename):
"""Export historical price data to CSV"""
prices = historical_data['data']['prices']
with open(filename, 'w', newline='') as csvfile:
fieldnames = ['created_at', 'code', 'price', 'currency', 'source']
writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
writer.writeheader()
for point in prices:
writer.writerow({
'created_at': point['created_at'],
'code': point['code'],
'price': point['price'],
'currency': point['currency'],
'source': point.get('source', ''),
})
print(f"Exported {len(prices)} records to {filename}")
OilPriceAPI returns spot price points (a single
priceper timestamp), not OHLCV candles. For period averages, request an aggregated interval (e.g.interval=1d).
WebSocket Responses (Professional and above)
Real-time streaming is available on the Professional plan and above. Each message carries a code and price consistent with the REST fields:
ws.onmessage = (event) => {
const message = JSON.parse(event.data);
if (message.type === "price_update") {
updatePriceDisplay(message.data); // { code, price, currency, created_at, ... }
}
};
See the WebSocket documentation for the authoritative message schema, authentication, and subscription model.
Next Steps
- Explore the API Reference for detailed endpoint documentation
- Learn about Marine Fuels specialty endpoints
- Set up WebSocket connections for real-time updates (Professional and above)