MetalAPI
XAU3,997.89−1.54%XAG53.936−1.51%XPT1,554.47−0.83%XPD1,047.03−0.44%XCU6.5080+2.29%WTI103.27+3.82%BRENT111.70−1.30%EUR1.120400−0.01%GBP1.337500−0.01%JPY0.0063761+0.14%CHF1.213445−0.25%TRY0.020125−0.12%BTC80,329.75−0.55%ETH2,329.36−1.13%
All articles
GoldAPITutorial

Gold Price API: The Complete Developer Guide

How to use a gold price API: get a key, call it from curl, JavaScript and Python, read the rates correctly, and handle history, errors, quotas and caching.

By MetalAPI Team··9 min read

What a gold price API gives you

A gold price API returns the current and historical price of gold as structured data, usually JSON over HTTPS, so your software can use it without scraping web pages or copying numbers by hand. In MetalAPI, gold has the symbol XAU, the ISO 4217 code for one troy ounce of gold. Its live price is the spot price: the midpoint between interbank bid and ask quotes.

Most integrations are not trading systems. Typical uses look like this:

  • Online shops and jewelers that reprice gold products as the market moves.
  • Calculators for scrap gold, karat jewelry or bullion premiums.
  • Portfolio trackers and personal finance apps that value holdings in a local currency.
  • Accounting and ERP systems that revalue inventory at a daily closing price.
  • Dashboards, newsletters and internal reports that need a chart of the last year.

The same API covers silver (XAG), platinum (XPT) and palladium (XPD), four industrial metals, five energy contracts, 57 currencies and 13 cryptocurrencies: 83 symbols in total, all listed on the symbols page. This guide focuses on gold, but everything in it applies to the other symbols too.

Get an API key

Create a free account. The Free plan needs no credit card and includes 100 requests per month with daily price updates, which is enough to build and test an integration. Paid plans add fresher prices, more requests and extra endpoints; the plan table further down summarizes them.

In the dashboard, open API keys and create a key. Keys start with mk_live_. The full key is shown only once, because only a hash of it is stored, so put it straight into an environment variable or a secrets manager:

Shell
export METALAPI_KEY="mk_live_your_key_here"

The API accepts the key in three equivalent ways: an api_key query parameter, an Authorization: Bearer header or an X-API-Key header. Prefer a header in production, because full URLs tend to end up in access logs, proxies and error trackers. The authentication guide also explains domain-restricted keys for calls from the browser.

Three ways to authenticate
# 1. Query parameter
curl "https://metalapi.com/api/v1/latest?api_key=$METALAPI_KEY&currencies=XAU"

# 2. Authorization header
curl "https://metalapi.com/api/v1/latest?currencies=XAU" \
  -H "Authorization: Bearer $METALAPI_KEY"

# 3. X-API-Key header
curl "https://metalapi.com/api/v1/latest?currencies=XAU" \
  -H "X-API-Key: $METALAPI_KEY"

Your first request in curl, JavaScript and Python

Every endpoint is a plain GET request under https://metalapi.com/api/v1/. The /latest endpoint returns current prices. base sets the currency the prices are expressed in (default USD) and currencies limits the response to the symbols you need. One request can return many symbols and still counts as a single request.

curl
curl "https://metalapi.com/api/v1/latest?base=USD&currencies=XAU,XAG" \
  -H "X-API-Key: $METALAPI_KEY"

A successful response looks like this. The numbers are the example values used throughout the MetalAPI documentation, not live prices:

Example response
{
  "success": true,
  "timestamp": 1792834892,
  "date": "2026-10-24",
  "base": "USD",
  "rates": {
    "XAU": 0.0002501319,
    "USDXAU": 3997.89,
    "XAG": 0.0185392857,
    "USDXAG": 53.94
  },
  "rate_limit": {
    "limit": 10000,
    "remaining": 9876,
    "reset": 1793491200,
    "used": 124,
    "plan": "Standard",
    "reset_date": "2026-11-01T00:00:00.000Z"
  }
}

The same call from Node.js 18 or later, which has fetch built in. Save it as gold.mjs so that top-level await works, then run node gold.mjs:

gold.mjs
const params = new URLSearchParams({ base: "USD", currencies: "XAU,XAG" });
const res = await fetch(`https://metalapi.com/api/v1/latest?${params}`, {
  headers: { Authorization: `Bearer ${process.env.METALAPI_KEY}` },
  signal: AbortSignal.timeout(10_000),
});
const data = await res.json();

if (!data.success) {
  throw new Error(`MetalAPI ${data.error.code} (${data.error.type}): ${data.error.info}`);
}

