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_KEY2. 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"])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 $/BBLThree 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"])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))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: float64That 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
| Topic | What EIA says | Source |
|---|---|---|
| Key | Free, sent to the email address you register. EIA's FAQ suggests whitelisting [email protected] if it does not arrive. | EIA API FAQs |
| Rows per response | 5,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 |
| Throttle | Not 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 field | None 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"],
)WTI_SPOT_USD 89.62 2026-11-11 eia_api current
BRENT_SPOT_USD 92.14 2026-11-11 eia_api currentSame 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_statusfield (currentin 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, seriesRWTCandRBRTE: all EIA outputs above, retrieved Nov 13, 2026 (UTC). - api.oilpriceapi.com
/v1/prices/latest, codesWTI_SPOT_USDandBRENT_SPOT_USD: retrieved Nov 13, 2026 (UTC).