{"openapi":"3.1.0","info":{"title":"Bullion API","version":"1.0.0","summary":"Gold spot prices as a simple JSON REST API.","description":"Live and historical gold prices in the currencies returned by authenticated GET /v1/symbols, the authoritative runtime catalogue. Responses are served from a cache; API requests never call upstream providers. Metal observations refresh hourly. Enabled projected prices combine that hourly canonical USD observation with a separate daily Bullion API conversion snapshot. The two timestamps remain distinct, and conversion snapshots become stale when more than seven UTC calendar days old. Internal UniRateAPI observations are not exposed through price responses. Currency conversion input attribution: [Exchange Rates By UniRateAPI](https://unirateapi.com/). Authenticate price-data requests with the canonical and preferred X-API-Key header, or use the api_key query parameter as a fallback when a client cannot set custom headers. Query credentials may appear in browser history, copied URLs, referrers, intermediary infrastructure, and logs. 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. Free and Pro are for non-commercial use only. Enterprise includes commercial display and integration. Raw or substantially equivalent data redistribution and resale are excluded from all standard plans.","termsOfService":"https://bullionapi.dev/terms","contact":{"name":"Bullion API","url":"https://bullionapi.dev","email":"hello@bullionapi.dev"}},"servers":[{"url":"https://api.bullionapi.dev"}],"security":[{"ApiKeyAuth":[]},{"ApiKeyQuery":[]}],"tags":[{"name":"prices","description":"Gold price data (API key required)"},{"name":"symbols","description":"Production-serviceable symbols (API key required)"},{"name":"macro","description":"Latest macroeconomic observations (API key required)"},{"name":"reserves","description":"Pro and Enterprise annual central-bank reserve data"},{"name":"central-bank-gold","description":"Pro and Enterprise monthly central-bank gold holdings"},{"name":"portfolio","description":"Private account-owned gold portfolio settings"},{"name":"account","description":"Account-scoped usage (session cookie)"},{"name":"meta","description":"Service metadata (public)"}],"paths":{"/v1/symbols":{"get":{"tags":["symbols"],"security":[{"ApiKeyAuth":[]},{"ApiKeyQuery":[]}],"summary":"Production-serviceable metals and currencies","description":"Returns the authoritative runtime catalogue of reviewed, configured currencies that have materialised production price data. Static documentation reflects deployment configuration but cannot verify materialised rows. The route never reads FX observations, constructs an upstream provider or makes an upstream request. It authenticates and retains the rolling per-key limit, but does not increment monthly quota, deny an account whose monthly quota is exhausted or record product activation. Monthly quota headers report the unchanged account state and successful calls are written to the request log.","operationId":"getSymbols","responses":{"200":{"description":"Reviewed production-serviceable metal and currency descriptions.","headers":{"x-ratelimit-limit":{"description":"Your plan's monthly request cap. This endpoint does not consume it.","schema":{"type":"integer","example":30}},"x-ratelimit-used":{"description":"Requests used this month. This endpoint does not increment the value.","schema":{"type":"integer","example":0}},"x-ratelimit-remaining":{"description":"Requests remaining this month. This endpoint does not decrement the value.","schema":{"type":"integer","example":30}},"x-ratelimit-plan":{"description":"The plan whose monthly quota state is reported.","schema":{"type":"string","enum":["free","pro","enterprise"],"example":"free"}},"x-ratelimit-reset":{"description":"When the quota resets (1st of next month, 00:00 UTC).","schema":{"type":"string","format":"date-time"}},"X-API-CURRENT":{"description":"Compatibility alias for the informational x-ratelimit-used value.","schema":{"type":"integer","example":0}},"X-API-QUOTA":{"description":"Compatibility alias for x-ratelimit-limit.","schema":{"type":"integer","example":30}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SymbolsResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/EmailNotVerified"},"429":{"$ref":"#/components/responses/ApiKeyRateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}},"post":{"tags":["symbols"],"security":[],"summary":"Not supported — read-only API","operationId":"postSymbolsNotAllowed","responses":{"405":{"$ref":"#/components/responses/MethodNotAllowed"}}}},"/v1/latest":{"get":{"tags":["prices"],"security":[{"ApiKeyAuth":[]},{"ApiKeyQuery":[]}],"summary":"Latest gold spot price","description":"Returns the most recent cached gold price in the requested base currency. USD, EUR and GBP retain their legacy provider-backed currencies map. For a configured projected base, that map is empty and internal UniRateAPI observations are never returned. Projected timestamps distinguish the hourly canonical USD metal observation from the daily Bullion API conversion snapshot. The conversion time is not a provider end-of-day timestamp; snapshots become stale when more than seven UTC calendar days old. Prices default to troy ounces. Pass unit=gram, unit=kilogram, or unit=troy_oz (including their documented aliases) to choose the response unit. Cached for 1 hour. Usage increments after API-key and quota checks but before endpoint validation.","operationId":"getLatest","parameters":[{"$ref":"#/components/parameters/Currency"},{"$ref":"#/components/parameters/Unit"}],"responses":{"200":{"description":"Latest prices, served from cache.","headers":{"x-ratelimit-limit":{"description":"Your plan's monthly request cap.","schema":{"type":"integer","example":30}},"x-ratelimit-used":{"description":"Requests used this month, including this one.","schema":{"type":"integer","example":1}},"x-ratelimit-remaining":{"description":"Requests remaining this month.","schema":{"type":"integer","example":29}},"x-ratelimit-plan":{"description":"The plan the quota is enforced against.","schema":{"type":"string","enum":["free","pro","enterprise"],"example":"free"}},"x-ratelimit-reset":{"description":"When the quota resets (1st of next month, 00:00 UTC).","schema":{"type":"string","format":"date-time"}},"X-API-CURRENT":{"description":"Compatibility alias for x-ratelimit-used.","schema":{"type":"integer","example":1}},"X-API-QUOTA":{"description":"Compatibility alias for x-ratelimit-limit.","schema":{"type":"integer","example":30}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LatestResponse"}}}},"400":{"description":"The requested unit is not supported.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LatestBadRequestError"},"example":{"status":"error","error":{"code":"invalid_unit","message":"The requested output unit is invalid."}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/EmailNotVerified"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalError"}}},"post":{"tags":["prices"],"summary":"Not supported — read-only API","operationId":"postLatestNotAllowed","responses":{"405":{"$ref":"#/components/responses/MethodNotAllowed"}}}},"/v1/carat":{"get":{"tags":["prices"],"security":[{"ApiKeyAuth":[]},{"ApiKeyQuery":[]}],"summary":"Cached gold karat prices per gram","description":"Returns cached gold prices per gram for the approved karat keys. Without date, the current cached gold row is used. With date=YYYY-MM-DD, the final cached gold observation within that UTC day is used. This is a database-only reader and never calls an upstream provider or bridges currencies.","operationId":"getCarat","parameters":[{"$ref":"#/components/parameters/CaratCurrency"},{"$ref":"#/components/parameters/CaratDate"}],"responses":{"200":{"description":"Cached gold karat prices per gram.","headers":{"x-ratelimit-limit":{"description":"Your plan's monthly request cap.","schema":{"type":"integer","example":30}},"x-ratelimit-used":{"description":"Requests used this month, including this one.","schema":{"type":"integer","example":1}},"x-ratelimit-remaining":{"description":"Requests remaining this month.","schema":{"type":"integer","example":29}},"x-ratelimit-plan":{"description":"The plan the quota is enforced against.","schema":{"type":"string","enum":["free","pro","enterprise"],"example":"free"}},"x-ratelimit-reset":{"description":"When the quota resets (1st of next month, 00:00 UTC).","schema":{"type":"string","format":"date-time"}},"X-API-CURRENT":{"description":"Compatibility alias for x-ratelimit-used.","schema":{"type":"integer","example":1}},"X-API-QUOTA":{"description":"Compatibility alias for x-ratelimit-limit.","schema":{"type":"integer","example":30}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CaratResponse"}}}},"400":{"description":"The currency or date query parameter is malformed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CaratBadRequestError"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/EmailNotVerified"},"404":{"description":"No cached gold row exists for the currency or requested UTC day.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CaratNoDataError"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalError"},"503":{"description":"The cached gold data could not be read.","headers":{"x-ratelimit-limit":{"description":"Your plan's monthly request cap.","schema":{"type":"integer","example":30}},"x-ratelimit-used":{"description":"Requests used this month, including this one.","schema":{"type":"integer","example":1}},"x-ratelimit-remaining":{"description":"Requests remaining this month.","schema":{"type":"integer","example":29}},"x-ratelimit-plan":{"description":"The plan the quota is enforced against.","schema":{"type":"string","enum":["free","pro","enterprise"],"example":"free"}},"x-ratelimit-reset":{"description":"When the quota resets (1st of next month, 00:00 UTC).","schema":{"type":"string","format":"date-time"}},"X-API-CURRENT":{"description":"Compatibility alias for x-ratelimit-used.","schema":{"type":"integer","example":1}},"X-API-QUOTA":{"description":"Compatibility alias for x-ratelimit-limit.","schema":{"type":"integer","example":30}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}},"post":{"tags":["prices"],"summary":"Not supported — read-only API","operationId":"postCaratNotAllowed","responses":{"405":{"$ref":"#/components/responses/MethodNotAllowed"}}}},"/v1/timeseries":{"get":{"tags":["prices"],"security":[{"ApiKeyAuth":[]},{"ApiKeyQuery":[]}],"summary":"Historical daily gold closes","description":"Returns daily gold closes across a date range. Only cached dates are included in `rates`; scheduled refresh and backfill jobs populate the cache. Prices default to troy ounces; pass unit=gram, unit=kilogram, or unit=troy_oz (including their documented aliases) to choose the response unit.","operationId":"getTimeseries","parameters":[{"$ref":"#/components/parameters/Currency"},{"$ref":"#/components/parameters/StartDate"},{"$ref":"#/components/parameters/EndDate"},{"$ref":"#/components/parameters/Unit"}],"responses":{"200":{"description":"Daily closes keyed by date. Dates with no cached row are omitted.","headers":{"x-ratelimit-limit":{"description":"Your plan's monthly request cap.","schema":{"type":"integer","example":30}},"x-ratelimit-used":{"description":"Requests used this month, including this one.","schema":{"type":"integer","example":1}},"x-ratelimit-remaining":{"description":"Requests remaining this month.","schema":{"type":"integer","example":29}},"x-ratelimit-plan":{"description":"The plan the quota is enforced against.","schema":{"type":"string","enum":["free","pro","enterprise"],"example":"free"}},"x-ratelimit-reset":{"description":"When the quota resets (1st of next month, 00:00 UTC).","schema":{"type":"string","format":"date-time"}},"X-API-CURRENT":{"description":"Compatibility alias for x-ratelimit-used.","schema":{"type":"integer","example":1}},"X-API-QUOTA":{"description":"Compatibility alias for x-ratelimit-limit.","schema":{"type":"integer","example":30}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TimeseriesResponse"}}}},"400":{"description":"Invalid date, range, or unit query parameter.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TimeseriesBadRequestError"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/EmailNotVerified"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/v1/change":{"get":{"tags":["prices"],"security":[{"ApiKeyAuth":[]},{"ApiKeyQuery":[]}],"summary":"Gold price change between cached observations","description":"Returns the absolute and percentage change in gold between the final cached observation in two requested UTC days. Use either start_date and end_date together, or one rolling date_type: yesterday, week, month, or year. The endpoint is a database-only reader and never calls upstream providers. The start price is the percentage-change baseline; a zero baseline returns invalid_change_baseline.","operationId":"getChange","parameters":[{"$ref":"#/components/parameters/Currency"},{"$ref":"#/components/parameters/ChangeStartDate"},{"$ref":"#/components/parameters/ChangeEndDate"},{"$ref":"#/components/parameters/ChangeDateType"}],"responses":{"200":{"description":"Absolute and percentage change for cached gold observations.","headers":{"x-ratelimit-limit":{"description":"Your plan's monthly request cap.","schema":{"type":"integer","example":30}},"x-ratelimit-used":{"description":"Requests used this month, including this one.","schema":{"type":"integer","example":1}},"x-ratelimit-remaining":{"description":"Requests remaining this month.","schema":{"type":"integer","example":29}},"x-ratelimit-plan":{"description":"The plan the quota is enforced against.","schema":{"type":"string","enum":["free","pro","enterprise"],"example":"free"}},"x-ratelimit-reset":{"description":"When the quota resets (1st of next month, 00:00 UTC).","schema":{"type":"string","format":"date-time"}},"X-API-CURRENT":{"description":"Compatibility alias for x-ratelimit-used.","schema":{"type":"integer","example":1}},"X-API-QUOTA":{"description":"Compatibility alias for x-ratelimit-limit.","schema":{"type":"integer","example":30}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChangeResponse"}}}},"400":{"description":"Invalid date, date type, conflicting forms, or range.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChangeBadRequestError"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/EmailNotVerified"},"404":{"description":"One or both requested UTC days have no cached gold observation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChangeNoDataError"}}}},"422":{"description":"The cached start price cannot be used as a percentage baseline.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChangeBaselineError"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalError"}}},"post":{"tags":["prices"],"summary":"Not supported — read-only API","operationId":"postChangeNotAllowed","responses":{"405":{"$ref":"#/components/responses/MethodNotAllowed"}}}},"/v1/rates/{region}":{"get":{"tags":["macro"],"security":[{"ApiKeyAuth":[]},{"ApiKeyQuery":[]}],"summary":"Latest stored US, UK, and euro-area macro rates","description":"Returns the latest stored policy, yield, and inflation observation for each available series in the requested region. Policy rates, monthly yield averages, breakevens, and CPI can have independent observation periods. Every observation includes its upstream series ID and source attribution. U.S. Treasury and European Central Bank AAA yields use valid daily observations from complete calendar months; breakevens use matched nominal and real dates; BLS CPI-U is the not-seasonally-adjusted CUUR0000SA0 series; euro-area HICP uses the metadata-verified all-items annual-rate series and ECB Consumer Expectations Survey values are weighted-median survey expectations, not market-implied. the euro-area response identifies its geography as `euro_area`. UK policy uses Bank of England IUDBEDR Bank Rate observations and UK inflation uses the Office for National Statistics D7G7 CPI annual rate. BoE and ONS data are attributed under the Open Government Licence: https://www.nationalarchives.gov.uk/doc/open-government-licence/version/3/. UK gilt data also includes BoE nominal and real zero-coupon spot yields plus RPI-linked implied inflation at 2Y, 5Y, 10Y, and 30Y tenors using official month-end archive dates and method month_end. European Central Bank statistics are freely available for reuse when accurately reproduced and cited. Free, Pro, and Enterprise keys receive the same data contract; only their existing monthly quota limits differ. API requests read the database and never call upstream sources.","operationId":"getMacroRates","parameters":[{"$ref":"#/components/parameters/MacroRegion"}],"responses":{"200":{"description":"Latest policy, yield, and inflation observations with per-observation provenance.","headers":{"x-ratelimit-limit":{"description":"Your plan's monthly request cap.","schema":{"type":"integer","example":30}},"x-ratelimit-used":{"description":"Requests used this month, including this one.","schema":{"type":"integer","example":1}},"x-ratelimit-remaining":{"description":"Requests remaining this month.","schema":{"type":"integer","example":29}},"x-ratelimit-plan":{"description":"The plan the quota is enforced against.","schema":{"type":"string","enum":["free","pro","enterprise"],"example":"free"}},"x-ratelimit-reset":{"description":"When the quota resets (1st of next month, 00:00 UTC).","schema":{"type":"string","format":"date-time"}},"X-API-CURRENT":{"description":"Compatibility alias for x-ratelimit-used.","schema":{"type":"integer","example":1}},"X-API-QUOTA":{"description":"Compatibility alias for x-ratelimit-limit.","schema":{"type":"integer","example":30}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MacroRatesResponse"}}}},"400":{"description":"The requested macro region is not supported.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MacroBadRequestError"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/EmailNotVerified"},"429":{"$ref":"#/components/responses/QuotaExceeded"},"500":{"$ref":"#/components/responses/InternalError"},"503":{"description":"No stored observations are available for the requested region.","headers":{"x-ratelimit-limit":{"description":"Your plan's monthly request cap.","schema":{"type":"integer","example":30}},"x-ratelimit-used":{"description":"Requests used this month, including this one.","schema":{"type":"integer","example":1}},"x-ratelimit-remaining":{"description":"Requests remaining this month.","schema":{"type":"integer","example":29}},"x-ratelimit-plan":{"description":"The plan the quota is enforced against.","schema":{"type":"string","enum":["free","pro","enterprise"],"example":"free"}},"x-ratelimit-reset":{"description":"When the quota resets (1st of next month, 00:00 UTC).","schema":{"type":"string","format":"date-time"}},"X-API-CURRENT":{"description":"Compatibility alias for x-ratelimit-used.","schema":{"type":"integer","example":1}},"X-API-QUOTA":{"description":"Compatibility alias for x-ratelimit-limit.","schema":{"type":"integer","example":30}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}},"post":{"tags":["macro"],"summary":"Not supported — read-only API","operationId":"postMacroRatesNotAllowed","responses":{"405":{"$ref":"#/components/responses/MethodNotAllowed"}}}},"/v1/central-bank-reserves":{"get":{"tags":["reserves"],"security":[{"ApiKeyAuth":[]},{"ApiKeyQuery":[]}],"summary":"Annual central-bank reserve values and gold share","description":"Annual reserve valuations for Pro and Enterprise accounts from World Bank, World Development Indicators, whose underlying provider is International Monetary Fund, International Financial Statistics. Bullion API derives gold reserve value, gold share, and consecutive-year value change from cached observations. Annual value change combines holdings changes, gold-price movement, and revisions; it does not measure tonnes bought or sold.","operationId":"getCentralBankReserves","parameters":[{"$ref":"#/components/parameters/CountryIso2"},{"$ref":"#/components/parameters/StartYear"},{"$ref":"#/components/parameters/EndYear"}],"responses":{"200":{"description":"Complete annual reserve observations served from the automated cache.","headers":{"x-ratelimit-limit":{"description":"Your plan's monthly request cap.","schema":{"type":"integer","example":30}},"x-ratelimit-used":{"description":"Requests used this month, including this one.","schema":{"type":"integer","example":1}},"x-ratelimit-remaining":{"description":"Requests remaining this month.","schema":{"type":"integer","example":29}},"x-ratelimit-plan":{"description":"The plan the quota is enforced against.","schema":{"type":"string","enum":["free","pro","enterprise"],"example":"free"}},"x-ratelimit-reset":{"description":"When the quota resets (1st of next month, 00:00 UTC).","schema":{"type":"string","format":"date-time"}},"X-API-CURRENT":{"description":"Compatibility alias for x-ratelimit-used.","schema":{"type":"integer","example":1}},"X-API-QUOTA":{"description":"Compatibility alias for x-ratelimit-limit.","schema":{"type":"integer","example":30}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CentralBankReservesResponse"}}}},"400":{"description":"Invalid country or year parameters. The request still consumes quota.","headers":{"x-ratelimit-limit":{"description":"Your plan's monthly request cap.","schema":{"type":"integer","example":30}},"x-ratelimit-used":{"description":"Requests used this month, including this one.","schema":{"type":"integer","example":1}},"x-ratelimit-remaining":{"description":"Requests remaining this month.","schema":{"type":"integer","example":29}},"x-ratelimit-plan":{"description":"The plan the quota is enforced against.","schema":{"type":"string","enum":["free","pro","enterprise"],"example":"free"}},"x-ratelimit-reset":{"description":"When the quota resets (1st of next month, 00:00 UTC).","schema":{"type":"string","format":"date-time"}},"X-API-CURRENT":{"description":"Compatibility alias for x-ratelimit-used.","schema":{"type":"integer","example":1}},"X-API-QUOTA":{"description":"Compatibility alias for x-ratelimit-limit.","schema":{"type":"integer","example":30}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CentralBankBadRequestError"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Email verification is required, followed by an active Pro or Enterprise plan.","content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/EmailNotVerifiedError"},{"$ref":"#/components/schemas/PlanRequiredError"}]},"examples":{"email_not_verified":{"value":{"status":"error","error":{"code":"email_not_verified","message":"Verify your email address before using the API"}}},"plan_required":{"value":{"status":"error","error":{"code":"plan_required","message":"The request requires an eligible subscription plan.","required_plans":["pro","enterprise"],"plan":"free"}}}}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalError"},"503":{"description":"The automated cache has no complete observations.","headers":{"x-ratelimit-limit":{"description":"Your plan's monthly request cap.","schema":{"type":"integer","example":30}},"x-ratelimit-used":{"description":"Requests used this month, including this one.","schema":{"type":"integer","example":1}},"x-ratelimit-remaining":{"description":"Requests remaining this month.","schema":{"type":"integer","example":29}},"x-ratelimit-plan":{"description":"The plan the quota is enforced against.","schema":{"type":"string","enum":["free","pro","enterprise"],"example":"free"}},"x-ratelimit-reset":{"description":"When the quota resets (1st of next month, 00:00 UTC).","schema":{"type":"string","format":"date-time"}},"X-API-CURRENT":{"description":"Compatibility alias for x-ratelimit-used.","schema":{"type":"integer","example":1}},"X-API-QUOTA":{"description":"Compatibility alias for x-ratelimit-limit.","schema":{"type":"integer","example":30}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"},"example":{"status":"error","error":{"code":"data_unavailable","message":"The requested data is not available."}}}}}}},"post":{"tags":["reserves"],"summary":"Not supported — read-only API","operationId":"postCentralBankReservesNotAllowed","responses":{"405":{"$ref":"#/components/responses/MethodNotAllowed"}}}},"/v1/central-bank-gold":{"get":{"tags":["central-bank-gold"],"security":[{"ApiKeyAuth":[]},{"ApiKeyQuery":[]}],"summary":"Monthly and fortnightly reported central-bank gold holdings","description":"Returns reported monthly country holdings from official national central-bank feeds for Pro and Enterprise accounts. Data is served from the scheduled database cache; API requests never call upstream providers. Reported holdings are normalised from million fine troy ounces to tonnes. Bank of England central-government gold (series PQMBAAE and XUMLGPD) is derived from the official USD value and month-end gold price, so UK observations carry holdings_basis derived. National Bank of Kazakhstan observations report monetary gold as an end-of-month USD reserve value instead. The State Bank of Pakistan reports Pakistan monetary gold directly in USD millions in its monthly Official Reserve Assets table, valued per its documentation at the closing London bullion market rate at the end of each fiscal year (30 June). U.S. Department of the Treasury weekly reserve-position releases report the US gold stock value in USD millions with directly reported fine-troy-ounce volume, carried as weekly-cadence observations. Bank Negara Malaysia reports a fortnightly USD-billion Gold valuation without a published quantity, so Malaysian tonnes remain unavailable. Month-to-month differences are changes in reported stock, not purchase or sale transactions. The estimated response object is retained for compatibility but is unavailable and never contains observations.","operationId":"getCentralBankGold","parameters":[{"$ref":"#/components/parameters/CentralBankGoldLayer"},{"$ref":"#/components/parameters/StartMonth"},{"$ref":"#/components/parameters/EndMonth"},{"$ref":"#/components/parameters/CountryIso2"}],"responses":{"200":{"description":"Reported central-bank gold holdings with an unavailable estimated compatibility layer.","headers":{"x-ratelimit-limit":{"description":"Your plan's monthly request cap.","schema":{"type":"integer","example":30}},"x-ratelimit-used":{"description":"Requests used this month, including this one.","schema":{"type":"integer","example":1}},"x-ratelimit-remaining":{"description":"Requests remaining this month.","schema":{"type":"integer","example":29}},"x-ratelimit-plan":{"description":"The plan the quota is enforced against.","schema":{"type":"string","enum":["free","pro","enterprise"],"example":"free"}},"x-ratelimit-reset":{"description":"When the quota resets (1st of next month, 00:00 UTC).","schema":{"type":"string","format":"date-time"}},"X-API-CURRENT":{"description":"Compatibility alias for x-ratelimit-used.","schema":{"type":"integer","example":1}},"X-API-QUOTA":{"description":"Compatibility alias for x-ratelimit-limit.","schema":{"type":"integer","example":30}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CentralBankGoldResponse"}}}},"400":{"description":"Invalid layer, country, month, or range. The request still consumes quota.","headers":{"x-ratelimit-limit":{"description":"Your plan's monthly request cap.","schema":{"type":"integer","example":30}},"x-ratelimit-used":{"description":"Requests used this month, including this one.","schema":{"type":"integer","example":1}},"x-ratelimit-remaining":{"description":"Requests remaining this month.","schema":{"type":"integer","example":29}},"x-ratelimit-plan":{"description":"The plan the quota is enforced against.","schema":{"type":"string","enum":["free","pro","enterprise"],"example":"free"}},"x-ratelimit-reset":{"description":"When the quota resets (1st of next month, 00:00 UTC).","schema":{"type":"string","format":"date-time"}},"X-API-CURRENT":{"description":"Compatibility alias for x-ratelimit-used.","schema":{"type":"integer","example":1}},"X-API-QUOTA":{"description":"Compatibility alias for x-ratelimit-limit.","schema":{"type":"integer","example":30}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CentralBankGoldBadRequestError"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/EmailNotVerified"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalError"}}},"post":{"tags":["central-bank-gold"],"summary":"Not supported — read-only API","operationId":"postCentralBankGoldNotAllowed","responses":{"405":{"$ref":"#/components/responses/MethodNotAllowed"}}}},"/v1/portfolio":{"get":{"tags":["portfolio"],"security":[{"ApiKeyAuth":[]}],"summary":"Read owned gold portfolio settings and stored-price summary","description":"Requires portfolio read permission and accepts X-API-Key only. Query-string credentials are not accepted. The request retains the rolling per-key and source-IP abuse limits but does not increment the monthly market-data quota. The summary values holdings from the latest stored gold spot price in the portfolio reporting currency; the request never calls an upstream provider and never bridges currencies. Valuation is current when the stored price is at most six hours old, stale when older, and unavailable when no matching-currency price exists. current_value, unrealised_pnl, spot_price, and price_timestamp are exact decimal strings or RFC 3339 timestamps when available, and null when unavailable; holdings and cost basis always remain available.","operationId":"getPortfolio","responses":{"200":{"description":"Owned portfolio settings and an exact-decimal summary with valuation status.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PortfolioResponse"}}}},"401":{"description":"The X-API-Key header is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"The key owner is unverified or the key lacks portfolio read permission.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"},"examples":{"insufficient_scope":{"value":{"status":"error","error":{"code":"insufficient_scope","message":"API key lacks the required portfolio scope"}}},"email_not_verified":{"value":{"status":"error","error":{"code":"email_not_verified","message":"Verify your email address before using the API"}}}}}}},"404":{"description":"The account does not have a configured portfolio.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"},"example":{"status":"error","error":{"code":"portfolio_not_found","message":"Portfolio not found"}}}}},"429":{"$ref":"#/components/responses/PortfolioRateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}},"put":{"tags":["portfolio"],"security":[{"ApiKeyAuth":[]}],"summary":"Create or update owned gold portfolio settings","description":"Requires portfolio write permission and accepts X-API-Key only. Write permission always includes read. The reporting currency can change while the ledger is empty. The request retains abuse limits but does not increment the monthly market-data quota.","operationId":"putPortfolio","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PortfolioSettingsInput"}}}},"responses":{"200":{"description":"Created or updated portfolio settings and empty summary.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PortfolioResponse"}}}},"400":{"description":"The reporting currency is missing or unsupported.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"},"example":{"status":"error","error":{"code":"invalid_currency","message":"The currency code is invalid."}}}}},"401":{"description":"The X-API-Key header is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"The key owner is unverified or the key lacks portfolio write permission.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"},"example":{"status":"error","error":{"code":"insufficient_scope","message":"API key lacks the required portfolio scope"}}}}},"429":{"$ref":"#/components/responses/PortfolioRateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/v1/portfolio/transactions":{"get":{"tags":["portfolio"],"security":[{"ApiKeyAuth":[]}],"summary":"Browse owned gold portfolio transactions","description":"Requires portfolio read permission and accepts X-API-Key only. Results are ordered by transaction_date DESC, created_at DESC, id DESC. Filter by type, form, CGT status, and by period: calendar_year covers 1 January through 31 December inclusive, while uk_tax_year uses the form 2025-26 (consecutive years) and covers 6 April through 5 April inclusive; the two period filters are mutually exclusive and empty values are ignored. Use the opaque cursor for keyset pagination; page size defaults to 50 and is capped at 100. Decimal quantities and monetary values are returned as strings. Portfolio reads do not increment the monthly market-data quota.","operationId":"getPortfolioTransactions","parameters":[{"$ref":"#/components/parameters/PortfolioTransactionType"},{"$ref":"#/components/parameters/PortfolioTransactionForm"},{"$ref":"#/components/parameters/PortfolioTransactionCgtStatus"},{"$ref":"#/components/parameters/PortfolioTransactionCalendarYear"},{"$ref":"#/components/parameters/PortfolioTransactionTaxYear"},{"$ref":"#/components/parameters/PortfolioTransactionLimit"},{"$ref":"#/components/parameters/PortfolioTransactionCursor"}],"responses":{"200":{"description":"A stable page of owned transactions and an opaque cursor for the next page.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PortfolioTransactionPage"}}}},"400":{"description":"The page size, cursor, filter value, or period is invalid (invalid_limit, invalid_cursor, invalid_filter, invalid_calendar_year, invalid_tax_year, conflicting_periods).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"The key owner is unverified or the key lacks portfolio read permission.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"404":{"description":"The account does not have a configured portfolio.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"429":{"$ref":"#/components/responses/PortfolioRateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}},"post":{"tags":["portfolio"],"security":[{"ApiKeyAuth":[]}],"summary":"Record an exact gold purchase, FIFO sale, or explicit-lot sale","description":"Requires portfolio write permission and an Idempotency-Key header. Quantity is normalized to grams at 15 decimal places. Money is rounded half-up and returned as 8-place decimal strings. Reusing a key with the same canonical request replays the original transaction; a changed request returns 409. Send X-Portfolio-Transaction-Type: sale to record a sale. By default sales consume eligible open lots FIFO by transaction date, creation time, then ID; sales cannot be future-dated or precede an allocated purchase. Alternatively send explicit lot allocations: every selected lot must belong to the account, match the sale form and CGT treatment, predate or equal the sale date, and have sufficient remaining quantity, and allocated quantities must sum exactly to the sale quantity. Any invalid allocation fails the whole sale with invalid_allocation (or insufficient_holdings for races) without mutation, while concurrent mutations are committed atomically at Serializable isolation.","operationId":"postPortfolioTransaction","parameters":[{"name":"Idempotency-Key","in":"header","required":true,"schema":{"type":"string","minLength":1,"maxLength":255},"description":"Opaque client-generated retry key. It is not stored or logged in raw form."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/PortfolioPurchaseInput"},{"$ref":"#/components/schemas/PortfolioSaleInput"}]}}}},"responses":{"200":{"description":"Created purchase or the original result for an idempotent replay.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PortfolioPurchaseResponse"}}}},"400":{"description":"Missing idempotency key, invalid fields, an invalid sale date, or invalid explicit lot allocations.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"The key owner is unverified or the key lacks portfolio write permission.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"404":{"description":"The account does not have a configured portfolio.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"409":{"description":"The idempotency key was reused for a changed request, or holdings are insufficient.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"429":{"$ref":"#/components/responses/PortfolioRateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/v1/portfolio/lots":{"get":{"tags":["portfolio"],"security":[{"ApiKeyAuth":[]}],"summary":"List owned open purchase lots eligible for sale allocation","description":"Requires portfolio read permission and accepts X-API-Key only. Returns only the authenticated owner's open purchase lots with a positive remaining quantity, ordered by purchase transaction_date, creation time, then ID for stable identifiers. Each lot carries its stable lot_id, the originating purchase_transaction_id, purchase date, form, CGT status, remaining quantity in grams, and remaining cost basis as decimal strings. A lot is eligible for an explicit sale allocation when it belongs to the account, matches the sale form and CGT treatment, predates or equals the sale date, has sufficient remaining quantity, and the allocated quantities sum exactly to the sale quantity.","operationId":"getPortfolioLots","responses":{"200":{"description":"The owned open lots available for explicit sale allocation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PortfolioLotsResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"The key owner is unverified or the key lacks portfolio read permission.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"404":{"description":"The account does not have a configured portfolio.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"429":{"$ref":"#/components/responses/PortfolioRateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/v1/portfolio/transactions/{id}":{"get":{"tags":["portfolio"],"security":[{"ApiKeyAuth":[]}],"summary":"Read one owned gold portfolio transaction","description":"Requires portfolio read permission and accepts X-API-Key only. The identifier is scoped to the authenticated account; missing, malformed, and cross-account identifiers share the transaction_not_found response. Every response carries an ETag header with the quoted transaction version; send it as If-Match when editing.","operationId":"getPortfolioTransaction","parameters":[{"$ref":"#/components/parameters/PortfolioTransactionId"}],"responses":{"200":{"description":"One owned transaction with its open lot and allocation details.","headers":{"ETag":{"description":"Quoted current version of the transaction.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PortfolioTransactionResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"The key owner is unverified or the key lacks portfolio read permission.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"404":{"description":"The transaction was not found for the authenticated account.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"},"example":{"status":"error","error":{"code":"transaction_not_found","message":"The transaction was not found."}}}}},"429":{"$ref":"#/components/responses/PortfolioRateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}},"delete":{"tags":["portfolio"],"security":[{"ApiKeyAuth":[]}],"summary":"Delete one owned gold portfolio transaction","description":"Requires portfolio write permission and accepts X-API-Key only. The request must send an If-Match header carrying the ETag-style quoted version from the transaction payload (\"1\"); 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; deleting a purchase that has funded a sale is refused with 409 transaction_in_use. Missing, malformed, and cross-account identifiers share the transaction_not_found response.","operationId":"deletePortfolioTransaction","parameters":[{"$ref":"#/components/parameters/PortfolioTransactionId"},{"name":"If-Match","in":"header","required":true,"schema":{"type":"string","pattern":"^(?:W/)?\"\\d+\"$"},"description":"Precondition header with the quoted transaction version, for example \"1\". A weak validator (W/\"1\") is accepted. Any other value fails the precondition before the ledger is touched."}],"responses":{"200":{"description":"The transaction was deleted and holdings, lots, and summary values were rebalanced.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PortfolioTransactionDeletedResponse"},"example":{"status":"success","deleted_transaction_id":"11111111-1111-4111-8111-111111111111"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"The key owner is unverified or the key lacks portfolio write permission.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"},"example":{"status":"error","error":{"code":"insufficient_scope","message":"API key lacks the required portfolio scope"}}}}},"404":{"description":"The transaction was not found for the authenticated account.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"},"example":{"status":"error","error":{"code":"transaction_not_found","message":"The transaction was not found."}}}}},"409":{"description":"The purchase has funded a sale and cannot be deleted.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"},"example":{"status":"error","error":{"code":"transaction_in_use","message":"Gold from this purchase has been sold and cannot be deleted."}}}}},"412":{"description":"The If-Match version does not match the stored transaction.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"},"example":{"status":"error","error":{"code":"stale_transaction","message":"Load the transaction again and retry with its latest ETag."}}}}},"429":{"$ref":"#/components/responses/PortfolioRateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}},"patch":{"tags":["portfolio"],"security":[{"ApiKeyAuth":[]}],"summary":"Edit one owned gold portfolio transaction with optimistic concurrency","description":"Requires portfolio write permission and accepts X-API-Key only. If-Match must quote the current version; a missing or stale precondition returns 412 stale_transaction without any mutation. 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 using their persisted fifo or explicit method: FIFO edits omit allocations, explicit edits resend them with the same eligibility and exact-sum rules as creation. Success increments the version, returns the new ETag, and recalculates lot totals, disposed cost, and realised P&L immediately. Any validation, allocation, or concurrency failure rolls the whole edit back at Serializable isolation.","operationId":"patchPortfolioTransaction","parameters":[{"$ref":"#/components/parameters/PortfolioTransactionId"},{"$ref":"#/components/parameters/PortfolioTransactionIfMatch"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/PortfolioPurchaseInput"},{"$ref":"#/components/schemas/PortfolioSaleInput"}]}}}},"responses":{"200":{"description":"Edited transaction with its incremented version.","headers":{"ETag":{"description":"Quoted new version of the edited transaction.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PortfolioTransactionResponse"}}}},"400":{"description":"Invalid fields or an edit incompatible with dependent sale allocations.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"The key owner is unverified or the key lacks portfolio write permission.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"404":{"description":"The transaction was not found for the authenticated account.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"},"example":{"status":"error","error":{"code":"transaction_not_found","message":"The transaction was not found."}}}}},"409":{"description":"The portfolio does not hold enough gold for the edited sale.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"412":{"description":"The If-Match header was missing, malformed, or did not match the current version.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"},"example":{"status":"error","error":{"code":"stale_transaction","message":"Load the transaction again and retry with its latest ETag."}}}}},"429":{"$ref":"#/components/responses/PortfolioRateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/v1/portfolio/gains":{"get":{"tags":["portfolio"],"security":[{"ApiKeyAuth":[]}],"summary":"Summarise owned realised gains by UK CGT treatment","description":"Requires portfolio read permission and accepts X-API-Key only. Sums realised profit or loss from owned sale transactions whose transaction_date falls inside one required period: calendar_year covers 1 January through 31 December inclusive, while uk_tax_year uses the form 2025-26 (consecutive years) and covers 6 April through 5 April inclusive. The two period filters are mutually exclusive and exactly one must be supplied. Buckets group sales by recorded CGT status; exempt and not_exempt sums use only transactions declared with that status and unknown collects the rest. All monetary values are exact 8-place decimal strings. The figures are informational only: they exclude annual allowances, matching and pooling rules, and tax rates, and are not tax advice. Portfolio reads do not increment the monthly market-data quota.","operationId":"getPortfolioGains","parameters":[{"$ref":"#/components/parameters/PortfolioTransactionCalendarYear"},{"$ref":"#/components/parameters/PortfolioTransactionTaxYear"}],"responses":{"200":{"description":"The resolved period echoed back with realised gains grouped by declared CGT treatment.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PortfolioGainsResponse"}}}},"400":{"description":"No period filter was supplied, both were supplied, or a label is malformed (missing_period, conflicting_periods, invalid_calendar_year, invalid_tax_year).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"},"examples":{"missing_period":{"value":{"status":"error","error":{"code":"missing_period","message":"Use either calendar_year or uk_tax_year."}}},"conflicting_periods":{"value":{"status":"error","error":{"code":"conflicting_periods","message":"Use either calendar_year or uk_tax_year, not both."}}},"invalid_calendar_year":{"value":{"status":"error","error":{"code":"invalid_calendar_year","message":"The transaction history calendar year is invalid."}}},"invalid_tax_year":{"value":{"status":"error","error":{"code":"invalid_tax_year","message":"The UK tax-year label is invalid."}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"The key owner is unverified or the key lacks portfolio read permission.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"404":{"description":"The account does not have a configured portfolio.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"},"example":{"status":"error","error":{"code":"portfolio_not_found","message":"Portfolio not found"}}}}},"429":{"$ref":"#/components/responses/PortfolioRateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/v1/usage":{"get":{"tags":["account"],"summary":"Plan, quota and recent requests for the signed-in user","description":"Authenticated via session cookie (NOT X-API-Key) so the dashboard can call it directly without storing an API key client-side. This spec is informational for this endpoint: browsers obtain the cookie by signing in at https://bullionapi.dev/login.","operationId":"getUsage","security":[{"CookieAuth":[]}],"responses":{"200":{"description":"Current plan, monthly quota state and the 50 most recent requests.","headers":{"x-ratelimit-limit":{"description":"Your plan's monthly request cap.","schema":{"type":"integer","example":30}},"x-ratelimit-used":{"description":"Requests used this month, including this one.","schema":{"type":"integer","example":1}},"x-ratelimit-remaining":{"description":"Requests remaining this month.","schema":{"type":"integer","example":29}},"x-ratelimit-plan":{"description":"The plan the quota is enforced against.","schema":{"type":"string","enum":["free","pro","enterprise"],"example":"free"}},"x-ratelimit-reset":{"description":"When the quota resets (1st of next month, 00:00 UTC).","schema":{"type":"string","format":"date-time"}},"X-API-CURRENT":{"description":"Compatibility alias for x-ratelimit-used.","schema":{"type":"integer","example":1}},"X-API-QUOTA":{"description":"Compatibility alias for x-ratelimit-limit.","schema":{"type":"integer","example":30}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UsageResponse"}}}},"401":{"description":"Not signed in.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"},"example":{"status":"error","error":{"code":"unauthenticated","message":"A signed-in session is required."}}}}},"500":{"$ref":"#/components/responses/InternalError"}}}},"/health":{"get":{"tags":["meta"],"summary":"Service health","description":"Public, no auth. Reports database reachability, metal-cache freshness and enabled projected-currency FX snapshot readiness. Returns 503 when the metal cache is stale or an enabled FX snapshot is unavailable or stale.","operationId":"getHealth","security":[],"responses":{"200":{"description":"Healthy: database reachable, metal cache fresh and FX disabled or fresh.","headers":{"x-bullion-build-sha":{"description":"Full Git commit SHA of the deployed build when supplied by the build environment.","schema":{"type":"string","pattern":"^[0-9a-f]{40}$"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HealthResponse"}}}},"503":{"description":"Degraded: database unreachable, metal cache stale or enabled FX unavailable or stale.","headers":{"x-bullion-build-sha":{"description":"Full Git commit SHA of the deployed build when supplied by the build environment.","schema":{"type":"string","pattern":"^[0-9a-f]{40}$"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"},"example":{"status":"error","error":{"code":"health_degraded","message":"The service health check is degraded.","db_reachable":false,"last_refresh_at":null,"currencies_with_data":[],"fx_snapshot_status":"disabled","last_fx_snapshot_at":null,"projected_currencies_with_data":[]}}}}}}}}},"components":{"securitySchemes":{"ApiKeyAuth":{"type":"apiKey","in":"header","name":"X-API-Key","description":"API key created in the dashboard, prefixed `bullion_`."},"ApiKeyQuery":{"type":"apiKey","in":"query","name":"api_key","description":"Fallback API key for clients that cannot set custom headers. Query credentials can be exposed by URLs; prefer ApiKeyAuth whenever possible."},"CookieAuth":{"type":"apiKey","in":"cookie","name":"better-auth.session_token","description":"Session cookie set on sign-in. Over HTTPS the name is prefixed: `__Secure-better-auth.session_token`."}},"parameters":{"Currency":{"name":"currency","in":"query","required":false,"schema":{"type":"string","pattern":"^[A-Za-z]{3}$","default":"USD","example":"EUR"},"description":"ISO 4217 base currency, case-insensitive. Use GET /v1/symbols for the active production catalogue. An unavailable code returns an empty price response."},"CaratCurrency":{"name":"currency","in":"query","required":false,"schema":{"type":"string","pattern":"^[A-Za-z]{3}$","default":"USD","example":"EUR"},"description":"Three-letter ISO 4217 currency code, case-insensitive. The request succeeds only when that currency has cached gold data."},"CaratDate":{"name":"date","in":"query","required":false,"schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$","example":"2026-08-10"},"description":"Optional exact UTC day. The final cached gold observation in that day is used; malformed or impossible dates return 400 invalid_date."},"Unit":{"name":"unit","in":"query","required":false,"schema":{"type":"string","enum":["gram","g","kilogram","kg","troy_oz","toz"],"default":"toz"},"description":"Output mass unit. Omit or leave empty for the legacy toz response. gram and g return grams; kilogram and kg return kilograms; troy_oz and toz return troy ounces. Values are converted to eight decimal places. Other non-empty values return 400 invalid_unit."},"StartDate":{"name":"start_date","in":"query","required":true,"schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$","example":"2026-01-01"},"description":"First day to include (inclusive), YYYY-MM-DD."},"EndDate":{"name":"end_date","in":"query","required":true,"schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$","example":"2026-01-31"},"description":"Last day to include (inclusive), YYYY-MM-DD. Must be >= start_date."},"ChangeStartDate":{"name":"start_date","in":"query","required":false,"schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$","example":"2026-01-01"},"description":"First UTC day to compare, used together with end_date."},"ChangeEndDate":{"name":"end_date","in":"query","required":false,"schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$","example":"2026-01-02"},"description":"Last UTC day to compare, used together with start_date."},"ChangeDateType":{"name":"date_type","in":"query","required":false,"schema":{"type":"string","enum":["yesterday","week","month","year"]},"description":"Rolling UTC window. yesterday is 1 day, week is 7 days, month is 30 days, and year is 365 days. Use instead of the explicit date pair."},"CountryIso2":{"name":"country","in":"query","required":false,"schema":{"type":"string","pattern":"^[A-Za-z]{2}$","example":"GB"},"description":"ISO 3166-1 alpha-2 country code, case-insensitive."},"CentralBankGoldLayer":{"name":"layer","in":"query","required":false,"schema":{"type":"string","enum":["reported","estimated","both"],"default":"both"},"description":"Use reported. estimated and both remain accepted for response compatibility."},"StartMonth":{"name":"start","in":"query","required":false,"schema":{"type":"string","pattern":"^\\d{4}-(0[1-9]|1[0-2])$","example":"2021-01"},"description":"First inclusive month. Supply together with end; the maximum range is 120 months."},"EndMonth":{"name":"end","in":"query","required":false,"schema":{"type":"string","pattern":"^\\d{4}-(0[1-9]|1[0-2])$","example":"2026-06"},"description":"Last inclusive month. Supply together with start; the maximum range is 120 months."},"StartYear":{"name":"start","in":"query","required":false,"schema":{"type":"string","pattern":"^\\d{4}$","example":"2020"},"description":"First observation year to include (inclusive)."},"EndYear":{"name":"end","in":"query","required":false,"schema":{"type":"string","pattern":"^\\d{4}$","example":"2025"},"description":"Last observation year to include (inclusive). Must be >= start."},"MacroRegion":{"name":"region","in":"path","required":true,"schema":{"type":"string","enum":["us","eu","uk"],"example":"us"},"description":"Macro region code. Use `us`, `uk`, or `eu` for the United States, United Kingdom, or euro area."},"PortfolioTransactionId":{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Owned transaction identifier."},"PortfolioTransactionIfMatch":{"name":"If-Match","in":"header","required":true,"schema":{"type":"string","pattern":"^\"\\d+\"$","example":"\"3\""},"description":"Quoted current version of the transaction, taken from the ETag response header or the version field. Wildcard and multi-tag forms are not accepted."},"PortfolioTransactionLimit":{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":100,"default":50},"description":"Number of rows to return. Defaults to 50 and cannot exceed 100."},"PortfolioTransactionCursor":{"name":"cursor","in":"query","required":false,"schema":{"type":"string"},"description":"Opaque cursor returned as next_cursor. It encodes the final transaction_date, created_at, and id ordering tuple."},"PortfolioTransactionType":{"name":"type","in":"query","required":false,"schema":{"type":"string","enum":["purchase","sale"]},"description":"Only include transactions of this type. Empty values are ignored."},"PortfolioTransactionForm":{"name":"form","in":"query","required":false,"schema":{"type":"string","enum":["bar","coin","other"]},"description":"Only include transactions with this physical form. Empty values are ignored."},"PortfolioTransactionCgtStatus":{"name":"cgt_status","in":"query","required":false,"schema":{"type":"string","enum":["exempt","not_exempt","unknown"]},"description":"Only include transactions recorded with this UK CGT status. Empty values are ignored."},"PortfolioTransactionCalendarYear":{"name":"calendar_year","in":"query","required":false,"schema":{"type":"string","pattern":"^\\d{4}$","example":"2025"},"description":"Restrict results to 1 January through 31 December of this year, inclusive. Mutually exclusive with uk_tax_year; combining both returns 400 conflicting_periods. Invalid labels return 400 invalid_calendar_year."},"PortfolioTransactionTaxYear":{"name":"uk_tax_year","in":"query","required":false,"schema":{"type":"string","pattern":"^\\d{4}-\\d{2}$","example":"2025-26"},"description":"Restrict results to a UK tax year labelled with consecutive years such as 2025-26, covering 6 April through 5 April inclusive. Mutually exclusive with calendar_year; combining both returns 400 conflicting_periods. Non-consecutive or malformed labels return 400 invalid_tax_year."}},"responses":{"Unauthorized":{"description":"Missing, conflicting, or invalid API key credentials.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"},"examples":{"missing":{"value":{"status":"error","error":{"code":"missing_api_key","message":"Missing API key"}}},"conflict":{"value":{"status":"error","error":{"code":"conflicting_credentials","message":"Conflicting API key credentials"}}},"invalid":{"value":{"status":"error","error":{"code":"invalid_api_key","message":"Invalid or revoked API key"}}}}}}},"EmailNotVerified":{"description":"The API-key owner must verify their email address before accessing price data.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"},"example":{"status":"error","error":{"code":"email_not_verified","message":"Verify your email address before using the API"}}}}},"QuotaExceeded":{"description":"Monthly quota reached. Quota resets on the 1st of next month, 00:00 UTC.","headers":{"x-ratelimit-limit":{"description":"Your plan's monthly request cap.","schema":{"type":"integer","example":30}},"x-ratelimit-used":{"description":"Requests used this month, including this one.","schema":{"type":"integer","example":1}},"x-ratelimit-remaining":{"description":"Requests remaining this month.","schema":{"type":"integer","example":29}},"x-ratelimit-plan":{"description":"The plan the quota is enforced against.","schema":{"type":"string","enum":["free","pro","enterprise"],"example":"free"}},"x-ratelimit-reset":{"description":"When the quota resets (1st of next month, 00:00 UTC).","schema":{"type":"string","format":"date-time"}},"X-API-CURRENT":{"description":"Compatibility alias for x-ratelimit-used.","schema":{"type":"integer","example":1}},"X-API-QUOTA":{"description":"Compatibility alias for x-ratelimit-limit.","schema":{"type":"integer","example":30}},"retry-after":{"description":"Seconds until the quota resets.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/QuotaExceededError"},"example":{"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"}}}}},"TooManyRequests":{"description":"The API-key rolling window or monthly quota has been exceeded.","headers":{"x-ratelimit-limit":{"description":"Your plan's monthly request cap.","schema":{"type":"integer","example":30}},"x-ratelimit-used":{"description":"Requests used this month, including this one.","schema":{"type":"integer","example":1}},"x-ratelimit-remaining":{"description":"Requests remaining this month.","schema":{"type":"integer","example":29}},"x-ratelimit-plan":{"description":"The plan the quota is enforced against.","schema":{"type":"string","enum":["free","pro","enterprise"],"example":"free"}},"x-ratelimit-reset":{"description":"When the quota resets (1st of next month, 00:00 UTC).","schema":{"type":"string","format":"date-time"}},"X-API-CURRENT":{"description":"Compatibility alias for x-ratelimit-used.","schema":{"type":"integer","example":1}},"X-API-QUOTA":{"description":"Compatibility alias for x-ratelimit-limit.","schema":{"type":"integer","example":30}},"retry-after":{"description":"Seconds until the applicable limit resets.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/QuotaExceededError"},{"$ref":"#/components/schemas/RateLimitedError"}]},"examples":{"rate_limited":{"value":{"status":"error","error":{"code":"rate_limited","message":"Rate limit exceeded"}}},"quota_exceeded":{"value":{"status":"error","error":{"code":"quota_exceeded","message":"Your monthly usage limit has been reached. Please upgrade your subscription plan."}}}}}}},"PortfolioRateLimited":{"description":"The API key has exceeded its rolling request window.","headers":{"retry-after":{"description":"Seconds until the per-key rolling limit resets.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimitedError"},"example":{"status":"error","error":{"code":"rate_limited","message":"Rate limit exceeded"}}}}},"ApiKeyRateLimited":{"description":"The API key has exceeded its rolling request window. Monthly quota exhaustion does not block this endpoint.","headers":{"retry-after":{"description":"Seconds until the per-key rolling limit resets.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimitedError"},"example":{"status":"error","error":{"code":"rate_limited","message":"Rate limit exceeded"}}}}},"MethodNotAllowed":{"description":"The API is read-only; writes are not supported.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"},"example":{"status":"error","error":{"code":"method_not_allowed","message":"Method not allowed"}}}}},"InternalError":{"description":"Server configuration error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"},"example":{"status":"error","error":{"code":"config_error","message":"The service is not configured correctly."}}}}}},"schemas":{"SymbolsResponse":{"type":"object","additionalProperties":false,"required":["status","metals","currencies"],"properties":{"status":{"type":"string","const":"success"},"metals":{"type":"object","additionalProperties":false,"required":["gold"],"description":"Currently deployed verified metal capabilities.","properties":{"gold":{"type":"string","const":"Gold (troy ounce)"}}},"currencies":{"type":"object","required":["USD","EUR","GBP"],"propertyNames":{"pattern":"^[A-Z]{3}$"},"additionalProperties":{"type":"string","minLength":1},"description":"Static deployment catalogue derived from reviewed PROJECTED_CURRENCIES configuration. Authenticated GET /v1/symbols is authoritative at runtime because projected currencies remain absent until their production price rows are materialised.","properties":{"USD":{"type":"string","const":"US Dollar"},"EUR":{"type":"string","const":"Euro"},"GBP":{"type":"string","const":"Pound Sterling"}}}},"example":{"status":"success","metals":{"gold":"Gold (troy ounce)"},"currencies":{"USD":"US Dollar","EUR":"Euro","GBP":"Pound Sterling"}}},"PortfolioSettingsInput":{"type":"object","additionalProperties":false,"required":["reporting_currency"],"properties":{"reporting_currency":{"type":"string","pattern":"^[A-Z]{3}$","description":"Active ISO 4217 currency from GET /v1/symbols.","example":"GBP"}}},"PortfolioSettings":{"type":"object","additionalProperties":false,"required":["id","metal","reporting_currency","created_at","updated_at"],"properties":{"id":{"type":"string","format":"uuid"},"metal":{"type":"string","const":"gold"},"reporting_currency":{"type":"string","pattern":"^[A-Z]{3}$","description":"Active ISO 4217 currency from GET /v1/symbols."},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}}},"PortfolioSummary":{"type":"object","additionalProperties":false,"required":["gold_quantity_grams","cost_basis","realised_pnl","valuation"],"properties":{"gold_quantity_grams":{"type":"string","example":"0.000000000000000"},"cost_basis":{"type":"string","example":"0.00000000"},"realised_pnl":{"type":"string","example":"0.00000000"},"valuation":{"type":"object","additionalProperties":false,"required":["status","spot_price","current_value","unrealised_pnl","price_timestamp"],"properties":{"status":{"type":"string","enum":["current","stale","unavailable"],"description":"current when the stored price is at most six hours old, stale when older, unavailable when no matching-currency price exists."},"spot_price":{"type":["string","null"],"description":"Stored gold spot price per troy ounce in the reporting currency."},"current_value":{"type":["string","null"],"description":"Open troy ounces multiplied by the stored spot price; null when unavailable."},"unrealised_pnl":{"type":["string","null"],"description":"current_value minus the remaining cost basis; null when unavailable."},"price_timestamp":{"type":["string","null"],"format":"date-time","description":"When the stored spot price was fetched; null when unavailable."}}}}},"PortfolioResponse":{"type":"object","additionalProperties":false,"required":["status","portfolio","summary"],"properties":{"status":{"type":"string","const":"success"},"portfolio":{"$ref":"#/components/schemas/PortfolioSettings"},"summary":{"$ref":"#/components/schemas/PortfolioSummary"}}},"PortfolioPurchaseInput":{"type":"object","additionalProperties":false,"required":["transaction_date","quantity","weight_unit","unit_price","delivery_cost","fees","form","cgt_status"],"properties":{"transaction_date":{"type":"string","format":"date","description":"Real UTC calendar date, today or earlier."},"quantity":{"type":"string","pattern":"^\\d+(?:\\.\\d{1,8})?$","example":"1.005"},"weight_unit":{"type":"string","enum":["gram","g","kilogram","kg","troy_oz","toz"]},"unit_price":{"type":"string","example":"2500.125"},"delivery_cost":{"type":"string","example":"12.345678945"},"fees":{"type":"string","example":"1.005"},"form":{"type":"string","enum":["bar","coin","other"]},"cgt_status":{"type":"string","enum":["exempt","not_exempt","unknown"]},"description":{"type":"string","maxLength":500}}},"PortfolioPurchase":{"type":"object","additionalProperties":false,"required":["id","transaction_date","entered_quantity","quantity_grams","weight_unit","unit_price","gross_cost","delivery_cost","fees","total_cost","cost_per_gram","form","cgt_status","description","created_at"],"properties":{"id":{"type":"string","format":"uuid"},"transaction_date":{"type":"string","format":"date"},"entered_quantity":{"type":"string","description":"The entered quantity in the submitted weight unit, returned as an 8-place decimal string.","example":"1.00500000"},"quantity_grams":{"type":"string","example":"31.258994184000000"},"weight_unit":{"type":"string","enum":["gram","kilogram","troy_oz"]},"unit_price":{"type":"string","example":"2500.12500000"},"gross_cost":{"type":"string","example":"2512.62562500"},"delivery_cost":{"type":"string","example":"12.34567895"},"fees":{"type":"string","example":"1.00500000"},"total_cost":{"type":"string","example":"2525.97630395"},"cost_per_gram":{"type":"string","example":"80.80798407"},"form":{"type":"string","enum":["bar","coin","other"]},"cgt_status":{"type":"string","enum":["exempt","not_exempt","unknown"]},"description":{"type":["string","null"]},"created_at":{"type":"string","format":"date-time"}}},"PortfolioPurchaseResponse":{"type":"object","additionalProperties":false,"required":["status","transaction"],"properties":{"status":{"type":"string","const":"success"},"transaction":{"$ref":"#/components/schemas/PortfolioPurchase"}}},"PortfolioSaleInput":{"type":"object","additionalProperties":false,"required":["transaction_date","quantity","weight_unit","unit_price","delivery_cost","fees","form","cgt_status"],"properties":{"transaction_date":{"type":"string","format":"date","description":"Real UTC calendar date, today or earlier. Selected lots must predate or equal it."},"quantity":{"type":"string","pattern":"^\\d+(?:\\.\\d{1,8})?$","example":"1.005"},"weight_unit":{"type":"string","enum":["gram","g","kilogram","kg","troy_oz","toz"]},"unit_price":{"type":"string","example":"2500.125"},"delivery_cost":{"type":"string","example":"12.345678945"},"fees":{"type":"string","example":"1.005"},"form":{"type":"string","enum":["bar","coin","other"]},"cgt_status":{"type":"string","enum":["exempt","not_exempt","unknown"]},"description":{"type":"string","maxLength":500},"allocations":{"type":"array","minItems":1,"items":{"type":"object","additionalProperties":false,"required":["lot_id","quantity_grams"],"properties":{"lot_id":{"type":"string","format":"uuid","description":"Stable identifier from GET /v1/portfolio/lots."},"quantity_grams":{"type":"string","pattern":"^\\d+(?:\\.\\d{1,15})?$","description":"Exact grams to dispose from this lot; must not exceed its remaining quantity and all allocated quantities must sum exactly to the sale quantity."}}},"description":"Opt-in explicit allocation. Every selected lot must belong to the account, match the sale form and CGT treatment, predate or equal the sale date, and have sufficient remaining quantity. Any missing, foreign, duplicated, incompatible, over-allocated, or under-allocated lot fails the whole sale without mutation."}}},"PortfolioOpenLot":{"type":"object","additionalProperties":false,"required":["lot_id","purchase_transaction_id","transaction_date","form","cgt_status","remaining_quantity_grams","remaining_cost_basis"],"properties":{"lot_id":{"type":"string","format":"uuid"},"purchase_transaction_id":{"type":"string","format":"uuid"},"transaction_date":{"type":"string","format":"date"},"form":{"type":"string","enum":["bar","coin","other"]},"cgt_status":{"type":"string","enum":["exempt","not_exempt","unknown"]},"remaining_quantity_grams":{"type":"string","description":"Remaining open quantity in grams as a 15-place decimal string."},"remaining_cost_basis":{"type":"string","description":"Remaining open cost basis as an 8-place decimal string."}}},"PortfolioLotsResponse":{"type":"object","additionalProperties":false,"required":["status","lots"],"properties":{"status":{"type":"string","const":"success"},"lots":{"type":"array","items":{"$ref":"#/components/schemas/PortfolioOpenLot"}}}},"PortfolioTransactionLot":{"type":"object","additionalProperties":false,"required":["id","open_quantity_grams","open_cost_basis"],"properties":{"id":{"type":"string","format":"uuid"},"open_quantity_grams":{"type":"string","description":"Open quantity in grams as a decimal string."},"open_cost_basis":{"type":"string","description":"Open cost basis as a decimal string."}}},"PortfolioTransaction":{"type":"object","additionalProperties":false,"required":["type","version","id","transaction_date","created_at","entered_quantity","weight_unit","quantity_grams","unit_price","gross_cost","delivery_cost","fees","total_cost","cost_per_gram","form","cgt_status","allocation_method","description","lots","allocations"],"properties":{"type":{"type":"string","enum":["purchase","sale"]},"version":{"type":"integer","minimum":1,"description":"Resource version for optimistic concurrency. It matches the quoted ETag response header and increments on every successful edit."},"id":{"type":"string","format":"uuid"},"transaction_date":{"type":"string","format":"date"},"created_at":{"type":"string","format":"date-time"},"entered_quantity":{"type":"string","description":"Submitted and normalized values use decimal strings."},"weight_unit":{"type":"string","enum":["gram","kilogram","troy_oz"]},"quantity_grams":{"type":"string","description":"Normalized gold quantity as a decimal string."},"unit_price":{"type":"string","description":"Unit price as an 8-place decimal string."},"gross_cost":{"type":"string","description":"Gross cost as an 8-place decimal string."},"delivery_cost":{"type":"string","description":"Delivery cost as an 8-place decimal string."},"fees":{"type":"string","description":"Fees as an 8-place decimal string."},"total_cost":{"type":"string","description":"Total cost as an 8-place decimal string."},"cost_per_gram":{"type":"string","description":"Cost per gram as an 8-place decimal string."},"form":{"type":"string","enum":["bar","coin","other"]},"cgt_status":{"type":"string","enum":["exempt","not_exempt","unknown"]},"allocation_method":{"type":"string","enum":["fifo","explicit"],"description":"How the sale selected its lots: fifo by default, or explicit when committed lot allocations were supplied."},"description":{"type":["string","null"]},"lots":{"type":"array","items":{"$ref":"#/components/schemas/PortfolioTransactionLot"}},"gross_proceeds":{"type":"string","description":"Gross sale proceeds as an 8-place decimal string."},"net_proceeds":{"type":"string","description":"Net sale proceeds after delivery and fees."},"disposed_cost":{"type":"string","description":"FIFO cost basis disposed by a sale."},"realised_pnl":{"type":"string","description":"Net proceeds less FIFO disposed cost."},"allocations":{"type":"array","items":{"type":"object","required":["purchase_transaction_id","lot_id","quantity_grams","cost_basis"],"properties":{"purchase_transaction_id":{"type":"string","format":"uuid"},"lot_id":{"type":"string","format":"uuid"},"quantity_grams":{"type":"string"},"cost_basis":{"type":"string"}}},"description":"Committed allocations ordered by purchase date, creation time, then ID. Present for both FIFO and explicit sales."}}},"PortfolioTransactionPage":{"type":"object","additionalProperties":false,"required":["status","transactions","next_cursor"],"properties":{"status":{"type":"string","const":"success"},"transactions":{"type":"array","items":{"$ref":"#/components/schemas/PortfolioTransaction"}},"next_cursor":{"type":["string","null"],"description":"Opaque cursor for the next page, or null."}}},"PortfolioTransactionResponse":{"type":"object","additionalProperties":false,"required":["status","transaction"],"properties":{"status":{"type":"string","const":"success"},"transaction":{"$ref":"#/components/schemas/PortfolioTransaction"}}},"PortfolioTransactionDeletedResponse":{"type":"object","additionalProperties":false,"required":["status","deleted_transaction_id"],"properties":{"status":{"type":"string","const":"success"},"deleted_transaction_id":{"type":"string","format":"uuid","description":"Identifier of the removed transaction."}}},"PortfolioGainsResponse":{"type":"object","additionalProperties":false,"required":["status","period","buckets","total"],"properties":{"status":{"type":"string","const":"success"},"period":{"type":"object","additionalProperties":false,"required":["kind","label","start","end"],"description":"The inclusive date window resolved from the supplied period filter.","properties":{"kind":{"type":"string","enum":["calendar_year","uk_tax_year"]},"label":{"type":"string","example":"2025-26"},"start":{"type":"string","format":"date","example":"2025-04-06"},"end":{"type":"string","format":"date","example":"2026-04-05"}}},"buckets":{"type":"object","additionalProperties":false,"required":["exempt","not_exempt","unknown"],"description":"Realised gains grouped by the CGT status recorded on each sale, as exact decimal strings.","properties":{"exempt":{"type":"string","description":"Sales declared CGT-exempt."},"not_exempt":{"type":"string","description":"Sales declared not exempt."},"unknown":{"type":"string","description":"Sales whose CGT treatment is unknown."}}},"total":{"type":"string","description":"Sum of all three buckets as an exact decimal string."}}},"LatestResponse":{"type":"object","required":["status","currency","unit","metals","currencies","timestamps"],"properties":{"status":{"type":"string","const":"success"},"currency":{"type":"string","description":"Base currency of the response."},"unit":{"type":"string","enum":["toz","troy_oz","gram","kilogram"],"description":"Normalized output mass unit. Legacy default is `toz`; explicit troy-ounce requests return `troy_oz`."},"metals":{"type":"object","additionalProperties":{"type":"number"},"description":"Spot prices keyed by metal. Gold only at launch: `{ \"gold\": 4527.86 }`.","example":{"gold":4527.86}},"currencies":{"type":"object","additionalProperties":{"type":"number"},"description":"Legacy provider-backed currency map for native USD, EUR and GBP bases. It is always empty for projected bases; internal UniRateAPI observations are never exposed here.","example":{"USD":1,"EUR":1.1724,"GBP":1.3521}},"timestamps":{"type":"object","required":["metal","currency"],"properties":{"metal":{"type":"string","description":"ISO 8601 metal observation time, or empty string if unseeded. For projected prices this is the hourly canonical USD metal observation used in the calculation."},"currency":{"type":"string","description":"ISO 8601 legacy native-currency observation time, or the separate daily Bullion API conversion snapshot time for a projected base. It is empty if unseeded and is not a provider end-of-day timestamp."}}}}},"CaratResponse":{"type":"object","additionalProperties":false,"required":["status","currency","unit","as_of","karats"],"properties":{"status":{"type":"string","const":"success"},"currency":{"type":"string","pattern":"^[A-Z]{3}$"},"unit":{"type":"string","const":"gram"},"as_of":{"type":"string","format":"date-time"},"karats":{"type":"object","additionalProperties":false,"required":["24k","23k","22k","21.6k","21k","18k","16k","14k","12k","10k","9k","8k","6k"],"properties":{"24k":{"type":"number"},"23k":{"type":"number"},"22k":{"type":"number"},"21.6k":{"type":"number"},"21k":{"type":"number"},"18k":{"type":"number"},"16k":{"type":"number"},"14k":{"type":"number"},"12k":{"type":"number"},"10k":{"type":"number"},"9k":{"type":"number"},"8k":{"type":"number"},"6k":{"type":"number"}}}}},"TimeseriesResponse":{"type":"object","required":["status","currency","unit","start_date","end_date","rates"],"properties":{"status":{"type":"string","const":"success"},"currency":{"type":"string"},"unit":{"type":"string","enum":["toz","troy_oz","gram","kilogram"],"description":"Normalized output mass unit. Omitted or empty input returns legacy `toz`."},"start_date":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"end_date":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"rates":{"type":"object","additionalProperties":{"$ref":"#/components/schemas/TimeseriesDay"},"description":"Daily closes keyed by YYYY-MM-DD. Only cached dates are present."}}},"ChangeResponse":{"type":"object","additionalProperties":false,"required":["status","currency","metal","start_date","end_date","start_price","end_price","change","change_pct"],"properties":{"status":{"type":"string","const":"success"},"currency":{"type":"string"},"metal":{"type":"string","const":"gold"},"start_date":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"end_date":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"start_price":{"type":"number"},"end_price":{"type":"number"},"change":{"type":"number","description":"End price minus start price, rounded to eight decimals."},"change_pct":{"type":"number","description":"Percentage change relative to start price, rounded to four decimals."}}},"TimeseriesDay":{"type":"object","required":["currencies","date","metals"],"properties":{"currencies":{"type":"object","additionalProperties":{"type":"number"},"example":{"EUR":0.9134,"GBP":0.7783}},"date":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"metals":{"type":"object","additionalProperties":{"type":"number"},"example":{"gold":3320.1}}}},"MacroRatesResponse":{"type":"object","required":["status","region","policy","yields","inflation"],"properties":{"status":{"type":"string","const":"success"},"region":{"type":"string","enum":["us","euro_area","uk"]},"policy":{"type":"object","additionalProperties":{"$ref":"#/components/schemas/MacroRateObservation"}},"yields":{"type":"object","additionalProperties":{"$ref":"#/components/schemas/MacroRateObservation"}},"inflation":{"type":"object","additionalProperties":{"$ref":"#/components/schemas/MacroRateObservation"}}}},"MacroRateObservation":{"type":"object","required":["series_id","value","unit","observation_period","method","source_series_id","source_id","attribution","fetched_at"],"properties":{"series_id":{"type":"string"},"value":{"type":"number"},"unit":{"type":"string","example":"percent"},"observation_period":{"type":"string","format":"date"},"method":{"type":"string","enum":["latest","monthly_average","month_end","derived_monthly_average","survey_expectation"]},"source_series_id":{"type":"string"},"source_id":{"type":"string"},"attribution":{"type":"string"},"fetched_at":{"type":"string","format":"date-time"}}},"MacroBadRequestError":{"type":"object","additionalProperties":false,"required":["status","error"],"properties":{"status":{"type":"string","const":"error"},"error":{"type":"object","required":["code","message"],"properties":{"code":{"type":"string","enum":["invalid_region"]},"message":{"type":"string"}}}}},"CentralBankReservesResponse":{"type":"object","required":["status","methodology","coverage","reserves"],"properties":{"status":{"type":"string","const":"success"},"methodology":{"$ref":"#/components/schemas/CentralBankReserveMethodology"},"coverage":{"$ref":"#/components/schemas/CentralBankReserveCoverage"},"reserves":{"type":"array","items":{"$ref":"#/components/schemas/CentralBankReserveObservation"}}}},"CentralBankGoldResponse":{"type":"object","required":["status","query","reported","estimated"],"properties":{"status":{"type":"string","const":"success"},"query":{"type":"object","required":["layer","start","end","country"],"properties":{"layer":{"type":"string","enum":["reported","estimated","both"]},"start":{"type":["string","null"]},"end":{"type":["string","null"]},"country":{"type":["string","null"]}}},"reported":{"$ref":"#/components/schemas/CentralBankGoldReportedLayer"},"estimated":{"$ref":"#/components/schemas/CentralBankGoldEstimatedLayer"}}},"CentralBankGoldReportedLayer":{"type":"object","required":["status","observations","latest_country_changes","source","coverage"],"properties":{"status":{"type":"string","enum":["ready","partial","stale","empty","unavailable","not_requested"]},"observations":{"type":"array","items":{"$ref":"#/components/schemas/CentralBankGoldReportedObservation"}},"latest_country_changes":{"type":"array","items":{"$ref":"#/components/schemas/CentralBankGoldCountryChange"}},"source":{"$ref":"#/components/schemas/CentralBankGoldSource"},"coverage":{"type":"object","required":["countries","start_month","end_month"],"properties":{"countries":{"type":"integer"},"start_month":{"type":["string","null"]},"end_month":{"type":["string","null"]}}}}},"CentralBankGoldEstimatedLayer":{"type":"object","deprecated":true,"description":"Compatibility object. Estimated demand is unavailable and observations are always empty.","required":["status","observations","source","coverage","revisions"],"properties":{"status":{"type":"string","enum":["ready","partial","stale","empty","unavailable","not_requested"]},"observations":{"type":"array","items":{"$ref":"#/components/schemas/CentralBankGoldEstimateObservation"}},"source":{"$ref":"#/components/schemas/CentralBankGoldSource"},"coverage":{"type":"object","required":["start_quarter","end_quarter"],"properties":{"start_quarter":{"type":["string","null"]},"end_quarter":{"type":["string","null"]}}},"revisions":{"type":"object","required":["latest_revision_id","latest_published_at"],"properties":{"latest_revision_id":{"type":["string","null"]},"latest_published_at":{"type":["string","null"]}}}}},"CentralBankGoldSource":{"type":"object","required":["id","name","url","methodology","updated_at","fetched_at"],"properties":{"id":{"type":"string"},"name":{"type":"string"},"url":{"type":"string","format":"uri"},"methodology":{"type":"string"},"updated_at":{"type":["string","null"],"format":"date"},"fetched_at":{"type":["string","null"],"format":"date-time"}}},"CentralBankGoldReportedObservation":{"type":"object","required":["country","country_iso3","country_name","month","reference_date","cadence","holdings_basis","holdings_million_fine_troy_oz","holdings_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","net_change_tonnes"],"properties":{"country":{"type":"string","pattern":"^[A-Z]{2}$"},"country_iso3":{"type":"string","pattern":"^[A-Z]{3}$"},"country_name":{"type":"string"},"month":{"type":"string","pattern":"^\\d{4}-\\d{2}$"},"reference_date":{"type":"string","format":"date"},"cadence":{"type":"string","enum":["monthly","weekly","fortnightly","irregular"]},"holdings_basis":{"type":"string","enum":["reported","derived"]},"holdings_million_fine_troy_oz":{"type":["number","null"]},"holdings_tonnes":{"type":["number","null"]},"reported_value_usd_millions":{"type":["number","null"]},"reported_value_change_usd_millions":{"type":["number","null"],"description":"Source-published month-on-month USD-million change when available. Descriptive only; it is not a purchase or sale transaction."},"reported_value_eur_millions":{"type":["number","null"]},"reported_value_pln_millions":{"type":["number","null"]},"reported_value_inr_crores":{"type":["number","null"]},"reported_value_sar_millions":{"type":["number","null"]},"reported_value_sgd_millions":{"type":["number","null"]},"net_change_tonnes":{"type":["number","null"]}}},"CentralBankGoldCountryChange":{"allOf":[{"$ref":"#/components/schemas/CentralBankGoldReportedObservation"},{"type":"object","required":["direction"],"properties":{"direction":{"type":"string","enum":["buying","selling","unchanged","unknown"]}}}]},"CentralBankGoldEstimateObservation":{"type":"object","required":["quarter","net_demand_tonnes","published_at","revision_id"],"properties":{"quarter":{"type":"string","pattern":"^\\d{4}-Q[1-4]$"},"net_demand_tonnes":{"type":"number"},"published_at":{"type":"string","format":"date"},"revision_id":{"type":["string","null"]}}},"CentralBankGoldBadRequestError":{"type":"object","required":["status","error"],"properties":{"status":{"type":"string","const":"error"},"error":{"type":"object","required":["code","message"],"properties":{"code":{"type":"string","enum":["invalid_layer","invalid_country","invalid_date","invalid_range"]},"message":{"type":"string"}}}}},"CentralBankReserveMethodology":{"type":"object","required":["gold_reserve_value_usd","annual_gold_value_change_usd","transformation_notice","warning"],"properties":{"gold_reserve_value_usd":{"type":"string","const":"total reserves including gold minus total reserves excluding gold"},"annual_gold_value_change_usd":{"type":"string","const":"current derived gold reserve value minus the prior calendar year"},"transformation_notice":{"type":"string","const":"Bullion API derives gold reserve values, shares, and annual value changes."},"warning":{"type":"string","const":"Annual value changes combine holdings changes, gold-price movement, and revisions and do not measure tonnes bought or sold."}}},"CentralBankReserveCoverage":{"type":"object","required":["countries","start_year","end_year","source_updated_at","fetched_at"],"properties":{"countries":{"type":"integer","minimum":0},"start_year":{"type":["integer","null"]},"end_year":{"type":["integer","null"]},"source_updated_at":{"type":"string","format":"date"},"fetched_at":{"type":"string","format":"date-time"}}},"CentralBankReserveObservation":{"type":"object","required":["country","country_iso3","country_name","as_of_year","total_reserves_usd","reserves_excluding_gold_usd","gold_reserve_value_usd","gold_share_pct","annual_gold_value_change_usd","data_quality"],"properties":{"country":{"type":"string","pattern":"^[A-Z]{2}$"},"country_iso3":{"type":"string","pattern":"^[A-Z]{3}$"},"country_name":{"type":"string"},"as_of_year":{"type":"integer"},"total_reserves_usd":{"type":"number"},"reserves_excluding_gold_usd":{"type":"number"},"gold_reserve_value_usd":{"type":["number","null"]},"gold_share_pct":{"type":["number","null"]},"annual_gold_value_change_usd":{"type":["number","null"]},"data_quality":{"type":["string","null"],"enum":["inconsistent_source_values",null]}}},"CentralBankBadRequestError":{"type":"object","additionalProperties":false,"required":["status","error"],"properties":{"status":{"type":"string","const":"error"},"error":{"type":"object","required":["code","message"],"properties":{"code":{"type":"string","enum":["invalid_country","invalid_year","invalid_range"]},"message":{"type":"string"}}}}},"EmailNotVerifiedError":{"type":"object","additionalProperties":false,"required":["status","error"],"properties":{"status":{"type":"string","const":"error"},"error":{"type":"object","required":["code","message"],"properties":{"code":{"type":"string","const":"email_not_verified"},"message":{"type":"string"}}}}},"PlanRequiredError":{"type":"object","additionalProperties":false,"required":["status","error"],"properties":{"status":{"type":"string","const":"error"},"error":{"type":"object","additionalProperties":false,"required":["code","message","required_plans","plan"],"properties":{"code":{"type":"string","const":"plan_required"},"message":{"type":"string"},"required_plans":{"type":"array","const":["pro","enterprise"]},"plan":{"type":"string","enum":["free","pro","enterprise"]}}}}},"UsageResponse":{"type":"object","required":["plan","limit","used","remaining","reset_at","subscription","recent","email","email_verified"],"properties":{"plan":{"type":"string","enum":["free","pro","enterprise"]},"limit":{"type":"integer","description":"Monthly request cap for the plan."},"used":{"type":"integer","description":"Requests used this month."},"remaining":{"type":"integer"},"reset_at":{"type":"string","format":"date-time"},"subscription":{"type":"object","required":["status"],"properties":{"status":{"type":["string","null"],"description":"Stripe subscription status, or null on the free plan."}}},"recent":{"type":"array","description":"The 50 most recent API requests, newest first.","items":{"$ref":"#/components/schemas/RecentRequest"}},"email":{"type":"string","format":"email"},"email_verified":{"type":"boolean"}}},"RecentRequest":{"type":"object","required":["endpoint","method","status_code","response_ms","requested_at"],"properties":{"endpoint":{"type":"string","example":"/v1/latest?currency=USD"},"method":{"type":"string","example":"GET"},"status_code":{"type":"integer","example":200},"response_ms":{"type":"integer"},"requested_at":{"type":"string","format":"date-time"}}},"HealthResponse":{"type":"object","required":["status","db_reachable","last_refresh_at","currencies_with_data","fx_snapshot_status","last_fx_snapshot_at","projected_currencies_with_data"],"properties":{"status":{"type":"string","const":"ok"},"db_reachable":{"type":"boolean"},"last_refresh_at":{"type":["string","null"],"format":"date-time","description":"ISO 8601 time of the last successful cache refresh."},"currencies_with_data":{"type":"array","items":{"type":"string"},"description":"Base currencies with at least one cached metal price."},"fx_snapshot_status":{"type":"string","enum":["disabled","unavailable","fresh","partial","stale"],"description":"Freshness state for the enabled projected-currency FX snapshot."},"last_fx_snapshot_at":{"type":["string","null"],"format":"date-time","description":"Completion time of the latest accepted daily FX snapshot, or null when unavailable."},"projected_currencies_with_data":{"type":"array","items":{"type":"string","pattern":"^[A-Z]{3}$"},"description":"Enabled projected currencies with a materialised gold price."}}},"ErrorEnvelope":{"type":"object","additionalProperties":false,"required":["status","error"],"properties":{"status":{"type":"string","const":"error"},"error":{"type":"object","required":["code","message"],"properties":{"code":{"type":"string","enum":["config_error","billing_state_conflict","annual_billing_unavailable","billing_interval_change_conflict","billing_interval_change_expired","billing_interval_change_failed","billing_interval_change_invalid","billing_interval_change_origin_forbidden","billing_interval_change_revalidation_conflict","billing_interval_change_processing","billing_cancellation_conflict","billing_cancellation_failed","billing_cancellation_invalid","conflicting_credentials","data_unavailable","email_not_verified","invalid_api_key","invalid_country","invalid_currency","invalid_date","invalid_layer","invalid_range","invalid_region","invalid_unit","invalid_year","method_not_allowed","missing_api_key","missing_params","no_data","plan_required","quota_exceeded","rate_limited","unauthenticated","api_key_not_found","insufficient_scope","invalid_portfolio_permission","invalid_price_alert","invalid_digest_preference","price_alert_exists","portfolio_not_found","portfolio_currency_locked","missing_idempotency_key","invalid_purchase","invalid_sale","invalid_allocation","idempotency_conflict","invalid_cursor","invalid_limit","invalid_filter","invalid_calendar_year","invalid_tax_year","missing_period","conflicting_periods","transaction_not_found","stale_transaction","transaction_in_use","sale_date_invalid","insufficient_holdings","conflicting_params","invalid_date_type","no_change_data","invalid_change_baseline","health_degraded","unknown_metal","internal_error","render_error","invalid_json","invalid_email","invalid_request","invalid_token","expired_token"],"description":"Machine-readable error code."},"message":{"type":"string","description":"Human-readable message."}},"additionalProperties":true}}},"TimeseriesBadRequestError":{"type":"object","additionalProperties":false,"required":["status","error"],"properties":{"status":{"type":"string","const":"error"},"error":{"type":"object","required":["code","message"],"properties":{"code":{"type":"string","enum":["missing_params","invalid_date","invalid_range","invalid_unit"]},"message":{"type":"string"}}}}},"ChangeBadRequestError":{"type":"object","additionalProperties":false,"required":["status","error"],"properties":{"status":{"type":"string","const":"error"},"error":{"type":"object","required":["code","message"],"properties":{"code":{"type":"string","enum":["missing_params","conflicting_params","invalid_date","invalid_range","invalid_date_type"]},"message":{"type":"string"}}}}},"ChangeNoDataError":{"type":"object","additionalProperties":false,"required":["status","error"],"properties":{"status":{"type":"string","const":"error"},"error":{"type":"object","required":["code","message"],"properties":{"code":{"type":"string","const":"no_change_data"},"message":{"type":"string"}}}}},"ChangeBaselineError":{"type":"object","additionalProperties":false,"required":["status","error"],"properties":{"status":{"type":"string","const":"error"},"error":{"type":"object","required":["code","message"],"properties":{"code":{"type":"string","const":"invalid_change_baseline"},"message":{"type":"string"}}}}},"LatestBadRequestError":{"type":"object","additionalProperties":false,"required":["status","error"],"properties":{"status":{"type":"string","const":"error"},"error":{"type":"object","required":["code","message"],"properties":{"code":{"type":"string","const":"invalid_unit"},"message":{"type":"string"}}}}},"CaratBadRequestError":{"type":"object","additionalProperties":false,"required":["status","error"],"properties":{"status":{"type":"string","const":"error"},"error":{"type":"object","required":["code","message"],"properties":{"code":{"type":"string","enum":["invalid_currency","invalid_date"]},"message":{"type":"string"}}}}},"CaratNoDataError":{"type":"object","additionalProperties":false,"required":["status","error"],"properties":{"status":{"type":"string","const":"error"},"error":{"type":"object","required":["code","message"],"properties":{"code":{"type":"string","const":"no_data"},"message":{"type":"string"}}}}},"QuotaExceededError":{"type":"object","additionalProperties":false,"required":["status","error"],"properties":{"status":{"type":"string","const":"error"},"error":{"type":"object","additionalProperties":false,"required":["code","message","plan","used","limit","reset_at"],"properties":{"code":{"type":"string","const":"quota_exceeded"},"message":{"type":"string","example":"Your monthly usage limit has been reached. Please upgrade your subscription plan."},"plan":{"type":"string","enum":["free","pro","enterprise"]},"used":{"type":"integer"},"limit":{"type":"integer"},"reset_at":{"type":"string","format":"date-time"}}}}},"RateLimitedError":{"type":"object","additionalProperties":false,"required":["status","error"],"properties":{"status":{"type":"string","const":"error"},"error":{"type":"object","additionalProperties":false,"required":["code","message"],"properties":{"code":{"type":"string","const":"rate_limited"},"message":{"type":"string","example":"Rate limit exceeded"}}}}}}}}