const gold = data.rates[`${data.base}XAU`];
const asOf = new Date(data.timestamp * 1000).toISOString();
console.log(`Gold: ${gold} ${data.base} per troy ounce (data from ${asOf})`);

And from Python with the requests library:

gold.py
import os

import requests

resp = requests.get(
    "https://metalapi.com/api/v1/latest",
    params={"base": "USD", "currencies": "XAU,XAG"},
    headers={"X-API-Key": os.environ["METALAPI_KEY"]},
    timeout=10,
)
data = resp.json()
if not data["success"]:
    err = data["error"]
    raise RuntimeError(f"MetalAPI {err['code']} ({err['type']}): {err['info']}")

base = data["base"]
print(f"Gold:   {data['rates'][base + 'XAU']} {base} per troy ounce")
print(f"Silver: {data['rates'][base + 'XAG']} {base} per troy ounce")

Both snippets build the rate key from data.base instead of hard-coding it. That small habit keeps the code correct when you later switch the base currency, and it leads to the most important part of this guide.

Read the rates format correctly

Every symbol appears twice in rates, and the two numbers are inverses of each other:

KeyMeaningExample value
USDXAUPrice of one troy ounce of gold in the base currency (USD)3997.89
XAUTroy ounces of gold that one unit of the base currency buys0.0002501319

The general rule: rates.<BASE><SYMBOL> is the price of one unit of the symbol in the base currency. The plain rates.<SYMBOL> key is the inverse, units of the symbol per one unit of the base. The inverse form is kept for compatibility with forex-style APIs and with older MetalAPI integrations.

The most common bug in gold price integrations is reading rates.XAU and treating it as the price. It is 1 ÷ price, so a product page shows 0.00025 instead of 3,997.89, and any calculation that multiplies by it is wrong by many orders of magnitude. Read the prefixed key, such as rates.USDXAU or rates.EURXAU.

If you already store inverse values from an older integration, you don't have to download them again: price = 1 ÷ inverse. With the example above, 1 ÷ 0.0002501319 gives 3997.89 again, apart from the last digits of rounding.

Currencies follow the same rule. With base USD, USDEUR is the price of one euro in dollars and EUR is euros per dollar. The data formats guide has more examples, including the bid and ask keys available on the Standard plan and above.

Base currencies and units

To price gold in another currency, change base. The rate key changes with it: base=EUR returns EURXAU, and base=INR returns INRXAU. Cross rates come from one snapshot, so gold in euros and the euro in dollars are always consistent with each other.

Metal prices are per troy ounce by default. The unit parameter returns them per another weight:

unitPrice perTypical use
troy_oz (default)Troy ounce, 31.1034768 gBullion quotes
gramGramJewelry and retail
kilogramKilogramKilo bars, wholesale
ounceAvoirdupois ounce, 28.349523125 gRarely right for gold
poundAvoirdupois poundCopper
metric_tonMetric ton (1,000 kg)Aluminium, zinc
short_tonShort ton (2,000 lb)Steel (HRC)

This request returns gold and silver in euros per gram. When unit is not the default, the response repeats it in a top-level unit field:

bash
curl "https://metalapi.com/api/v1/latest?base=EUR&currencies=XAU,XAG&unit=gram" \
  -H "X-API-Key: $METALAPI_KEY"

unit=ounce (and its short form oz) is the avoirdupois ounce, about 8.9% lighter than a troy ounce. For bullion prices, leave unit out or pass troy_oz.

unit only affects precious and industrial metals. Energy, currencies and crypto are not changed by it. For the details on troy ounces, grams, tolas and rounding, read Troy ounces, grams and kilograms. If you sell jewelry, Gold karat prices explained covers 22K and 18K prices and the /carat endpoint.

Historical gold prices

Past prices use the same parameters and the same response format. History starts in 2000 for metals and energy, on 1999-01-04 for currencies and in 2014 for crypto. Four endpoints cover most needs:

  • /YYYY-MM-DD (or /historical?date=YYYY-MM-DD, or /yesterday): closing prices of one day. For weekends and holidays, the most recent trading day's close is returned.
  • /timeframe (alias /timeseries): daily closes between start_date and end_date, up to 30 days per request on Free and 365 days on paid plans.
  • /change: start rate, end rate, absolute and percentage change between two dates.
  • /ohlc: open, high, low and close of one day.
bash
# Gold and silver at the close of 15 January 2024
curl "https://metalapi.com/api/v1/2024-01-15?currencies=XAU,XAG" \
  -H "X-API-Key: $METALAPI_KEY"

# Daily gold closes in euros for September 2026
curl "https://metalapi.com/api/v1/timeframe?start_date=2026-09-01&end_date=2026-09-30&base=EUR&currencies=XAU" \
  -H "X-API-Key: $METALAPI_KEY"

