API documentation
Use the X-API-Key header as the canonical and preferred credential transport. Clients that cannot set custom headers can use the api_key query parameter as a fallback. Base URL is https://api.bullionapi.dev. Usage increments after API-key and quota checks but before endpoint validation.
Query credentials may appear in browser history, copied URLs, referrers, intermediary infrastructure, and logs. Use the X-API-Key header whenever your client supports custom headers. Query authentication does not enable cross-origin browser fetch; CORS remains separate. If both transports are supplied, they must match; differing values return 401 conflicting_credentials.
Quick start
Get live prices in 30 seconds. If you don’t have an API key yet, get one here.
curl -H "X-API-Key: bullion_your_key" \
"https://api.bullionapi.dev/v1/latest?currency=USD"Latest prices
Returns the most recent cached gold price in the requested base currency. It does not provide a standalone currency-data feed.
Query parameters
| Name | Type | Description |
|---|---|---|
currency | string | ISO 4217 currency code. Defaults to USD. See supported currencies below. |
unit | string | Optional output mass unit: gram/g, kilogram/kg, or troy_oz/toz. Omit or leave empty for legacy toz; invalid values return 400. |
api_key | string | Fallback credential for clients that cannot set the X-API-Key header. Prefer the header transport whenever possible. |
Spreadsheet and no-code example
For a client that cannot set custom headers, request the URL below. Treat the URL as a secret and prefer the header transport whenever your client supports it.
GET https://api.bullionapi.dev/v1/latest?currency=USD&api_key=bullion_your_keyResponse
{
"status": "success",
"currency": "USD",
"unit": "toz",
"metals": { "gold": 4527.86 },
"currencies": { "USD": 1, "EUR": 1.1724, "GBP": 1.3521 },
"timestamps": { "metal": "2026-08-24T22:00:00.000Z", "currency": "2026-08-24T21:45:00.000Z" }
}Cached for 1 hour and refreshed by the scheduled data pipeline. Prices default to troy ounces (toz). Request unit=gram or unit=kilogram for converted values; aliases are g, kg, and troy_oz. Results are rounded to eight decimal places.
USD, EUR, and GBP retain the legacy provider-backed currencies map shown above. For a configured projected base, that legacy map is empty. timestamps.metal is the hourly canonical USD metal observation used in the projected price, while timestamps.currency is the separate daily Bullion API conversion snapshot time. The conversion timestamp is not a provider end-of-day observation. A snapshot is accepted through seven UTC calendar days old and becomes stale when older.
Conversion input attribution: Exchange Rates By UniRateAPI. Internal UniRateAPI observations are used only to derive informational metal prices and are never returned by this endpoint.
Gold karat prices per gram
Returns cached gold prices per gram for common karats. Gold is the only supported metal and grams are the only response unit.
Query parameters
| Name | Type | Description |
|---|---|---|
currency | string | Three-letter ISO 4217 code, case-insensitive. Defaults to USD; the currency must have cached gold data. |
date | YYYY-MM-DD | Optional exact UTC day. The final cached gold observation in that day is used. |
api_key | string | Fallback credential for clients that cannot set the X-API-Key header. Prefer the header transport whenever possible. |
Example
curl -H "X-API-Key: bullion_your_key" \
"https://api.bullionapi.dev/v1/carat?currency=USD"Response
{
"status": "success",
"currency": "USD",
"unit": "gram",
"as_of": "2026-08-10T14:30:00.000Z",
"karats": {
"24k": 145.57407936,
"23k": 139.50849272,
"22k": 133.44290608,
"21.6k": 131.01667142,
"21k": 127.37731944,
"18k": 109.18055952,
"16k": 97.04938624,
"14k": 84.91821296,
"12k": 72.78703968,
"10k": 60.6558664,
"9k": 54.59027976,
"8k": 48.52469312,
"6k": 36.39351984
}
}Without date, the current cached gold row is used. With date, the final observation in that UTC day is used. Values are calculated from the exact karat ratio and rounded to eight decimal places. Malformed currency or date values return invalid_currency or invalid_date; a valid but unseeded currency or day returns no_data. Cache read failures return data_unavailable. Requests never bridge currencies or call upstream providers.
Historical prices
Returns daily gold closes across a date range. Up to 365 days per request.
Query parameters
| Name | Type | Description |
|---|---|---|
start_date | YYYY-MM-DD | First day to include (inclusive). |
end_date | YYYY-MM-DD | Last day to include (inclusive). Max 365-day range. |
currency | string | ISO 4217 base currency. Defaults to USD. |
unit | string | Optional output mass unit: gram/g, kilogram/kg, or troy_oz/toz. Omit or leave empty for legacy toz; invalid values return 400. |
api_key | string | Fallback credential for clients that cannot set the X-API-Key header. Prefer the header transport whenever possible. |
Example
curl -H "X-API-Key: bullion_your_key" \
"https://api.bullionapi.dev/v1/timeseries?currency=USD&start_date=2025-07-08&end_date=2026-07-08"Response
{
"status": "success",
"currency": "USD",
"unit": "toz",
"start_date": "2025-07-08",
"end_date": "2026-07-08",
"rates": {
"2025-07-08": {
"currencies": { "EUR": 0.9134, "GBP": 0.7783 },
"date": "2025-07-08",
"metals": { "gold": 3320.10 }
},
"2025-07-09": {
"currencies": { "EUR": 0.9141, "GBP": 0.7790 },
"date": "2025-07-09",
"metals": { "gold": 3342.55 }
}
}
}Only cached dates are included in rates. Scheduled refresh and backfill jobs populate the cache; API requests never call upstream providers. Prices default to troy ounces (toz). Request unit=gram or unit=kilogram for converted values; aliases are g, kg, and troy_oz. Results are rounded to eight decimal places; FX rates and date metadata are unchanged.
Gold price change
Returns gold's absolute and percentage change between the final cached observation in two UTC days. The endpoint reads the database cache and never calls an upstream provider.
Query parameters
| Name | Type | Description |
|---|---|---|
currency | string | ISO 4217 base currency. Defaults to USD. |
start_date | YYYY-MM-DD | First UTC day to compare. Use together with end_date. |
end_date | YYYY-MM-DD | Last UTC day to compare. Use together with start_date. |
date_type | string | Rolling UTC window: yesterday (1 day), week (7 days), month (30 days), or year (365 days). Use instead of the explicit date pair. |
api_key | string | Fallback credential for clients that cannot set the X-API-Key header. Prefer the header transport whenever possible. |
Example
curl -H "X-API-Key: bullion_your_key" \
"https://api.bullionapi.dev/v1/change?currency=USD&date_type=week"Response
{
"status": "success",
"currency": "USD",
"metal": "gold",
"start_date": "2026-08-03",
"end_date": "2026-08-10",
"start_price": 4500,
"end_price": 4600,
"change": 100,
"change_pct": 2.2222
}The final observation in each UTC day is selected deterministically. Use either both explicit dates or one date_type; mixing them returns conflicting_params. Absolute change is rounded to eight decimal places and change_pct to four. Invalid dates return invalid_date; reversed or overlong ranges return invalid_range; unsupported date types return invalid_date_type. Missing cached data returns no_change_data; a zero start price returns invalid_change_baseline.
US, UK, and euro-area macro rates
Returns the latest stored macro observations for us, uk, or eu. Each series keeps its own observation period, so the response does not expose one region-wide as_of value. The euro-area response identifies its geography as euro_area; uk provides Bank Rate and CPI.
Path and query parameters
| Name | Type | Description |
|---|---|---|
region | path | Use us, uk, or eu for the requested macro region. |
api_key | string | Fallback credential for clients that cannot set the X-API-Key header. Prefer the header transport whenever possible. |
Example
curl -H "X-API-Key: bullion_your_key" \
"https://api.bullionapi.dev/v1/rates/us"curl -H "X-API-Key: bullion_your_key" \
"https://api.bullionapi.dev/v1/rates/eu"Response
{
"status": "success",
"region": "us",
"policy": {
"policy_target_lower": {
"series_id": "policy_target_lower",
"value": 3.5,
"unit": "percent",
"observation_period": "2026-07-30",
"method": "latest",
"source_series_id": "EFFR.targetRateFrom",
"source_id": "ny_fed",
"attribution": "Federal Reserve Bank of New York",
"fetched_at": "2026-08-01T06:00:00.000Z"
},
"policy_target_upper": {
"series_id": "policy_target_upper",
"value": 3.75,
"unit": "percent",
"observation_period": "2026-07-30",
"method": "latest",
"source_series_id": "EFFR.targetRateTo",
"source_id": "ny_fed",
"attribution": "Federal Reserve Bank of New York",
"fetched_at": "2026-08-01T06:00:00.000Z"
},
"effective_federal_funds_rate": {
"series_id": "effective_federal_funds_rate",
"value": 3.63,
"unit": "percent",
"observation_period": "2026-07-30",
"method": "latest",
"source_series_id": "EFFR",
"source_id": "ny_fed",
"attribution": "Federal Reserve Bank of New York",
"fetched_at": "2026-08-01T06:00:00.000Z"
}
},
"yields": {
"treasury_nominal_yield_10y": {
"series_id": "treasury_nominal_yield_10y",
"value": 4.25,
"unit": "percent",
"observation_period": "2026-06-30",
"method": "monthly_average",
"source_series_id": "BC_10YEAR",
"source_id": "us_treasury",
"attribution": "U.S. Department of the Treasury",
"fetched_at": "2026-08-01T06:00:00.000Z"
}
},
"inflation": {
"cpi_u_yoy": {
"series_id": "cpi_u_yoy",
"value": 2.7,
"unit": "percent",
"observation_period": "2026-06-30",
"method": "latest",
"source_series_id": "CUUR0000SA0",
"source_id": "bls",
"attribution": "U.S. Bureau of Labor Statistics",
"fetched_at": "2026-08-01T06:00:00.000Z"
}
}
}Every observation includes its stable Bullion API series ID, numeric value, unit, observation period, calculation method, source series ID, source ID, attribution, and fetch time. UK gilt curves include nominal and real zero-coupon spot yields plus RPI-linked implied inflation at 2Y, 5Y, 10Y, and 30Y tenors, using official month-end archive dates with the month_end method. The API reads the database cache; protected cron collection is the only upstream caller. Free, Pro, and Enterprise keys receive the same data contract, with only existing quota limits differing.
Methodology: Treasury nominal and real yields cover nominal 2Y, 5Y, 10Y, and 30Y plus real 5Y, 10Y, and 30Y series; each is an arithmetic average of valid daily observations in complete UTC calendar months. Treasury 5Y, 10Y, and 30Y breakevens are daily nominal-minus-real differences calculated only on common dates, then averaged by month. Empty, missing, and N/A Treasury values are absent rather than treated as zero. The BLS CPI-U NSA series CUUR0000SA0 is exposed as year-over-year percent; when BLS does not publish an annual-percent field, Bullion API calculates the twelve-month change from the same month in the prior year. UK Bank Rate uses the latest valid BoE IUDBEDR observation for each date in the refresh range; UK CPI uses the latest numeric monthly ONS D7G7 annual-rate value, stored at month end. UK curve values are direct BoE spot observations and BoE-derived RPI-linked implied inflation values; source dates are preserved, including final business days. Neither UK curve values nor UK Bank Rate/CPI are averaged.
Euro-area data includes ECB deposit facility, main refinancing, and marginal lending rates; AAA nominal 2Y, 5Y, 10Y, and 30Y spot yields; metadata-verified all-items HICP year-over-year inflation as ecb_hicp_yoy; and Consumer Expectations Survey weighted-median 12-month and five-year inflation expectations. CES values use survey_expectationand are not market-implied. ECB AAA yields are arithmetic averages of valid observations from complete UTC calendar months. HICP uses HICP.M.U2.N.000000.4D0.ANR and the provider validates its geography, adjustment, all-items, unit, and annual-rate metadata before accepting rows.
Sources: Federal Reserve Bank of New York reference rates, U.S. Department of the Treasury Daily Interest Rate XML Feed, Bank of England Database and BoE yield curve archives, plus Office for National Statistics D7G7 CPI series, and U.S. Bureau of Labor Statistics Public Data API, and the European Central Bank Data Portal. BLS disclaimer: BLS.gov cannot vouch for the data or analyses derived from these data after retrieval.
Annual central-bank reserves
Annual central-bank reserve values and gold share for Pro and Enterprise accounts. This endpoint is served from Bullion API’s automated cache.
Query parameters
| Name | Type | Description |
|---|---|---|
country | ISO2 | Optional case-insensitive country code, such as GB. |
start | YYYY | Optional first observation year to include (inclusive). |
end | YYYY | Optional last observation year to include (inclusive). |
Example
curl -H "X-API-Key: bullion_your_key" \
"https://api.bullionapi.dev/v1/central-bank-reserves?country=GB&start=2020&end=2025"Response
{
"status": "success",
"methodology": {
"gold_reserve_value_usd": "total reserves including gold minus total reserves excluding gold",
"annual_gold_value_change_usd": "current derived gold reserve value minus the prior calendar year",
"transformation_notice": "Bullion API derives gold reserve values, shares, and annual value changes.",
"warning": "Annual value changes combine holdings changes, gold-price movement, and revisions and do not measure tonnes bought or sold."
},
"coverage": {
"countries": 1,
"start_year": 2020,
"end_year": 2025,
"source_updated_at": "2026-07-13",
"fetched_at": "2026-07-30T07:10:00.000Z"
},
"reserves": [{
"country": "GB",
"country_iso3": "GBR",
"country_name": "United Kingdom",
"as_of_year": 2025,
"total_reserves_usd": 200000000000,
"reserves_excluding_gold_usd": 175000000000,
"gold_reserve_value_usd": 25000000000,
"gold_share_pct": 12.5,
"annual_gold_value_change_usd": 10000000000,
"data_quality": null
}]
}Source: World Bank, World Development Indicators. The underlying provider is International Monetary Fund, International Financial Statistics. Annual value changes combine holdings changes, gold-price movement, and revisions and do not measure tonnes bought or sold.
Central-bank gold holdings
Monthly central-bank gold holdings, including fortnightly sources for Pro and Enterprise accounts, served from Bullion API’s scheduled cache of official national central-bank feeds.
Query parameters
| Name | Type | Description |
|---|---|---|
layer | reported | estimated | both | Use reported. estimated and both remain accepted for response compatibility. |
start / end | YYYY-MM | Paired inclusive month bounds, up to 120 months. Omit both for the latest twelve months. |
country | ISO2 | Optional case-insensitive reported-layer country filter, such as GB. |
Example
curl -H "X-API-Key: bullion_your_key" \
"https://api.bullionapi.dev/v1/central-bank-gold?layer=reported&start=2021-01&end=2026-06"Response
{
"status": "success",
"query": { "layer": "reported", "start": "2021-01", "end": "2026-06", "country": null },
"reported": {
"status": "ready",
"observations": [{
"country": "GB", "country_iso3": "GBR", "country_name": "United Kingdom",
"month": "2026-06", "reference_date": "2026-06-01", "cadence": "monthly",
"holdings_basis": "reported", "holdings_million_fine_troy_oz": 27.4,
"holdings_tonnes": 852.24,
"reported_value_usd_millions": null, "reported_value_change_usd_millions": null, "reported_value_eur_millions": null, "reported_value_pln_millions": null, "reported_value_inr_crores": null, "reported_value_sar_millions": null,
"net_change_tonnes": 6.22
}, {
"country": "SG", "month": "2026-06", "holdings_tonnes": null,
"reported_value_sgd_millions": 539183.2
}],
"latest_country_changes": [{
"country": "GB", "country_iso3": "GBR", "country_name": "United Kingdom",
"month": "2026-06", "reference_date": "2026-06-01", "cadence": "monthly",
"holdings_basis": "reported", "holdings_million_fine_troy_oz": 27.4,
"holdings_tonnes": 852.24,
"reported_value_usd_millions": null, "reported_value_change_usd_millions": null, "reported_value_eur_millions": null, "reported_value_pln_millions": null, "reported_value_inr_crores": null, "reported_value_sar_millions": null,
"net_change_tonnes": 6.22, "direction": "buying"
}]
},
"estimated": {
"status": "not_requested",
"observations": []
}
}Reported holdings are sourced from Official national central-bank feeds, including the ECB Data Portal, the De Nederlandsche Bank gold holdings page, the Bank of England Database, and the Reserve Bank of Australia Official Reserve Assets release, and the Banco de España monetary-gold dataset. The Bank of Korea’s monthly Official Foreign Reserves release and DNB page provide reported holdings; these datasets report stocks of gold held; month-to-month changes do not measure purchases or sales. The DNB page is a published holdings summary, not a historical time series; its entries use an irregular cadence and never fill missing monthly history. Entries are reported holdings and valuations; changes do not represent purchases. Australia’s reported physical gold holdings have been unchanged since 1997, with lent gold reported separately. The Czech National Bank international reserves structure.CNB holdings are reported reserve gold including deposits and swapped gold, so monthly changes reflect reported holdings, not purchases. The Central Bank of the Republic of China (Taiwan) reserve template reports official gold holdings at each reference date; these figures do not measure gold purchases or sales. The Saudi Central Bank (SAMA) Data Template on International Reserves and Foreign Currency Liquidity reports reserve holdings; these reserve stocks do not represent gold purchased or sold, and its directly reported million-riyal valuation is exposed as reported_value_sar_millionswhile tonnes are converted only from the reported volume. The Bank Negara Malaysia National Summary Data Page reports a fortnightly USD-billion Gold valuation without a published quantity, so tonnes remain unavailable and valuation changes are not purchase or sale claims.
Gold portfolio settings
Configure one private gold portfolio and its reporting currency. Both methods accept X-API-Key only; the api_key query parameter is not accepted for portfolio routes.
Existing and new keys start with portfolio access set to None. Grant Read for GET, or Read and write for GET and PUT, from the API keys dashboard. Write always includes read. Portfolio requests keep the rolling per-key and source-IP abuse limits but do not increment the monthly market-data quota.
Create or change the reporting currency
Send { "reporting_currency": "GBP" } with PUT. The value must be one of the supported currencies below and can change while the portfolio has no transactions.
Read the portfolio
curl -H "X-API-Key: bullion_your_key" \
"https://api.bullionapi.dev/v1/portfolio"Empty portfolio response
{
"status": "success",
"portfolio": {
"id": "d799789a-e1af-4c41-a79a-611e18f0df31",
"metal": "gold",
"reporting_currency": "GBP",
"created_at": "2026-08-14T11:00:00.000Z",
"updated_at": "2026-08-14T11:00:00.000Z"
},
"summary": {
"gold_quantity_grams": "0.000000000000000",
"cost_basis": "0.00000000",
"realised_pnl": "0.00000000",
"valuation": {
"status": "unavailable",
"spot_price": null,
"current_value": null,
"unrealised_pnl": null,
"price_timestamp": null
}
}
}Decimal summary values remain strings, including zero. Valuation comes from the latest stored gold spot price in the reporting currency and is current when that price is at most six hours old, stale when older, and unavailable when no matching-currency price exists — there is no cross-currency fallback and portfolio reads never call an upstream provider. When unavailable, spot_price, current_value, unrealised_pnl, and price_timestamp are null rather than zero; holdings and cost basis always remain available. Missing portfolios return portfolio_not_found, and keys without the required action return insufficient_scope.
Record an exact purchase
POST /v1/portfolio/transactions with portfolio write access and an opaque Idempotency-Key header. Quantity accepts up to eight fractional digits in grams, kilograms, or troy ounces and is returned in grams at 15 decimal places. Money is returned as eight-place decimal strings using round-half-up.
curl -X POST "https://api.bullionapi.dev/v1/portfolio/transactions" \
-H "X-API-Key: bullion_your_key" \
-H "Idempotency-Key: purchase-2026-08-19-001" \
-H "content-type: application/json" \
--data '{"transaction_date":"2026-08-19","quantity":"1.005","weight_unit":"troy_oz","unit_price":"2500.125","delivery_cost":"12.345678945","fees":"1.005","form":"coin","cgt_status":"exempt"}'A repeated canonical request with the same key replays its original result. A changed request with that key returns 409 idempotency_conflict. Dates must be real calendar dates no later than today; descriptions are optional and limited to 500 characters. Invalid fields return 400 invalid_purchase with field details.
List open lots and allocate a sale explicitly
GET /v1/portfolio/lots lists only your open purchase lots with a positive remaining quantity, ordered by purchase date, creation time, then ID. Each lot carries its stable lot_id, originating purchase_transaction_id, date, form, CGT status, remaining quantity, and remaining cost basis.
curl -H "X-API-Key: bullion_your_key" \
"https://api.bullionapi.dev/v1/portfolio/lots"{
"status": "success",
"lots": [{
"lot_id": "22222222-2222-4222-8222-222222222222",
"purchase_transaction_id": "11111111-1111-4111-8111-111111111111",
"transaction_date": "2026-08-19",
"form": "coin",
"cgt_status": "exempt",
"remaining_quantity_grams": "31.258994184000000",
"remaining_cost_basis": "2525.97630395"
}]
}A sale defaults to FIFO lot selection. To choose the disposed lots yourself, send allocations on the sale body: an array of { "lot_id", "quantity_grams" } objects whose quantities sum exactly to the sale quantity. Every selected lot must belong to your account, match the sale's form and CGT treatment, predate or equal the sale date, and have enough remaining quantity. Any missing, foreign, duplicated, incompatible, over-allocated, or under-allocated lot fails the whole sale without mutation: eligibility and exact-sum violations return 400 invalid_allocation, lots dated after the sale return 400 sale_date_invalid, and exhausted selections return 409 insufficient_holdings. Committed sales report their allocation_method as fifo or explicit and return the committed allocations.
Browse transaction history
GET /v1/portfolio/transactions returns only the authenticated owner's transactions. Results use the stable order transaction_date DESC, created_at DESC, id DESC. The limit defaults to 50 and is capped at 100. Follow the opaque cursor in next_cursorwith cursor for the next page; do not depend on its internal format.
Filter with type, form, and cgt_status, and restrict the period with either calendar_year=2025 (1 January through 31 December inclusive) or uk_tax_year=2025-26 (6 April through 5 April inclusive; the two years in the label must be consecutive). The two period filters are mutually exclusive. Invalid values return invalid_filter, invalid_calendar_year, invalid_tax_year, or conflicting_periods.
curl -H "X-API-Key: bullion_your_key" \
"https://api.bullionapi.dev/v1/portfolio/transactions?limit=50&uk_tax_year=2025-26"{
"status": "success",
"transactions": [{
"type": "purchase",
"version": 1,
"id": "11111111-1111-4111-8111-111111111111",
"transaction_date": "2026-08-19",
"created_at": "2026-08-19T12:00:00.000Z",
"entered_quantity": "1.00500000",
"weight_unit": "troy_oz",
"quantity_grams": "31.258994184000000",
"unit_price": "2500.12500000",
"gross_cost": "2512.62562500",
"delivery_cost": "12.34567895",
"fees": "1.00500000",
"total_cost": "2525.97630395",
"cost_per_gram": "80.80798407",
"form": "coin",
"cgt_status": "exempt",
"description": "One-ounce gold coin",
"lots": [{
"id": "22222222-2222-4222-8222-222222222222",
"open_quantity_grams": "31.258994184000000",
"open_cost_basis": "2525.97630395"
}],
"allocations": [],
"gross_proceeds": "0.00000000",
"net_proceeds": "0.00000000",
"disposed_cost": "0.00000000",
"realised_pnl": "0.00000000"
}],
"next_cursor": null
}GET /v1/portfolio/transactions/{id} returns the same transaction representation, including its open lot and allocation collections. Decimal quantities and monetary values are always strings. Invalid page, filter, or period parameters return invalid_limit, invalid_cursor, invalid_filter, invalid_calendar_year, invalid_tax_year, or conflicting_periods; missing and cross-account identifiers return transaction_not_found. Every response carries an ETag header with the quoted transaction version.
Edit a recorded transaction
PATCH /v1/portfolio/transactions/{id} corrects one owned transaction without corrupting its allocations. Send If-Match: "3" quoting the current version from the ETag header. The body replaces every mutable field — date, quantity, weight unit, unit price, delivery cost, fees, form, CGT status, and description — while type and ownership stay fixed.
curl -X PATCH "https://api.bullionapi.dev/v1/portfolio/transactions/11111111-1111-4111-8111-111111111111" \
-H "X-API-Key: bullion_your_key" \
-H 'If-Match: "1"' \
-H "content-type: application/json" \
--data '{"transaction_date":"2026-08-19","quantity":"1.005","weight_unit":"troy_oz","unit_price":"2510.5","delivery_cost":"12.34","fees":"1.005","form":"coin","cgt_status":"exempt"}'A successful edit increments the version, returns the fresh ETag, and recalculates affected values immediately: purchases refresh their lot totals, and sales release and rebuild only their own allocations using the stored FIFO or explicit method before recomputing disposed cost and realised P&L. A purchase edit must leave enough quantity and basis for every sale that allocated it, cannot move after such a sale, and cannot change form or CGT treatment while those allocations exist. A missing or stale precondition returns 412 stale_transaction: reload the latest record and resubmit deliberately. Foreign identifiers return 404 transaction_not_found, and every rejected edit rolls back completely.
Delete a transaction
DELETE /v1/portfolio/transactions/{id} removes an owned purchase or sale with portfolio write access and rebalances your holdings. Send the transaction's version as an ETag-style If-Match header ("1"); a missing or mismatched version returns 412 stale_transaction without mutating anything. Deleting a sale restores every allocated quantity and cost basis to its originating purchase lot.
curl -X DELETE "https://api.bullionapi.dev/v1/portfolio/transactions/11111111-1111-4111-8111-111111111111" \
-H "X-API-Key: bullion_your_key" \
-H 'If-Match: "1"'{ "status": "success", "deleted_transaction_id": "11111111-1111-4111-8111-111111111111" }A purchase that has already funded a sale cannot be deleted and returns 409 transaction_in_use; delete the dependent sale first. Missing and cross-account identifiers return transaction_not_found.
Tax-year gains
GET /v1/portfolio/gains sums realised profit or loss from your sale transactions inside one period and groups it by the CGT status recorded on each sale. Supply exactly one of calendar_year=2025 (1 January through 31 December inclusive) or uk_tax_year=2025-26 (6 April through 5 April inclusive; the two years in the label must be consecutive) — the two filters are mutually exclusive.
curl -H "X-API-Key: bullion_your_key" \
"https://api.bullionapi.dev/v1/portfolio/gains?uk_tax_year=2025-26"curl -H "X-API-Key: bullion_your_key" \
"https://api.bullionapi.dev/v1/portfolio/gains?calendar_year=2025"{
"status": "success",
"period": {
"kind": "uk_tax_year",
"label": "2025-26",
"start": "2025-04-06",
"end": "2026-04-05"
},
"buckets": {
"exempt": "1234.50500000",
"not_exempt": "99.99400000",
"unknown": "0.00000000"
},
"total": "1334.49900000"
}The response echoes the resolved period and returns buckets.exempt, buckets.not_exempt, and buckets.unknown plus their total, all as exact decimal strings. Omitting both filters returns missing_period; supplying both returns conflicting_periods; malformed labels return invalid_calendar_year or invalid_tax_year. Figures exclude annual allowances, matching and pooling rules, and tax rates. They are not tax advice.
Tax-year gains
GET /v1/portfolio/gains sums realised profit or loss from your sale transactions inside one period and groups it by the CGT status recorded on each sale. Supply exactly one of calendar_year=2025 (1 January through 31 December inclusive) or uk_tax_year=2025-26 (6 April through 5 April inclusive; the two years in the label must be consecutive) — the two filters are mutually exclusive.
curl -H "X-API-Key: bullion_your_key" \
"https://api.bullionapi.dev/v1/portfolio/gains?uk_tax_year=2025-26"curl -H "X-API-Key: bullion_your_key" \
"https://api.bullionapi.dev/v1/portfolio/gains?calendar_year=2025"{
"status": "success",
"period": {
"kind": "uk_tax_year",
"label": "2025-26",
"start": "2025-04-06",
"end": "2026-04-05"
},
"buckets": {
"exempt": "1234.50500000",
"not_exempt": "99.99400000",
"unknown": "0.00000000"
},
"total": "1334.49900000"
}The response echoes the resolved period and returns buckets.exempt, buckets.not_exempt, and buckets.unknown plus their total, all as exact decimal strings. Omitting both filters returns missing_period; supplying both returns conflicting_periods; malformed labels return invalid_calendar_year or invalid_tax_year. Figures exclude annual allowances, matching and pooling rules, and tax rates. They are not tax advice.
Rate limits
Authenticated successful market-data /v1 responses include these headers so you can track usage in real time. X-API-CURRENT and X-API-QUOTA are additive compatibility aliases; the existing x-ratelimit-* names remain supported. Portfolio responses omit these monthly quota headers because portfolio operations do not use the monthly market-data quota. Each API key also permits 60 requests per rolling 60-second window across REST and hosted MCP; short-window denials return rate_limitedand do not consume monthly quota.
| Header | Type | Description |
|---|---|---|
| x-ratelimit-limit | integer | Your plan’s monthly cap. |
| x-ratelimit-used | integer | Requests used this month. |
| x-ratelimit-remaining | integer | Requests remaining. |
| x-ratelimit-reset | ISO 8601 | When the quota resets (1st 00:00 UTC). |
| X-API-CURRENT | integer | Compatibility alias for x-ratelimit-used. |
| X-API-QUOTA | integer | Compatibility alias for x-ratelimit-limit. |
Error responses
This is a breaking change: every application-authored JSON HTTP error now uses one nested envelope. Clients must stop reading legacy top-level error, code, and quota fields. The HTTP status remains the source of truth. Better Auth, framework-owned responses, and MCP JSON-RPC errors keep their protocol contracts.
401 — Missing API key
HTTP/1.1 401 Unauthorized
content-type: application/json
{
"status": "error",
"error": {
"code": "missing_api_key",
"message": "Missing API key"
}
}401 — Conflicting credentials
HTTP/1.1 401 Unauthorized
content-type: application/json
{
"status": "error",
"error": {
"code": "conflicting_credentials",
"message": "Conflicting API key credentials"
}
}429 — Monthly quota exceeded
HTTP/1.1 429 Too Many Requests
x-ratelimit-limit: 30
x-ratelimit-used: 30
x-ratelimit-remaining: 0
x-ratelimit-reset: 2026-08-01T00:00:00.000Z
X-API-CURRENT: 30
X-API-QUOTA: 30
retry-after: 86400
{
"status": "error",
"error": {
"code": "quota_exceeded",
"message": "Your monthly usage limit has been reached. Please upgrade your subscription plan.",
"plan": "free",
"used": 30,
"limit": 30,
"reset_at": "2026-08-01T00:00:00.000Z"
}
}429 — Rolling API-key limit
HTTP/1.1 429 Too Many Requests
retry-after: 42
{
"status": "error",
"error": {
"code": "rate_limited",
"message": "Rate limit exceeded"
}
}400 — Bad request
HTTP/1.1 400 Bad Request
content-type: application/json
{
"status": "error",
"error": {
"code": "invalid_range",
"message": "The requested range is invalid."
}
}400 — Invalid unit
HTTP/1.1 400 Bad Request
content-type: application/json
{
"status": "error",
"error": {
"code": "invalid_unit",
"message": "The requested output unit is invalid."
}
}Registered error codes
| Code | Status | Description |
|---|---|---|
| config_error | 500 | The API is not configured correctly. |
| billing_state_conflict | 503 | The billing state needs support attention. |
| annual_billing_unavailable | 503 | Annual billing is currently unavailable. |
| billing_interval_change_conflict | 409 | A billing interval change conflicts with the current billing state. |
| billing_interval_change_expired | 409 | The billing interval change preview has expired. |
| billing_interval_change_failed | 402 | The billing interval change payment failed. |
| billing_interval_change_invalid | 400 | The billing interval change is invalid. |
| billing_interval_change_origin_forbidden | 403 | This billing action requires a trusted origin. |
| billing_interval_change_revalidation_conflict | 409 | The subscription could not be verified for an annual-to-monthly change. |
| billing_interval_change_processing | 409 | The billing interval change is already processing. |
| billing_cancellation_conflict | 409 | The subscription cancellation conflicts with the current billing state. |
| billing_cancellation_failed | 503 | The subscription cancellation could not be applied. |
| billing_cancellation_invalid | 400 | The subscription cancellation is invalid. |
| conflicting_credentials | 401 | The supplied API-key credentials do not match. |
| data_unavailable | 503 | The requested data is not available. |
| email_not_verified | 403 | The API-key owner must verify their email address. |
| invalid_api_key | 401 | The API key is invalid or revoked. |
| invalid_country | 400 | The country code is invalid. |
| invalid_currency | 400 | The currency code is invalid. |
| invalid_date | 400 | The date format is invalid. |
| invalid_layer | 400 | The central-bank gold layer is invalid. |
| invalid_range | 400 | The requested range is invalid. |
| invalid_region | 400 | The macro region is invalid. |
| invalid_unit | 400 | The requested output unit is invalid. |
| invalid_year | 400 | The year format is invalid. |
| method_not_allowed | 405 | The API does not support this method. |
| missing_api_key | 401 | An API key is required. |
| missing_params | 400 | Required request parameters are missing. |
| no_data | 404 | Cached data is not available. |
| plan_required | 403 | The request requires an eligible subscription plan. |
| quota_exceeded | 429 | The monthly request quota has been exceeded. |
| rate_limited | 429 | The API key has exceeded its rolling request window. |
| unauthenticated | 401 | A signed-in session is required. |
| api_key_not_found | 404 | The API key was not found. |
| insufficient_scope | 403 | API key lacks the required portfolio scope |
| invalid_portfolio_permission | 400 | The portfolio permission is invalid. |
| portfolio_not_found | 404 | The account does not have a portfolio. |
| portfolio_currency_locked | 409 | The reporting currency is locked after the first purchase. |
| missing_idempotency_key | 400 | An Idempotency-Key header is required for portfolio mutations. |
| invalid_purchase | 400 | The purchase request is invalid |
| invalid_sale | 400 | The sale request is invalid |
| invalid_allocation | 400 | The selected sale lots are invalid. |
| idempotency_conflict | 409 | The Idempotency-Key was already used with another request. |
| invalid_cursor | 400 | The transaction history cursor is invalid. |
| invalid_limit | 400 | The transaction history page size is invalid. |
| invalid_filter | 400 | The transaction history filter value is invalid. |
| invalid_calendar_year | 400 | The transaction history calendar year is invalid. |
| invalid_tax_year | 400 | The UK tax-year label is invalid. |
| missing_period | 400 | Use either calendar_year or uk_tax_year. |
| conflicting_periods | 400 | Use either calendar_year or uk_tax_year, not both. |
| transaction_not_found | 404 | The transaction was not found. |
| stale_transaction | 412 | Load the transaction again and retry with its latest ETag. |
| transaction_in_use | 409 | Gold from this purchase has been sold and cannot be deleted. |
| sale_date_invalid | 400 | The sale date is invalid. |
| insufficient_holdings | 409 | The portfolio does not have enough gold for this sale. |
| conflicting_params | 400 | The request parameters conflict. |
| invalid_date_type | 400 | The date type is invalid. |
| no_change_data | 404 | Cached gold data is missing for one or both requested dates. |
| invalid_change_baseline | 422 | The starting gold price must be non-zero. |
| health_degraded | 503 | The database is unreachable or cached data is stale. |
| unknown_metal | 400 | The requested metal is invalid. |
| internal_error | 500 | The daily image data could not be loaded. |
| render_error | 500 | The daily image could not be rendered. |
| invalid_json | 400 | Invalid JSON |
| invalid_email | 400 | Invalid email |
| invalid_request | 400 | Invalid one-click unsubscribe request |
| invalid_token | 400 | Invalid or expired unsubscribe token |
| expired_token | 400 | Invalid or expired unsubscribe token |
Supported currencies
Reviewed currencies in this deployment’s static configuration: USD, EUR, GBP. Static documentation does not check materialised price rows. Authenticated GET /v1/symbols is authoritative for currently serviceable runtime currency choices. USD is the default.
Currency conversion input attribution: Exchange Rates By UniRateAPI. These are Bullion API conversion snapshots, not provider end-of-day rates.
Use from Claude, ChatGPT, Cursor
Bullion hosts an MCP server at https://mcp.bullionapi.dev. AI clients call the get_gold_latest tool directly — same API key, same quota.
Install in Cursor · Install in VS Code
Claude Desktop, ChatGPT (Settings → Connectors) and other clients: add the server with your API key as a Bearer token.
{
"mcpServers": {
"bullion": {
"type": "http",
"url": "https://mcp.bullionapi.dev",
"headers": { "Authorization": "Bearer bullion_your_key" }
}
}
}Use the CLI
For people working in a terminal, @paulund/bullion-cli provides a focused command-line view of the cached gold price data.
npm install --global @paulund/bullion-cli
export BULLION_API_KEY=your_key_here
bullion --asset gold
npx @paulund/bullion-cli --asset gold --currency EURThis is the terminal-user path. For AI agents, see the MCP server documentation.
OpenAPI spec
The full API contract as OpenAPI 3.1, for code-gen, Postman and AI function-calling clients: api.bullionapi.dev/openapi.json.