OilPriceAPI Docs
Quick Start
API Reference
  • SDKs and languages
  • Tutorials
  • Articles
  • Documentation directory
  • Quick reference
  • API explorer
  • FAQ
  • Changelog
  • Status
  • Dashboard
GitHub
Quick Start
API Reference
  • SDKs and languages
  • Tutorials
  • Articles
  • Documentation directory
  • Quick reference
  • API explorer
  • FAQ
  • Changelog
  • Status
  • Dashboard
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 and Operational Targets
    • 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. Exhausting it returns 402 Payment Required (error_code: "PAYMENT_REQUIRED") on the Free tier, and 429 Too Many Requests with error_code: "MONTHLY_QUOTA_EXCEEDED" (or "TRIAL_LIMIT_EXCEEDED" during the trial) on paid plans and trials. See Rate Limit Error Responses.
  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 with error_code: "RATE_LIMIT_EXCEEDED".

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

Current tier limits are machine-readable at GET https://api.oilpriceapi.com/v1/meta/limits (no key required); this table is rendered from it at build time. Read limits from the endpoint rather than hardcoding them.

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 quota, or key suspended5 per rolling 60sAPI key
Marine-fuel endpoints30 per rolling 60sAPI key
Marine-fuel historical5 per rolling 60sAPI key

Once you have exhausted your quota (or the key is suspended), your rate limit drops from 60/min to 5/min until the quota resets — so a client that ignores the quota response and keeps hammering will also start seeing the rolling-limit 429 (RATE_LIMIT_EXCEEDED).

Free Trial

New accounts receive a free trial with:

  • Commodity access according to the account's dataset entitlements
  • Full API functionality (Professional-level features)
  • No credit card required

The Free plan includes 50 requests per day. New accounts start with a 7-day free trial of 10,000 requests; paid plans carry quotas from 10,000 / month (Developer) to 1,000,000 / month (Scale).

After the trial ends, the account continues on the Free tier. Upgrade when you need higher 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. Paid-plan quotas reset on the 1st of each month at 00:00 UTC; the Free quota resets daily. The exact time is in every response's X-RateLimit-Reset header and, on a quota block, in the body's reset.date. 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 request quota. The window is daily on Free and monthly on paid plans; X-RateLimit-Window says which:

HeaderDescriptionExample
X-RateLimit-LimitYour plan's request quota for the current window10000
X-RateLimit-RemainingRequests remaining in the current window9876
X-RateLimit-UsedRequests used in the current window124
X-RateLimit-ResetUnix timestamp when the quota window resets1785542399
X-RateLimit-WindowCounting window: daily_counter (Free), monthly_counter (paid), trial_counter (trial)monthly_counter
X-RateLimit-StateEntitlement state, e.g. paid_active, trial_active, free, exhausted, unconfirmed_cappedpaid_active
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

Treat Link as an optimisation, not a contract: it is not present on every response. X-Total-Pages with page=N is always available and is the reliable way to page.

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

Two different blocks share the 429 status, so branch on error_code, not on the status alone.

Quota exhausted — 402 on Free, 429 on paid plans and trials

When the request quota for the window is used up, the status depends on the plan:

PlanStatuserrorerror_code
Free (after the trial)402Payment RequiredPAYMENT_REQUIRED
Free, trial has just ended402Trial expiredTRIAL_EXPIRED
Trial, 10,000 used429Trial request limit exceededTRIAL_LIMIT_EXCEEDED
Paid plan429Monthly request limit exceededMONTHLY_QUOTA_EXCEEDED

MONTHLY_QUOTA_EXCEEDED is a fixed client contract and keeps that name even when the window is daily; the reset object and the headers carry the actual window.

The body is the same shape for all of them. Values below are illustrative:

{
  "error": "Payment Required",
  "error_code": "PAYMENT_REQUIRED",
  "block_reason": "request_limit_exceeded",
  "message": "You've used all 50 requests for Free tier today",
  "current_usage": {
    "used": 50,
    "limit": 50,
    "remaining": 0,
    "percentage_used": 100.0,
    "tier": "free"
  },
  "entitlement": { "...": "the entitlement snapshot the decision was made from" },
  "reset": {
    "date": "2026-08-31T00:00:00Z",
    "days_until_reset": 1,
    "human_readable": "Your limit resets on August 31, 2026"
  },
  "options": {
    "upgrade": {
      "message": "Upgrade to restore access",
      "url": "https://www.oilpriceapi.com/pricing?...",
      "recommended_tier": { "name": "Developer", "requests": 10000, "price_monthly": 19.0, "price_yearly": 171.0 }
    },
    "purchase_credits": { "message": "Buy one-time credit packs for temporary overages", "url": "https://www.oilpriceapi.com/pricing#credits", "endpoint": "POST /billing/credit_purchases" },
    "contact_sales": { "message": "Need custom limits? Contact us for enterprise pricing", "email": "karl@oilpriceapi.com" }
  },
  "efficiency": {
    "message": "Before upgrading, check your polling interval. ...",
    "fastest_series_update_seconds": 150,
    "suggested_interval_seconds": 300,
    "requests_per_day_at_suggested_interval": 288,
    "note": "Refined products such as DIESEL_USD and GASOLINE_USD update roughly twice a day. ...",
    "documentation": "https://docs.oilpriceapi.com/guides/rate-limiting#if-you-are-already-seeing-402"
  },
  "reduce_usage": {
    "summary": "These cost nothing and often remove the need for a larger plan.",
    "conditional_requests": {
      "action": "Send the ETag from your previous response back as If-None-Match.",
      "effect": "An unchanged price returns 304 Not Modified, which does not count against your quota.",
      "docs": "https://docs.oilpriceapi.com/api-reference/prices/latest#conditional-requests"
    },
    "batch_codes": {
      "action": "Request several codes in one call: by_code=WTI_USD,BRENT_CRUDE_USD,GASOIL_USD",
      "effect": "Up to 20 codes count as a single request. Above 20 returns 400.",
      "docs": "https://docs.oilpriceapi.com/api-reference/prices/latest"
    },
    "polling_interval": {
      "action": "Match your polling interval to how often the series actually publishes.",
      "effect": "Each response carries freshness metadata showing how current the value is.",
      "docs": "https://docs.oilpriceapi.com/data/publication-schedules"
    }
  },
  "upgrade_url": "https://www.oilpriceapi.com/pricing?utm_source=api&utm_medium=402&...",
  "cta": {
    "headline": "You've hit your free daily limit — upgrade to keep building",
    "subtext": "Your existing API key keeps working the moment you upgrade — no code change.",
    "action": "Upgrade now",
    "url": "https://www.oilpriceapi.com/pricing?..."
  },
  "upgrade_options": [
    { "plan": "developer", "price": 1900, "currency": "usd", "requests": 10000, "checkout_url": "https://www.oilpriceapi.com/pricing?...&plan=developer" },
    { "plan": "starter_v2", "price": 4900, "currency": "usd", "requests": 50000, "checkout_url": "..." }
  ],
  "recommended_plan": {
    "name": "Developer",
    "plan": "developer",
    "requests": 10000,
    "price_monthly_cents": 1900,
    "checkout_url": "https://www.oilpriceapi.com/pricing?...&plan=developer",
    "matched_to_usage": 1250
  },
  "volume_matched_message": "Based on your usage (1,250 requests), Developer ($19/mo, 10,000 requests) is the right fit. ...",
  "docs": "https://www.oilpriceapi.com/pricing"
}

Field notes:

  • reset.date is when the quota window reopens (the next day on Free, the 1st of next month on paid plans, the trial end for TRIAL_LIMIT_EXCEEDED). days_until_reset is whole days from today; human_readable is the same date as a sentence.
  • current_usage and reset are present on every quota block. options, efficiency, and reduce_usage are present on both the 402 and the quota 429.
  • upgrade_url, cta, upgrade_options, recommended_plan, volume_matched_message, and docs are only on the Free-tier 402. recommended_plan and options.upgrade.recommended_tier are sized on your recent attempted volume, including the requests that were refused.
  • trial (active, expired, days_remaining, ends_at, requests_used, request_limit) is present for trial and just-expired-trial accounts.
  • options.confirm_email is present when the account's email is unconfirmed (the unconfirmed cap is lower than the plan's quota). On paid and trial accounts the error_code is then EMAIL_CONFIRMATION_REQUIRED; on the Free tier it stays PAYMENT_REQUIRED.
  • reduce_usage lists the three ways to need fewer requests, all free: conditional requests (a 304 is not charged), batching up to 20 codes in one call (billed once), and matching your interval to the series' publication schedule.

Headers on a quota block carry the same X-RateLimit-* set as a 200, with X-RateLimit-Remaining: 0, X-RateLimit-State: exhausted, X-RateLimit-Reset as a Unix timestamp, and Retry-After as the number of seconds until that reset — which can be hours or days. Do not retry before it.

429 Too Many Requests — rolling rate limit

When you exceed 60 requests in a rolling 60-second window:

{
  "error": "Rate limit exceeded",
  "error_code": "RATE_LIMIT_EXCEEDED",
  "message": "Too many requests. Upgrade for higher limits.",
  "retry_after": 37,
  "upgrade": { "url": "https://www.oilpriceapi.com/pricing?...", "plans": [ "..." ] }
}

Response Headers on the rolling-limit 429

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

On this response Retry-After and X-RateLimit-Reset are seconds until the window resets, not a timestamp. Sleep for that long and retry. (On a quota block, X-RateLimit-Reset is a Unix timestamp — see above.)

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.quota_remaining = None
        self.quota_window = None

    def log_response(self, response):
        # X-RateLimit-* headers describe the quota window (daily on Free,
        # monthly on paid plans), not the 60-second rate limit.
        self.quota_remaining = response.headers.get('X-RateLimit-Remaining')
        self.quota_window = response.headers.get('X-RateLimit-Window')
        self.requests_made += 1

        # Log if approaching the quota
        if self.quota_remaining and int(self.quota_remaining) < 100:
            logging.warning(f"Only {self.quota_remaining} requests remaining in {self.quota_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: 9/5/26, 8:42 PM
Prev
Service Level Agreement and Operational Targets
Next
Troubleshooting Guide