Gold Price API in Python: Fetch, Cache and Plot Prices
Use a gold price API in Python: get live gold prices with requests, handle errors and retries, cache responses, and plot a pandas time series with matplotlib.
Gold price API in Python: what you need
This tutorial shows how to use a gold price API in Python, from the first request to a cached client and a chart of the last ten months. It uses MetalAPI, a JSON REST API for precious metals, industrial metals, energy, currencies and crypto, together with three well-known libraries: requests for HTTP, pandas for the time series and matplotlib for the chart. Every snippet runs on Python 3.9 or newer.
Create a free account and create an API key under API keys in the dashboard. Keys start with mk_live_ and are shown only once, so copy yours straight into an environment variable. The Free plan needs no credit card and includes 100 requests a month with daily price updates, which is enough for everything in this post.
pip install requests pandas matplotlib
# macOS and Linux
export METALAPI_KEY="mk_live_your_key_here"
# Windows PowerShell
$env:METALAPI_KEY = "mk_live_your_key_here"Read the key with os.environ["METALAPI_KEY"], which fails loudly when the variable is missing, instead of pasting it into a script. If you prefer a .env file, the python-dotenv package loads it with one call to load_dotenv(). Add .env to .gitignore so the key never reaches your repository.
How to get the gold price in Python
Every MetalAPI endpoint is a GET request under https://metalapi.com/api/v1. The key can go in an api_key query parameter, an Authorization: Bearer header or an X-API-Key header. Use a header: full URLs end up in logs, and some requests exceptions print the URL with its query string. The /latest endpoint returns current prices, base sets the currency and currencies limits the response to the symbols you need:
import os
import requests
resp = requests.get(
"https://metalapi.com/api/v1/latest",
params={"base": "USD", "currencies": "XAU"},
headers={"X-API-Key": os.environ["METALAPI_KEY"]},
timeout=10,
)
data = resp.json()
if not data["success"]:
raise SystemExit(f"MetalAPI error {data['error']['code']}: {data['error']['info']}")
price = data["rates"][data["base"] + "XAU"]
print(f"Gold: {price:,.2f} {data['base']} per troy ounce")The response looks like this. The numbers are the example values used throughout the MetalAPI documentation, not live prices:
{
"success": true,
"timestamp": 1792834892,
"date": "2026-10-24",
"base": "USD",
"rates": { "XAU": 0.0002501319, "USDXAU": 3997.89 },
"rate_limit": { "limit": 100, "remaining": 99, "used": 1, "plan": "Free", "...": "..." }
}With these values the script prints Gold: 3,997.89 USD per troy ounce. rates["USDXAU"] is the price of one troy ounce of gold in the base currency. rates["XAU"] is its inverse, ounces of gold per dollar, and reading it by mistake is the most common bug in gold price code. Building the key from data["base"] keeps the script correct when you switch to base=EUR (key EURXAU) or base=INR. Add unit=gram or unit=kilogram for prices per gram or kilogram. timestamp is the time of the price data in Unix seconds, not the time of your request.
Handling gold price API errors in Python
A failed call returns an HTTP error status and a JSON body with success: false and an error object that holds a numeric code, a type and an info message. Branch on the code, never on the message text. Only a few codes are worth retrying:
104(too_many_requests, HTTP 429): the per-minute limit. Wait for the number of seconds in theRetry-Afterheader.500,503and network errors: retry with exponential backoff.- Everything else, such as
101or102(missing or invalid key),105(monthly quota used up),106(endpoint not in your plan) and the2xxvalidation errors: fix the request, key or plan instead.
This small module wraps that logic. It reuses one requests.Session, so connections stay open between calls, and raises an exception you can inspect:
import os
import time
import requests
API_URL = "https://metalapi.com/api/v1"
session = requests.Session()
session.headers["X-API-Key"] = os.environ["METALAPI_KEY"]
class MetalAPIError(Exception):
def __init__(self, code, type_, info, retry_after=None):
super().__init__(f"MetalAPI {code} ({type_}): {info}")
self.code = code
self.type = type_
self.retry_after = retry_after
def _get(path, params):
r = session.get(f"{API_URL}/{path}", params=params, timeout=10)
try:
data = r.json()
except ValueError: # not JSON, e.g. an HTML error page from a proxy
raise MetalAPIError(r.status_code, "http_error", r.reason) from None
if not data.get("success"):
err = data["error"]
retry_after = r.headers.get("Retry-After")
raise MetalAPIError(err["code"], err["type"], err["info"], int(retry_after) if retry_after else None)
return data
def call(path, params=None, retries=3):
"""GET an endpoint. Retries rate limits, server errors and network failures only."""
for attempt in range(retries + 1):
try:
return _get(path, params or {})
except (MetalAPIError, requests.RequestException) as exc:
code = getattr(exc, "code", None) # None for network errors
if attempt == retries or (code is not None and code != 104 and code < 500):
raise
time.sleep(getattr(exc, "retry_after", None) or 2**attempt)Backoff waits 1, 2 and 4 seconds before giving up. Retrying costs nothing here, because requests rejected with 104 or a server error are not billed. Validation errors are billed, which is one more reason never to retry them in a loop. The error handling guide lists every code with its HTTP status.
Caching gold prices to save requests
Each plan refreshes prices at a fixed interval: once a day on Free, every 30 minutes on Lite, every 10 minutes on Standard and every 60 seconds on Advanced and Pro. The updates fall on multiples of that interval counted from 00:00 UTC, so asking again before the next one returns the same numbers and spends quota for nothing. This cache keeps each response until the next update and serves the last good prices if the API cannot be reached:
import math
import time
import requests
from metalapi import MetalAPIError, call
# Seconds. Free: 86_400, Lite: 3_600 (hourly fits its 1,000 requests), Standard: 600
UPDATE_INTERVAL = 600
_cache = {} # (base, symbols, unit) -> (expires_at, data)
def latest(base="USD", symbols="XAU,XAG", unit="troy_oz"):
key = (base, symbols, unit)
now = time.time()
hit = _cache.get(key)
if hit and now < hit[0]:
return hit[1]
try:
data = call("latest", {"base": base, "currencies": symbols, "unit": unit})
except (MetalAPIError, requests.RequestException):
if not hit:
raise
_cache[key] = (now + 60, hit[1]) # serve the last good prices, try again in a minute
return hit[1]
next_update = (math.floor(now / UPDATE_INTERVAL) + 1) * UPDATE_INTERVAL
_cache[key] = (next_update + 5, data) # expire a few seconds after the next update
return datalatest("EUR", "XAU", "gram")["rates"]["EURXAU"] returns the euro price per gram. Ask for every symbol you need in one call: XAU,XAG,XPT,XPD costs one request, not four. Keep in mind that this cache lives in memory. A script started by cron begins with an empty cache every time, so there the schedule is the cache: run it at most once per update interval. To watch your quota, call /usage, which never counts towards it.
Loading a gold price time series into pandas
For history, the /timeframe endpoint (alias /timeseries) returns daily closing prices between start_date and end_date, keyed by date. One request covers up to 30 days on the Free plan and 365 days on paid plans, and a longer range fails with error 207. This function splits any range into allowed chunks and returns a pandas Series:
from datetime import date, timedelta
import pandas as pd
from metalapi import call
def gold_history(start, end, base="USD", max_days=30): # max_days=365 on paid plans
rates = {}
chunk_start = start
while chunk_start <= end:
chunk_end = min(chunk_start + timedelta(days=max_days - 1), end)
data = call("timeframe", {
"start_date": chunk_start.isoformat(),
"end_date": chunk_end.isoformat(),
"currencies": "XAU",
"base": base,
})
rates.update(data["rates"])
chunk_start = chunk_end + timedelta(days=1)
df = pd.DataFrame.from_dict(rates, orient="index")
df.index = pd.to_datetime(df.index)
return df[f"{base}XAU"].sort_index().rename(f"gold_{base.lower()}")
gold = gold_history(date(2026, 1, 1), date(2026, 10, 23))
gold.to_csv("gold_usd.csv")
print(f"{len(gold)} trading days, high {gold.max():,.2f}, low {gold.min():,.2f}")On the Free plan these ten months take 10 requests. The Series has one row per trading day: weekends and market holidays are missing rather than filled, so expect fewer rows than calendar days. If you need a value for every day, gold.asfreq("D").ffill() carries Friday's close over the weekend. From here the usual pandas tools apply, such as gold.pct_change() for daily returns or gold.rolling(20).mean() for a moving average.
Past closing prices never change, so the CSV file is your cache for history: load it next time instead of requesting the same range again. For a full backfill since 2000 into a database, see Historical gold and silver prices via API.
Plotting the gold price with matplotlib
The chart reads the CSV file, so redrawing it costs no requests. It plots the daily close together with a 20-day moving average:
import matplotlib.pyplot as plt
import pandas as pd
gold = pd.read_csv("gold_usd.csv", index_col=0, parse_dates=True)["gold_usd"]
sma20 = gold.rolling(20).mean()
fig, ax = plt.subplots(figsize=(10, 5))
ax.plot(gold.index, gold, linewidth=1, label="Gold, USD per troy oz")
ax.plot(sma20.index, sma20, linewidth=2, label="20-day moving average")
ax.set_title(f"Gold price, {gold.index[0]:%d %b %Y} to {gold.index[-1]:%d %b %Y}")
ax.yaxis.set_major_formatter("{x:,.0f}")
ax.grid(alpha=0.3)
ax.legend()
fig.autofmt_xdate()
fig.tight_layout()
fig.savefig("gold.png", dpi=150)Because the index holds real dates, matplotlib spaces the points by time, and the moving average starts on the 20th trading day. To chart gold in another currency, call gold_history with base="EUR": the rate keys keep the same pattern, so the function works unchanged.
Next steps
You now have what most Python integrations need: an authenticated request, error handling with sensible retries, a cache that follows your plan's update interval, and history in pandas. Build and test it on the Free plan. When you need fresher prices, choose a plan by update interval and volume: a job that runs every 10 minutes uses about 4,500 requests a month, which fits Standard's 10,000. Compare the plans on the pricing page.
If the prices end up on a website, Gold price API in JavaScript shows how to serve them from a Node.js route without exposing your key.
FAQ
Is there a free gold price API for Python?
Yes. MetalAPI's Free plan includes 100 requests a month with daily price updates and needs no credit card, and every snippet in this post works on it. Its latest prices are the previous day's close, and one /timeframe request covers up to 30 days. Free gold price API explains the free tier in more detail.
Why is the XAU rate such a small number?
rates["XAU"] is the inverse rate: troy ounces of gold per one unit of the base currency. The price is under the prefixed key, rates["USDXAU"] for base USD or rates["EURXAU"] for base EUR. With the example values, 1 ÷ 0.0002501319 gives 3997.89 again.
How do I get the gold price per gram in Python?
Add unit=gram to the request, for example call("latest", {"base": "EUR", "currencies": "XAU", "unit": "gram"}). rates["EURXAU"] is then the price in euros per gram, and the response includes "unit": "gram". Avoid unit=ounce: it is the avoirdupois ounce, not the troy ounce gold is priced in.
How far back does gold price history go?
Daily gold prices start in 2000, and every plan can query the full history. Plans only differ in how many days one /timeframe request may cover: 30 on Free and 365 on paid plans. With max_days=365, everything from 2000 to today takes 27 requests.
How often should a Python script call the API?
No more often than your plan updates prices: once a day on Free, after 00:00 UTC; hourly on Lite, because every 30 minutes would take about 1,490 requests a month against a quota of 1,000; every 10 minutes on Standard. Expect flat prices over the weekend, when spot gold does not trade from Friday 17:00 to Sunday 17:00 New York time.
Try it with your own data
Create a free account and make your first request in under two minutes. 100 requests a month, no credit card.
Keep reading
XAU/USD Meaning: What XAU, XAG, XPT and XPD Stand For
XAU/USD meaning explained: why gold has an ISO 4217 currency code, what XAG, XPT and XPD stand for, and how to read quote direction in prices and API data.
Free Gold Price API: What 100 Requests a Month Can Do
A free gold price API, honestly explained: what MetalAPI's Free plan includes, how to stretch 100 requests a month with caching and batching, when to upgrade.
Gold Price API in JavaScript: Node.js, TypeScript and Next.js
Get the gold price in JavaScript with a gold price API: async/await fetch, TypeScript response types, a cached Express or Next.js route and safe API key use.