OilPriceAPI insights

EIA API in Python: pull oil price series into pandas

By OilPriceAPI · Published · All samples run Nov 13, 2026 (UTC) with Python 3.14.7, requests 2.34.2 and pandas 3.1.1

The U.S. Energy Information Administration's API v2 is free of charge and returns JSON, so a daily WTI or Brent spot series is one requests.get call away. The parts that take longer are finding the route and series IDs, and paging past the 5,000-row cap. This walk-through does all three for the petroleum spot-price route, and prints the output each script gave on Nov 13, 2026.

For the API's rules without the Python, see our EIA API v2 reference page. The last section shows the same two series from OilPriceAPI and says when calling EIA directly is the better choice.

1. Get a key and install two packages

Register at eia.gov/opendata/register.php; EIA emails the key. Keep it in an environment variable rather than in the script. The samples read EIA_API_KEY.

python -m pip install requests pandas
export EIA_API_KEY=YOUR_EIA_API_KEY

2. First request: the latest WTI and Brent spot prices

Data requests are GET calls to https://api.eia.gov/v2/<route>/data/. The route for petroleum spot prices is petroleum/pri/spt. A facets[series][] parameter picks the series: RWTC is WTI at Cushing and RBRTE is Europe Brent. Passing a Python list makes requests repeat the parameter, which is the form EIA's technical documentation uses for facets.

import os

import requests

API_KEY = os.environ["EIA_API_KEY"]
URL = "https://api.eia.gov/v2/petroleum/pri/spt/data/"

params = {
    "frequency": "daily",
    "data[0]": "value",
    "facets[series][]": ["RWTC", "RBRTE"],
    "sort[0][column]": "period",
    "sort[0][direction]": "desc",
    "length": 6,
}
resp = requests.get(URL, params=params, headers={"X-Api-Key": API_KEY}, timeout=30)
resp.raise_for_status()
body = resp.json()["response"]

print("total rows available:", body["total"])
for row in body["data"]:
    print(row["period"], row["series"], row["value"], row["units"])
Output, run Nov 13, 2026 (UTC):
total rows available: 20311
2026-11-11 RBRTE 92.14 $/BBL
2026-11-11 RWTC 89.62 $/BBL
2026-11-10 RBRTE 90.16 $/BBL
2026-11-10 RWTC 89.62 $/BBL
2026-11-09 RBRTE 91.03 $/BBL
2026-11-09 RWTC 90.4 $/BBL

Three things to notice. total is the number of rows matching the request (20,311 for the two series together), not the number returned. Each value arrives as a JSON string ("89.62"), so convert before doing arithmetic. And the newest row on Nov 13 was dated Nov 11, so do not treat the route as a live quote.

The documented place for the key is an api_key URL parameter. The samples send it in an X-Api-Key header instead, which returned HTTP 200 in every run here and keeps the key out of logged URLs. Without a key the same request returned HTTP 403 with error code API_KEY_MISSING; with a wrong key, 403 with API_KEY_INVALID.

3. Find routes, facets and series IDs

Drop /data/ from the URL and the API describes the route instead: its frequencies, facets and data columns. Add /facet/series to list the series IDs with their names. EIA's API browser shows the same tree in a web page.

import os

import requests

HEADERS = {"X-Api-Key": os.environ["EIA_API_KEY"]}
ROUTE = "https://api.eia.gov/v2/petroleum/pri/spt"

route = requests.get(ROUTE, headers=HEADERS, timeout=30).json()["response"]
print("frequencies:", [f["id"] for f in route["frequency"]])
print("facets:", [f["id"] for f in route["facets"]])
print("data columns:", list(route["data"].keys()))
print("default frequency:", route["defaultFrequency"])

facet = requests.get(f"{ROUTE}/facet/series", headers=HEADERS, timeout=30).json()
print("series in this route:", facet["response"]["totalFacets"])
for item in facet["response"]["facets"]:
    if item["id"] in ("RWTC", "RBRTE"):
        print(item["id"], "=", item["name"])
Output, run Nov 13, 2026 (UTC):
frequencies: ['monthly', 'weekly', 'annual', 'daily']
facets: ['duoarea', 'product', 'process', 'series']
data columns: ['value']
default frequency: weekly
series in this route: 11
RBRTE = Europe Brent Spot Price FOB (Dollars per Barrel)
RWTC = Cushing, OK WTI Spot Price FOB (Dollars per Barrel)

The route's default frequency is weekly, which is why the first script sets frequency to daily explicitly.

4. Full history into pandas, past the 5,000-row cap

One response holds at most 5,000 rows. Asking for length=6000 on this route returned 5,000 rows and a warning that reads "The maximum value for length is 5000 records; the first 5000 are returned." The loop below pages with offset until it has total rows, then builds a date-indexed frame.

import os
import time

import pandas as pd
import requests

API_KEY = os.environ["EIA_API_KEY"]
URL = "https://api.eia.gov/v2/petroleum/pri/spt/data/"
PAGE = 5000  # EIA's documented JSON row cap


