OilPriceAPI Docs
GitHub
GitHub
  • Guides

    • Reviewed Product Facts
    • Authentication
    • Security & Compliance
    • Premium Features and Plan Requirements
    • Currency Conversion
    • Data Usage Policy Guide
    • CI API Key Rotation
    • Production Go-Live and Plan Fit
    • Use-Case Tutorials
    • API Versioning
    • Data Freshness and Source Timestamps
    • Data Quality and Validation
    • Coal Price Data - Complete Guide
    • Testing & Development
    • Error Codes Reference
    • API Error Recovery
    • SDK Code Examples
    • Webhook Signature Verification
    • Production Deployment Checklist
    • Service Level Agreement (SLA)
    • Rate Limiting & Response Headers
    • Troubleshooting Guide
    • Incident Response Guide
    • Video Tutorials

Rate Limiting & Response Headers

Understanding rate limits and response headers is crucial for building reliable integrations with the OilPriceAPI.


Rate Limits by Plan

Two independent limits apply:

  1. A request quota, which varies by plan — monthly on every paid plan, daily on Free. Exceeding it returns 402 Payment Required.
  2. A request rate limit of 60 requests per rolling 60-second window, per API key, which is the same on every plan. Exceeding it returns 429 Too Many Requests.

The rate limit is a window, not a per-second cap. Ten requests fired in the same second are fine — you are only throttled once you have made more than 60 requests in the preceding 60 seconds. There is no requirement to space requests one second apart.

PlanRequest QuotaRequest Rate Limit
Free50 / day60 per rolling 60s
Free Trial (7 days)10,00060 per rolling 60s
Developer10,000 / month60 per rolling 60s
Starter50,000 / month60 per rolling 60s
Professional100,000 / month60 per rolling 60s
Scale1,000,000 / month60 per rolling 60s

Plan prices are on the pricing page. Plans differ by request quota, not by request rate.

Other Rate-Limit Lanes

A few request classes have their own limits:

Request classLimitKeyed by
Authenticated /v1/* (the default)60 per rolling 60sAPI key
Unauthenticated /v1/* (e.g. demo endpoints)10 per rolling 60sClient IP
Authenticated but over monthly quota5 per rolling 60sAPI key
Marine-fuel endpoints30 per rolling 60sAPI key
Marine-fuel historical5 per rolling 60sAPI key

If you have blown through your monthly quota, your rate limit drops from 60/min to 5/min until the quota resets — so a client that ignores 402 responses and keeps hammering will also start seeing 429.

Free Trial

New accounts receive a 7-day free trial with:

  • 10,000 API requests during the trial period
  • Commodity access according to the account's dataset entitlements
  • Full API functionality (Professional-level features)
  • No credit card required

After the trial ends, the account continues on the Free tier with 50 requests/day. Upgrade when you need higher monthly quotas or paid-tier features.

Note: The request rate limit is 60 requests per rolling 60-second window on all plans; exceeding it returns 429. Monthly quotas reset on the 1st of each month at 00:00 UTC. Trial quota converts when you upgrade to a paid plan.


Rate Limit Headers

Every API response includes headers that help you monitor your usage:

Rate Limit Headers

Every response includes headers that track your monthly quota:

HeaderDescriptionExample
X-RateLimit-LimitYour plan's monthly request quota10000
X-RateLimit-RemainingRequests remaining this month9876
X-RateLimit-UsedRequests used this month124
X-RateLimit-ResetUnix timestamp when the monthly quota resets1785542399
X-RateLimit-TierYour current plan tierdeveloper
X-Request-IdUnique request ID — include it in support tickets8c1fc2b1-…

Pagination Headers

When retrieving paginated data (historical prices, etc.):

HeaderDescriptionExample
X-TotalTotal number of records available2016
X-Total-PagesTotal number of pages21
X-PageCurrent page number1
X-Per-PageNumber of records per page100
LinkRFC 5988 pagination linksSee below

Link Header Format

Link: <https://api.oilpriceapi.com/v1/prices/past_week?page=1>; rel="first",
      <https://api.oilpriceapi.com/v1/prices/past_week?page=2>; rel="next",
      <https://api.oilpriceapi.com/v1/prices/past_week?page=21>; rel="last"

Diagnostic Headers

HeaderDescriptionExample
X-Request-IdUnique request identifier for supportdf77d32e-f606-4f69-8dee-4508439fcb79
X-CacheCache status (HIT/MISS)HIT

Quote the X-Request-Id from an affected response when contacting support — it lets us trace the exact request.

Handling Rate Limits

Check Headers Before Making Requests

import requests
import time

class RateLimitedClient:
    def __init__(self, api_key):
        self.api_key = api_key
        self.remaining = None
        self.reset_time = None

    def make_request(self, endpoint, params=None):
        # Check if we need to wait
        if self.remaining == 0 and self.reset_time:
            wait_time = self.reset_time - time.time()
            if wait_time > 0:
                print(f"Rate limited. Waiting {wait_time:.0f} seconds...")
                time.sleep(wait_time)

        response = requests.get(
            f'https://api.oilpriceapi.com/v1{endpoint}',
            headers={'Authorization': f'Token {self.api_key}'},
            params=params
        )

        # Update rate limit info
        self.remaining = int(response.headers.get('X-RateLimit-Remaining', 0))
        self.reset_time = int(response.headers.get('X-RateLimit-Reset', 0))

        return response

Implement Exponential Backoff

async function fetchWithRetry(url, options, maxRetries = 3) {
  for (let i = 0; i < maxRetries; i++) {
    const response = await fetch(url, options);

    // Check rate limit headers
    const remaining = response.headers.get("X-RateLimit-Remaining");
    const resetAfter = response.headers.get("X-RateLimit-Reset-After");

    console.log(`Remaining requests: ${remaining}`);

    if (response.status === 429) {
      // Rate limited - wait and retry
      const waitTime = parseInt(resetAfter || "60") * 1000;
      console.log(`Rate limited. Waiting ${waitTime}ms...`);
      await new Promise((resolve) => setTimeout(resolve, waitTime));
      continue;
    }

    if (response.ok) {
      return response;
    }

    // Exponential backoff for other errors
    const delay = Math.min(1000 * Math.pow(2, i), 10000);
    await new Promise((resolve) => setTimeout(resolve, delay));
  }

  throw new Error("Max retries exceeded");
}

Batch Requests Efficiently

One request can carry up to 20 commodity codes, and it counts as one request against your quota. This is the single most effective thing you can do with your allowance.

def fetch_multiple_commodities_efficiently():
    # ❌ Three requests, three against quota
    # for code in ['WTI_USD', 'BRENT_CRUDE_USD', 'NATURAL_GAS_USD']:
    #     requests.get(f'/v1/prices/latest?by_code={code}')

    # ✅ One request, ONE against quota — up to 20 codes
    response = requests.get(
        'https://api.oilpriceapi.com/v1/prices/latest',
        headers={'Authorization': 'Token YOUR_API_KEY'},
        params={'by_code': 'WTI_USD,BRENT_CRUDE_USD,NATURAL_GAS_USD'}
    )
    return response.json()   # -> data.prices[] when you pass more than one code

A single code returns a flat data object; two or more return data.prices[]. Asking for more than 20 returns 400 with Too many commodity codes requested (max: 20, requested: N). An unrecognised code also returns 400, with a "did you mean" suggestion — so validate your code list once rather than on every poll.


How Often To Poll

This is the section most integrations get wrong, and it is the most common reason a working integration starts returning 402.

How often the data actually changes

Polling faster than this cannot return new information:

SeriesNew value roughly every
BRENT_CRUDE_USD2.5 minutes
GOLD_USD3 minutes
WTI_USD5 minutes
NATURAL_GAS_USD5 minutes
Refined products (DIESEL_USD, GASOLINE_USD)twice a day

Nothing we publish moves faster than about every 2.5 minutes. A one-second poll and a two-minute poll return the same number — the first just spends forty times more of your quota to do it.

The interval that fits your plan

Assuming you batch up to 20 codes per request (see above):

PlanQuotaPoll everyRequests / daySeries covered
Free50 / day30 minutes4820
Developer10,000 / month5 minutes28820
Starter50,000 / month5 minutes1,440100
Professional100,000 / month5 minutes2,880220
Scale1,000,000 / month5 minutes28,8002,000+

Two things worth noticing:

The free plan covers 20 series. Fifty requests a day sounds small, and it is not, because each request carries twenty codes. Twenty series refreshed every half hour fits inside the free plan with requests to spare.

Above Developer, extra quota buys additional series rather than a shorter timer. Since nothing updates faster than ~2.5 minutes, a five-minute poll is already at the data's resolution. Spend the allowance on more codes or more endpoints, not on a faster loop.

A default that fits the free plan

Start here. It works on every plan, including free:

import os, time, requests

API_KEY  = os.getenv("OILPRICEAPI_KEY")
CODES    = "BRENT_CRUDE_USD,WTI_USD,NATURAL_GAS_USD"   # up to 20
INTERVAL = 1800          # 30 minutes — fits the free plan (48 requests/day)
                         # Developer and above: drop to 300 (5 minutes)

_cache = {"data": None, "at": 0}

def latest():
    """Return the most recent prices, refreshing at most once per INTERVAL."""
    if _cache["data"] and time.time() - _cache["at"] < INTERVAL:
        return _cache["data"]

    r = requests.get(
        "https://api.oilpriceapi.com/v1/prices/latest",
        headers={"Authorization": f"Token {API_KEY}"},
        params={"by_code": CODES},
        timeout=10,
    )
    if r.status_code == 402:
        # Quota exhausted. Serve the last good value rather than failing, and
        # lengthen INTERVAL — do not retry immediately.
        return _cache["data"]
    r.raise_for_status()

    _cache.update(data=r.json()["data"], at=time.time())
    return _cache["data"]
const INTERVAL = 30 * 60 * 1000; // 30 minutes — fits the free plan
// Developer and above: 5 * 60 * 1000

let cache = { data: null, at: 0 };

async function latest() {
  if (cache.data && Date.now() - cache.at < INTERVAL) return cache.data;

  const res = await fetch(
    "https://api.oilpriceapi.com/v1/prices/latest?by_code=" +
      "BRENT_CRUDE_USD,WTI_USD,NATURAL_GAS_USD",
    { headers: { Authorization: `Token ${process.env.OILPRICEAPI_KEY}` } },
  );
  if (res.status === 402) return cache.data; // quota spent — serve last good value
  if (!res.ok) throw new Error(`OilPriceAPI ${res.status}`);

  cache = { data: (await res.json()).data, at: Date.now() };
  return cache.data;
}

Series that publish on a schedule

Refined products, inventories and report-driven series publish on a cadence, not continuously. Polling them every five minutes spends quota re-reading yesterday's number.

Series familyPublishesPoll
Spot crude, gas, metalscontinuouslyevery 5 minutes
Refined products (diesel, gasoline)~twice a dayevery 6 hours
Weekly inventory and rig countsweeklyonce a day
Monthly reports (OPEC, EIA STEO)monthlyonce a day
# A monthly series does not need a fast timer — one check a day is generous.
SCHEDULES = {
    "BRENT_CRUDE_USD": 300,      # 5 minutes
    "DIESEL_USD":      21600,    # 6 hours
    "US_RIG_COUNT":    86400,    # daily
}

If you are already seeing 402

Lengthen the interval and batch your codes before upgrading. Most integrations that hit the wall are polling several times a minute for data that changes every few minutes — the fix is usually a timer change, not a bigger plan.

Rate Limit Error Responses

429 Too Many Requests

When you exceed rate limits:

{
  "error": "Rate limit exceeded",
  "error_code": "RATE_LIMIT_EXCEEDED",
  "message": "Too many requests. Upgrade for higher limits.",
  "retry_after": 37
}

Response Headers on 429

HTTP/2 429
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 37
Retry-After: 37

Retry-After and X-RateLimit-Reset are seconds until the window resets, not a timestamp. Sleep for that long and retry.

Best Practices

1. Cache Responses

class CachedAPIClient {
  constructor(apiKey, cacheTTL = 300000) {
    // 5 minutes default
    this.apiKey = apiKey;
    this.cache = new Map();
    this.cacheTTL = cacheTTL;
  }

  async fetch(endpoint, params = {}) {
    const cacheKey = `${endpoint}:${JSON.stringify(params)}`;
    const cached = this.cache.get(cacheKey);

    if (cached && Date.now() - cached.timestamp < this.cacheTTL) {
      console.log("Cache hit");
      return cached.data;
    }

    const response = await fetch(
      `https://api.oilpriceapi.com/v1${endpoint}?${new URLSearchParams(params)}`,
      { headers: { Authorization: `Token ${this.apiKey}` } },
    );

    const data = await response.json();

    this.cache.set(cacheKey, {
      data,
      timestamp: Date.now(),
    });

    return data;
  }
}

2. Use Webhooks for Real-time Updates

Instead of polling, use webhooks (available on Starter and above):

# Instead of polling every minute
# ❌ while True:
#     data = fetch_prices()
#     time.sleep(60)

# ✅ Set up a webhook endpoint
from flask import Flask, request

app = Flask(__name__)

@app.route('/webhook/prices', methods=['POST'])
def handle_price_update():
    data = request.json
    # Process real-time price update
    return '', 200

3. Implement Request Queuing

class RequestQueue {
  constructor(apiKey, requestsPerMinute = 100) {
    this.apiKey = apiKey;
    this.queue = [];
    this.interval = 60000 / requestsPerMinute;
    this.processing = false;
  }

  async add(endpoint, params) {
    return new Promise((resolve, reject) => {
      this.queue.push({ endpoint, params, resolve, reject });
      if (!this.processing) {
        this.process();
      }
    });
  }

  async process() {
    this.processing = true;

    while (this.queue.length > 0) {
      const { endpoint, params, resolve, reject } = this.queue.shift();

      try {
        const response = await fetch(
          `https://api.oilpriceapi.com/v1${endpoint}`,
          {
            headers: { Authorization: `Token ${this.apiKey}` },
            params,
          },
        );
        resolve(await response.json());
      } catch (error) {
        reject(error);
      }

      // Wait before next request
      await new Promise((r) => setTimeout(r, this.interval));
    }

    this.processing = false;
  }
}

Monitoring Your Usage

Track Usage in Your Application

import logging
from datetime import datetime

class UsageMonitor:
    def __init__(self):
        self.requests_made = 0
        self.monthly_remaining = None
        self.rate_limit_remaining = None

    def log_response(self, response):
        # Extract headers
        self.monthly_remaining = response.headers.get('X-Monthly-Remaining')
        self.rate_limit_remaining = response.headers.get('X-RateLimit-Remaining')
        self.requests_made += 1

        # Log if approaching limits
        if self.monthly_remaining and int(self.monthly_remaining) < 100:
            logging.warning(f"Only {self.monthly_remaining} monthly requests remaining!")

        if self.rate_limit_remaining and int(self.rate_limit_remaining) < 10:
            logging.warning(f"Only {self.rate_limit_remaining} requests remaining in rate limit window!")

Dashboard Monitoring

Monitor your usage at oilpriceapi.com/dashboard:

  • Real-time request count
  • Usage by endpoint
  • Error rates
  • Response times
  • Geographic distribution

Upgrading Your Plan

If you consistently hit rate limits, consider upgrading:

  1. Monitor your usage patterns
  2. Calculate required limits
  3. Visit oilpriceapi.com/pricing
  4. Upgrade instantly without downtime

Related Documentation

  • Authentication
  • Error Handling
  • Webhooks
  • Production Checklist
Last Updated: 8/23/26, 12:26 PM
Prev
Service Level Agreement (SLA)
Next
Troubleshooting Guide