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
  • Interactive Explorer

    • Interactive API Explorer
  • Price Data

    • API Reference
    • Get Latest Prices
    • Historical Prices
    • Batch Prices
    • Excel Latest Price Gateway
    • Price Widget Endpoints
    • Price Utility Endpoints
    • Demo API (No Authentication)
    • Natural Gas Intelligence
  • Commodities

    • List Commodities
    • Get Commodity Details
    • Data Publication & Collection Schedules
  • AI Agents (MCP)

    • Get Market Brief
    • Subscriptions / Watches
    • Agent Subscriptions with MCP
  • Marine Fuels

    • Marine Fuel API
    • List Marine Fuel Ports
    • Get Port Details with Prices
    • Bunker Fuels
    • Maritime Fuels
  • Fuel Surcharges

    • Fuel Surcharge API
  • Premium Endpoints

    • All Prices API - One Call, All Commodities
    • Cushing Oil Storage Intelligence API
    • Drilling Intelligence API
    • Marine Fuels API
    • ICE Brent Futures API
    • Benchmark Close
    • Aviation Fuel Pilot
  • Spreads & Margins

    • Spreads & Margins
    • Crack Spreads
    • European Gasoil Crack
    • Basis Spreads
    • Refining Margins
    • Curve Structure
    • Physical Premiums
  • Market Indicators

    • Market Indicators
    • Fuel Switching
    • Price Context
    • Storage Analytics
    • CFTC Positioning
    • Congressional Trades
    • Market Annotations
  • Futures

    • Futures API
    • ICE Brent Futures
    • ICE WTI Futures
    • Continuous Futures (Brent, WTI)
    • ICE Gas Oil Futures
    • NYMEX Natural Gas Futures
    • ICE EUA Carbon Futures
    • EU Carbon Futures
    • TTF Gas Futures
    • LNG JKM Futures
    • UK Carbon Futures
  • Dark Data (Premium)

    • Energy Intelligence API
    • Rig Counts
    • Well Permits
    • Well Production Beta
    • Drilling Intelligence (Public)
    • Oil Inventories
    • OPEC Production
    • Drilling Productivity
    • Forecasts (STEO)
  • Well Production (Beta)

    • Well Production Data
    • Well Production API Reference
  • Analytics

    • Analytics API
  • Webhooks API

    • Webhooks API Reference
    • Webhook Endpoint Reference
  • Alerts

    • Price Alerts API
  • Account & Billing

    • Account API
    • API Keys
    • Subscriptions
    • Organizations

Get Latest Prices

Status: βœ… Available | πŸ•’ Source timestamps | πŸ“Š Catalog endpoint

Returns the latest available price for a commodity, with source and freshness context when available. Call with a single by_code, or pass a comma-separated list to get several at once. Called with no by_code, it returns a single default commodity (BRENT_CRUDE_USD) β€” not every commodity.

GET/v1/prices/latest

Authentication

See Authentication Guide for API key setup.

Parameters

ParameterTypeRequiredDescription
by_codestringNoCommodity code, or several comma-separated (e.g. WTI_USD,BRENT_CRUDE_USD). Omit to get the default code.

Response

Success (200) β€” Single commodity

data is a flat price object (not nested by commodity code):

{
  "status": "success",
  "data": {
    "price": 68.58,
    "formatted": "$68.58",
    "currency": "USD",
    "code": "WTI_USD",
    "created_at": "2026-07-03T13:43:01.099Z",
    "updated_at": "2026-07-03T13:43:01.099Z",
    "type": "spot_price",
    "unit": "barrel",
    "source": "market_reporting",
    "data_status": "current",
    "freshness": {
      "status": "current",
      "age_seconds": 275,
      "expected_max_age_seconds": 1800
    },
    "changes": {
      "24h": {
        "amount": 0.72,
        "percent": 1.06,
        "previous_price": 67.86,
        "previous_timestamp": "2026-07-02T13:43:01Z",
        "measured_at": "2026-07-03T13:43:01Z",
        "span_hours": 24.0
      },
      "7d": { "amount": -1.01, "percent": -1.45, "previous_price": 69.59 },
      "30d": { "amount": -26.41, "percent": -27.8, "previous_price": 94.99 },
      "90d": { "amount": -42.96, "percent": -38.52, "previous_price": 111.54 }
    },
    "metadata": {
      "source": "market_reporting",
      "source_description": "Investing.com"
    }
  }
}