def fetch_series(series_id: str) -> pd.DataFrame:
    rows, offset = [], 0
    while True:
        params = {
            "frequency": "daily",
            "data[0]": "value",
            "facets[series][]": series_id,
            "sort[0][column]": "period",
            "sort[0][direction]": "asc",
            "offset": offset,
            "length": PAGE,
        }
        resp = requests.get(
            URL, params=params, headers={"X-Api-Key": API_KEY}, timeout=60
        )
        resp.raise_for_status()
        body = resp.json()["response"]
        rows.extend(body["data"])
        offset += PAGE
        if offset >= int(body["total"]):
            break
        time.sleep(0.5)  # stay far below EIA's burst guidance
    frame = pd.DataFrame(rows)
    frame["period"] = pd.to_datetime(frame["period"])
    frame["value"] = pd.to_numeric(frame["value"], errors="coerce")
    return frame.set_index("period")[["value"]].rename(columns={"value": series_id})


wti = fetch_series("RWTC")
print(len(wti), "rows from", wti.index.min().date(), "to", wti.index.max().date())
print("missing values:", int(wti["RWTC"].isna().sum()))
print(wti.tail(3))
print(wti["RWTC"].resample("ME").mean().round(2).tail(3))
Output, run Nov 13, 2026 (UTC):
10292 rows from 1986-01-02 to 2026-11-11
missing values: 0
             RWTC
period           
2026-11-09  90.40
2026-11-10  89.62
2026-11-11  89.62
period
2026-09-30     97.10
2026-10-31    102.07
2026-11-30     93.69
Freq: ME, Name: RWTC, dtype: float64

That is 10,292 daily WTI observations from Jan 2, 1986 to Nov 11, 2026 in three requests. The last block is the monthly mean in dollars per barrel; the row labelled 2026-11-30 covers only the days published so far in November, so it is not a full-month average.

5. Limits to design around

TopicWhat EIA saysSource
KeyFree, sent to the email address you register. EIA's FAQ suggests whitelisting [email protected] if it does not arrive.EIA API FAQs
Rows per response5,000 in JSON (300 in XML). The total field gives the full count; page with offset and length.EIA API FAQs; observed in the run below
ThrottleNot published exactly. EIA's guidance: a sustained rate under about 9,000 requests per hour and a burst rate under 5 per second.EIA API FAQs
Last-updated fieldNone in API v2. EIA's FAQ says it was removed for performance reasons.EIA API FAQs

Source: EIA API FAQs, read Nov 13, 2026.

With no last-updated field, a scheduled job cannot ask whether a series changed. Request the newest few rows, compare the latest period with what you stored, and only then pull more.

6. The same two series from OilPriceAPI

OilPriceAPI carries these two EIA series as WTI_SPOT_USD and BRENT_SPOT_USD. The request needs an OilPriceAPI key, which is separate from the EIA key.

import os

import requests

resp = requests.get(
    "https://api.oilpriceapi.com/v1/prices/latest",
    params={"by_code": "WTI_SPOT_USD,BRENT_SPOT_USD"},
    headers={"Authorization": f"Token {os.environ['OILPRICEAPI_KEY']}"},
    timeout=30,
)
resp.raise_for_status()
for quote in resp.json()["data"]["prices"]:
    print(
        quote["code"],
        quote["price"],
        quote["source_date"],
        quote["source"],
        quote["data_status"],
    )
Output, run Nov 13, 2026 (UTC):
WTI_SPOT_USD 89.62 2026-11-11 eia_api current
BRENT_SPOT_USD 92.14 2026-11-11 eia_api current

Same values, same Nov 11 source date, and the response names eia_api as its source. This is a copy of EIA's data, not a different price.

When to call EIA directly

  • You need a series we do not carry. We mirror a short list of EIA series, shown on the EIA API page; EIA's API covers its whole catalog.
  • You want the full history in one free pull, as in step 4.
  • You want the original source with nothing in between. EIA is where these numbers come from.

When our copy saves work

  • You already call OilPriceAPI for other codes and want the EIA spot series in the same request and JSON shape.
  • You want the freshness check done for you: each response carries the source date and a data_status field (current in the run above).

OilPriceAPI is not a replacement for the EIA API. We ran the request above with our own test key and did not check which plans include these two codes; see pricing for plan details. FRED republishes the same two EIA series; the FRED oil price data tutorial covers that route.

Sources

  • EIA API technical documentation: request shape, facets, the 5,000-row JSON limit. Read Nov 13, 2026.
  • EIA API FAQs: throttle guidance, key delivery, row limits, the removed update field. Read Nov 13, 2026.
  • api.eia.gov route /v2/petroleum/pri/spt, series RWTC and RBRTE: all EIA outputs above, retrieved Nov 13, 2026 (UTC).
  • api.oilpriceapi.com /v1/prices/latest, codes WTI_SPOT_USD and BRENT_SPOT_USD: retrieved Nov 13, 2026 (UTC).