{"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 30 currencies. Responses are served from a cache refreshed hourly by a scheduled job; API requests never call upstream providers. Authenticate every /v1/* request with an X-API-Key header.","contact":{"name":"Bullion API","url":"https://bullionapi.dev","email":"hello@bullionapi.dev"}},"servers":[{"url":"https://api.bullionapi.dev"}],"security":[{"ApiKeyAuth":[]}],"tags":[{"name":"prices","description":"Gold price data (API key required)"},{"name":"account","description":"Account-scoped usage (session cookie)"},{"name":"meta","description":"Service metadata (public)"}],"paths":{"/v1/latest":{"get":{"tags":["prices"],"summary":"Latest gold spot price","description":"Returns the most recent gold spot price and the FX rates needed to convert it into the requested base currency. Cached for 1 hour. Usage increments after API-key and quota checks but before endpoint validation.","operationId":"getLatest","parameters":[{"$ref":"#/components/parameters/Currency"}],"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"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LatestResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/QuotaExceeded"},"500":{"$ref":"#/components/responses/InternalError"}}},"post":{"tags":["prices"],"summary":"Not supported — read-only API","operationId":"postLatestNotAllowed","responses":{"405":{"$ref":"#/components/responses/MethodNotAllowed"}}}},"/v1/timeseries":{"get":{"tags":["prices"],"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.","operationId":"getTimeseries","parameters":[{"$ref":"#/components/parameters/Currency"},{"$ref":"#/components/parameters/StartDate"},{"$ref":"#/components/parameters/EndDate"}],"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"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TimeseriesResponse"}}}},"400":{"description":"Invalid query parameters.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TimeseriesBadRequestError"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/QuotaExceeded"},"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.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UsageResponse"}}}},"401":{"description":"Not signed in.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"},"example":{"error":"Not signed in","code":"unauthenticated"}}}},"500":{"$ref":"#/components/responses/InternalError"}}}},"/health":{"get":{"tags":["meta"],"summary":"Service health","description":"Public, no auth. Reports database reachability and cache freshness. Returns 503 when the cache has not been refreshed within the staleness budget.","operationId":"getHealth","security":[],"responses":{"200":{"description":"Healthy: database reachable and cache fresh.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HealthResponse"}}}},"503":{"description":"Degraded: database unreachable or cache stale.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HealthResponse"}}}}}}}},"components":{"securitySchemes":{"ApiKeyAuth":{"type":"apiKey","in":"header","name":"X-API-Key","description":"API key created in the dashboard, prefixed `bullion_`."},"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","default":"USD","example":"EUR"},"description":"ISO 4217 base currency, case-insensitive. Supported: USD, EUR, GBP, JPY, CHF, CAD, AUD, NZD, CNY, HKD, SGD, SEK, NOK, DKK, PLN, CZK, HUF, TRY, ZAR, INR, KRW, BRL, MXN, AED, SAR, ILS, THB, IDR, MYR, PHP. Any other code is accepted and returns 200 with empty `metals` / `currencies` maps until that currency is seeded."},"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."}},"responses":{"Unauthorized":{"description":"Missing or invalid API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"},"examples":{"missing":{"value":{"error":"Missing X-API-Key header","code":"missing_api_key"}},"invalid":{"value":{"error":"Invalid or revoked API key","code":"invalid_api_key"}}}}}},"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"}},"retry-after":{"description":"Seconds until the quota resets.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/QuotaExceededError"}}}},"MethodNotAllowed":{"description":"The API is read-only; writes are not supported.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"},"example":{"error":"Method not allowed","code":"method_not_allowed"}}}},"InternalError":{"description":"Server configuration error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"},"example":{"error":"DATABASE_URL not set","code":"config_error"}}}}},"schemas":{"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","const":"toz","description":"Troy ounces."},"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":"FX rates from the base currency keyed by target currency.","example":{"EUR":0.9134,"GBP":0.7783,"JPY":154.22,"CAD":1.3715}},"timestamps":{"type":"object","required":["metal","currency"],"properties":{"metal":{"type":"string","description":"ISO 8601 fetch time of the metal price, or empty string if unseeded."},"currency":{"type":"string","description":"ISO 8601 fetch time of the FX rates, or empty string if unseeded."}}}}},"TimeseriesResponse":{"type":"object","required":["status","currency","unit","start_date","end_date","rates"],"properties":{"status":{"type":"string","const":"success"},"currency":{"type":"string"},"unit":{"type":"string","const":"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."}}},"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}}}},"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"],"properties":{"status":{"type":"string","enum":["ok","degraded"]},"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."}}},"ErrorEnvelope":{"type":"object","required":["error","code"],"properties":{"error":{"type":"string","description":"Human-readable message."},"code":{"type":"string","description":"Machine-readable error code."}}},"TimeseriesBadRequestError":{"type":"object","required":["error","code"],"properties":{"error":{"type":"string"},"code":{"type":"string","enum":["missing_params","invalid_date","invalid_range"]}}},"QuotaExceededError":{"type":"object","required":["error","message","plan","used","limit","reset_at"],"properties":{"error":{"type":"string","const":"quota_exceeded"},"message":{"type":"string","example":"Monthly request limit reached for free plan (30/30)."},"plan":{"type":"string","enum":["free","pro","enterprise"]},"used":{"type":"integer"},"limit":{"type":"integer"},"reset_at":{"type":"string","format":"date-time"}}}}}}