Success (200) β€” Multiple commodities

When you pass several codes, prices come back as an array under data.prices (not keyed by code), alongside a data.metadata block:

{
  "status": "success",
  "data": {
    "prices": [
      {
        "price": 68.58,
        "code": "WTI_USD",
        "formatted": "$68.58",
        "currency": "USD",
        "type": "spot_price",
        "unit": "barrel",
        "data_status": "current"
      },
      {
        "price": 71.98,
        "code": "BRENT_CRUDE_USD",
        "formatted": "$71.98",
        "currency": "USD",
        "type": "spot_price",
        "unit": "barrel",
        "data_status": "current"
      }
    ],
    "metadata": {
      "request_id": "6390b34903a8d3f0",
      "timestamp": "2026-07-03T13:47:37Z",
      "version": "v1"
    }
  }
}

Response Fields

Note: For a single code the price object sits directly in data. For multiple codes the objects are in the data.prices array.

FieldTypeDescription
pricenumberCurrent price value (rounded to 2 decimals)
formattedstringPrice formatted with currency symbol (e.g., "$68.58")
currencystringPrice currency code (USD, EUR, GBP)
codestringCommodity code (e.g., "WTI_USD")
created_atstringISO 8601 timestamp with milliseconds (UTC)
observed_atstringISO 8601 source observation timestamp when available; for source-dated daily data this is the reference date
source_datestringDate-only source reference date when available, e.g. OPEC Basket daily values
updated_atstringISO 8601 timestamp of the last update (UTC)
typestringPrice type, e.g. "spot_price"
unitstringUnit of measure (e.g. "barrel")
sourcestringData source identifier
data_statusstringFreshness label β€” "current", or "stale" when the point is outside its series' expected cadence
freshnessobjectstatus, age_seconds, expected_max_age_seconds β€” how recent the point is
staleboolPresent and true only when the point is outside its series' cadence. Read this β€” see Stale prices
age_daysnumberWhole days since the point was last real (as_of). Present on every price
stale_reasonstringWhy the point is stale, e.g. "no_recent_data", "discontinued". Present when stale is true
as_ofstringISO 8601 moment the price was last a genuine reading from its source. On a carried-forward row it resolves back to the real print being copied, so a copy chain cannot make a dead series look fresh. age_days is measured from it
collected_atstringISO 8601 timestamp of when we fetched and stored the row. On a fresh observation this is approximately as_of; on a carried-forward row it is later β€” the gap is how long the value has been carried
syntheticbooltrue when the row is a carried-forward copy of an earlier price (served with source: "heartbeat") rather than a fresh source observation; false for genuine source readings
changesobjectPrice moves keyed by window (24h, 7d, 30d, 90d) β€” see Price changes below
changes_omittedobjectPresent when one or more change windows are withheld β€” reason, omitted_periods, message. It can appear alongside surviving changes windows
instrument_disclosureobjectPresent only on codes that are calendar-month average swaps rather than spot prices (SINGAPORE_JET_KEROSENE_USD, SINGAPORE_MOGAS_92_USD). Omitted from CSV output β€” see Swap disclosures
metadata.discoverobjectOptional and additive: { "endpoint", "description" } suggesting one endpoint your account has not called yet. Absent for most accounts and for every non-hinted request; safe to ignore
metadata.sourcestringData source identifier
metadata.source_descriptionstringHuman-readable description of the data source

