# Bullion API documentation

Base URL: `https://api.bullionapi.dev`

New accounts: verify your email and open the dashboard to receive your first API key and a ready-to-run cURL automatically. Save the full key when it is shown; it is not recoverable later. Existing accounts can create a key from the API keys page. Keys have no portfolio access by default.

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. 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 HTTP 401 with code `conflicting_credentials`.

Usage rights: Free and Pro are for non-commercial use of the Bullion API Service only. Business includes commercial display and integration. Standard plans do not permit redistribution of raw or substantially equivalent market-price data or redistribution or resale of API access. Where the source grants reuse or redistribution rights in particular public statistics, those rights remain unaffected by plan and are subject to the source terms. See https://bullionapi.dev/pricing and https://bullionapi.dev/terms.

Currency conversion input attribution: [Exchange Rates By UniRateAPI](https://unirateapi.com/). Bullion API stores informational conversion snapshots; they are not provider end-of-day rates or a standalone currency-data product.

## Quick start

Read the latest price. If you don't have an API key yet, get one at https://bullionapi.dev/signup.

Build indicative valuations, reference displays and charts from metal prices. Metal observations refresh hourly; projected currencies use an accepted daily FX snapshot. These estimates are not executable checkout prices or binding quotes and must not be used for trading or settlement.

A market-data request consumes one monthly request once it passes authentication and quota checks and reaches endpoint execution, even if it returns a validation error, missing data or a controlled service error. Repeated calls and retries each count; there is no automatic refund. Requests rejected before this point do not consume monthly allowance. Missing or invalid credentials, unverified accounts, insufficient MCP scopes, short-window rate-limit denials and exhausted monthly quota do not consume a new monthly request. MCP tool discovery and input schema failures, the currency catalogue, usage reads and portfolio operations also do not consume monthly market-data allowance. Endpoint validation after a quota claim is different from an MCP input schema rejection before execution.

```bash
curl -H "X-API-Key: bullion_your_key" \
  "https://api.bullionapi.dev/v1/gold/price?currency=USD"
```

```ts
const res = await fetch('https://api.bullionapi.dev/v1/gold/price?currency=USD', {
  headers: { 'X-API-Key': process.env.BULLION_API_KEY! },
});
const { series } = await res.json();
console.log(`Gold: $${series[0].observations[0].value}/oz`);
```

```py
import os, requests
r = requests.get(
  'https://api.bullionapi.dev/v1/gold/price',
  params={'currency': 'USD'},
  headers={'X-API-Key': os.environ['BULLION_API_KEY']},
)
r.raise_for_status()
print(f"Gold: ${r.json()['series'][0]['observations'][0]['value']}/oz")
```

For spreadsheet or no-code clients that cannot set custom headers, use the fallback query URL below. Treat it as a secret and prefer the header transport whenever possible.

```text
GET https://api.bullionapi.dev/v1/gold/price?currency=USD&api_key=bullion_your_key
```

## GET /v1/{metal}/price

Returns the spot price of one metal in the requested base currency as a single `<metal>_price` series: the latest price, or daily history when `start` and `end` are supplied. Each metal has its own route: `/v1/gold/price`, `/v1/silver/price`, `/v1/palladium/price`, `/v1/copper/price`, and `/v1/platinum/price`. This is an authenticated database-only reader that never calls an upstream provider.

### Query parameters

| Name | Type | Description |
|---|---|---|
| currency | string | ISO 4217 currency code. Defaults to USD. |
| unit | string | Optional output mass unit: gram/g, kilogram/kg, or troy_oz/toz. Defaults to troy_oz; invalid values return 400 invalid_unit. |
| start | YYYY-MM-DD | Optional first inclusive UTC day. Omit start and end for the latest price only; start alone runs to today. |
| end | YYYY-MM-DD | Optional last inclusive UTC day. Requires start; the range may span at most five years. |
| api_key | string | Fallback credential for clients that cannot set the X-API-Key header. Prefer the header transport whenever possible. |

### Response (latest)

```json
{
  "status": "success",
  "metric": "price",
  "metal": "gold",
  "currency": "USD",
  "unit": "troy_oz",
  "start": null,
  "end": null,
  "series": [
    {
      "id": "gold_price",
      "label": "Gold spot price",
      "unit": "troy_oz",
      "frequency": "hourly",
      "method": "latest",
      "observations": [{ "date": "2026-08-24T22:00:00.000Z", "value": "4527.86" }]
    }
  ]
}
```

Without a range the series holds one observation (`frequency: hourly`, `method: latest`) whose `date` is the full UTC observation timestamp. Every `value` is a decimal string in the requested unit, rounded to eight decimal places. Unsupported currencies return `400 invalid_currency`, unsupported units `400 invalid_unit`, and a missing cached price `404 no_data`; cache read failures return `503 data_unavailable`.

### Historical prices

History series include `coverage.earliest`, `coverage.latest` and `coverage.observation_count` for usable stored weekday closes before request time, independently of the requested range. The five-year query limit does not guarantee five years or 365 days of data. Dates and counts do not establish uninterrupted history or freshness. Projected history begins at activation without backfill.

```bash
curl -H "X-API-Key: bullion_your_key" \
  "https://api.bullionapi.dev/v1/gold/price?currency=USD&start=2025-07-08&end=2026-07-08"
```

```json
{
  "status": "success",
  "metric": "price",
  "metal": "gold",
  "currency": "USD",
  "unit": "troy_oz",
  "start": "2025-07-08",
  "end": "2026-07-08",
  "series": [
    {
      "id": "gold_price",
      "label": "Gold spot price",
      "unit": "troy_oz",
      "frequency": "daily",
      "method": "weekday_close",
      "coverage": { "earliest": "2025-07-08", "latest": "2026-07-08", "observation_count": 250 },
      "observations": [
        { "date": "2025-07-08", "value": "3320.1" },
        { "date": "2025-07-09", "value": "3342.55" }
      ]
    }
  ]
}
```

With a range, each observation is the final cached price of a UTC weekday, dated `YYYY-MM-DD` (`frequency: daily`, `method: weekday_close`). Saturday and Sunday UTC observations, including the Sunday-evening market reopen, are excluded. Only cached days are included; non-gold history accrues from hourly refreshes and is not backfilled from gold-only history. A range with no weekday close returns `404 no_data`. `end` without `start` returns `400 missing_params`; malformed dates return `400 invalid_date`; reversed ranges or ranges over five years return `400 invalid_range`.

### Projected-currency provenance

For a configured projected currency the observation time is the hourly canonical USD metal observation used in the projected price, and the series adds a `conversion` object; it is omitted for USD, EUR, and GBP:

```json
"conversion": {
  "snapshot_at": "2026-08-24T23:10:00.000Z",
  "stale": false,
  "attribution": "Exchange Rates By UniRateAPI",
  "attribution_url": "https://unirateapi.com/"
}
```

`snapshot_at` is the newest daily Bullion API conversion snapshot used by the series, not a provider end-of-day observation. `stale` is true when a snapshot is more than seven UTC calendar days older than the price it converts. Once the latest snapshot is more than seven UTC calendar days old, the latest price is withheld (404 `no_data`) until a new snapshot is accepted. Internal UniRateAPI observations are never returned by this endpoint.

The legacy `GET /v1/latest` and `GET /v1/metals/{metal}` routes were removed on 26 September 2026; like any unknown `/v1` path they now return `404 not_found` with the JSON error envelope. Request each metal from its `/v1/{metal}/price` route instead.

## GET /v1/gold/carat

Returns gold prices per gram for the approved karat keys: `24k`, `23k`, `22k`, `21.6k`, `21k`, `18k`, `16k`, `14k`, `12k`, `10k`, `9k`, `8k`, and `6k`. Gold is the only supported metal and grams are the only response unit. Without `date`, the current cached gold row is used; with `date=YYYY-MM-DD`, the final cached gold observation in that UTC day is used. This is a database-only reader and never calls upstream providers or bridges currencies.

### Query parameters

| Name | Type | Description |
|---|---|---|
| currency | string | Three-letter ISO 4217 code, case-insensitive. Defaults to USD and must have cached gold data. |
| date | YYYY-MM-DD | Optional exact UTC day. The final cached 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

```bash
curl -H "X-API-Key: bullion_your_key" \
  "https://api.bullionapi.dev/v1/gold/carat?currency=USD"
```

### Response

```json
{
  "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.65586640",
    "9k": "54.59027976",
    "8k": "48.52469312",
    "6k": "36.39351984"
  }
}
```

Each value is a decimal string, calculated from the exact karat ratio against the unrounded pure-gold per-gram value and rounded half-up to eight decimal places. Malformed currency or date values return `400 invalid_currency` or `400 invalid_date`; a valid but unseeded currency or day returns `404 no_data`; cache read failures return `503 data_unavailable`.

## GET /v1/{metal}/change

Returns one metal's absolute and percentage change between the final cached observation of two UTC days. Each metal has its own route: `/v1/gold/change`, `/v1/silver/change`, `/v1/palladium/change`, `/v1/copper/change`, and `/v1/platinum/change`. The API reads the database cache and never calls an upstream provider.

### Query parameters

| Name | Type | Description |
|---|---|---|
| currency | string | ISO 4217 base currency. Defaults to USD. |
| unit | string | Optional output mass unit: gram/g, kilogram/kg, or troy_oz/toz. Defaults to troy_oz. |
| start | YYYY-MM-DD | First UTC day to compare. end defaults to today and may be at most 365 days later. |
| end | YYYY-MM-DD | Last UTC day to compare. Requires start. |
| period | string | Rolling UTC window ending today: 1d (1 day), 1w (7 days), 1m (30 days), or 1y (365 days). Use instead of start and end. |
| api_key | string | Fallback credential for clients that cannot set the X-API-Key header. Prefer the header transport whenever possible. |

Provide either start (with an optional end) or one period, not both.

### Example

```bash
curl -H "X-API-Key: bullion_your_key" \
  "https://api.bullionapi.dev/v1/silver/change?currency=USD&period=1w"
```

### Response

```json
{
  "status": "success",
  "metric": "change",
  "metal": "silver",
  "currency": "USD",
  "unit": "troy_oz",
  "start": { "date": "2026-08-03", "value": "45" },
  "end": { "date": "2026-08-10", "value": "46" },
  "change": "1",
  "change_percent": "2.2222"
}
```

The final observation within each UTC day is selected deterministically, weekends included. All values are decimal strings: `change` is in the requested unit and `change_percent` is relative to the start price, rounded to four decimal places. Gram and kilogram start and end values are rounded to eight decimal places before `change` is taken. Mixing date forms returns `400 conflicting_params` and supplying neither returns `400 missing_params`; invalid dates return `400 invalid_date`, reversed or overlong ranges return `400 invalid_range`, unsupported periods return `400 invalid_period`, and unsupported currencies or units return `400 invalid_currency` or `400 invalid_unit`. Missing cached data returns `404 no_change_data`; a zero start price returns `422 invalid_change_baseline`.

## GET /v1/gold-silver-ratio

Returns the gold-to-silver ratio from the local price cache. Omit `start` and `end` for the latest stored ratio, or supply both for final stored observations in each UTC day within the inclusive range. Missing days are omitted and no history is fabricated. Each `value` is an exact decimal string rounded half-up to eight places (a JSON number before 2026-10-07). The calculation is cached in memory for five minutes per currency and range. API requests never call upstream providers.

### Query parameters

| Name | Type | Description |
|---|---|---|
| currency | string | ISO 4217 base currency. Defaults to USD. |
| start | YYYY-MM-DD | Optional first inclusive UTC day. Use with end. |
| end | YYYY-MM-DD | Optional last inclusive UTC day. Maximum range is five years. |
| api_key | string | Fallback credential for clients that cannot set the X-API-Key header. Prefer the header transport whenever possible. |

### Example

```bash
curl -H "X-API-Key: bullion_your_key" \
  "https://api.bullionapi.dev/v1/gold-silver-ratio?currency=USD"
```

### Response

```json
{
  "status": "success",
  "currency": "USD",
  "start_date": null,
  "end_date": null,
  "points": [{ "date": "2026-09-24", "value": "68.40312345" }]
}
```

Invalid currencies and incomplete, reversed or overlong date ranges return `400`. A latest request without both stored metal rows, or whose gold and silver observations are more than six hours apart, returns `404 no_data`; a historical request with no overlapping observations returns `200` with an empty `points` array.

## GET /v1/gold/moving-average

Returns the simple moving average of final daily gold closes. Each value uses up to `window` valid stored closes including the current close; values before enough closes exist are partial warm-up averages. Uses the final stored gold observation for each UTC weekday; weekend rows are excluded and missing days are omitted. Values are decimal strings rounded half-up to eight places. The derived series is cached in memory for five minutes per currency and range. API requests never call upstream providers.

### Query parameters

| Name | Type | Description |
|---|---|---|
| window | integer | Required. 50 or 200 stored daily closes; a missing or other value returns `400 invalid_window`. |
| currency | string | ISO 4217 base currency. Defaults to USD. |
| start | YYYY-MM-DD | Required first inclusive UTC day. |
| end | YYYY-MM-DD | Required last inclusive UTC day. Maximum range is five years. |
| api_key | string | Fallback credential for clients that cannot set the X-API-Key header. Prefer the header transport whenever possible. |

### Example

```bash
curl -H "X-API-Key: bullion_your_key" \
  "https://api.bullionapi.dev/v1/gold/moving-average?window=50&currency=USD&start=2026-01-01&end=2026-12-31"
```

### Response

```json
{
  "status": "success",
  "currency": "USD",
  "window": 50,
  "unit": "toz",
  "start_date": "2026-01-01",
  "end_date": "2026-12-31",
  "points": [{ "date": "2026-01-01", "value": "5100.00000000" }]
}
```

Omitting either date returns `400 missing_params`; invalid dates, reversed or overlong ranges, and invalid currencies return `400`.

## GET /v1/gold/volatility

Returns annualised sample volatility of 30 daily gold returns as a fraction in an exact decimal string rounded half-up to eight places, so `"0.15000000"` is 15% (a JSON number before 2026-10-07). Days before 30 returns exist are omitted, so a short range can return an empty `points` array. Uses the final stored gold observation for each UTC weekday; weekend rows are excluded and missing days are omitted. The calculation is cached in memory for five minutes per currency and range. API requests never call upstream providers.

### Query parameters

| Name | Type | Description |
|---|---|---|
| currency | string | ISO 4217 base currency. Defaults to USD. |
| start | YYYY-MM-DD | Required first inclusive UTC day. |
| end | YYYY-MM-DD | Required last inclusive UTC day. Maximum range is five years. |
| api_key | string | Fallback credential for clients that cannot set the X-API-Key header. Prefer the header transport whenever possible. |

### Example

```bash
curl -H "X-API-Key: bullion_your_key" \
  "https://api.bullionapi.dev/v1/gold/volatility?currency=USD&start=2026-01-01&end=2026-12-31"
```

### Response

```json
{
  "status": "success",
  "currency": "USD",
  "window": 30,
  "start_date": "2026-01-01",
  "end_date": "2026-12-31",
  "points": [{ "date": "2026-01-01", "value": "0.15000000" }]
}
```

Omitting either date returns `400 missing_params`; invalid dates, reversed or overlong ranges, and invalid currencies return `400`.

## GET /v1/gold/real-price and GET /v1/gold/yield-correlation

Both require inclusive `start`/`end` dates, at most five years, and USD only.
`real_price` returns stored weekday USD gold × latest stored seasonally adjusted
CPI-U index / index for the observation month, with `reference_period` (the
published month end). Missing CPI months are omitted without carrying forward.
`corr_10y_yield` returns Pearson correlation over 90 aligned simple daily gold
returns and daily 10-year real Treasury yield changes, with `window: 90`.
Both current and previous dates must match. Up to one calendar year of lookback
is read; incomplete and zero-variance windows are omitted. Stored inputs are
selected by SQL, then calculated exactly as eight-place decimal strings. There
are no upstream calls during customer requests. Successful nonempty series have
a five-minute cache. Missing source data returns `503 dependency_not_ready`
with a public dependency name and metric; other endpoints remain independent.
Responses include source-institution attribution. All plans have access.
These calculations use revised stored data; they are not point-in-time backtests.

## GET /v1/gold/drawdown

Returns how far each trading-day gold close sits below its running peak over up to 1,260 trading-day closes (about five years), as a fraction in an exact decimal string rounded half-up to eight places: `"-0.25000000"` is 25% below the peak (a JSON number before 2026-10-07). Values before enough observations exist are partial warm-up drawdowns. Uses the final stored gold observation for each UTC weekday; weekend rows are excluded and missing days are omitted. The calculation is cached in memory for five minutes per currency and range. API requests never call upstream providers.

### Query parameters

| Name | Type | Description |
|---|---|---|
| currency | string | ISO 4217 base currency. Defaults to USD. |
| start | YYYY-MM-DD | Required first inclusive UTC day. |
| end | YYYY-MM-DD | Required last inclusive UTC day. Maximum range is five years. |
| api_key | string | Fallback credential for clients that cannot set the X-API-Key header. Prefer the header transport whenever possible. |

### Example

```bash
curl -H "X-API-Key: bullion_your_key" \
  "https://api.bullionapi.dev/v1/gold/drawdown?currency=USD&start=2026-01-01&end=2026-12-31"
```

### Response

```json
{
  "status": "success",
  "currency": "USD",
  "lookback_observations": 1260,
  "start_date": "2026-01-01",
  "end_date": "2026-12-31",
  "points": [{ "date": "2026-01-01", "value": "-0.05000000" }]
}
```

Omitting either date returns `400 missing_params`; invalid dates, reversed or overlong ranges, and invalid currencies return `400`.

## GET /v1/policy-rates/{country}

**Central-bank policy rates, one country per request.** US series are the federal funds target range lower and upper bounds (`policy_target_lower`, `policy_target_upper`) and the effective federal funds rate; euro-area series are the ECB deposit facility, main refinancing and marginal lending rates; the UK series is BoE Bank Rate (`IUDBEDR`). Policy rates are stored per daily source observation (`frequency: daily`). `GET /v1/policy-rates` returns the latest value for every covered country.

## GET /v1/inflation/{country}

**Inflation measures, one country per request.** Filter with `basis=year_over_year|breakeven|implied|expectation`. BLS CPI-U, ONS CPI (`D7G7`) and euro-area HICP are year on year; US Treasury 5Y/10Y/30Y breakevens and BoE RPI-linked implied inflation at 2Y/5Y/10Y/30Y carry a `tenor`; ECB Consumer Expectations Survey series (`tenor` `12m` or `5y`) use `survey_expectation` and are not market-implied. All are monthly. `GET /v1/inflation` returns the latest value for every covered country.

## GET /v1/bond-yields/{country}

**Government bond yields, one country per request.** Filter with `tenor=2y|5y|10y|30y` and `basis=nominal|real`. US Treasury nominal 2Y/5Y/10Y/30Y and real 5Y/10Y/30Y and ECB AAA euro-area nominal 2Y/5Y/10Y/30Y yields are `monthly_average` values; BoE gilt nominal and real zero-coupon spot yields at 2Y/5Y/10Y/30Y preserve official month-end archive dates and use `month_end`. `GET /v1/bond-yields` returns the latest value for every covered country.

### Parameters

This table applies to all three endpoints: policy rates, inflation and bond yields.

| Name | Type | Description |
|---|---|---|
| country | path | ISO 3166-1 alpha-2 code or `EA`, case-insensitive; `uk` and `eu` are aliases. `US`, `GB` and `EA` are covered; any other valid code returns HTTP 404 `no_data`, an unknown code HTTP 400 `invalid_country`. |
| start | date | First inclusive date (`YYYY-MM-DD`). Omit start and end for the latest observation of each series. The collection routes ignore `start` and `end`. |
| end | date | Last inclusive date. Requires `start` and defaults to today. Ranges may span at most 10 years; longer ranges return 400 `invalid_range`. |
| tenor, basis | string | Inflation (`basis`) and bond yields (`tenor`, `basis`) only; policy rates take no filters. Unknown values return HTTP 400 `invalid_series_filter`. They also apply to the collection routes. |
| api_key | string | Fallback credential for clients that cannot set the X-API-Key header. Prefer the header transport whenever possible. |

### Example

```bash
curl -H "X-API-Key: bullion_your_key" \
  "https://api.bullionapi.dev/v1/bond-yields/us?tenor=10y&start=2026-05-01&end=2026-06-30"
```

### Response

```json
{
  "status": "success",
  "metric": "bond_yields",
  "country": "US",
  "country_name": "United States",
  "start": "2026-05-01",
  "end": "2026-06-30",
  "series": [
    {
      "id": "treasury_nominal_yield_10y",
      "label": "10-year Treasury yield (nominal)",
      "basis": "nominal",
      "tenor": "10y",
      "unit": "percent",
      "frequency": "monthly",
      "method": "monthly_average",
      "source": { "id": "us_treasury", "series_id": "daily_treasury_yield_curve.BC_10YEAR", "attribution": "U.S. Department of the Treasury" },
      "observations": [
        { "date": "2026-05-31", "value": "4.41" },
        { "date": "2026-06-30", "value": "4.25" }
      ]
    }
  ]
}
```

`GET /v1/policy-rates` returns the latest observation per series for every covered country (abridged: the full response also lists the US target range bounds and EA):

```json
{
  "status": "success",
  "metric": "policy_rates",
  "countries": [
    {
      "country": "US",
      "country_name": "United States",
      "series": [
        {
          "id": "effective_federal_funds_rate",
          "label": "Effective federal funds rate",
          "unit": "percent",
          "frequency": "daily",
          "method": "latest",
          "source": { "id": "ny_fed", "series_id": "EFFR.percentRate", "attribution": "Federal Reserve Bank of New York" },
          "observations": [{ "date": "2026-06-30", "value": "3.63" }]
        }
      ]
    },
    {
      "country": "GB",
      "country_name": "United Kingdom",
      "series": [
        {
          "id": "bank_rate",
          "label": "Bank Rate",
          "unit": "percent",
          "frequency": "daily",
          "method": "latest",
          "source": { "id": "boe", "series_id": "IUDBEDR", "attribution": "Bank of England" },
          "observations": [{ "date": "2026-06-30", "value": "3.75" }]
        }
      ]
    }
  ]
}
```

Values are decimal strings. Series without observations in the range are left out, and a request with none returns HTTP 404 `no_data`. Collection responses list `countries: [{ country, country_name, series }]` in the order US, GB, EA and count as one quota-consuming request. The API reads the database cache; protected cron collection is the only upstream caller.

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 curves are direct BoE nominal and real zero-coupon spot values plus BoE-derived RPI-linked implied inflation values from month-end archives; source dates are preserved, including final business days. Neither UK curve values nor UK Bank Rate/CPI are averaged.

Sources: [Federal Reserve Bank of New York reference rates](https://www.newyorkfed.org/markets/reference-rates/effr), [U.S. Department of the Treasury Daily Interest Rate XML Feed](https://home.treasury.gov/resource-center/data-chart-center/interest-rates/pages/xml), [U.S. Bureau of Labor Statistics Public Data API](https://api.bls.gov/publicAPI/v1/timeseries/data/), [Bank of England Database](https://www.bankofengland.co.uk/boeapps/database/_iadb-fromshowcolumns.asp), [BoE yield curve archives](https://www.bankofengland.co.uk/statistics/yield-curves), and [Office for National Statistics D7G7 CPI series](https://www.ons.gov.uk/economy/inflationandpriceindices/timeseries/d7g7/mm23/data). Reuse and attribution follow the linked source terms and the [Open Government Licence](https://www.nationalarchives.gov.uk/doc/open-government-licence/version/3/). BLS disclaimer: *BLS.gov cannot vouch for the data or analyses derived from these data after retrieval.*

### Euro-area series

Use the `EA` country code (alias `eu`). Euro-area inflation includes metadata-verified all-items HICP year-over-year inflation as `ecb_hicp_yoy` plus ECB Consumer Expectations Survey weighted-median 12-month and five-year expectations. CES observations use `survey_expectation` and are not market-implied.

The HICP series is `HICP.M.U2.N.000000.4D0.ANR`: monthly, euro area changing composition (`U2`), neither seasonally nor working-day adjusted (`N`), all-items (`000000`), Eurostat (`4D0`), annual rate of change (`ANR`). ECB AAA yields use `YC.B.U2.EUR.4F.G_N_A.SV_C_YM` with `SR_2Y`, `SR_5Y`, `SR_10Y`, and `SR_30Y` series. The API reads the database cache; protected cron collection is the only upstream caller.

Sources: [European Central Bank Data API](https://data-api.ecb.europa.eu/service/data) and the [ECB Data Portal](https://data.ecb.europa.eu/). European Central Bank statistical information is freely available for reuse when accurately reproduced and cited under the [ECB policy on reuse of ESCB statistics](https://www.ecb.europa.eu/stats/ecb_statistics/governance_and_quality_framework/html/usage_policy.en.html); ECB source conditions and disclaimers apply. ECB yields are arithmetic averages of valid observations from complete UTC calendar months.

## Monetary metrics endpoints

Use GET /v1/money-supply/{country} or GET /v1/central-bank-balance-sheet/{country} for one country, or omit {country} for the latest registered series across countries. Country routes accept inclusive start and end dates; end requires start. Omit both for the latest observation per series. Valid countries without registered data return HTTP 404 no_data. These endpoints read the database cache; protected cron collection is the only upstream caller.

Monetary values are exact decimal strings. Each series includes its upstream source id, attribution and applicable reuse notice. The Bank of Canada M2 series use Valet vectors V41552786 and V41552796, published in millions of dollars; source month-start dates are represented as calendar month-end periods and values are normalized exactly to base CAD. For M2, top-level `method` and `stock_semantics` describe the latest methodology; each observation carries its period-resolved method and stock semantics, and `methodology_history` lists inclusive effective periods. Both vectors use `monthly_average` / `period_average` with Wednesday observations through January 1994 and daily observations from February 1994 through September 2020, then `month_end` / `period_end` from October 2020. Bank of Canada total assets use weekly Wednesday vector V36610, also converted exactly from millions of dollars to CAD. Valet value cells must be non-empty decimal strings; JSON numbers and empty cells fail refresh validation, and empty cells are not interpreted as withdrawals. Accepted values are normalized exactly and returned as decimal strings. Third-party M2++ components are excluded.

The underlying source data is available on the [Bank of Canada website](https://www.bankofcanada.ca/valet/) free of charge. Attribute the Bank as the source and indicate changes without implying endorsement. See the [Bank of Canada terms](https://www.bankofcanada.ca/terms/); third-party content is excluded unless separately licensed. Prospective purchasers should be told before purchase that this source data is available free of charge.

Australian money supply comprises the RBA's original, not-seasonally-adjusted monthly D3 levels: au_money_supply_m1 (DMAM1N, from 1975-02-28), au_money_supply_m3 (DMAM3N, from 1959-07-31), and au_money_supply_broad_money (DMABMN, from 1976-08-31). Values are stored in base AUD from native AUD billions using the exact factor 1000000000. These levels are revised and not break-adjusted; Bullion API does not derive growth rates. The A1 weekly au_central_bank_total_assets series (ARBAATAW, from 2013-07-03) is a point-in-time observation dated on the source's actual Wednesday and stored in base AUD from AUD millions using the exact factor 1000000. ARBALTLEW is total liabilities and equity and is not represented as liabilities.

Australian attribution is “Source: Reserve Bank of Australia [year]”; the API supplies the year of the read. RBA financial data is governed by section 5 of the RBA Copyright and Disclaimer Notice, not CC BY 4.0. The underlying data is published by the RBA free of charge. Paid Bullion API plans charge for API access, caching, normalization and delivery, not for the underlying RBA data. The RBA does not endorse Bullion API or products or uses based on these data. See https://www.rba.gov.au/copyright/.

Example: GET /v1/money-supply/ca?start=2026-07-01&end=2026-07-31. The Canada response below is a static, not live, example using the Bank of Canada's published July 2026 V41552786 value of 2,853,114 million dollars, normalized exactly to 2853114000000 CAD. See the [Bank of Canada Valet observation](https://www.bankofcanada.ca/valet/observations/V41552786/json?start_date=2026-07-01&end_date=2026-07-01). The response includes source metadata and exact decimal-string observations.

```json
{
  "status": "success",
  "metric": "money_supply",
  "country": "CA",
  "country_name": "Canada",
  "start": "2026-07-01",
  "end": "2026-07-31",
  "series": [
    {
      "id": "boc_m2_gross_monthly",
      "label": "M2 (gross), unadjusted",
      "unit": "cad",
      "frequency": "monthly",
      "method": "month_end",
      "stock_semantics": "period_end",
      "methodology_history": [
        {
          "effective_from": "1968-01-31",
          "effective_through": "1994-01-31",
          "method": "monthly_average",
          "stock_semantics": "period_average",
          "description": "Monthly average of Wednesday observations through January 1994."
        },
        {
          "effective_from": "1994-02-28",
          "effective_through": "2020-09-30",
          "method": "monthly_average",
          "stock_semantics": "period_average",
          "description": "Monthly average of daily observations from February 1994 through September 2020."
        },
        {
          "effective_from": "2020-10-31",
          "effective_through": null,
          "method": "month_end",
          "stock_semantics": "period_end",
          "description": "Month-end methodology for M2 beginning with October 2020 data."
        }
      ],
      "source": {
        "id": "boc",
        "series_id": "V41552786",
        "attribution": "Source: Bank of Canada.",
        "notice": "Attribute the Bank of Canada as the source and indicate changes; do not imply Bank endorsement. Changes made: monthly source dates are represented as calendar month-end periods, and source decimal values are normalized exactly to base Canadian dollars. For content included in a paid service or document for sale, tell prospective purchasers before distribution or sale that it came from the Bank of Canada and is available on the Bank of Canada's website free of charge. Third-party content is excluded unless separately licensed."
      },
      "observations": [
        {
          "date": "2026-07-31",
          "value": "2853114000000",
          "method": "month_end",
          "stock_semantics": "period_end",
          "methodology_description": "Month-end methodology for M2 beginning with October 2020 data."
        }
      ]
    }
  ]
}
```

## Government finance endpoints

`GET /v1/government-debt/{country}`, `GET /v1/government-borrowing/{country}`, `GET /v1/debt-interest/{country}`, and the collections `GET /v1/government-debt`, `GET /v1/government-borrowing`, and `GET /v1/debt-interest`.

**Government debt, borrowing, and debt interest, one endpoint per metric.** `/v1/government-debt` (metric `government_debt`), `/v1/government-borrowing` (`government_borrowing`), and `/v1/debt-interest` (`debt_interest`) return the shared country-series envelope. Per-metric path coverage is generated from registered, servable government-finance series: debt: `us`, `gb` (or `uk`), `au`, `is`, `ea` (or `eu`), `sg`, `at`, `be`, `bg`, `cy`, `cz`, `de`, `dk`, `ee`, `es`, `fi`, `fr`, `gr`, `hr`, `hu`, `ie`, `it`, `lt`, `lu`, `lv`, `mt`, `nl`, `pl`, `pt`, `ro`, `se`, `si`, `sk`, `no`, `ca`, `ch`, `qa`, `jp`, `pe`; borrowing: `us`, `gb` (or `uk`), `is`, `ea` (or `eu`), `at`, `be`, `bg`, `cy`, `cz`, `de`, `dk`, `ee`, `es`, `fi`, `fr`, `gr`, `hr`, `hu`, `ie`, `it`, `lt`, `lu`, `lv`, `mt`, `nl`, `pl`, `pt`, `ro`, `se`, `si`, `sk`, `no`, `ca`, `ch`; debt interest: `gb` (or `uk`), `is`, `ea` (or `eu`), `at`, `be`, `bg`, `cy`, `cz`, `de`, `dk`, `ee`, `es`, `fi`, `fr`, `gr`, `hr`, `hu`, `ie`, `it`, `lt`, `lu`, `lv`, `mt`, `nl`, `pl`, `pt`, `ro`, `se`, `si`, `sk`, `no`, `ca`. These are request path codes; aliases in parentheses resolve to the same geography, and `EA` is the euro-area aggregate rather than an ISO country. Coverage describes registered metric capability, not a guarantee of current stored observations. The bare path returns the latest observation of every series for each covered geography, in registry order, as one quota-counted request. An unknown country code returns HTTP 400 `invalid_country`; any other valid country, an unsupported category, or a range with no observations, returns HTTP 404 `no_data`. Every plan can call these endpoints. They are database-only readers.

### Parameters

| Name | Type | Description |
|---|---|---|
| country | path | Case-insensitive path code; coverage by metric: debt: `us`, `gb` (or `uk`), `au`, `is`, `ea` (or `eu`), `sg`, `at`, `be`, `bg`, `cy`, `cz`, `de`, `dk`, `ee`, `es`, `fi`, `fr`, `gr`, `hr`, `hu`, `ie`, `it`, `lt`, `lu`, `lv`, `mt`, `nl`, `pl`, `pt`, `ro`, `se`, `si`, `sk`, `no`, `ca`, `ch`, `qa`, `jp`, `pe`; borrowing: `us`, `gb` (or `uk`), `is`, `ea` (or `eu`), `at`, `be`, `bg`, `cy`, `cz`, `de`, `dk`, `ee`, `es`, `fi`, `fr`, `gr`, `hr`, `hu`, `ie`, `it`, `lt`, `lu`, `lv`, `mt`, `nl`, `pl`, `pt`, `ro`, `se`, `si`, `sk`, `no`, `ca`, `ch`; debt interest: `gb` (or `uk`), `is`, `ea` (or `eu`), `at`, `be`, `bg`, `cy`, `cz`, `de`, `dk`, `ee`, `es`, `fi`, `fr`, `gr`, `hr`, `hu`, `ie`, `it`, `lt`, `lu`, `lv`, `mt`, `nl`, `pl`, `pt`, `ro`, `se`, `si`, `sk`, `no`, `ca`. Responses echo the canonical upper-case code. |
| start | YYYY-MM-DD | Optional inclusive first period end. Ranges may span at most 10 calendar years; longer ranges return 400 `invalid_range`. Omit `start` and `end` for the latest observation of each series. |
| end | YYYY-MM-DD | Optional inclusive last period end; requires `start` and defaults to today. |
| api_key | string | Fallback credential for clients that cannot set the X-API-Key header. Prefer the header transport whenever possible. |

Each returned government-finance series includes `coverage.earliest` and `coverage.latest`: the earliest and latest usable stored observation period ends, independent of the requested range. Coverage excludes withdrawn or unusable observations and does not imply that every intervening period is available.

Each series has `id`, `label`, `scope` (government scope such as `central_government`), `basis` (for example `gross_debt` or `surplus_or_deficit`), `unit` (for example `usd_millions`, `aud_millions`, or `percent_of_gdp`), `frequency` (`monthly`, `quarterly`, or `annual`), `period_type` (`point_in_time`, `month`, `quarter`, or `fiscal_year`), `method` (`reported`, `derived_ratio`, or `derived_cumulative`), a plain-language `definition` with its sign convention, `derivation` (the `formula` and `inputs`, each with `source_id`, `source_series_id`, and `description`; only on derived series), and `source` (`id`, upstream `series_id`, and `attribution`; a derived series is attributed as a Bullion API calculation naming every input source). Unlike measures are never renamed into a common definition; compare series only when scope and basis match. Each observation has a decimal-string `value` and `date` (the period end); flows such as borrowing and interest add `period_start`. Where published, observations also include source `status` (`actual`, `provisional`, or `revised`). Forecasts, projections, and editorial classifications are never returned.

United States series (U.S. Treasury Fiscal Service data distributed by FRED, millions of U.S. dollars, central-government scope): `treasury_total_public_debt_outstanding` (FRED `GFDEBTN`) and `treasury_debt_held_by_the_public` (`FYGFDPUN`) at the end of each calendar quarter; `treasury_surplus_or_deficit_monthly` from the Monthly Treasury Statement (`MTSDS133FMS`), where a deficit is negative; `treasury_surplus_or_deficit_fiscal_year_to_date` (method `derived_cumulative`), a Bullion API sum of the monthly values from 1 October, the federal fiscal-year start; and `treasury_federal_debt_to_gdp`, a quarterly Bullion API ratio (`derived_ratio`, unit `percent_of_gdp`) of total public debt outstanding at quarter end to BEA nominal GDP (NIPA Table 1.1.5 series `A191RC`, seasonally adjusted annual rate), with both inputs listed in `derivation`.

United Kingdom series (ONS Public sector finances, dataset `PUSF`, current prices, not seasonally adjusted): `ons_public_sector_net_debt` (series `KSE6`, public sector net debt excluding public sector banks at month end, millions of pounds, `public_sector` scope); `ons_net_debt_to_gdp` (`HF6X`, the same measure as a percentage of GDP, reported by ONS with a denominator of GDP for the 12 months centred on the month, which includes projected GDP, so recent ratios are revised as outturn GDP arrives); `ons_net_borrowing_monthly` (`DZLS`, public sector net borrowing excluding public sector banks for the calendar month, millions of pounds, positive for borrowing and negative for a surplus); and `ons_central_government_debt_interest` (`NMFX`, central government debt interest payable for the calendar month, millions of pounds, `central_government` scope, including the capital uplift on index-linked gilts). `ons_net_borrowing_fiscal_year_to_date` (method `derived_cumulative`, period from 1 April to the month end) is a Bullion API calculation, not an ONS-published series: the exact sum of `DZLS` from 1 April, the UK fiscal-year start, produced only when every month since April is published. ONS revises earlier months in each monthly release, and a revised month flows into every later cumulative value.

Euro-area series (`EA`; Eurostat, general-government scope, sector `S13`, euro-area aggregate `EA21`: the 21 member states in the euro area from 2026, back-calculated for earlier periods): `eurostat_general_government_gross_debt_annual` and `eurostat_gross_debt_to_gdp_annual` (consolidated gross debt `GD` at 31 December, `eur_millions` and Eurostat-published `percent_of_gdp`, `point_in_time`), `eurostat_general_government_net_lending_annual` (net lending (+) or net borrowing (-), `B9`) and `eurostat_interest_expenditure_annual` (interest expenditure, `D41PAY`) for the calendar year as `fiscal_year`, all from `gov_10dd_edpt1`; `eurostat_general_government_gross_debt_quarterly` and `eurostat_gross_debt_to_gdp_quarterly` at the quarter end from `gov_10q_ggdebt`; and `eurostat_general_government_net_lending_quarterly` and `eurostat_interest_expenditure_quarterly` for the calendar quarter, not seasonally adjusted, from `gov_10q_ggnfa`. A negative net lending value is net borrowing. Ratios are published by Eurostat, not calculated by Bullion API. Recent quarterly values are Eurostat provisional figures and may be revised; forecast-flagged values are never stored.

Peru series (`PE`, BCRPData, non-financial-public-sector scope): `peru_nonfinancial_public_sector_debt` (series `PN03371FQ`, quarter-end debt balance in millions of Peruvian soles) and `peru_nonfinancial_public_sector_debt_to_gdp` (series `PN03432FQ`, BCRPData-published percentage of PBI for that balance). Both are reported quarterly observations; Bullion API does not calculate the ratio or infer its GDP-denominator method. BCRPData metadata credits the Ministry of Economy and Finance, Banco de la Nación, Ministry of Transportation and Communications, ONP and Cofide as source institutions. BCRPData terms permit total or partial reproduction with source attribution, do not state a non-commercial-only restriction, and are not a Creative Commons licence; responsibility for externally sourced statistics remains with the original provider. Its keyless API is refreshed daily, with recent revisions re-collected for 12 months. Peru borrowing and debt interest are not covered.

Singapore (`SG`) serves annual central-government `singstat_total_government_debt_annual` (SingStat `M130071`, `sgd_millions`, 31 December) and `singstat_government_debt_to_gdp_annual` (Bullion API `derived_ratio` using same-calendar-year nominal GDP, `M015731`, multiplied by 100). Includes non-publicly held instruments, advance deposits and non-spending-purpose borrowing whose proceeds are invested and cannot be spent, not net or consolidated general-government debt. No Singapore borrowing, interest or quarterly feeds are served. Both official data.gov.sg catalogues assign Singapore Open Data Licence v1.0 (https://data.gov.sg/open-data-licence): commercial distribution with source acknowledgement and licence link, no endorsement, limited application/website sublicensing and no rights for downstream sub-licensees. Customers needing additional rights must obtain the original licence directly. Each series' attribution links the licence and both original catalogues. GDP historical estimates may be revised; no forecasts are emitted.
EU member states (`AT`, `BE`, `BG`, `CY`, `CZ`, `DE`, `DK`, `EE`, `ES`, `FI`, `FR`, `GR`, `HR`, `HU`, `IE`, `IT`, `LT`, `LU`, `LV`, `MT`, `NL`, `PL`, `PT`, `RO`, `SE`, `SI`, `SK`) serve the same eight Eurostat series ids with the same definitions, general-government scope and units, for that country instead of the aggregate; Greece is `GR` although Eurostat publishes it as `EL`.

Iceland series (`IS`; Statistics Iceland, annual general-government figures): `statice_gross_debt_annual` and `statice_gross_debt_to_gdp_annual` from THJ05181 at 31 December; `statice_financial_balance_annual` (surplus positive, deficit negative) and `statice_interest_payments_annual` from THJ05121 for the calendar year. Millions of ISK at current prices or reported percent of GDP. Debt includes pension/insurance liabilities and payables, **not Maastricht debt**. Full PX decimal precision is preserved, not rounded CSV/JSON display precision. Source: Statistics Iceland, THJ05181 and THJ05121, under [CC BY 4.0](https://www.statice.is/publications/open-data-access/); modifications are not attributed to Statistics Iceland.

Norway series (`NO`; Statistics Norway (SSB) StatBank, national-accounts general-government scope, `nok_millions`): `ssb_general_government_gross_debt_annual` (table `08753`, at 31 December) and `ssb_general_government_gross_debt_quarterly` (table `11559`, at the quarter end), both `point_in_time`; `ssb_general_government_net_lending_annual` (table `14668`, `fiscal_year`) and `ssb_general_government_net_lending_quarterly` (table `14667`, not seasonally adjusted, `quarter`), where a negative value is net borrowing; and `ssb_interest_expenditure_annual` (table `14668`, item `B41`, calendar year), which is annual only because Statistics Norway publishes no quarterly interest item. Statistics Norway publishes no debt-to-GDP ratio, so `ssb_gross_debt_to_gdp_annual` and `ssb_gross_debt_to_gdp_quarterly` (`percent_of_gdp`) are Bullion API calculations (`derived_ratio`, formula and inputs in `derivation`) of that debt to GDP at current market prices including petroleum (tables `09189` and `09190`; quarterly debt over the sum of the last four quarters of GDP).

Qatar (`QA`) is also covered by `/v1/government-debt` only: `qatar_public_debt_annual` is national central-government debt at 31 December, in millions of Qatari riyals, from Ministry of Finance dataset `MOF-OD-C0-00004`, through 2025. This is not general-government or Maastricht debt. `qatar_gross_debt_to_gdp_annual` is a Bullion API `derived_ratio` of the debt over the same-year nominal GDP total in National Planning Council dataset `NPC-OD-C0-01288`; matched-year ratios are served only through 2024, with no inferred 2025 GDP or ratio. Both datasets identify [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/), and the ratio is attributed as a Bullion API calculation. Catalogue metadata observed 2026-09-30 reported the generic `data-trust-index.ready-for-publication=false` flag; it is not a publisher certification, data withdrawal or deprecation. The government-debt collection appends `QA` after `CH`; Qatar has no borrowing or interest series.

Canada series (`CA`; Statistics Canada, national-accounts general-government scope, all quarterly, `cad_millions` or `percent_of_gdp`): `statcan_general_government_gross_debt_quarterly` (table `36-10-0467-01`) and `statcan_gross_debt_to_gdp_quarterly` (table `38-10-0237-01`, a ratio published by Statistics Canada, not calculated by Bullion API; its gross debt is defined more widely than the debt series), both `point_in_time` at the quarter end; `statcan_general_government_net_lending_quarterly` (net lending (+) or net borrowing (-)) and `statcan_interest_expenditure_quarterly` (interest on debt), both from table `36-10-0477-01`, unadjusted, `quarter`. Recent quarters are revised.

Switzerland series (`CH`; Swiss Federal Finance Administration, IMF GFSM 2014 model, whole-state general government, annual accounts years only, `chf_millions` or `percent_of_gdp`): `efv_general_government_gross_debt_annual` (gross debt excluding financial derivatives, IMF definition, variable `fremdkapital`) and `efv_general_government_maastricht_style_debt_annual` (debt in line with the Maastricht definition, variable `maastricht_schuld`, not verified as comparable with Eurostat), both `point_in_time` at 31 December; `efv_maastricht_style_debt_to_gdp_annual` (the ratio the Administration publishes for that debt, in percent, GDP as used by the Administration); and `efv_general_government_net_lending_annual` (net lending (+) or net borrowing (-), variable `saldo`, `fiscal_year`). Forecast years are never returned and accounts years can be revised. There is no Swiss interest series.

Japan series (`JP`; Cabinet Office Economic and Social Research Institute, consolidated general-government scope, quarterly): `cabinet_jp_general_government_gross_debt_quarterly` is face-value gross debt in millions of yen from row 6 of the `GGD` workbook, including SDRs, currency and deposits, debt securities, loans and payables and excluding unfunded pay-as-you-go pension entitlements and certain Fiscal Investment and Loan Program liabilities. `cabinet_jp_general_government_gross_debt_to_gdp_quarterly` is a Bullion API `derived_ratio`: quarter-end debt divided by nominal current-price GDP (Expenditure Approach, original series) summed over the same and three preceding quarters. The ratio is not published by the Cabinet Office; GDP is not seasonally adjusted or annualised. The [Cabinet Office notice](https://www.cao.go.jp/en/notice-e.html) says numerical data and simple tables are not protected by copyright and may be used freely; the website Terms of Use govern website content and do not apply to those data. Source attribution and disclosure of Bullion API's calculation are Bullion API policy, not conditions imposed on those numerical data. This is not a claim of rights to separate third-party copyrighted material, and the Cabinet Office does not endorse this product. The provider discovers the official GDP CSV link from the release page but reads financial values only from the official workbook and CSV. It uses no API key or third-party IMF/e-Stat data.

Australia series (`AU`; ABS, general-government scope): `abs_general_government_gross_debt_annual` is gross L2 debt at 30 June from Government Finance Statistics, Annual Table 14 (row `Equals L2`, column `All Australia general government`), in `aud_millions`, valued at current market value using the ABS creditor approach. `abs_general_government_gross_debt_to_gdp_annual` is a Bullion API calculation, not an ABS-published ratio: Table 14 gross L2 debt divided by the sum of the matching four original, current-price quarterly GDP observations from ABS SDMX flow `ANA_AGG` version `1.0.0`, key `M3.GPM.10.AUS.Q`, stored to eight decimal places. It is not the net L2 ratio in Table 16. Source: Australian Bureau of Statistics, [Government Finance Statistics, Annual](https://www.abs.gov.au/statistics/economy/government/government-finance-statistics-annual) and [Australian National Accounts](https://www.abs.gov.au/statistics/economy/national-accounts/australian-national-accounts-national-income-expenditure-and-product). CC BY 4.0 permits commercial reuse of ABS website material, subject to the exclusions and third-party rights identified in the [ABS copyright terms](https://www.abs.gov.au/website-privacy-copyright-and-disclaimer#copyright-and-creative-commons). Those terms require attribution to the ABS for material obtained from its website; do not imply ABS endorsement. The ABS Data API is Beta and may change or be unavailable.

### Example

```bash
curl -H "X-API-Key: bullion_your_key" \
  "https://api.bullionapi.dev/v1/government-debt/us"
curl -H "X-API-Key: bullion_your_key" \
  "https://api.bullionapi.dev/v1/government-debt/pe"
curl -H "X-API-Key: bullion_your_key" \
  "https://api.bullionapi.dev/v1/government-borrowing/uk?start=2026-01-01&end=2026-08-31"
curl -H "X-API-Key: bullion_your_key" \
  "https://api.bullionapi.dev/v1/debt-interest"
```

United States debt interest is not currently available: `/v1/debt-interest/us` returns HTTP 404 `no_data` and the bare path omits the US.

Peru data: Source: [Banco Central de Reserva del Perú BCRPData](https://estadisticas.bcrp.gob.pe/estadisticas/series/ayuda/condiciones-de-uso); reproduction is permitted with source attribution under its terms, which are not a Creative Commons licence. BCRPData metadata names the original statistical source institutions in the Peru series definitions.
Sources: U.S. Department of the Treasury, Bureau of the Fiscal Service data via [FRED](https://fred.stlouisfed.org/), Federal Reserve Bank of St. Louis. This product uses the FRED® API but is not endorsed or certified by the Federal Reserve Bank of St. Louis. Bullion API is not endorsed by the U.S. Department of the Treasury. GDP for the debt-to-GDP ratio: [U.S. Bureau of Economic Analysis Data API](https://apps.bea.gov/api/data). This product uses the Bureau of Economic Analysis (BEA) Data API but is not endorsed or certified by BEA. United Kingdom data: Source: Office for National Statistics, [Public Sector Finances](https://www.ons.gov.uk/economy/governmentpublicsectorandtaxes/publicsectorfinance/datasets/publicsectorfinances). Contains public sector information licensed under the [Open Government Licence v3.0](https://www.nationalarchives.gov.uk/doc/open-government-licence/version/3/). Euro-area data: Source: Eurostat, [government finance statistics](https://ec.europa.eu/eurostat/web/government-finance-statistics) (datasets `gov_10dd_edpt1`, `gov_10q_ggdebt`, `gov_10q_ggnfa`); reuse is authorised provided the source is acknowledged. Norway data: Source: Statistics Norway (SSB), www.ssb.no, licensed under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/); changes were made (Bullion API calculates the debt-to-GDP ratios), and Statistics Norway does not endorse Bullion API. Canada data: Source: Statistics Canada, tables 36-10-0467-01, 38-10-0237-01 and 36-10-0477-01, reference date shown on each observation. Reproduced and distributed on an "as is" basis with the permission of Statistics Canada ([Statistics Canada Open Licence](https://www.statcan.gc.ca/en/terms-conditions/open-licence)). This does not constitute an endorsement by Statistics Canada of this product. Switzerland data: Source: Swiss Federal Finance Administration (EFV/FFA), [Main aggregates and forecasts with FS- and GFS-Model](https://opendata.swiss/en/dataset/hauptaggregate-und-prognosen-im-fs-und-gfs-modell), published as open government data under the opendata.swiss [Open use](https://opendata.swiss/en/terms-of-use) terms (commercial use permitted, source recommended); provided without verification of the derived series and not endorsed by the Federal Finance Administration.

## Market-indicator endpoints

`GET /v1/gold/mine-production`, `GET /v1/gold/recycling`, `GET /v1/gold/trade`, `GET /v1/silver/mine-production`, `GET /v1/silver/recycling`, `GET /v1/silver/consumption`, and `GET /v1/silver/trade`.

**Annual gold and silver statistics from the U.S. Geological Survey (USGS), one endpoint per metal and indicator.** Every plan can call these endpoints; each request needs an API key and consumes monthly quota. They read Bullion API's database only and never call USGS. Values are decimal strings in metric tonnes of metal content, one observation per calendar year, dated 31 December with `period_start` 1 January.

| Endpoint | Series |
|---|---|
| `/v1/gold/mine-production` | `world-mine-production`, `us-mine-production` |
| `/v1/gold/recycling` | `us-secondary-production` |
| `/v1/gold/trade` | `us-imports`, `us-exports` |
| `/v1/silver/mine-production` | `world-mine-production`, `us-mine-production` |
| `/v1/silver/recycling` | `us-secondary-production` |
| `/v1/silver/consumption` | `us-apparent-consumption` |
| `/v1/silver/trade` | `us-imports`, `us-exports` |

World mine production is the only global measure; recycling (secondary production), consumption and trade are United States figures, and each series states its `geography` (`world` or `US`). There is no gold consumption endpoint, because the USGS gold consumption figures come from third-party sources rather than USGS. These are partial market indicators, not total demand, a market balance or a forecast. The annual statistics have a multi-year publication lag. When USGS identifies an observation as estimated, it carries an `estimated` status; historical observations without a reported estimate status omit that field.

### Parameters

| Name | Type | Description |
|---|---|---|
| start / end | YYYY-MM-DD | Optional inclusive dates. Omit both for each series' latest year; with a range, every year whose 31 December falls inside it is returned in chronological order. `start` alone runs to today; `end` without `start` returns `missing_params`. Ranges may span at most 10 years; longer ranges return `invalid_range`. |

Each series carries `id`, `label`, `unit` (`metric_tonnes`), `frequency` (`annual`), `method` (`reported`), `geography`, `category`, a plain-language `definition` of what the series measures and its limits, `collected_at` (when Bullion API last collected the source), `freshness` and a `source` object with its attribution and notice. `freshness` is `stale` when scheduled collection is overdue; the last collected data are still served. It does not describe the multi-year USGS publication lag. Years USGS marks as not available are omitted, never returned as zero. A malformed date returns `invalid_date`, a reversed range `invalid_range`, and a request with no stored observation `no_data`.

### Example

```bash
curl -H "X-API-Key: bullion_your_key" \
  "https://api.bullionapi.dev/v1/gold/mine-production"
curl -H "X-API-Key: bullion_your_key" \
  "https://api.bullionapi.dev/v1/silver/consumption?start=2012-01-01&end=2021-12-31"
```

### Response

```json
{
  "status": "success",
  "metric": "gold-mine-production",
  "metal": "gold",
  "start": null,
  "end": null,
  "series": [{
    "id": "world-mine-production",
    "label": "World gold mine production",
    "unit": "metric_tonnes",
    "frequency": "annual",
    "method": "reported",
    "geography": "world",
    "category": "supply",
    "definition": "Gold content of worldwide mine production in the calendar year, in metric tonnes. …",
    "collected_at": "2026-10-07T07:55:04.000Z",
    "freshness": "current",
    "source": {
      "attribution": "Source: U.S. Geological Survey.",
      "notice": "U.S. Geological Survey data are in the U.S. public domain. Credit: U.S. Geological Survey. The USGS does not endorse this product. …"
    },
    "observations": [{ "date": "2022-12-31", "value": "3000", "period_start": "2022-01-01" }]
  }]
}
```

Illustrative values only; the example value is not a USGS figure. USGS data are in the U.S. public domain. Credit the U.S. Geological Survey when you display them; the USGS does not endorse this product.

## GET /v1/central-bank-reserves/{country}

**Annual central-bank reserve values and gold share.** This endpoint is available on every plan and is served from Bullion API's automated database cache. The bare `/v1/central-bank-reserves` returns the latest observation of each series for every covered country, ordered by code.

### Parameters

| Name | Type | Description |
|---|---|---|
| country | path | ISO 3166-1 alpha-2 code, case-insensitive; `uk` is accepted for GB. |
| start | YYYY-MM-DD | Optional first inclusive date. Omit start and end for the latest observation of each series. |
| end | YYYY-MM-DD | Optional last inclusive date; requires start and defaults to today. Ranges may span at most 10 years; longer ranges return 400 `invalid_range`. Annual observations are dated at year end. |

Series: `total_reserves_usd` and `reserves_excluding_gold_usd` are reported; `gold_reserve_value_usd`, `gold_share_pct` and `annual_gold_value_change_usd` are derived by Bullion API with exact decimal arithmetic and carry `notes`. Values are decimal strings. A year whose source values are inconsistent is omitted from the derived series and listed in the reported series' `quality_flags`. An unknown code returns `400 invalid_country`; a country without observations in range returns `404 no_data`.

### Example

```bash
curl -H "X-API-Key: bullion_your_key" \
  "https://api.bullionapi.dev/v1/central-bank-reserves/gb?start=2024-01-01&end=2025-12-31"
```

### Response

```json
{
  "status": "success",
  "metric": "central_bank_reserves",
  "country": "GB",
  "country_name": "United Kingdom",
  "start": "2024-01-01",
  "end": "2025-12-31",
  "series": [
    {
      "id": "total_reserves_usd",
      "label": "Total reserves including gold",
      "unit": "usd",
      "frequency": "annual",
      "method": "reported",
      "observations": [
        { "date": "2024-12-31", "value": "180000000000" },
        { "date": "2025-12-31", "value": "200000000000" }
      ]
    },
    {
      "id": "gold_reserve_value_usd",
      "label": "Gold reserve value",
      "unit": "usd",
      "frequency": "annual",
      "method": "derived",
      "notes": "Derived by Bullion API from the reported annual reserve values. Total reserves including gold minus total reserves excluding gold. Years whose source values are inconsistent are omitted.",
      "observations": [
        { "date": "2024-12-31", "value": "15000000000" },
        { "date": "2025-12-31", "value": "25000000000" }
      ]
    }
  ]
}
```

Illustrative synthetic values only; this example is not live reserve data.

Without `start` and `end`, each series returns its own latest observation, so dates can differ across series. Use `/v1/countries` to discover which countries currently have a queryable reserve dataset; coverage is per dataset, and an absent observation is not a zero value. Annual value changes combine holdings changes, gold-price movement, and revisions and do not measure tonnes bought or sold. Reuse rights for public statistics remain separate from API plan terms and are governed by applicable terms. Redistribution of Bullion API price data or API access remains subject to the Terms of Service.

## GET /v1/countries

**Which datasets each country has.** `/v1/countries` lists every country (ISO 3166-1 alpha-2, plus `EA` for the euro area) with at least one dataset, sorted by code; `/v1/countries/{country}` returns one. Each dataset names its metric and the path to request it. Coverage reflects the series Bullion API serves, so a dataset is listed only where its endpoint can answer. `aliases` lists accepted path aliases (`uk`, `eu`). An unknown code returns `400 invalid_country`; a valid code with no dataset returns `404 no_data`.

```bash
curl -H "X-API-Key: bullion_your_key" "https://api.bullionapi.dev/v1/countries/uk"
```

Abridged response; the full `datasets` list includes every metric served for the country:

```json
{
  "status": "success",
  "country": "GB",
  "country_name": "United Kingdom",
  "aliases": ["uk"],
  "datasets": [
    { "metric": "policy_rates", "path": "/v1/policy-rates/gb" },
    { "metric": "inflation", "path": "/v1/inflation/gb" },
    { "metric": "central_bank_reserves", "path": "/v1/central-bank-reserves/gb" }
  ]
}
```

## GET /v1/central-bank-gold

**Central-bank gold holdings.** Returns cached reported gold holdings by country on every plan; API requests never
call upstream providers. `GET /v1/central-bank-gold/{country}` returns one
country's series in the shared single-country envelope (`metric: central_bank_gold`), and
`GET /v1/central-bank-gold` returns the latest observation of each series for every covered country, ordered by
country code (sort client-side for leaderboards). Use `/v1/countries` to discover current per-country coverage; a
country can have a subset of measures, and a missing observation is not a zero value.

| Name | Type | Description |
|---|---|---|
| country | path, ISO2 | Case-insensitive country code; `uk` is accepted for GB. Unknown codes return `invalid_country`; countries without coverage return `no_data`. |
| start / end | YYYY-MM-DD | Optional inclusive dates, at most 10 years apart. Omit both for the latest observation of each series; `start` alone runs to today. |

Each series is one measure at one cadence: `holdings_tonnes`, `holdings_million_fine_troy_oz`,
`holdings_change_tonnes`, `reported_value_usd_millions`, `reported_value_change_usd_millions`,
`reported_value_eur_millions`, `reported_value_pln_millions`, `reported_value_inr_crores`,
`reported_value_sar_millions`, `reported_value_sgd_millions`, `reported_value_dkk_millions`,
`reported_value_isk_millions`, `reported_value_tjs_millions`, `reported_value_omr_millions` or
`reported_value_tzs_millions`. Series ids combine
the measure and cadence, for example `holdings_tonnes_monthly` or `reported_value_usd_millions_weekly`, with a `_derived` suffix for derived
holdings, so weekly, fortnightly, monthly, quarterly, annual and irregular releases are never combined. Values are decimal
strings. Monthly, quarterly, annual and irregular observations are dated at the end of their reference month; weekly and
fortnightly observations retain their reference date. Every series carries `holdings_basis`, `cadence`, a `stale`
flag (true when its newest successful refresh is more than 45 days old, or when its newest observation is more than
six months older than the country's newest observation in any series; stale observations stay visible), a
`provenance` object and a `reuse_notice` covering the returned observations; a stock-change series also covers the
prior observation used to calculate the change. A range with no data returns `no_data`. Reuse rights for public
statistics remain separate from API plan terms and are governed by applicable terms. When weight is reported in fine
troy ounces, the tonnes series label states that Bullion API converted the reported weight, while the original ounce
series remains available. Quantities derived from a reported value are labelled separately and are never presented
as reported weight.

```json
{
  "status": "success",
  "metric": "central_bank_gold",
  "country": "DE",
  "country_name": "Germany",
  "start": null,
  "end": null,
  "series": [{
    "id": "holdings_tonnes_monthly",
    "label": "Gold holdings converted by Bullion API from reported weight (tonnes)",
    "measure": "holdings_tonnes",
    "unit": "tonnes",
    "frequency": "monthly",
    "cadence": "monthly",
    "method": "fixed_unit_conversion_from_reported_weight",
    "holdings_basis": "reported",
    "stale": false,
    "provenance": {
      "name": "Synthetic example",
      "methodology": "Synthetic example: monthly gold holdings reported in million fine troy ounces.",
      "updated_at": "2026-07-15"
    },
    "reuse_notice": "Synthetic example: source terms and Bullion API plan terms apply; standard plans do not authorize raw-data redistribution or resale.",
    "observations": [{ "date": "2026-06-30", "value": "1000.00" }]
  }]
}
```

Illustrative synthetic values only; this example is not live data.

`holdings_change_tonnes` is calculated by Bullion API as a change between consecutive reported stocks of one series,
not purchases or sales, and is omitted where reported stocks are not directly comparable. `holdings_basis` is
`reported` when the quantity originates in a reported physical weight and `derived` when tonnes are computed from a
reported USD value and month-end gold price (the publisher's own price, or Bullion API's stored USD
gold close where the publisher values gold at market price); a derived quantity keeps its reported value as a separate
reported series and derived weights from Bullion API's close have no change series. Other value-only measures
(`reported_value_*`) carry no quantity series rather than invented tonnes. Some
valuations use a fixed official price rather than market prices, and a reported zero is valid data, not a missing
value. `reported_value_sgd_millions` is a combined gold and foreign-exchange reserve value, not a gold-only figure.
Changes between observations describe changes in reported stock, valuation and revisions, not purchases or sales; a
flat reported stock does not establish that no transactions occurred.

```bash
curl -H "X-API-Key: bullion_your_key" \
  "https://api.bullionapi.dev/v1/central-bank-gold/de?start=2026-01-01&end=2026-06-30"

curl -H "X-API-Key: bullion_your_key" \
  "https://api.bullionapi.dev/v1/central-bank-gold"
```

## GET and PUT /v1/portfolio

Configure and read one private multi-metal portfolio for the API-key owner's account. Portfolio endpoints use X-API-Key only; the api_key query parameter is not accepted.

Existing and newly created keys have no portfolio access by default. The account owner selects one permission in the API keys dashboard:

- None: no portfolio access.
- Read: stored as `portfolio: read` and permits GET.
- Read and write: stored as `portfolio: read, write` and permits GET and PUT. 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.

PUT accepts one supported reporting currency while the portfolio ledger is empty:

```json
{ "reporting_currency": "GBP" }
```

GET returns settings and an exact summary. The empty-portfolio example:

```json
{
  "status": "success",
  "portfolio": {
    "id": "d799789a-e1af-4c41-a79a-611e18f0df31",
    "metals": ["gold", "silver", "palladium", "copper", "platinum"],
    "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",
    "metals": [],
    "cost_basis": "0.00000000",
    "realised_pnl": "0.00000000",
    "valuation": {
      "status": "unavailable",
      "spot_price": null,
      "current_value": null,
      "unrealised_pnl": null,
      "price_timestamp": null
    }
  }
}
```

Decimal values remain strings, including zero. The summary includes a metals array with separate quantities,
cost basis, realised gains and valuation for each recorded metal. The legacy gold_quantity_grams field
contains only gold; it never combines different metals. Mixed-metal aggregate spot_price is null.
Valuation uses only each matching latest stored metal spot price in the portfolio reporting currency and
never calls an upstream provider or bridges currencies. `status` is
`current` when that stored price is at most six hours old, `stale` when it is older, and `unavailable`
when no matching-currency price exists; unavailable valuation returns `null` for `spot_price`,
`current_value`, `unrealised_pnl`, and `price_timestamp` instead of zeros, while holdings and cost basis
stay available. Open troy ounces multiplied by the stored spot price give `current_value`; subtracting the
remaining cost basis gives `unrealised_pnl`. Missing settings return `portfolio_not_found`; an unscoped or
read-only key used for a forbidden action returns `insufficient_scope`.

## Multi-metal portfolio behaviour

Each account keeps one portfolio and one reporting currency. Transaction metal values are gold, silver,
palladium, copper and platinum; omitted metal defaults to gold for existing clients. Holdings are kept
per metal, and FIFO or explicit sales consume only the matching metal's lots. Each metal is valued from
its matching cached reporting-currency observation without provider calls or cross-currency fallback.

## Price alerts

The authenticated dashboard supports multiple USD target alerts per supported metal. Each target fires
independently once. Owners can edit targets or delete individual alerts; an identical active target for
the same metal is rejected. Editing an inactive alert uses an explicit Save and reactivate action.
Evaluation receives a matching fresh persisted native-metal observation only after the complete hourly
refresh succeeds; stale, omitted, or partially persisted data does not trigger delivery. Emails identify
the selected metal. Changes and unsubscribe affect only the selected alert, including when the owner has
other targets for the same metal.

## POST /v1/portfolio/transactions

Record an owned purchase or sale with an API key holding portfolio write access. Send an opaque client-generated `Idempotency-Key` header on every request. Send `X-Portfolio-Transaction-Type: sale` for a sale. The same key and canonical request returns the original transaction; reusing a key with changed input returns `409 idempotency_conflict`. Raw keys are never stored or logged.

Quantity accepts at most eight fractional digits in `gram`/`g`, `kilogram`/`kg`, or `troy_oz`/`toz`, then returns normalized grams at scale 15. Unit price, delivery, fees, gross cost, total cost, and cost per gram are decimal strings at scale 8, using round-half-up. Transaction dates must be real UTC calendar dates not in the future. Forms are `bar`, `coin`, or `other`; CGT status is `exempt`, `not_exempt`, or `unknown`.

`missing_idempotency_key`, `invalid_purchase`, and `invalid_sale` return 400. Sales consume eligible open purchase lots FIFO by transaction date, creation time, then ID by default; sale dates cannot be future-dated or precede allocated purchases. `insufficient_holdings` returns 409 and rolls back the entire mutation. Net proceeds, disposed cost, and realised P&L are exact decimal strings; idempotent replay never reallocates a completed sale.

Sales can instead opt into explicit allocation by sending `allocations`: an array of `{ "lot_id", "quantity_grams" }` objects whose quantities must sum exactly to the sale quantity. List the account's open lots with GET /v1/portfolio/lots. Every selected lot must belong to the account, match the sale's metal, form and CGT treatment, predate or equal the sale date, and have sufficient remaining quantity. A missing, foreign, duplicated, incompatible, over-allocated, or under-allocated lot fails the whole sale without mutation: `invalid_allocation` returns 400 for eligibility and exact-sum violations, `sale_date_invalid` for lots dated after the sale, and `insufficient_holdings` when a selected lot no longer has enough remaining quantity. Committed sales identify their `allocation_method` as `fifo` or `explicit` and return the committed allocations ordered by purchase date, creation time, then ID.

## GET /v1/portfolio/lots

List only the authenticated account owner's open purchase lots with a positive remaining quantity, ordered by `transaction_date, created_at, id`. Each lot exposes its stable `lot_id`, originating `purchase_transaction_id`, purchase date, form, CGT status, remaining quantity in grams, and remaining cost basis as decimal strings (`remaining_quantity_grams` at scale 15, `remaining_cost_basis` at scale 8). Use these identifiers to build explicit sale allocations; closed lots and other accounts' lots are never returned.

```bash
curl -H "X-API-Key: bullion_your_key" \
  "https://api.bullionapi.dev/v1/portfolio/lots"
```

```json
{
  "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"
  }]
}
```

## GET /v1/portfolio/transactions

Browse only the authenticated account owner's portfolio transactions. Results are ordered by
`transaction_date DESC, created_at DESC, id DESC` and use keyset pagination. The default page size is 50 and
the maximum is 100. The returned `next_cursor` is an opaque cursor; send it as `cursor` to fetch the next
page and do not depend on its internal format. All quantities and monetary values remain decimal strings.

Filter the page with `type` (`purchase` or `sale`), `form` (`bar`, `coin`, or `other`), and
`cgt_status` (`exempt`, `not_exempt`, or `unknown`). Restrict the period with either
`calendar_year=2025` for 1 January through 31 December inclusive, or `uk_tax_year=2025-26` for 6 April
through 5 April inclusive, where the two years in the label must be consecutive. The two period filters are
mutually exclusive: combining them returns 400 `conflicting_periods`. Empty query values are ignored.
Malformed labels return 400 `invalid_calendar_year` or `invalid_tax_year`; unknown attribute values return
400 `invalid_filter`. Filters compose with `cursor` and `limit` so a filtered view paginates stably.

```bash
curl -H "X-API-Key: bullion_your_key" \
  "https://api.bullionapi.dev/v1/portfolio/transactions?limit=50&uk_tax_year=2025-26"
```

```json
{
  "status": "success",
  "transactions": [{
    "metal": "gold",
    "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": [{
      "metal": "gold",
      "id": "22222222-2222-4222-8222-222222222222",
      "open_quantity_grams": "31.258994184000000",
      "open_cost_basis": "2525.97630395"
    }],
    "allocations": []
  }],
  "next_cursor": null
}
```

## GET /v1/portfolio/transactions/{id}

Return one owned transaction using the same representation as the list endpoint. Missing, malformed, and
cross-account identifiers return 404 `transaction_not_found` rather than revealing another account's data.
Invalid page, filter, or period parameters return 400 `invalid_limit`, `invalid_cursor`,
`invalid_filter`, `invalid_calendar_year`, `invalid_tax_year`, or `conflicting_periods`.

## DELETE /v1/portfolio/transactions/{id}

Delete one owned transaction with a portfolio write key. Send an `If-Match` header carrying the ETag-style
quoted `version` from the transaction payload (`"1"`; the weak form `W/"1"` is also accepted). A missing or
mismatched version returns 412 `stale_transaction` without mutation. Deleting a sale restores every allocated
quantity and cost basis to its originating purchase lot and rebalances holdings. A purchase that has funded a
sale cannot be deleted: it returns 409 `transaction_in_use`, so delete the dependent sale first. Missing,
malformed, and cross-account identifiers return 404 `transaction_not_found`.

```bash
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"'
```

```json
{ "status": "success", "deleted_transaction_id": "11111111-1111-4111-8111-111111111111" }
```

## GET /v1/portfolio/gains

Summarise realised profit or loss from owned sale transactions inside one period, grouped by the CGT
status recorded on each sale. Supply exactly one period filter: `calendar_year=2025` covers 1 January
through 31 December inclusive, and `uk_tax_year=2025-26` covers 6 April through 5 April inclusive with
consecutive years in the label. The response echoes the resolved `period` (`kind`, `label`, inclusive
`start` and `end` dates) and returns `buckets.exempt`, `buckets.not_exempt`, and `buckets.unknown`
plus their `total`, all exact 8-place decimal strings.

Omitting both filters returns 400 `missing_period`; supplying both returns 400 `conflicting_periods`;
malformed labels return 400 `invalid_calendar_year` or `invalid_tax_year`. An account without a portfolio
returns 404 `portfolio_not_found`. These figures exclude annual allowances, matching and pooling rules,
and tax rates. They are not tax advice.

```bash
curl -H "X-API-Key: bullion_your_key" \
  "https://api.bullionapi.dev/v1/portfolio/gains?uk_tax_year=2025-26"
```

```bash
curl -H "X-API-Key: bullion_your_key" \
  "https://api.bullionapi.dev/v1/portfolio/gains?calendar_year=2025"
```

```json
{
  "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",
  "metals": [{ "metal": "gold", "total": "1334.49900000" }]
}```

## DELETE /v1/portfolio/transactions/{id}

Delete one owned transaction with a portfolio write key. Send an `If-Match` header carrying the ETag-style
quoted `version` from the transaction payload (`"1"`; the weak form `W/"1"` is also accepted). A missing or
mismatched version returns 412 `stale_transaction` without mutation. Deleting a sale restores every allocated
quantity and cost basis to its originating purchase lot and rebalances holdings. A purchase that has funded a
sale cannot be deleted: it returns 409 `transaction_in_use`, so delete the dependent sale first. Missing,
malformed, and cross-account identifiers return 404 `transaction_not_found`.

```bash
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"'
```

```json
{ "status": "success", "deleted_transaction_id": "11111111-1111-4111-8111-111111111111" }
```

Invalid page parameters return 400 `invalid_limit` or `invalid_cursor`. Every response carries an `ETag`
header quoting the transaction version.

## PATCH /v1/portfolio/transactions/{id}

Edit one owned transaction with optimistic concurrency. Send `If-Match: "<version>"` quoting the current
version from the ETag header or the `version` field; wildcard and multi-tag forms are rejected. The body replaces
every mutable field (`transaction_date`, `quantity`, `weight_unit`, `unit_price`, `delivery_cost`,
`fees`, `form`, `cgt_status`, `description`); type and owner are immutable. Purchases must keep enough
quantity and cost for every dependent sale allocation, cannot move after an allocating sale, and cannot change
form or CGT treatment while sales depend on them. Sales release and rebuild only their own allocations with the
persisted method: FIFO edits omit `allocations` (sending them is an error) while explicit edits must resend the
full lot allocation list under the same eligibility and exact-sum rules as creation. Success increments
`version`, returns the new ETag, and recalculates lot totals, disposed cost, and realised P&L immediately.
Missing or stale preconditions return 412 `stale_transaction` with no mutation; foreign or malformed
identifiers return 404 `transaction_not_found`; any failure rolls the whole edit back.

```bash
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":"31.1034768","weight_unit":"gram","unit_price":"80.80798407","delivery_cost":"12.34","fees":"1.005","form":"coin","cgt_status":"exempt"}'
```

## Rate limits

Authenticated successful market-data /v1 responses include these headers so you can track usage in real time. Portfolio responses omit these monthly quota headers because portfolio operations do not use the monthly market-data quota:

X-API-CURRENT and X-API-QUOTA are additive compatibility aliases; the existing x-ratelimit-* names remain supported.

| 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. |

Each API key permits 60 requests per rolling 60-second window across REST. The next request
returns HTTP 429 with code `rate_limited` and a `Retry-After` header. Short-window denials do not change
the monthly usage counter. Monthly exhaustion is a separate HTTP 429 with code `quota_exceeded`.

## 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 and framework-owned responses keep their protocol contracts.

Any `/v1` path without an API operation returns `404` with code `not_found` for every method, without authentication or quota use.

### 401 — Missing API key

```http
HTTP/1.1 401 Unauthorized
content-type: application/json

{
  "status": "error",
  "error": {
    "code": "missing_api_key",
    "message": "Missing API key"
  }
}
```

### 401 — Conflicting credentials

```http
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
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"
  }
}
```

### 400 — Bad request

```http
HTTP/1.1 400 Bad Request
content-type: application/json

{
  "status": "error",
  "error": {
    "code": "invalid_range",
    "message": "The requested range is invalid."
  }
}
```

### 400 — Invalid latest price unit

```http
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 | HTTP status | Description |
|---|---:|---|
| dependency_not_ready | 503 | Required data for this metric is not ready. |
| 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. |
| plan_change_conflict | 409 | The plan change conflicts with the current billing state. |
| plan_change_expired | 409 | The plan change preview has expired. Please preview it again. |
| plan_change_failed | 402 | The plan change payment failed. Your plan has not changed. |
| plan_change_invalid | 400 | The plan change is invalid. |
| 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_range | 400 | The requested range is invalid. |
| invalid_series_filter | 400 | The series filter value is invalid. |
| invalid_unit | 400 | The requested output unit is invalid. |
| invalid_window | 400 | The requested window 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. |
| not_found | 404 | No API operation exists at the requested path. |
| 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. |
| invalid_price_alert | 400 | The price alert request is invalid. |
| invalid_digest_preference | 400 | The digest preference request is invalid. |
| price_alert_exists | 409 | You already have an active price alert for this currency, metal, and target. |
| price_alert_limit | 409 | You can have up to 10 active price alerts. |
| price_alert_not_found | 404 | The price alert was not found. |
| price_alert_conflict | 409 | The alert has changed or its email is being sent. Refresh your alerts and try again. |
| 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_period | 400 | The period is invalid. |
| no_change_data | 404 | Cached metal data is missing for one or both requested dates. |
| invalid_change_baseline | 422 | The starting price must be non-zero. |
| health_degraded | 503 | The database is unreachable or cached data is stale. |
| runtime_unavailable | 503 | The service is temporarily unavailable. |
| 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 |
| payload_too_large | 413 | The JSON request body exceeds the size limit. |
| origin_forbidden | 403 | A signed-in dashboard change was sent without a trusted Origin or Referer. |
| 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 |

## GET /v1/symbols

Returns the supported metal descriptions plus native currencies and configured projected currencies that can serve complete five-metal prices with FX provenance. It is authenticated and request-logged, but does not consume monthly product quota or call an upstream provider. The runtime response is authoritative for available currency choices; static documentation reflects configuration only.

Supported metal slugs are `gold`, `silver`, `palladium`, `copper`, and `platinum`. All metal prices default to troy ounces. `/v1/gold/carat` is gold-only.

## 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.

Prices are informational only. They are not live FX rates, executable prices, transaction benchmarks or binding quotes, and must not be used for trading or settlement. See [Price methodology](#price-methodology) for how native and projected prices are produced.

## Price methodology

Native prices: USD, EUR and GBP prices are metal observations from the scheduled price pipeline. Metal observations refresh hourly and API responses are served from the cache.

Projected prices: every other currency listed by GET /v1/symbols is derived by converting the hourly canonical USD metal observation with the most recent accepted Bullion API daily FX snapshot. Snapshots are captured on weekdays at about 23:10 UTC, with a retry if that capture fails. Weekends, and any weekday when both captures fail, keep using the last accepted snapshot, which keeps its original date. A snapshot more than seven UTC calendar days older than the price it converts is treated as stale. Once the latest snapshot is more than seven UTC calendar days old, latest projected prices are withheld until a new snapshot is accepted, so an old conversion is never shown as current.

The snapshot time is when Bullion API captured the snapshot. It is not a provider end-of-day reference rate. The FX source does not supply a verified upstream observation time, so none is stored or returned, and Bullion API never invents one. Projected-currency history holds at most one daily point per metal for each weekday snapshot, recorded when that metal has an observation on the snapshot day, starting on the date a currency is activated; earlier history is not backfilled.

Prices are informational only. They are not live FX rates, executable prices, transaction benchmarks or binding quotes, and must not be used for trading or settlement. FX rates are internal conversion inputs: Bullion API does not publish, redistribute or resell raw FX rates and has no FX or currency-conversion endpoint.

Currency conversion input attribution: [Exchange Rates By UniRateAPI](https://unirateapi.com/).
