{"openapi":"3.1.0","info":{"title":"Gold API","description":"Gold, silver and copper spot prices, with delayed futures settlement prices and gold conversion across 31 currencies using exchangerate.dev FX. Paid plans get live spot prices; Free and keyless access get the same prices 15 minutes late. BGN is fixed to the euro (1.95583 BGN per EUR): BGN prices follow the euro, not a separate market rate.","contact":{"name":"goldprice.dev","url":"https://goldprice.dev/"},"license":{"name":"Commercial API license","url":"https://goldprice.dev/terms"},"version":"0.1.0"},"servers":[{"url":"https://api.goldprice.dev","description":"Production"}],"paths":{"/v1/health":{"get":{"tags":["system"],"summary":"Liveness probe","description":"Minimal 200 OK if the process is up. No downstream checks.\n\nGET is the documented public operation. HEAD is registered separately\nbelow so uptime monitors work without creating duplicate OpenAPI\noperation IDs for generated clients.","operationId":"health_v1_health_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":{"type":"string"},"type":"object","title":"Response Health V1 Health Get"}}}}}}},"/v1/status":{"get":{"tags":["system"],"summary":"Readiness probe — Redis + per-scraper freshness","description":"Returns Redis reachability + per-scraper freshness from scraper_runs.\n\nOverall `status` is `ok` only when Redis is up, every required scraper's\nlatest successful run is within 2 * expected_interval_s, every enabled\ncanonical metal bar stream is fresh, and its writer owns the current lease.\nOtherwise `status` is `degraded`.\nDead-man thresholds are read from scraper_runs rather than hardcoded\nconstants (C1).\n\nThe `value_staleness` block (F1 per audit 2026-04-23) surfaces live FX\nrates that have stopped moving while the scraper keeps fetching. Per\nscope §3.2 Q2, entries in this block are INFORMATIONAL-ONLY — they do\nnot flip overall `status` to `degraded`, so one stuck currency cannot\nleave `/v1/status` permanently degraded and drown out other alerts.\nMonitoring consumers alert on the presence of entries directly.\n\n`status` and `checks` are computed at most once per _STATUS_REUSE_S per\nprocess and shared by concurrent callers; `checks_at` says when.","operationId":"status_v1_status_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Status V1 Status Get"}}}}}}},"/v1/prices":{"get":{"tags":["prices"],"summary":"Latest aggregated price per (symbol, currency, contract_type)","description":"Paid plans get live prices. Free and keyless callers get the same prices 15 minutes late. `bid`/`ask` are paid-only and omitted when no two-sided quote is available.","operationId":"get_prices_v1_prices_get","parameters":[{"name":"symbol","in":"query","required":false,"schema":{"anyOf":[{"type":"string","maxLength":64},{"type":"null"}],"title":"Symbol"}},{"name":"include","in":"query","required":false,"schema":{"anyOf":[{"type":"string","maxLength":64},{"type":"null"}],"description":"Comma-separated field groups to include in the response. Tokens: sources, karat, stats, all. Unknown tokens are ignored. Omit for the lean default. The stats group requires authentication.","title":"Include"},"description":"Comma-separated field groups to include in the response. Tokens: sources, karat, stats, all. Unknown tokens are ignored. Omit for the lean default. The stats group requires authentication."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SpotListResponse"}}}},"400":{"description":"Bad request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Authentication failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The caller's tier does not allow this request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit or quota exceeded. `error` is one of: `rate_limited` (keyless caller over the per-IP hourly cap, X-RateLimit-Scope anon-ip-hour), `rate_limit_exceeded` (API key over its per-minute limit, scope tier-rate-minute) or `quota_exceeded` (monthly quota used, scope tier-quota-month). Wait for Retry-After seconds before retrying; do not retry a monthly quota error automatically.","headers":{"X-RateLimit-Limit":{"description":"Limit for the window named by X-RateLimit-Scope.","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Requests left in that window (0 on a 429).","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix time (seconds) when the window resets.","schema":{"type":"integer"}},"X-RateLimit-Scope":{"description":"Which limit applied: anon-ip-hour, tier-rate-minute or tier-quota-month.","schema":{"type":"string"}},"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The operation failed internally.","content":{"text/plain":{"schema":{"type":"string"}}}},"503":{"description":"A required backend or data source is temporarily unavailable.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RequestValidationErrorResponse"}}}}},"security":[{},{"BearerAuth":[]},{"ApiKeyAuth":[]}]}},"/v1/spot/{symbol}":{"get":{"tags":["prices"],"summary":"Latest spot price for one symbol — flat shape, no envelope (parity convenience)","description":"Paid plans get live prices. Free and keyless callers get the same prices 15 minutes late. `bid`/`ask` are paid-only and omitted when no two-sided quote is available.","operationId":"get_spot_v1_spot__symbol__get","parameters":[{"name":"symbol","in":"path","required":true,"schema":{"type":"string","title":"Symbol"}},{"name":"include","in":"query","required":false,"schema":{"anyOf":[{"type":"string","maxLength":64},{"type":"null"}],"description":"Comma-separated field groups to include in the response. Tokens: sources, karat, stats, all. Unknown tokens are ignored. Omit for the lean default. The stats group requires authentication.","title":"Include"},"description":"Comma-separated field groups to include in the response. Tokens: sources, karat, stats, all. Unknown tokens are ignored. Omit for the lean default. The stats group requires authentication."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SpotResponse"}}}},"400":{"description":"Bad request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Authentication failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The caller's tier does not allow this request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"The requested resource is unavailable or not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit or quota exceeded. `error` is one of: `rate_limited` (keyless caller over the per-IP hourly cap, X-RateLimit-Scope anon-ip-hour), `rate_limit_exceeded` (API key over its per-minute limit, scope tier-rate-minute) or `quota_exceeded` (monthly quota used, scope tier-quota-month). Wait for Retry-After seconds before retrying; do not retry a monthly quota error automatically.","headers":{"X-RateLimit-Limit":{"description":"Limit for the window named by X-RateLimit-Scope.","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Requests left in that window (0 on a 429).","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix time (seconds) when the window resets.","schema":{"type":"integer"}},"X-RateLimit-Scope":{"description":"Which limit applied: anon-ip-hour, tier-rate-minute or tier-quota-month.","schema":{"type":"string"}},"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The operation failed internally.","content":{"text/plain":{"schema":{"type":"string"}}}},"503":{"description":"A required backend or data source is temporarily unavailable.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RequestValidationErrorResponse"}}}}},"security":[{},{"BearerAuth":[]},{"ApiKeyAuth":[]}]}},"/v1/prices/stream":{"get":{"tags":["prices"],"summary":"Live spot SSE stream — Pro-tier (XAU, XAG, and HG)","operationId":"stream_prices_v1_prices_stream_get","parameters":[{"name":"symbol","in":"query","required":true,"schema":{"type":"string","description":"Streamable symbol; supports XAU-USD-SPOT, XAG-USD-SPOT, and HG-USD-SPOT","title":"Symbol"},"description":"Streamable symbol; supports XAU-USD-SPOT, XAG-USD-SPOT, and HG-USD-SPOT"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"400":{"description":"Bad request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Authentication failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The caller's tier does not allow this request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit or quota exceeded. `error` is one of: `rate_limited` (keyless caller over the per-IP hourly cap, X-RateLimit-Scope anon-ip-hour), `rate_limit_exceeded` (API key over its per-minute limit, scope tier-rate-minute) or `quota_exceeded` (monthly quota used, scope tier-quota-month). Wait for Retry-After seconds before retrying; do not retry a monthly quota error automatically.","headers":{"X-RateLimit-Limit":{"description":"Limit for the window named by X-RateLimit-Scope.","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Requests left in that window (0 on a 429).","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix time (seconds) when the window resets.","schema":{"type":"integer"}},"X-RateLimit-Scope":{"description":"Which limit applied: anon-ip-hour, tier-rate-minute or tier-quota-month.","schema":{"type":"string"}},"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The operation failed internally.","content":{"text/plain":{"schema":{"type":"string"}}}},"503":{"description":"A required backend or data source is temporarily unavailable.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RequestValidationErrorResponse"}}}}}}},"/v1/prices/divergence":{"get":{"tags":["prices"],"summary":"Symbols whose cross-source price spread exceeds a threshold","operationId":"get_prices_divergence_v1_prices_divergence_get","parameters":[{"name":"threshold","in":"query","required":false,"schema":{"type":"integer","maximum":10000,"minimum":1,"description":"Spread floor in basis points. Default 50 bps.","default":50,"title":"Threshold"},"description":"Spread floor in basis points. Default 50 bps."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"401":{"description":"Authentication failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit or quota exceeded. `error` is one of: `rate_limited` (keyless caller over the per-IP hourly cap, X-RateLimit-Scope anon-ip-hour), `rate_limit_exceeded` (API key over its per-minute limit, scope tier-rate-minute) or `quota_exceeded` (monthly quota used, scope tier-quota-month). Wait for Retry-After seconds before retrying; do not retry a monthly quota error automatically.","headers":{"X-RateLimit-Limit":{"description":"Limit for the window named by X-RateLimit-Scope.","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Requests left in that window (0 on a 429).","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix time (seconds) when the window resets.","schema":{"type":"integer"}},"X-RateLimit-Scope":{"description":"Which limit applied: anon-ip-hour, tier-rate-minute or tier-quota-month.","schema":{"type":"string"}},"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The operation failed internally.","content":{"text/plain":{"schema":{"type":"string"}}}},"503":{"description":"A required backend or data source is temporarily unavailable.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RequestValidationErrorResponse"}}}}},"security":[{},{"BearerAuth":[]},{"ApiKeyAuth":[]}]}},"/v1/settlements":{"get":{"tags":["prices"],"summary":"Futures settlement series for GC / SI / HG (Pro+)","operationId":"get_settlements_v1_settlements_get","parameters":[{"name":"symbol","in":"query","required":true,"schema":{"type":"string","minLength":1,"maxLength":8,"title":"Symbol"}},{"name":"from","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"From"}},{"name":"to","in":"query","required":false,"schema":{"anyOf":[{"type":"string","maxLength":10},{"type":"null"}],"title":"To"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":365,"minimum":1,"default":30,"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"400":{"description":"Bad request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Authentication failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The caller's tier does not allow this request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit or quota exceeded. `error` is one of: `rate_limited` (keyless caller over the per-IP hourly cap, X-RateLimit-Scope anon-ip-hour), `rate_limit_exceeded` (API key over its per-minute limit, scope tier-rate-minute) or `quota_exceeded` (monthly quota used, scope tier-quota-month). Wait for Retry-After seconds before retrying; do not retry a monthly quota error automatically.","headers":{"X-RateLimit-Limit":{"description":"Limit for the window named by X-RateLimit-Scope.","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Requests left in that window (0 on a 429).","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix time (seconds) when the window resets.","schema":{"type":"integer"}},"X-RateLimit-Scope":{"description":"Which limit applied: anon-ip-hour, tier-rate-minute or tier-quota-month.","schema":{"type":"string"}},"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The operation failed internally.","content":{"text/plain":{"schema":{"type":"string"}}}},"503":{"description":"A required backend or data source is temporarily unavailable.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RequestValidationErrorResponse"}}}}},"security":[{"BearerAuth":[]},{"ApiKeyAuth":[]}]}},"/v1/forward-view/{asset}":{"get":{"tags":["prices"],"summary":"Multi-horizon forward view: futures settlement curve","operationId":"get_forward_view_v1_forward_view__asset__get","parameters":[{"name":"asset","in":"path","required":true,"schema":{"type":"string","title":"Asset"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"400":{"description":"Bad request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Authentication failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The caller's tier does not allow this request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit or quota exceeded. `error` is one of: `rate_limited` (keyless caller over the per-IP hourly cap, X-RateLimit-Scope anon-ip-hour), `rate_limit_exceeded` (API key over its per-minute limit, scope tier-rate-minute) or `quota_exceeded` (monthly quota used, scope tier-quota-month). Wait for Retry-After seconds before retrying; do not retry a monthly quota error automatically.","headers":{"X-RateLimit-Limit":{"description":"Limit for the window named by X-RateLimit-Scope.","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Requests left in that window (0 on a 429).","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix time (seconds) when the window resets.","schema":{"type":"integer"}},"X-RateLimit-Scope":{"description":"Which limit applied: anon-ip-hour, tier-rate-minute or tier-quota-month.","schema":{"type":"string"}},"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The operation failed internally.","content":{"text/plain":{"schema":{"type":"string"}}}},"503":{"description":"A required backend or data source is temporarily unavailable.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RequestValidationErrorResponse"}}}}},"security":[{},{"BearerAuth":[]},{"ApiKeyAuth":[]}]}},"/v1/prices/history":{"get":{"tags":["prices"],"summary":"[DEPRECATED — use /v1/bars] Aggregated close history for a symbol — interval ∈ {1d, 1h}.","description":"Dispatch to the per-interval handler. The handler is the Module\nInterface; per-interval helpers are Deep Modules hiding the\ninterval-specific policy + storage shape.\n\nDEPRECATED in favour of /v1/bars (C4). This endpoint retains the\nlegacy close-only series[] wire shape for backward compatibility.\nDeprecation + Sunset + Link headers are injected on every 200 response\n(and on 403 plan_gated, where the caller may need to upgrade before\nmigrating). The sunset DATE is founder-controlled (see config\nprices_history_sunset_date) — NOT hardcoded here.","operationId":"get_history_v1_prices_history_get","parameters":[{"name":"symbol","in":"query","required":true,"schema":{"type":"string","minLength":1,"maxLength":64,"title":"Symbol"}},{"name":"from","in":"query","required":true,"schema":{"type":"string","maxLength":32,"title":"From"}},{"name":"to","in":"query","required":true,"schema":{"type":"string","maxLength":32,"title":"To"}},{"name":"interval","in":"query","required":false,"schema":{"type":"string","pattern":"^(1d|1h)$","default":"1d","title":"Interval"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":10000,"minimum":1,"default":100,"title":"Limit"}}],"responses":{"200":{"description":"Success. See Deprecation + Sunset + Link headers.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HistoryResponse"}}}},"400":{"description":"Bad request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Authentication failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The caller's tier does not allow this request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit or quota exceeded. `error` is one of: `rate_limited` (keyless caller over the per-IP hourly cap, X-RateLimit-Scope anon-ip-hour), `rate_limit_exceeded` (API key over its per-minute limit, scope tier-rate-minute) or `quota_exceeded` (monthly quota used, scope tier-quota-month). Wait for Retry-After seconds before retrying; do not retry a monthly quota error automatically.","headers":{"X-RateLimit-Limit":{"description":"Limit for the window named by X-RateLimit-Scope.","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Requests left in that window (0 on a 429).","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix time (seconds) when the window resets.","schema":{"type":"integer"}},"X-RateLimit-Scope":{"description":"Which limit applied: anon-ip-hour, tier-rate-minute or tier-quota-month.","schema":{"type":"string"}},"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The operation failed internally.","content":{"text/plain":{"schema":{"type":"string"}}}},"503":{"description":"A required backend or data source is temporarily unavailable.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RequestValidationErrorResponse"}}}}},"security":[{},{"BearerAuth":[]},{"ApiKeyAuth":[]}]}},"/v1/convert":{"get":{"tags":["convert"],"summary":"Convert between metals, currencies, and units","description":"Convert an amount between metals, currencies, and units.\n\n- metal → currency: spot price in that currency via FX cross through USD\n- currency → metal: inverse of the above\n- metal → metal: ratio of USD spot prices\n- currency → currency: FX cross through USD (live rate first, then the\n  last stored rate only up to three days old)\n\nTwo-leg source skew: currency → currency conversions resolve EACH leg\nindependently through the fresh-first/last-stored chain, so the two\nlegs can have different ages (e.g. a fresh EUR rate crossed with an\nolder stored THB rate). The response `timestamp` reflects\nthe `to` leg only — the `from` leg's timestamp is discarded — so it\ncan understate (or overstate) the age of the other half of the cross.\nBoth legs share the USD anchor, so cross drift is bounded by the\nstaleness of the older leg — never compounded.\n\nThe `unit` param applies to the metal side only (oz / gram / kg).\nHistorical conversion via `date` is not supported; use /v1/bars +\n/v1/prices/history for historical spot data.","operationId":"get_convert_v1_convert_get","parameters":[{"name":"from","in":"query","required":true,"schema":{"type":"string","description":"Source symbol: XAU, XAG, USD, EUR, …","title":"From"},"description":"Source symbol: XAU, XAG, USD, EUR, …"},{"name":"to","in":"query","required":true,"schema":{"type":"string","description":"Target symbol: USD, EUR, XAU, …","title":"To"},"description":"Target symbol: USD, EUR, XAU, …"},{"name":"amount","in":"query","required":false,"schema":{"type":"number","description":"Amount to convert (default 1).","default":1.0,"title":"Amount"},"description":"Amount to convert (default 1)."},{"name":"unit","in":"query","required":false,"schema":{"type":"string","description":"Unit for the metal side: oz | gram | kg (default oz).","default":"oz","title":"Unit"},"description":"Unit for the metal side: oz | gram | kg (default oz)."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConvertResponse"}}}},"400":{"description":"Bad request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Authentication failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit or quota exceeded. `error` is one of: `rate_limited` (keyless caller over the per-IP hourly cap, X-RateLimit-Scope anon-ip-hour), `rate_limit_exceeded` (API key over its per-minute limit, scope tier-rate-minute) or `quota_exceeded` (monthly quota used, scope tier-quota-month). Wait for Retry-After seconds before retrying; do not retry a monthly quota error automatically.","headers":{"X-RateLimit-Limit":{"description":"Limit for the window named by X-RateLimit-Scope.","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Requests left in that window (0 on a 429).","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix time (seconds) when the window resets.","schema":{"type":"integer"}},"X-RateLimit-Scope":{"description":"Which limit applied: anon-ip-hour, tier-rate-minute or tier-quota-month.","schema":{"type":"string"}},"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The operation failed internally.","content":{"text/plain":{"schema":{"type":"string"}}}},"503":{"description":"A required backend or data source is temporarily unavailable.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RequestValidationErrorResponse"}}}}},"security":[{},{"BearerAuth":[]},{"ApiKeyAuth":[]}]}},"/v1/carat":{"get":{"tags":["convert"],"summary":"Per-gram gold price by karat purity","description":"Per-gram XAU price for karats 24 / 22 / 21 / 20 / 18 / 16 / 14 / 10.\n\nLogic: per_gram_24k = spot_xau_usd_per_oz / GRAMS_PER_TROY_OUNCE * fx_rate;\nper_gram_Nk = per_gram_24k * N/24.\n\nHistorical `date` support is not implemented; omit the param or use\n/v1/prices/history + do the math client-side.","operationId":"get_carat_v1_carat_get","parameters":[{"name":"currency","in":"query","required":false,"schema":{"type":"string","description":"Quote currency (default USD).","default":"USD","title":"Currency"},"description":"Quote currency (default USD)."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CaratResponse"}}}},"400":{"description":"Bad request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Authentication failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit or quota exceeded. `error` is one of: `rate_limited` (keyless caller over the per-IP hourly cap, X-RateLimit-Scope anon-ip-hour), `rate_limit_exceeded` (API key over its per-minute limit, scope tier-rate-minute) or `quota_exceeded` (monthly quota used, scope tier-quota-month). Wait for Retry-After seconds before retrying; do not retry a monthly quota error automatically.","headers":{"X-RateLimit-Limit":{"description":"Limit for the window named by X-RateLimit-Scope.","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Requests left in that window (0 on a 429).","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix time (seconds) when the window resets.","schema":{"type":"integer"}},"X-RateLimit-Scope":{"description":"Which limit applied: anon-ip-hour, tier-rate-minute or tier-quota-month.","schema":{"type":"string"}},"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The operation failed internally.","content":{"text/plain":{"schema":{"type":"string"}}}},"503":{"description":"A required backend or data source is temporarily unavailable.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RequestValidationErrorResponse"}}}}},"security":[{},{"BearerAuth":[]},{"ApiKeyAuth":[]}]}},"/v1/physical":{"get":{"tags":["physical"],"summary":"Physical gold — live country index","description":"§5.1 — which countries are live, how many sources, freshest fetch.\n\n`?metal=` lists the countries with quotes of that metal and adds\n`products_by_metal` (every metal's products) to each country.","operationId":"physical_index_v1_physical_get","parameters":[{"name":"metal","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Metal"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"title":"Response Physical Index V1 Physical Get"}}}},"400":{"description":"Bad request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Authentication failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The caller's tier does not allow this request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit or quota exceeded. `error` is one of: `rate_limited` (keyless caller over the per-IP hourly cap, X-RateLimit-Scope anon-ip-hour), `rate_limit_exceeded` (API key over its per-minute limit, scope tier-rate-minute) or `quota_exceeded` (monthly quota used, scope tier-quota-month). Wait for Retry-After seconds before retrying; do not retry a monthly quota error automatically.","headers":{"X-RateLimit-Limit":{"description":"Limit for the window named by X-RateLimit-Scope.","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Requests left in that window (0 on a 429).","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix time (seconds) when the window resets.","schema":{"type":"integer"}},"X-RateLimit-Scope":{"description":"Which limit applied: anon-ip-hour, tier-rate-minute or tier-quota-month.","schema":{"type":"string"}},"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The operation failed internally.","content":{"text/plain":{"schema":{"type":"string"}}}},"503":{"description":"A required backend or data source is temporarily unavailable.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RequestValidationErrorResponse"}}}}},"security":[{"BearerAuth":[]},{"ApiKeyAuth":[]}]}},"/v1/physical/{country}":{"get":{"tags":["physical"],"summary":"Physical gold — dealer quotes for one country","description":"§5.2 — latest dealer quotes + premium_over_spot_bps for one country.","operationId":"physical_country_v1_physical__country__get","parameters":[{"name":"country","in":"path","required":true,"schema":{"type":"string","title":"Country"}},{"name":"metal","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Metal"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"title":"Response Physical Country V1 Physical  Country  Get"}}}},"400":{"description":"Bad request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Authentication failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The caller's tier does not allow this request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"The requested resource is unavailable or not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit or quota exceeded. `error` is one of: `rate_limited` (keyless caller over the per-IP hourly cap, X-RateLimit-Scope anon-ip-hour), `rate_limit_exceeded` (API key over its per-minute limit, scope tier-rate-minute) or `quota_exceeded` (monthly quota used, scope tier-quota-month). Wait for Retry-After seconds before retrying; do not retry a monthly quota error automatically.","headers":{"X-RateLimit-Limit":{"description":"Limit for the window named by X-RateLimit-Scope.","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Requests left in that window (0 on a 429).","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix time (seconds) when the window resets.","schema":{"type":"integer"}},"X-RateLimit-Scope":{"description":"Which limit applied: anon-ip-hour, tier-rate-minute or tier-quota-month.","schema":{"type":"string"}},"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The operation failed internally.","content":{"text/plain":{"schema":{"type":"string"}}}},"503":{"description":"A required backend or data source is temporarily unavailable.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RequestValidationErrorResponse"}}}}},"security":[{"BearerAuth":[]},{"ApiKeyAuth":[]}]}},"/v1/bars":{"get":{"tags":["bars"],"summary":"OHLCV bar history — cursor-paginated, real data, is_closed label","description":"Paginated OHLCV bar history from the canonical price_bars store.\n\nResults are ordered DESC by bar_start (newest first). Paginate via\nnext_cursor — pass it as ?cursor= on the next request. When next_cursor\nis null the result is exhausted. Empty range returns 200 with bars: [].\n\nDaily (1d) depth by plan:\n- Free:     last 30 days, gold (XAU) only\n- Physical: last 1 year, gold (XAU) only\n- Pro and Realtime Pro: full daily history (XAU/USD from 1996; other\n  symbols start later, e.g. silver from 2007)\n\nIntraday intervals (1m, 5m, 15m, 30m, 1h, 2h, 4h) require Pro or\nRealtime Pro (or Pro 2026); Plus 2026 gets 1h and 4h. Depth is 30 days\n(older ranges return 400 range_too_deep; use /v1/download), except Pro\n2026, which reaches full depth.\n\nFree and keyless callers get bars 15 minutes late.","operationId":"get_bars_endpoint_v1_bars_get","parameters":[{"name":"symbol","in":"query","required":true,"schema":{"type":"string","minLength":1,"maxLength":64,"description":"Symbol in slash or dash form. E.g. \"XAU/USD\", \"XAU-USD-SPOT\", \"GLD\".","title":"Symbol"},"description":"Symbol in slash or dash form. E.g. \"XAU/USD\", \"XAU-USD-SPOT\", \"GLD\"."},{"name":"interval","in":"query","required":false,"schema":{"type":"string","pattern":"^(1m|5m|15m|30m|1h|2h|4h|1d)$","description":"Bar interval. One of: 1m, 5m, 15m, 30m, 1h, 2h, 4h, 1d.","default":"1d","title":"Interval"},"description":"Bar interval. One of: 1m, 5m, 15m, 30m, 1h, 2h, 4h, 1d."},{"name":"from","in":"query","required":true,"schema":{"type":"string","maxLength":32,"description":"ISO-8601 start (inclusive).","title":"From"},"description":"ISO-8601 start (inclusive)."},{"name":"to","in":"query","required":true,"schema":{"type":"string","maxLength":32,"description":"ISO-8601 end (inclusive).","title":"To"},"description":"ISO-8601 end (inclusive)."},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":10000,"minimum":1,"description":"Max bars per page (1-10000). Default 100.","default":100,"title":"Limit"},"description":"Max bars per page (1-10000). Default 100."},{"name":"cursor","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Opaque keyset pagination cursor from a prior response's next_cursor. When set, returns bars older than the cursor within the from/to window.","title":"Cursor"},"description":"Opaque keyset pagination cursor from a prior response's next_cursor. When set, returns bars older than the cursor within the from/to window."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BarsResponse"}}}},"400":{"description":"Bad request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Authentication failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The caller's tier does not allow this request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"The requested resource is unavailable or not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit or quota exceeded. `error` is one of: `rate_limited` (keyless caller over the per-IP hourly cap, X-RateLimit-Scope anon-ip-hour), `rate_limit_exceeded` (API key over its per-minute limit, scope tier-rate-minute) or `quota_exceeded` (monthly quota used, scope tier-quota-month). Wait for Retry-After seconds before retrying; do not retry a monthly quota error automatically.","headers":{"X-RateLimit-Limit":{"description":"Limit for the window named by X-RateLimit-Scope.","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Requests left in that window (0 on a 429).","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix time (seconds) when the window resets.","schema":{"type":"integer"}},"X-RateLimit-Scope":{"description":"Which limit applied: anon-ip-hour, tier-rate-minute or tier-quota-month.","schema":{"type":"string"}},"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The operation failed internally.","content":{"text/plain":{"schema":{"type":"string"}}}},"503":{"description":"A required backend or data source is temporarily unavailable.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RequestValidationErrorResponse"}}}}},"security":[{},{"BearerAuth":[]},{"ApiKeyAuth":[]}]}},"/v1/bars/latest":{"get":{"tags":["bars"],"summary":"Latest bar for a symbol+interval — the most-recent settled or forming bar","description":"Return the single most-recent bar for a symbol+interval.\n\nOrdered by bar_start DESC, returns the first row. Provisional (forming)\nbars are returned with is_closed=False. Empty → 200 with bar: null, never\n404. No tier gate on this endpoint (latest bar is the lightest read;\ndepth-gating only applies to ranges).","operationId":"get_latest_bar_v1_bars_latest_get","parameters":[{"name":"symbol","in":"query","required":true,"schema":{"type":"string","minLength":1,"maxLength":64,"description":"Symbol in slash or dash form. E.g. \"XAU/USD\", \"GLD\".","title":"Symbol"},"description":"Symbol in slash or dash form. E.g. \"XAU/USD\", \"GLD\"."},{"name":"interval","in":"query","required":false,"schema":{"type":"string","pattern":"^(1m|5m|15m|30m|1h|2h|4h|1d)$","description":"Bar interval. One of: 1m, 5m, 15m, 30m, 1h, 2h, 4h, 1d.","default":"1d","title":"Interval"},"description":"Bar interval. One of: 1m, 5m, 15m, 30m, 1h, 2h, 4h, 1d."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LatestBarResponse"}}}},"401":{"description":"Authentication failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The caller's tier does not allow this request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"The requested resource is unavailable or not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit or quota exceeded. `error` is one of: `rate_limited` (keyless caller over the per-IP hourly cap, X-RateLimit-Scope anon-ip-hour), `rate_limit_exceeded` (API key over its per-minute limit, scope tier-rate-minute) or `quota_exceeded` (monthly quota used, scope tier-quota-month). Wait for Retry-After seconds before retrying; do not retry a monthly quota error automatically.","headers":{"X-RateLimit-Limit":{"description":"Limit for the window named by X-RateLimit-Scope.","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Requests left in that window (0 on a 429).","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix time (seconds) when the window resets.","schema":{"type":"integer"}},"X-RateLimit-Scope":{"description":"Which limit applied: anon-ip-hour, tier-rate-minute or tier-quota-month.","schema":{"type":"string"}},"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The operation failed internally.","content":{"text/plain":{"schema":{"type":"string"}}}},"503":{"description":"A required backend or data source is temporarily unavailable.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RequestValidationErrorResponse"}}}}},"security":[{},{"BearerAuth":[]},{"ApiKeyAuth":[]}]}},"/v1/ratios":{"get":{"tags":["ratios"],"summary":"Cross-commodity ratio time-series — date-joined daily closes (Pro+)","description":"Paginated cross-commodity ratio series from the canonical price_bars store.\n\nResults are ordered DESC by date (newest first). Paginate via\nnext_cursor — pass it as ?cursor= on the next request. When next_cursor\nis null the result is exhausted. Empty range returns 200 with points: [].\n\nA date where either leg has no bar is absent — no forward-fill, no\nfabrication (Invariant #1 honesty).\n\nTier depth gates (same as /v1/bars 1d):\n- Free:  30 days (403 plan_gated)\n- Physical: 1 year  (403 plan_gated)\n- Pro+:  full history\nAll pairs require Pro+ (all pairs involve a non-XAU or non-spot leg).","operationId":"get_ratios_v1_ratios_get","parameters":[{"name":"pair","in":"query","required":true,"schema":{"type":"string","minLength":1,"maxLength":64,"description":"Pair slug. With data: gold-silver, gld-xau. Platinum and palladium pairs (gold-platinum, silver-platinum, gold-palladium) are accepted but not available yet: they return no points.","title":"Pair"},"description":"Pair slug. With data: gold-silver, gld-xau. Platinum and palladium pairs (gold-platinum, silver-platinum, gold-palladium) are accepted but not available yet: they return no points."},{"name":"from","in":"query","required":true,"schema":{"type":"string","maxLength":32,"description":"ISO-8601 start (inclusive).","title":"From"},"description":"ISO-8601 start (inclusive)."},{"name":"to","in":"query","required":true,"schema":{"type":"string","maxLength":32,"description":"ISO-8601 end (inclusive).","title":"To"},"description":"ISO-8601 end (inclusive)."},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":10000,"minimum":1,"description":"Max points per page (1-10000). Default 100.","default":100,"title":"Limit"},"description":"Max points per page (1-10000). Default 100."},{"name":"cursor","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Opaque keyset pagination cursor from a prior response's next_cursor. When set, returns points older than the cursor within the from/to window.","title":"Cursor"},"description":"Opaque keyset pagination cursor from a prior response's next_cursor. When set, returns points older than the cursor within the from/to window."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RatiosResponse"}}}},"400":{"description":"Bad request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Authentication failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The caller's tier does not allow this request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"The requested resource is unavailable or not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit or quota exceeded. `error` is one of: `rate_limited` (keyless caller over the per-IP hourly cap, X-RateLimit-Scope anon-ip-hour), `rate_limit_exceeded` (API key over its per-minute limit, scope tier-rate-minute) or `quota_exceeded` (monthly quota used, scope tier-quota-month). Wait for Retry-After seconds before retrying; do not retry a monthly quota error automatically.","headers":{"X-RateLimit-Limit":{"description":"Limit for the window named by X-RateLimit-Scope.","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Requests left in that window (0 on a 429).","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix time (seconds) when the window resets.","schema":{"type":"integer"}},"X-RateLimit-Scope":{"description":"Which limit applied: anon-ip-hour, tier-rate-minute or tier-quota-month.","schema":{"type":"string"}},"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The operation failed internally.","content":{"text/plain":{"schema":{"type":"string"}}}},"503":{"description":"A required backend or data source is temporarily unavailable.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RequestValidationErrorResponse"}}}}},"security":[{"BearerAuth":[]},{"ApiKeyAuth":[]}]}},"/v1/ratios/latest":{"get":{"tags":["ratios"],"summary":"Latest ratio point for a pair — the most-recent shared-date ratio (Pro+)","description":"Return the single most-recent shared-date ratio point for a pair.\n\nReturns 200 with point: null when no shared date exists (e.g. before\neither leg is backfilled) — never 404 on empty (mirrors /v1/bars/latest).","operationId":"get_latest_ratio_v1_ratios_latest_get","parameters":[{"name":"pair","in":"query","required":true,"schema":{"type":"string","minLength":1,"maxLength":64,"description":"Pair slug: gold-silver or gld-xau. Platinum and palladium pairs are accepted but not available yet (point: null).","title":"Pair"},"description":"Pair slug: gold-silver or gld-xau. Platinum and palladium pairs are accepted but not available yet (point: null)."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LatestRatioResponse"}}}},"400":{"description":"Bad request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Authentication failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The caller's tier does not allow this request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"The requested resource is unavailable or not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit or quota exceeded. `error` is one of: `rate_limited` (keyless caller over the per-IP hourly cap, X-RateLimit-Scope anon-ip-hour), `rate_limit_exceeded` (API key over its per-minute limit, scope tier-rate-minute) or `quota_exceeded` (monthly quota used, scope tier-quota-month). Wait for Retry-After seconds before retrying; do not retry a monthly quota error automatically.","headers":{"X-RateLimit-Limit":{"description":"Limit for the window named by X-RateLimit-Scope.","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Requests left in that window (0 on a 429).","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix time (seconds) when the window resets.","schema":{"type":"integer"}},"X-RateLimit-Scope":{"description":"Which limit applied: anon-ip-hour, tier-rate-minute or tier-quota-month.","schema":{"type":"string"}},"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The operation failed internally.","content":{"text/plain":{"schema":{"type":"string"}}}},"503":{"description":"A required backend or data source is temporarily unavailable.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RequestValidationErrorResponse"}}}}},"security":[{"BearerAuth":[]},{"ApiKeyAuth":[]}]}},"/v1/download":{"get":{"tags":["download"],"summary":"Bulk OHLCV download — presigned Parquet or CSV file (paid plans)","description":"Bulk download of OHLCV bars for a symbol+interval+year from the archive.\n\nReturns a 307 redirect to a time-limited presigned URL (1 hour TTL).\nFree and anonymous callers are blocked — bulk download is the paid depth moat.\n\nTier gates:\n  Free/anonymous: 403 plan_gated.\n  Physical, Basic: 365-day window — years within the last year only.\n  Pro, Realtime Pro, Plus, Pro (2026): full history (back to 1996 for XAU/USD daily).","operationId":"download_bars_v1_download_get","parameters":[{"name":"symbol","in":"query","required":true,"schema":{"type":"string","minLength":1,"maxLength":64,"description":"Symbol to download. E.g. \"XAU/USD\", \"XAU-USD-SPOT\", \"GLD\". Only symbols with archived data are available.","title":"Symbol"},"description":"Symbol to download. E.g. \"XAU/USD\", \"XAU-USD-SPOT\", \"GLD\". Only symbols with archived data are available."},{"name":"interval","in":"query","required":false,"schema":{"type":"string","pattern":"^(1m|5m|15m|30m|1h|2h|4h|1d)$","description":"Bar interval. One of: 1m, 5m, 15m, 30m, 1h, 2h, 4h, 1d.","default":"1d","title":"Interval"},"description":"Bar interval. One of: 1m, 5m, 15m, 30m, 1h, 2h, 4h, 1d."},{"name":"year","in":"query","required":true,"schema":{"type":"integer","maximum":2026,"minimum":1996,"description":"Calendar year to download (1996-2026). Each year is one archive partition.","title":"Year"},"description":"Calendar year to download (1996-2026). Each year is one archive partition."},{"name":"fmt","in":"query","required":false,"schema":{"type":"string","pattern":"^(parquet|csv)$","description":"Download format. \"parquet\" (default) = presigned Parquet URL. \"csv\" = precomputed CSV sidecar from the same yearly archive partition.","default":"parquet","title":"Fmt"},"description":"Download format. \"parquet\" (default) = presigned Parquet URL. \"csv\" = precomputed CSV sidecar from the same yearly archive partition."}],"responses":{"307":{"description":"Redirect to a presigned Parquet or CSV file."},"401":{"description":"Authentication failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The caller's tier does not allow this request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"The requested resource is unavailable or not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit or quota exceeded. `error` is one of: `rate_limited` (keyless caller over the per-IP hourly cap, X-RateLimit-Scope anon-ip-hour), `rate_limit_exceeded` (API key over its per-minute limit, scope tier-rate-minute) or `quota_exceeded` (monthly quota used, scope tier-quota-month). Wait for Retry-After seconds before retrying; do not retry a monthly quota error automatically.","headers":{"X-RateLimit-Limit":{"description":"Limit for the window named by X-RateLimit-Scope.","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Requests left in that window (0 on a 429).","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix time (seconds) when the window resets.","schema":{"type":"integer"}},"X-RateLimit-Scope":{"description":"Which limit applied: anon-ip-hour, tier-rate-minute or tier-quota-month.","schema":{"type":"string"}},"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"503":{"description":"A required backend or data source is temporarily unavailable.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The operation failed internally.","content":{"text/plain":{"schema":{"type":"string"}},"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RequestValidationErrorResponse"}}}}},"security":[{"BearerAuth":[]},{"ApiKeyAuth":[]}]}},"/v1/public/xau-history":{"get":{"tags":["public"],"summary":"Public XAU/USD daily-close history (1 year, hourly refresh, no auth)","description":"Year-back daily-close series for XAU-USD-SPOT. No auth, no tier\ngate, no per-IP cap. Refreshes once per hour to deter scraping —\ncallers within the same hour receive identical bytes.\n\nUsed by the /data/gold forward-view chart (left-half history) and\nany future marketing surface that needs price context without\nasking the visitor for an API key.","operationId":"get_public_xau_history_v1_public_xau_history_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/v1/metadata/tiers":{"get":{"tags":["system"],"summary":"Public tier catalog","description":"Ordered list of tiers. Match on `key`; show only rows with `hidden` false.\n\nOnce the 2026 plans are on sale, Free/Basic/Plus/Pro are the visible rows and the\nlegacy plans stay listed as hidden for existing subscribers.\n\nStatic catalog — same response for every caller. 1h edge-cache is safe\nbecause tier definitions only change on redeploy.","operationId":"public_tiers_v1_metadata_tiers_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/TierPublic"},"type":"array","title":"Response Public Tiers V1 Metadata Tiers Get"}}}},"401":{"description":"Authentication failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit or quota exceeded. `error` is one of: `rate_limited` (keyless caller over the per-IP hourly cap, X-RateLimit-Scope anon-ip-hour), `rate_limit_exceeded` (API key over its per-minute limit, scope tier-rate-minute) or `quota_exceeded` (monthly quota used, scope tier-quota-month). Wait for Retry-After seconds before retrying; do not retry a monthly quota error automatically.","headers":{"X-RateLimit-Limit":{"description":"Limit for the window named by X-RateLimit-Scope.","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Requests left in that window (0 on a 429).","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix time (seconds) when the window resets.","schema":{"type":"integer"}},"X-RateLimit-Scope":{"description":"Which limit applied: anon-ip-hour, tier-rate-minute or tier-quota-month.","schema":{"type":"string"}},"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The operation failed internally.","content":{"text/plain":{"schema":{"type":"string"}}}},"503":{"description":"A required backend or data source is temporarily unavailable.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{},{"BearerAuth":[]},{"ApiKeyAuth":[]}]}},"/v1/account/keys":{"get":{"tags":["system"],"summary":"List active API keys for the authed organization","operationId":"list_keys_v1_account_keys_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/KeyPublic"},"type":"array","title":"Response List Keys V1 Account Keys Get"}}}},"401":{"description":"Authentication failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit or quota exceeded. `error` is one of: `rate_limited` (keyless caller over the per-IP hourly cap, X-RateLimit-Scope anon-ip-hour), `rate_limit_exceeded` (API key over its per-minute limit, scope tier-rate-minute) or `quota_exceeded` (monthly quota used, scope tier-quota-month). Wait for Retry-After seconds before retrying; do not retry a monthly quota error automatically.","headers":{"X-RateLimit-Limit":{"description":"Limit for the window named by X-RateLimit-Scope.","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Requests left in that window (0 on a 429).","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix time (seconds) when the window resets.","schema":{"type":"integer"}},"X-RateLimit-Scope":{"description":"Which limit applied: anon-ip-hour, tier-rate-minute or tier-quota-month.","schema":{"type":"string"}},"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The operation failed internally.","content":{"text/plain":{"schema":{"type":"string"}}}},"503":{"description":"A required backend or data source is temporarily unavailable.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"BearerAuth":[]},{"ApiKeyAuth":[]}]},"post":{"tags":["system"],"summary":"Mint a new API key; old key enters a 24h grace period","operationId":"create_key_v1_account_keys_post","responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/KeyMintResponse"}}}},"401":{"description":"Authentication failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit or quota exceeded. `error` is one of: `rate_limited` (keyless caller over the per-IP hourly cap, X-RateLimit-Scope anon-ip-hour), `rate_limit_exceeded` (API key over its per-minute limit, scope tier-rate-minute) or `quota_exceeded` (monthly quota used, scope tier-quota-month). Wait for Retry-After seconds before retrying; do not retry a monthly quota error automatically.","headers":{"X-RateLimit-Limit":{"description":"Limit for the window named by X-RateLimit-Scope.","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Requests left in that window (0 on a 429).","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix time (seconds) when the window resets.","schema":{"type":"integer"}},"X-RateLimit-Scope":{"description":"Which limit applied: anon-ip-hour, tier-rate-minute or tier-quota-month.","schema":{"type":"string"}},"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"503":{"description":"A required backend or data source is temporarily unavailable.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The operation failed internally.","content":{"text/plain":{"schema":{"type":"string"}},"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"BearerAuth":[]},{"ApiKeyAuth":[]}]}},"/v1/account/keys/{key_id}":{"delete":{"tags":["system"],"summary":"Hard-revoke a key immediately (revoked_at = now)","operationId":"delete_key_v1_account_keys__key_id__delete","parameters":[{"name":"key_id","in":"path","required":true,"schema":{"type":"string","title":"Key Id"}}],"responses":{"204":{"description":"Successful Response"},"401":{"description":"Authentication failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"The requested resource is unavailable or not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit or quota exceeded. `error` is one of: `rate_limited` (keyless caller over the per-IP hourly cap, X-RateLimit-Scope anon-ip-hour), `rate_limit_exceeded` (API key over its per-minute limit, scope tier-rate-minute) or `quota_exceeded` (monthly quota used, scope tier-quota-month). Wait for Retry-After seconds before retrying; do not retry a monthly quota error automatically.","headers":{"X-RateLimit-Limit":{"description":"Limit for the window named by X-RateLimit-Scope.","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Requests left in that window (0 on a 429).","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix time (seconds) when the window resets.","schema":{"type":"integer"}},"X-RateLimit-Scope":{"description":"Which limit applied: anon-ip-hour, tier-rate-minute or tier-quota-month.","schema":{"type":"string"}},"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The operation failed internally.","content":{"text/plain":{"schema":{"type":"string"}}}},"503":{"description":"A required backend or data source is temporarily unavailable.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RequestValidationErrorResponse"}}}}},"security":[{"BearerAuth":[]},{"ApiKeyAuth":[]}]}},"/v1/account/checkout":{"post":{"tags":["system"],"summary":"Create Checkout","operationId":"create_checkout_v1_account_checkout_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CheckoutRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CheckoutResponse"}}}},"401":{"description":"Authentication failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"The requested account action cannot be completed in its current state.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The operation failed internally.","content":{"text/plain":{"schema":{"type":"string"}}}},"503":{"description":"A required backend or data source is temporarily unavailable.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RequestValidationErrorResponse"}}}}},"security":[{"BearerAuth":[]},{"ApiKeyAuth":[]}]}},"/v1/account/usage":{"get":{"tags":["system"],"summary":"Authed key's day + month call counters","operationId":"get_usage_v1_account_usage_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UsagePublic"}}}},"401":{"description":"Authentication failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit or quota exceeded. `error` is one of: `rate_limited` (keyless caller over the per-IP hourly cap, X-RateLimit-Scope anon-ip-hour), `rate_limit_exceeded` (API key over its per-minute limit, scope tier-rate-minute) or `quota_exceeded` (monthly quota used, scope tier-quota-month). Wait for Retry-After seconds before retrying; do not retry a monthly quota error automatically.","headers":{"X-RateLimit-Limit":{"description":"Limit for the window named by X-RateLimit-Scope.","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Requests left in that window (0 on a 429).","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix time (seconds) when the window resets.","schema":{"type":"integer"}},"X-RateLimit-Scope":{"description":"Which limit applied: anon-ip-hour, tier-rate-minute or tier-quota-month.","schema":{"type":"string"}},"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The operation failed internally.","content":{"text/plain":{"schema":{"type":"string"}}}},"503":{"description":"A required backend or data source is temporarily unavailable.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"BearerAuth":[]},{"ApiKeyAuth":[]}]}}},"components":{"schemas":{"Bar":{"properties":{"bar_start":{"type":"string","title":"Bar Start","description":"Bar open timestamp, ISO-8601 UTC with Z suffix (e.g. \"2026-06-15T00:00:00Z\"). The bar covers the half-open interval [bar_start, bar_start + interval)."},"open":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Open","description":"Open price as decimal string. Null only on data gaps (never fabricated)."},"high":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"High","description":"High price as decimal string."},"low":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Low","description":"Low price as decimal string."},"close":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Close","description":"Close price as decimal string."},"volume":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Volume","description":"Trading volume as decimal string. Null for spot instruments (XAU/USD, XAG/USD, XCU/USD — no consolidated spot volume). Real for futures (GC, SI, HG) and ETFs (GLD). Never 0, never fabricated."},"is_closed":{"type":"boolean","title":"Is Closed","description":"True if the bar's time window has ended and the OHLC values are final (settled). False for the currently-forming bar (provisional)."}},"additionalProperties":false,"type":"object","required":["bar_start","is_closed"],"title":"Bar","description":"One OHLCV bar in the /v1/bars wire shape.\n\nPrices are Decimal-as-string to match the existing /v1/prices/history\ncontract and avoid float drift on the consumer side."},"BarsMeta":{"properties":{"tier":{"type":"string","title":"Tier"},"count":{"type":"integer","title":"Count"},"source_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Source Id","description":"Provenance tag from the price_bars store. Examples: 'csv_xau_15m_v1', 'csv_xau_rollup_v1', 'gld_daily', 'live_oracle', 'live_xau_canonical'. Null when bars list is empty."}},"additionalProperties":false,"type":"object","required":["tier","count"],"title":"BarsMeta","description":"Metadata block in BarsResponse.\n\nsource_id is a PAGE-SCOPED provenance summary derived from the bars in this\npage — NOT a per-bar attribute.  It equals the source_id of the first bar\nin the page, or \"mixed\" if the page spans multiple source_ids (e.g. a range\nthat crosses from CSV history into a live feed).  It is non-authoritative\nfor mixed pages: consumers wanting per-bar provenance should wait for a future\nversion that exposes bar.source_id per-bar.  That per-bar field is a reserved\nfuture decision (flagged in the §9.4 public-surface audit); do NOT add it to\nthe current Bar wire shape without a version bump.\n\nExamples: 'csv_xau_15m_v1', 'csv_xau_rollup_v1', 'gld_daily', 'mixed'.\nNull when bars list is empty."},"BarsResponse":{"properties":{"symbol":{"type":"string","title":"Symbol","description":"The requested symbol (e.g. 'XAU/USD', 'GLD')."},"interval":{"type":"string","title":"Interval","description":"The requested interval (e.g. '1d', '15m')."},"bars":{"items":{"$ref":"#/components/schemas/Bar"},"type":"array","title":"Bars","description":"OHLCV bars, newest-first (DESC by bar_start)."},"next_cursor":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Next Cursor","description":"Opaque cursor for the next page. Pass as ?cursor= on the next request. Null when the result is exhausted. Encoding: base64url of the bar_start_utc ISO-8601 string of the last bar in this page."},"meta":{"$ref":"#/components/schemas/BarsMeta"}},"additionalProperties":false,"type":"object","required":["symbol","interval","bars","meta"],"title":"BarsResponse","description":"Response envelope for GET /v1/bars.\n\nKeyset pagination via next_cursor. When next_cursor is null, the result\nis exhausted (no more pages). An empty bars list with next_cursor=null\nis the canonical empty-range response (200, never 404)."},"CaratResponse":{"properties":{"currency":{"type":"string","title":"Currency"},"timestamp":{"type":"string","title":"Timestamp"},"price_gram_24k":{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d{0,8}0*$","title":"Price Gram 24K","description":"Price value as Decimal (up to 8 decimal places). JSON-serialized as a string by Pydantic to avoid float drift; callers should parse with Decimal(), not float()."},"price_gram_22k":{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d{0,8}0*$","title":"Price Gram 22K","description":"Price value as Decimal (up to 8 decimal places). JSON-serialized as a string by Pydantic to avoid float drift; callers should parse with Decimal(), not float()."},"price_gram_21k":{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d{0,8}0*$","title":"Price Gram 21K","description":"Price value as Decimal (up to 8 decimal places). JSON-serialized as a string by Pydantic to avoid float drift; callers should parse with Decimal(), not float()."},"price_gram_20k":{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d{0,8}0*$","title":"Price Gram 20K","description":"Price value as Decimal (up to 8 decimal places). JSON-serialized as a string by Pydantic to avoid float drift; callers should parse with Decimal(), not float()."},"price_gram_18k":{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d{0,8}0*$","title":"Price Gram 18K","description":"Price value as Decimal (up to 8 decimal places). JSON-serialized as a string by Pydantic to avoid float drift; callers should parse with Decimal(), not float()."},"price_gram_16k":{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d{0,8}0*$","title":"Price Gram 16K","description":"Price value as Decimal (up to 8 decimal places). JSON-serialized as a string by Pydantic to avoid float drift; callers should parse with Decimal(), not float()."},"price_gram_14k":{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d{0,8}0*$","title":"Price Gram 14K","description":"Price value as Decimal (up to 8 decimal places). JSON-serialized as a string by Pydantic to avoid float drift; callers should parse with Decimal(), not float()."},"price_gram_10k":{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d{0,8}0*$","title":"Price Gram 10K","description":"Price value as Decimal (up to 8 decimal places). JSON-serialized as a string by Pydantic to avoid float drift; callers should parse with Decimal(), not float()."}},"type":"object","required":["currency","timestamp","price_gram_24k","price_gram_22k","price_gram_21k","price_gram_20k","price_gram_18k","price_gram_16k","price_gram_14k","price_gram_10k"],"title":"CaratResponse","description":"Response for GET /v1/carat."},"CheckoutRequest":{"properties":{"plan":{"type":"string","enum":["basic_2026","plus_2026","pro_2026"],"title":"Plan"}},"additionalProperties":false,"type":"object","required":["plan"],"title":"CheckoutRequest"},"CheckoutResponse":{"properties":{"result":{"type":"string","const":"success","title":"Result","default":"success"},"action":{"type":"string","enum":["checkout","manage_subscription"],"title":"Action"},"url":{"type":"string","title":"Url"},"expires_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Expires At"}},"type":"object","required":["action","url"],"title":"CheckoutResponse"},"ConvertResponse":{"properties":{"from":{"type":"string","title":"From","description":"Source symbol (e.g. XAU, USD)."},"to":{"type":"string","title":"To","description":"Target symbol (e.g. USD, EUR)."},"amount":{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d{0,8}0*$","title":"Amount","description":"Input amount."},"rate":{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d{0,8}0*$","title":"Rate","description":"Conversion rate (1 from-unit = rate to-units)."},"result":{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d{0,8}0*$","title":"Result","description":"amount x rate."},"unit":{"type":"string","title":"Unit","description":"Unit applied to the metal side (oz, gram, kg)."},"timestamp":{"type":"string","title":"Timestamp","description":"UTC ISO-8601 timestamp of the source data. For currency-to-currency conversions the two FX legs resolve independently (live-first with daily fallback) and may differ in source/age; this reflects one leg only."}},"type":"object","required":["from","to","amount","rate","result","unit","timestamp"],"title":"ConvertResponse","description":"Response for GET /v1/convert."},"ErrorResponse":{"properties":{"error":{"type":"string","title":"Error","description":"Machine-readable error code, e.g. 'not_found'."},"message":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Message","description":"Human-readable explanation, when the error path provides one."}},"type":"object","required":["error"],"title":"ErrorResponse","description":"Canonical error envelope for documented failure responses.\n\nMirrors the runtime shape emitted by the HTTPException/validation\nexception handlers and the rate/quota middleware: a machine-readable\n`error` code plus an optional human `message`. Individual error paths may\ncarry additional keys (e.g. `upgrade_url`, `retry_after_seconds`); this\nmodel documents the stable floor, not an exhaustive contract."},"HistoryMeta":{"properties":{"tier":{"type":"string","title":"Tier"},"count":{"type":"integer","title":"Count"},"ohlc_note":{"type":"string","title":"Ohlc Note"}},"additionalProperties":true,"type":"object","required":["tier","count","ohlc_note"],"title":"HistoryMeta","description":"Meta block in HistoryResponse."},"HistoryResponse":{"properties":{"symbol":{"type":"string","title":"Symbol"},"interval":{"type":"string","title":"Interval","description":"One of \"1d\", \"1h\"."},"from":{"type":"string","title":"From","description":"ISO-8601 date or timestamp."},"to":{"type":"string","title":"To"},"series":{"items":{"$ref":"#/components/schemas/HistorySeriesEntry"},"type":"array","title":"Series"},"meta":{"$ref":"#/components/schemas/HistoryMeta"}},"additionalProperties":true,"type":"object","required":["symbol","interval","from","to","series","meta"],"title":"HistoryResponse","description":"Response envelope for GET /v1/prices/history (DEPRECATED — use /v1/bars).\n\nIMPORTANT: This model is for OpenAPI documentation only. The handler\nreturns JSONResponse directly; Pydantic validation does not run on the\nactual response (preserves byte-identical wire contract). Do not add\nfields here and expect them to appear in production responses without\nalso updating the handler."},"HistorySeriesEntry":{"properties":{"date":{"type":"string","title":"Date","description":"Daily bars: \"YYYY-MM-DD\". Intraday bars: ISO-8601 UTC timestamp with Z suffix."},"open":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Open","description":"Open price as decimal string."},"high":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"High","description":"High price as decimal string."},"low":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Low","description":"Low price as decimal string."},"close":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Close","description":"Close price as decimal string."},"volume":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Volume","description":"Always null in the current implementation (close-only legacy path)."},"is_stale":{"type":"boolean","title":"Is Stale","description":"True if the underlying data row was stale when fetched."},"is_closed":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Is Closed","description":"True = settled bar; False = currently-forming (provisional)."}},"additionalProperties":true,"type":"object","required":["date","is_stale"],"title":"HistorySeriesEntry","description":"One element of the series[] array in /v1/prices/history.\n\nvolume is always null in the current implementation (close-only path);\nis_closed comes from the stored bar (the real-bars path serves every response)."},"KeyMintResponse":{"properties":{"id":{"type":"string","title":"Id"},"plaintext":{"type":"string","title":"Plaintext"},"key_prefix":{"type":"string","title":"Key Prefix"},"created_at":{"type":"string","title":"Created At"},"previous_key_revokes_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Previous Key Revokes At"}},"type":"object","required":["id","plaintext","key_prefix","created_at"],"title":"KeyMintResponse"},"KeyPublic":{"properties":{"id":{"type":"string","title":"Id"},"key_prefix":{"type":"string","title":"Key Prefix"},"name":{"type":"string","title":"Name"},"created_at":{"type":"string","title":"Created At"},"last_used_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Last Used At"},"revoked_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Revoked At"}},"type":"object","required":["id","key_prefix","name","created_at"],"title":"KeyPublic"},"LatestBarResponse":{"properties":{"symbol":{"type":"string","title":"Symbol"},"interval":{"type":"string","title":"Interval"},"bar":{"anyOf":[{"$ref":"#/components/schemas/Bar"},{"type":"null"}],"description":"The most-recent bar for this symbol+interval. Null when no bar exists (e.g. before the backfill or for an unsupported interval)."},"meta":{"$ref":"#/components/schemas/BarsMeta"}},"additionalProperties":false,"type":"object","required":["symbol","interval","meta"],"title":"LatestBarResponse","description":"Response envelope for GET /v1/bars/latest.\n\nbar is null (not present) when no bar exists for the given symbol+interval.\nThe response is always 200 — never 404 on empty."},"LatestRatioResponse":{"properties":{"pair":{"type":"string","title":"Pair"},"numerator":{"type":"string","title":"Numerator"},"denominator":{"type":"string","title":"Denominator"},"interval":{"type":"string","title":"Interval","default":"1d"},"point":{"anyOf":[{"$ref":"#/components/schemas/RatioPoint"},{"type":"null"}],"description":"The most-recent shared-date ratio point. Null when no shared date exists (e.g. before either leg is backfilled)."},"meta":{"$ref":"#/components/schemas/RatiosMeta"}},"additionalProperties":false,"type":"object","required":["pair","numerator","denominator","meta"],"title":"LatestRatioResponse","description":"Response envelope for GET /v1/ratios/latest.\n\npoint is null (not present) when no shared-date bar exists for both legs.\nThe response is always 200 — never 404 on empty (mirrors /v1/bars/latest)."},"RatioPoint":{"properties":{"date":{"type":"string","title":"Date","description":"Bar date in YYYY-MM-DD format (UTC midnight = bar_start_utc). Only emitted when both numerator and denominator have a settled bar for this date — no forward-fill, no fabricated rows."},"ratio":{"type":"string","title":"Ratio","description":"numerator_close / denominator_close as a decimal string (4 dp). Example: '88.1234'. Never null — a date without both legs is simply absent from the series."},"numerator_close":{"type":"string","title":"Numerator Close","description":"Closing price of the numerator instrument as a decimal string."},"denominator_close":{"type":"string","title":"Denominator Close","description":"Closing price of the denominator instrument as a decimal string."}},"additionalProperties":false,"type":"object","required":["date","ratio","numerator_close","denominator_close"],"title":"RatioPoint","description":"One data point in the ratios series.\n\ndate is YYYY-MM-DD (daily). ratio/numerator_close/denominator_close are\nDecimal-as-string (4 dp), matching the price_bars NUMERIC(14,4) store.\nA point is only emitted when BOTH legs have a bar for that date — no\nforward-fill, no fabrication (Invariant #1 honesty)."},"RatiosMeta":{"properties":{"tier":{"type":"string","title":"Tier"},"count":{"type":"integer","title":"Count"}},"additionalProperties":false,"type":"object","required":["tier","count"],"title":"RatiosMeta","description":"Metadata block in RatiosResponse. Mirrors BarsMeta."},"RatiosResponse":{"properties":{"pair":{"type":"string","title":"Pair","description":"The pair slug (e.g. 'gold-silver', 'gld-xau'). Echoed from the request for self-describing responses."},"numerator":{"type":"string","title":"Numerator","description":"Resolved numerator instrument symbol (e.g. 'XAU/USD')."},"denominator":{"type":"string","title":"Denominator","description":"Resolved denominator instrument symbol (e.g. 'XAG/USD')."},"interval":{"type":"string","title":"Interval","description":"Bar interval. Currently always '1d' (intraday ratios out of scope at launch).","default":"1d"},"points":{"items":{"$ref":"#/components/schemas/RatioPoint"},"type":"array","title":"Points","description":"Ratio data points, newest-first (DESC by date)."},"next_cursor":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Next Cursor","description":"Opaque cursor for the next page. Pass as ?cursor= on the next request. Null when the result is exhausted. Encoding: base64url of the date string ('YYYY-MM-DD') of the last point in this page."},"meta":{"$ref":"#/components/schemas/RatiosMeta"}},"additionalProperties":false,"type":"object","required":["pair","numerator","denominator","points","meta"],"title":"RatiosResponse","description":"Response envelope for GET /v1/ratios.\n\nKeyset pagination via next_cursor. When next_cursor is null, the result\nis exhausted (no more pages). An empty points list with next_cursor=null\nis the canonical empty-range response (200, never 404).\n\nResults are ordered DESC by date (newest first), mirroring /v1/bars."},"SourceEntry":{"properties":{"source":{"type":"string","title":"Source"},"display_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Display Name"},"price":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Price"},"unit":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Unit"},"license":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"License"},"is_stale":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Is Stale"},"informational":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Informational"}},"additionalProperties":true,"type":"object","required":["source"],"title":"SourceEntry","description":"One row inside `sources[]`. Shape varies across feeds; only\n`source` is mandatory.\n`extra=\"allow\"` keeps the model forward-compatible with new feed\nmetadata without a schema bump."},"SpotListResponse":{"properties":{"symbols":{"items":{"$ref":"#/components/schemas/SpotResponse"},"type":"array","title":"Symbols"}},"type":"object","required":["symbols"],"title":"SpotListResponse","description":"Envelope for `/v1/prices` — array of zero-or-more SpotResponse\nrows, one per (symbol, quote_currency, contract_type) latest."},"SpotResponse":{"properties":{"symbol":{"type":"string","title":"Symbol"},"quote_currency":{"type":"string","title":"Quote Currency"},"unit":{"type":"string","title":"Unit"},"contract_type":{"type":"string","title":"Contract Type"},"price":{"anyOf":[{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d{0,8}0*$","description":"Price value as Decimal (up to 8 decimal places). JSON-serialized as a string by Pydantic to avoid float drift; callers should parse with Decimal(), not float()."},{"type":"null"}],"title":"Price","description":"Decimal string. Latest USD spot quotes use nearest half-up rounding: XAU 2 decimal places, XAG 3, HG 4. Other instruments keep their precision up to 8 decimal places, rounded half-up beyond that."},"bid":{"anyOf":[{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d{0,8}0*$","description":"Price value as Decimal (up to 8 decimal places). JSON-serialized as a string by Pydantic to avoid float drift; callers should parse with Decimal(), not float()."},{"type":"null"}],"title":"Bid","description":"Optional. Paid plans only, and only while the primary spot feed supplies a two-sided quote. Omitted for Free and keyless callers, when the backup feed is serving, or when no coherent quote exists."},"ask":{"anyOf":[{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d{0,8}0*$","description":"Price value as Decimal (up to 8 decimal places). JSON-serialized as a string by Pydantic to avoid float drift; callers should parse with Decimal(), not float()."},{"type":"null"}],"title":"Ask","description":"Optional. Same availability as `bid`; omitted when unavailable."},"is_stale":{"type":"boolean","title":"Is Stale"},"divergence_flag":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Divergence Flag","description":"Present only when requested via ?include= (stats or all). True when a tokenized-gold source diverges > 0.5% from primary."},"computed_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Computed At"},"sources":{"anyOf":[{"items":{"$ref":"#/components/schemas/SourceEntry"},"type":"array"},{"type":"null"}],"title":"Sources","description":"Present only when requested via ?include= (sources or all). Tier-filtered provenance array; vendor keys obfuscated."},"divergence_bps":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Divergence Bps"},"live_quality":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Live Quality","description":"Selected live quote quality when a live source is served."},"price_gram_24k":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Price Gram 24K"},"price_gram_22k":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Price Gram 22K"},"price_gram_21k":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Price Gram 21K"},"price_gram_20k":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Price Gram 20K"},"price_gram_18k":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Price Gram 18K"},"price_gram_16k":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Price Gram 16K"},"price_gram_14k":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Price Gram 14K"},"price_gram_10k":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Price Gram 10K"},"open_price":{"anyOf":[{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d{0,8}0*$","description":"Price value as Decimal (up to 8 decimal places). JSON-serialized as a string by Pydantic to avoid float drift; callers should parse with Decimal(), not float()."},{"type":"null"}],"title":"Open Price"},"high_price":{"anyOf":[{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d{0,8}0*$","description":"Price value as Decimal (up to 8 decimal places). JSON-serialized as a string by Pydantic to avoid float drift; callers should parse with Decimal(), not float()."},{"type":"null"}],"title":"High Price"},"low_price":{"anyOf":[{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d{0,8}0*$","description":"Price value as Decimal (up to 8 decimal places). JSON-serialized as a string by Pydantic to avoid float drift; callers should parse with Decimal(), not float()."},{"type":"null"}],"title":"Low Price"},"prev_close_price":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Prev Close Price"},"ch":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Ch"},"chp":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Chp"},"open_time":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Open Time"}},"type":"object","required":["symbol","quote_currency","unit","contract_type","is_stale"],"title":"SpotResponse","description":"Single tier-filtered spot row. Shared shape between the\nenvelope (`/v1/prices` returns `{\"symbols\": [SpotResponse, ...]}`)\nand the convenience endpoint (`/v1/spot/{symbol}` returns the\nSpotResponse unwrapped)."},"TierPublic":{"properties":{"key":{"type":"string","title":"Key"},"name":{"type":"string","title":"Name"},"monthly_price_usd":{"type":"string","title":"Monthly Price Usd"},"yearly_price_usd":{"type":"string","title":"Yearly Price Usd"},"rate_limit_per_minute":{"type":"integer","title":"Rate Limit Per Minute"},"monthly_quota":{"type":"integer","title":"Monthly Quota"},"allowed_bases":{"items":{"type":"string"},"type":"array","title":"Allowed Bases"},"allowed_contracts":{"items":{"type":"string"},"type":"array","title":"Allowed Contracts"},"includes_mcp":{"type":"boolean","title":"Includes Mcp"},"commercial_use":{"type":"boolean","title":"Commercial Use"},"support_sla_hours":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Support Sla Hours"},"max_concurrent_streams":{"type":"integer","title":"Max Concurrent Streams"},"hidden":{"type":"boolean","title":"Hidden"}},"type":"object","required":["key","name","monthly_price_usd","yearly_price_usd","rate_limit_per_minute","monthly_quota","allowed_bases","allowed_contracts","includes_mcp","commercial_use","support_sla_hours","max_concurrent_streams","hidden"],"title":"TierPublic"},"UsagePublic":{"properties":{"calls_today":{"type":"integer","title":"Calls Today"},"calls_this_month":{"type":"integer","title":"Calls This Month"},"active_streams":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Active Streams"},"max_streams":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Max Streams"}},"type":"object","required":["calls_today","calls_this_month"],"title":"UsagePublic"},"RequestValidationErrorResponse":{"type":"object","required":["error","message","details"],"properties":{"error":{"type":"string","const":"validation_error"},"message":{"type":"string"},"details":{"type":"array","items":{"type":"object","required":["loc","msg","type"],"properties":{"loc":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"integer"}]}},"msg":{"type":"string"},"type":{"type":"string"}}}}}}},"securitySchemes":{"BearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"goldprice.dev API key"},"ApiKeyAuth":{"type":"apiKey","in":"header","name":"X-API-Key"}}},"tags":[{"name":"system","description":"Liveness + readiness probes"},{"name":"prices","description":"Latest Assessment per symbol"}]}