Stale prices

A commodity code stays valid after its source stops publishing. When that happens the endpoint does not 404 β€” a polling client would see an error where the correct answer is "this is the last real value, and it is old." Instead the price is served with its age stated:

{
  "status": "success",
  "data": {
    "price": 34.04,
    "code": "CME_COAL_USD",
    "created_at": "2020-03-27T12:00:00.000Z",
    "stale": true,
    "age_days": 2350,
    "data_status": "stale",
    "stale_reason": "discontinued",
    "discontinued": true,
    "as_of": "2020-03-27T12:00:00.000Z",
    "changes_omitted": {
      "reason": "measurement_older_than_window",
      "omitted_periods": ["24h", "30d", "7d"],
      "message": "Some price-change windows were omitted because the most recent observation is older than the window it would have been labelled with. See `as_of` for when this price was last real."
    }
  }
}

This is a real response for CME_COAL_USD (a discontinued futures series), captured 2026-09-02 and trimmed to the freshness fields β€” a good code to try this against yourself. A series that has merely paused publishing looks the same, with stale_reason: "no_recent_data" instead of "discontinued".

price alone is not enough to act on. Check stale (or data_status) on every response and decide what your system should do with an old number β€” that decision belongs to you, not to us. age_days and as_of tell you how old it is.

No changes block is returned for a stale price. Every comparison window is measured from the served point's own timestamp, so on a series that has stopped publishing a "24h change" would describe the last day that existed, not the last day β€” which reads as recent movement when there has been none.

Single code and multiple codes behave differently

This is the one asymmetry worth knowing about, because it changes which codes you get back:

RequestA stale code is…
by_code=CME_COAL_USD (one code)returned, with stale: true and age_days
by_code=CME_COAL_USD,BRENT_CRUDE_USD (several)withheld from prices and named in missing

With several codes, anything outside the freshness window is moved into a sibling missing array rather than sitting in prices where a dashboard would render it as live (response captured 2026-09-02; the counters drift daily):

{
  "status": "success",
  "data": {
    "prices": [{ "code": "BRENT_CRUDE_USD", "price": 95.44, "stale": false }],
    "missing": [
      {
        "code": "CME_COAL_USD",
        "reason": "discontinued",
        "discontinued": true,
        "last_seen": "2025-12-19T00:00:18Z",
        "days_stale": 257,
        "message": "CME Coal Futures (Discontinued) has been discontinued and returned no current price."
      }
    ]
  }
}

reason is one of stale, discontinued, no_data (a known code that has never produced a price) or unknown. Two codes are enough to get this behaviour. The same information is also returned in an X-Missing-Codes response header, so CSV callers see it too.

Practical consequence: batching your codes into one request gives you the stale check for free. Anything that cannot be honestly served arrives in missing, with a reason, instead of as a number you have to validate yourself.

A single GET /v1/prices/latest accepts up to 20 comma-separated codes and counts as one request against your plan. A 21st code returns 400:

{
  "status": "fail",
  "data": {
    "error": "Too many commodity codes requested (max: 20, requested: 21)"
  }
}

So twenty codes fetched one at a time cost twenty requests and give you twenty prices you must each check; the same twenty in one call cost one request and hand you the problems already separated out.

Swap disclosures

Two codes are exchange-traded calendar-month average futures, not spot assessments: SINGAPORE_JET_KEROSENE_USD (NYMEX ch. 670) and SINGAPORE_MOGAS_92_USD (NYMEX ch. 720). They are stored and routed as type: "spot_price", so the response states what the number is in an instrument_disclosure object next to the price:

{
  "status": "success",
  "data": {
    "price": 144.3,
    "code": "SINGAPORE_JET_KEROSENE_USD",
    "type": "spot_price",
    "unit": "barrel",
    "instrument_disclosure": {
      "instrument": "calendar_month_average_swap",
      "display_name": "Singapore Jet Kerosene (Platts) calendar-month average futures",
      "underlying": "Platts Asia-Pacific Marketscan, Singapore Cargoes of Jet Kerosene",
      "quote_basis": "contract_month_average",
      "contract_reference": "NYMEX ch. 670 (Globex AKS)",
      "settlement": "Settles to the arithmetic average of the Platts Singapore quotations for each business day the assessment is determined during the contract month. …",
      "averaging": "Do not average this series to obtain a monthly average. Each point already references the whole contract month, so averaging them averages a moving blend of forward and realised fixings. The realised monthly average is a single number: the final settlement of that month's contract.",
      "identity_caveat": "…",
      "contract_month": "2026-10"
    }
  }
}
FieldDescription
instrumentcalendar_month_average_swap
quote_basiscontract_month_average β€” each quote references a whole contract month, not the day you fetched it
contract_referenceThe exchange rulebook chapter and Globex code the series tracks
settlement, averaging, identity_caveatProse explaining how the contract settles, why averaging the daily series does not give a monthly average, and what the series does not yet identify
contract_monthYYYY-MM delivery month of this observation. Present only when the stored row records it; consecutive observations can reference different months

The same object is attached to GET /v1/commodities/{code} for these codes, and the series endpoints (past_*, historical) carry it once per response under metadata.instrument_disclosures[] instead of on every row. CSV responses omit it.

Price changes

Every /v1/prices/latest response carries a changes object. There is no parameter to request it β€” it is always included, and it costs no additional API call.

Each window is an object:

FieldTypeDescription
amountnumberAbsolute move in the price currency (e.g. 0.72 = up $0.72)
percentnumberPercentage move (e.g. 1.06 = up 1.06%)
previous_pricenumberThe price compared against
previous_timestampstringISO 8601 timestamp of that earlier price
measured_atstringISO 8601 timestamp of the current price
span_hoursnumberHours actually spanned between the two points β€” may exceed the window label
const pct = response.data.changes["24h"].percent; // 1.06

span_hours β€” read this before rendering a daily ticker

A 24h change legitimately spans longer than 24 hours when the market was closed. A Monday quote compares against Friday, so span_hours will read around 72.0. This is correct, not an error β€” we publish the span we actually measured rather than asserting the label, so you can decide whether to render it.

If you display a "today's move" figure, check span_hours before labelling it as such.

A window may be absent β€” check before reading it

When no honest comparison exists, the window is omitted rather than reported as zero. This happens when no comparison price was published within the permitted lookback (for 24h, a generous multi-day allowance that absorbs weekends and holidays, but not a weekly-published series), or when the change measurement itself is older than the window it would be labelled with. Omission is evaluated per window: valid windows remain under changes, while changes_omitted.omitted_periods names the windows withheld. The entire changes object is absent only when no window survives.

Omission is deliberate: a fabricated percentage is worse than no percentage. Sparsely-published commodities may never carry a 24h window at all.

Always guard before reading:

const c = response.data.changes?.["24h"];
if (c) console.log(`${c.percent}% over ${c.span_hours}h`);

Embedded devices should do the same β€” on Arduino, use an ArduinoJson filter to keep only the fields you need rather than growing the document buffer.

Windows available by plan

PlanWindows returned
Free, Developer, Exploration24h
Starter24h, 7d
Professional, Production24h, 7d, 30d
Scale, Reservoir, Enterprise24h, 7d, 30d, 90d

Windows above your plan are omitted from the object β€” the response shape is otherwise unchanged.

Errors

CodeStatusDescription
UNAUTHORIZED401Missing or invalid API key (returned as error.code)
invalid_code400Unknown commodity code (returned under data with status: "fail")

invalid_code body

An unknown code returns near-miss suggestions (up to 12 codes) and describes the first three in did_you_mean[] so you can recognise the commodity without a second call:

{
  "status": "fail",
  "data": {
    "error": "invalid_code",
    "message": "Code 'SINGAPORE_JET_KERO' not found. Did you mean: 'SINGAPORE_JET_KEROSENE_USD'? See /v1/commodities for all available codes.",
    "suggestions": ["SINGAPORE_JET_KEROSENE_USD"],
    "invalid_codes": ["SINGAPORE_JET_KERO"],
    "did_you_mean": [
      {
        "code": "SINGAPORE_JET_KEROSENE_USD",
        "name": "Singapore Jet Kerosene (Platts) Calendar-Month Average Futures",
        "description": "Singapore Jet Kerosene (Platts) futures, NYMEX chapter 670. …",
        "available": true
      }
    ],
    "all_codes_url": "https://www.oilpriceapi.com/v1/prices/codes"
  }
}
FieldDescription
did_you_mean[].codeA callable code
did_you_mean[].name, descriptionCatalog name and description, when the code has a catalog entry
did_you_mean[].availableAlways present. true when the code appears in the default /v1/commodities listing
did_you_mean[].unavailable_reasonPresent only when available is false. The code is still callable; it is omitted from the default listing because it is not backed by data β€” GET /v1/commodities?include_unavailable=true lists it

When a suggested code is not in the default listing the message says so and points at ?include_unavailable=true rather than at the bare listing. Spellings customers commonly type are accepted as aliases and never reach this error β€” for Western Canadian Select, WCS, CANADIAN_CRUDE, CANADIAN_CRUDE_USD, WESTERN_CANADIAN_SELECT and WESTERN_CANADIAN_SELECT_USD all resolve to WCS_CRUDE_USD.

Examples

# Get the default commodity (BRENT_CRUDE_USD)
curl "https://api.oilpriceapi.com/v1/prices/latest" \
  -H "Authorization: Token YOUR_API_KEY"

# Get a specific commodity
curl "https://api.oilpriceapi.com/v1/prices/latest?by_code=WTI_USD" \
  -H "Authorization: Token YOUR_API_KEY"

# Get multiple commodities (returned as a data.prices array)
curl "https://api.oilpriceapi.com/v1/prices/latest?by_code=WTI_USD,BRENT_CRUDE_USD" \
  -H "Authorization: Token YOUR_API_KEY"

# Get the latest official OPEC Reference Basket price
curl "https://api.oilpriceapi.com/v1/prices/latest?by_code=OPEC_BASKET_USD" \
  -H "Authorization: Token YOUR_API_KEY"

OPEC Basket responses include source-date fields:

{
  "status": "success",
  "data": {
    "price": 69.33,
    "formatted": "$69.33",
    "currency": "USD",
    "code": "OPEC_BASKET_USD",
    "created_at": "2026-07-02T12:00:00.000Z",
    "observed_at": "2026-07-02T12:00:00.000Z",
    "source_date": "2026-07-02",
    "type": "spot_price",
    "unit": "barrel",
    "source": "opec.org"
  }
}
// JavaScript
async function getLatestPrice() {
  const response = await fetch(
    "https://api.oilpriceapi.com/v1/prices/latest?by_code=WTI_USD",
    {
      headers: {
        Authorization: "Token YOUR_API_KEY",
      },
    },
  );

  const data = await response.json();

  // For a single code, the price object is directly in data.data
  console.log(`Current price: ${data.data.formatted}`); // "$68.58"
  console.log(`Numeric value: ${data.data.price}`); // 68.58
  console.log(`24h change: ${data.data.changes["24h"].percent}%`);
  console.log(`Source: ${data.data.metadata.source_description}`);

  return data.data.price;
}
# Python
import requests

def get_latest_price(commodity_code='WTI_USD'):
    url = 'https://api.oilpriceapi.com/v1/prices/latest'
    headers = {'Authorization': 'Token YOUR_API_KEY'}
    params = {'by_code': commodity_code}

    response = requests.get(url, headers=headers, params=params)
    data = response.json()

    price = data['data']['price']
    formatted = data['data']['formatted']

    print(f"Current {commodity_code}: {formatted}")
    return price