Closing prices of past days don't change, so store them once instead of requesting them again. Historical gold and silver prices via API walks through paging 25 years of data, storing it locally and charting it. The historical data guide has a JavaScript helper for long ranges.

Errors, quotas and plan limits

A failed request returns an HTTP error status and a body with success: false. The error object has a numeric code, a machine-readable type and a human-readable info. Branch on code or type, never on the info text. Rate limit (104), quota (105), plan restriction (106) and timeframe (207) errors also include an upgrade_url:

Example error
{
  "success": false,
  "error": {
    "code": 106,
    "type": "plan_restriction",
    "info": "The carat endpoint requires a paid subscription plan",
    "upgrade_url": "https://metalapi.com/dashboard/billing"
  }
}

The codes you are most likely to meet:

CodeTypeHTTPWhat to do
101, 102no_api_key, invalid_api_key401Send a valid key and check your environment variable
104too_many_requests429Wait for the Retry-After seconds, then retry
105quota_exceeded429Monthly quota used up; it resets on the 1st (UTC)
106plan_restriction403Endpoint or parameter not included in your plan
205invalid_date400Fix a malformed or future date
207timeframe_too_long400Split the range into shorter requests
300no_results404No data, for example before history starts
500, 503server_error, service_unavailable500, 503Retry with exponential backoff

Every authenticated response reports your quota, both as X-RateLimit-* headers and as the rate_limit object in the body. /usage and /symbols never count towards the quota, so monitoring can call them freely. Requests rejected with 104, 106, 107 or a server error are not billed, but validation errors are. The error handling guide lists every code and includes a retry helper.

Plans differ mainly in request volume and in how fresh the prices are:

PlanPrice / monthRequests / monthUpdatesAdds
Free$0100Daily30-day ranges
Lite$4.991,00030 minKarat prices, 365-day ranges
Standard$11.9910,00010 minBid/ask prices
Advanced$24.9950,0001 minWebhook alerts
Pro$49.99100,0001 minRealtime stream
Ultra$99.99500,00015 sHigher volume
Business$149.99500,00015 sUptime SLA
Enterprise$299.991,500,0005 sHighest volume

Yearly billing saves up to 25%. The pricing page always has the current details.

Caching and production tips

Requesting /latest more often than your plan updates returns the same numbers and spends quota for nothing. Cache the latest prices for at least your plan's update interval, and cache past days permanently. This Python helper caches the latest prices and keeps serving the last good response if the API cannot be reached:

cache.py
import os
import time

import requests

TTL_SECONDS = 600  # Standard plan: prices update every 10 minutes
session = requests.Session()
session.headers["X-API-Key"] = os.environ["METALAPI_KEY"]
_cache = {"at": 0.0, "data": None}


def latest_prices():
    now = time.monotonic()
    if _cache["data"] and now - _cache["at"] < TTL_SECONDS:
        return _cache["data"]
    try:
        r = session.get(
            "https://metalapi.com/api/v1/latest",
            params={"currencies": "XAU,XAG,XPT,XPD"},
            timeout=10,
        )
        data = r.json()
    except (requests.RequestException, ValueError):
        data = None
    if data and data.get("success"):
        _cache.update(at=now, data=data)
        return data
    if _cache["data"]:
        return _cache["data"]  # last good prices; show their timestamp
    raise RuntimeError(data["error"] if data else "MetalAPI unreachable")

A few more habits that keep an integration cheap and reliable:

  • Ask for every symbol in one call. currencies=XAU,XAG,XPT,XPD costs one request; four separate calls cost four.
  • Keep the key on the server. If a browser has to call the API, use a domain-restricted key. To show prices on a website without writing code, an embeddable widget keeps the key out of the page entirely.
  • Show the timestamp. timestamp is the time of the data, not of your request. Display it next to the price, especially when you serve a cached value or the market is closed for the weekend.
  • Budget your polling. Polling every 10 minutes uses about 4,500 requests a month, and polling every minute about 45,000. From the Pro plan up you can use the realtime stream instead, and from Advanced up you can receive webhook price alerts.
  • Use timeouts and selective retries. Set a timeout of around 10 seconds, retry 104 after Retry-After and 5xx errors with backoff, and never retry validation errors.
  • Validate symbols up front. Load /symbols once at startup (it is free) and reject unknown codes before they cost a request.

If you work with AI assistants, the MetalAPI MCP server at https://metalapi.com/mcp exposes the same data as tools, so Claude, ChatGPT, Cursor or VS Code can look up gold prices and history on your plan; see AI & MCP. For every endpoint and parameter, the API docs are the reference.

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