# Get multiple commodities. With several codes, prices are in data['prices']
def get_multiple_prices(codes=['WTI_USD', 'BRENT_CRUDE_USD', 'NATURAL_GAS_USD']):
    url = 'https://api.oilpriceapi.com/v1/prices/latest'
    headers = {'Authorization': 'Token YOUR_API_KEY'}
    params = {'by_code': ','.join(codes)}

    response = requests.get(url, headers=headers, params=params)
    prices = response.json()['data']['prices']  # a list, not keyed by code
    return {p['code']: p for p in prices}
# Ruby
require 'net/http'
require 'json'
require 'uri'

def get_latest_price(commodity_code = 'WTI_USD')
  uri = URI("https://api.oilpriceapi.com/v1/prices/latest?by_code=#{commodity_code}")

  http = Net::HTTP.new(uri.host, uri.port)
  http.use_ssl = true

  request = Net::HTTP::Get.new(uri)
  request['Authorization'] = 'Token YOUR_API_KEY'

  response = http.request(request)
  data = JSON.parse(response.body)

  price = data['data']['price']
  puts "Current #{commodity_code}: $#{price}"

  price
end

Conditional requests

Most polling returns a price you already have. Across all customers, a request is served roughly 200 times for every new value published on the busiest codes, and far more than that on slower ones. Conditional requests let you stop paying for those.

Every response carries validators:

etag: W/"e89ed6a199b9e26d3389cd3e2d5e0464"
last-modified: Tue, 25 Aug 2026 10:04:19 GMT

Send the ETag back on your next call and you get 304 Not Modified with an empty body if nothing has changed:

curl -H "Authorization: Token YOUR_KEY" \
     -H 'If-None-Match: W/"e89ed6a199b9e26d3389cd3e2d5e0464"' \
     "https://api.oilpriceapi.com/v1/prices/latest?by_code=BRENT_CRUDE_USD"

A 304 does not count against your request quota. It transferred no data, so it is not charged. A conditional request that misses β€” because the price did change β€” returns a normal 200 with the new body and is charged like any other call.

It still consumes one slot in the 60-per-60-second rate limit, so this raises how much you can poll within your plan, not how fast.

Practically, this means you can poll at whatever interval your application needs and pay only for the values that actually moved. A display refreshing every minute against a benchmark that settles once a day costs you one charged request a day, not 1,440.

Last-Modified and If-Modified-Since work the same way if that fits your client better.

Rate Limits

All plans share the same request rate limit of 60 requests per rolling 60-second window per API key. Plans differ by request quota, not by request rate β€” and the quota window differs too: the Free plan is metered per day, the paid plans per month. Bursts within the window are fine. See Rate Limiting for details.

PlanRequest quotaRequest Rate Limit
Free50 per day60 per rolling 60s
Developer10,000 per month60 per rolling 60s
Starter50,000 per month60 per rolling 60s
Professional100,000 per month60 per rolling 60s
Scale1,000,000 per month60 per rolling 60s

Rate limit headers are included in responses. X-RateLimit-Limit / -Remaining / -Used track your monthly quota:

X-RateLimit-Limit: 10000
X-RateLimit-Remaining: 9876
X-RateLimit-Reset: 1785542399

Best Practices

  • Cache responses: Use the response freshness block and source timestamps to decide when to refetch; cadence varies by source, market hours, dataset, and plan
  • Request specific commodities: Use by_code when possible, comma-separated for several at once
  • Handle rate limits: Check X-RateLimit-* headers and implement backoff

Related Endpoints

  • Historical Prices - Past price data
  • Commodities List - Available commodity codes
  • WebSocket Transport - Availability depends on account entitlement
Last Updated: 9/5/26, 8:08 PM
Prev
API Reference
Next
Historical Prices