Every error is JSON with an error code and, usually, a message. Limits are per minute and per month on each plan, and keyless callers are capped per IP. A 429 tells you how long to wait in Retry-After. All routes live under /v1, and a retired route is announced in the changelog.
Error format
An error response is JSON with a machine-readable error code and, on most paths, a human message. Some errors add fields, such as tier, upgrade_url, hint or retry_after_seconds. Branch on the HTTP status and error. The message text can change.
One failure is not JSON: an unhandled server error returns 500 with a plain text body. Since 19 June 2026 every endpoint uses this shape, as the changelog records. HTTP status codes did not change then.
curl -i "https://api.goldprice.dev/v1/spot/FOO"{ "error": "invalid_symbol", "symbol": "FOO", "hint": "expected format BASE-QUOTE-CONTRACT, e.g. XAU-USD-SPOT"}A request that fails parameter validation returns 422 with error: validation_error and a details list. Each item has loc (where the field is), msg and type.
curl -i "https://api.goldprice.dev/v1/convert?from=XAU"{ "error": "validation_error", "message": "Request validation failed", "details": [ { "loc": [ "query", "to" ], "msg": "Field required", "type": "missing" } ]}Error codes
These are the codes an integration meets most. Each endpoint page lists its own: prices, carat and convert. The OpenAPI spec documents the error responses of every route.
| Status | error | When |
|---|---|---|
| 400 | invalid_symbol | The symbol is not in the form BASE-QUOTE-CONTRACT, for example XAU-USD-SPOT. |
| 400 | range_too_deep | A bars request reaches back further than the plan's intraday depth. The response carries max_depth_days. |
| 400 | interval_not_supported_for_symbol | The interval is not available for that symbol. |
| 401 | invalid_api_key | The bearer token is missing, malformed or revoked. A request that sends a key is never answered as keyless. |
| 403 | plan_gated | The plan does not include this route, interval or history depth. The response names your tier and an upgrade_url. |
| 404 | unknown_country | No physical prices for that country. The response lists the countries that have them. |
| 404 | not_found | The requested resource is unavailable or not found. |
| 422 | validation_error | A parameter is missing or invalid. details says which. |
| 429 | rate_limited | A keyless caller is over the per-IP hourly cap. |
| 429 | rate_limit_exceeded | Over the per-minute limit: the plan's for a key, the keyless limit for an IP. |
| 429 | quota_exceeded | The plan's monthly requests are used up. The quota resets when the next month starts. |
| 429 | stream_connection_limit | The plan's limit on open streams is reached. Retry-After is 5 seconds. |
| 503 | auth_unavailable | The key could not be checked. Retry-After is 30 seconds. |
| 503 | spot_unavailable, fx_unavailable | A price or exchange rate the request needs is temporarily unavailable. |
| 503 | stream_unavailable | The live feed behind a stream is down. |
| 500 | none | The operation failed internally. The body is plain text. |
Rate limits and plan limits
Three limits apply. Requests a minute and requests a month come from the plan. Callers with no key are limited per IP: 10 a minute and 100 an hour. Free plan limits has every plan as a table, and pricing has the prices.
| Plan | Requests a minute | Requests a month |
|---|---|---|
| No key (per IP) | 10, and 100 an hour | Not applicable |
| Free | 10 | 5,000 |
| Basic | 60 | 100,000 |
| Plus | 120 | 300,000 |
| Pro | 300 | 1,000,000 |
Free and keyless prices are 15 minutes late. Paid plans get live prices. History depth, intraday intervals, streaming and dealer prices also differ by plan, and a request outside the plan returns plan_gated.
The 429 response
A 429 carries five headers. X-RateLimit-Scope says which limit you hit.
| Header | Meaning |
|---|---|
X-RateLimit-Limit | The limit for the window named by the scope. |
X-RateLimit-Remaining | Requests left in that window. 0 on a 429. |
X-RateLimit-Reset | Unix time, in seconds, when the window resets. |
X-RateLimit-Scope | anon-ip-hour, tier-rate-minute or tier-quota-month. |
Retry-After | Seconds to wait before retrying. |
A monthly 429 also sends X-Quota-Exceeded: true. This is a keyless caller over the per-minute limit, run against the live API. The numbers change on every call.
x-ratelimit-limit: 10x-ratelimit-remaining: 0x-ratelimit-reset: 1794543300x-ratelimit-scope: tier-rate-minuteretry-after: 43{ "error": "rate_limit_exceeded", "tier": "free_2026", "limit_per_minute": 10, "retry_after_seconds": 43}Retry guidance
Data endpoints are read-only GET requests, so repeating one changes nothing.
| Response | What to do |
|---|---|
429 rate_limit_exceeded or rate_limited | Wait Retry-After seconds, then retry. The same number is in retry_after_seconds in the body. |
429 quota_exceeded | Do not retry automatically. The month's requests are used up. Retry-After is the time to the reset. Wait for it or change plan. |
429 stream_connection_limit | Close a stream you no longer need, or wait 5 seconds and reconnect. |
503 auth_unavailable | Retry after 30 seconds. The key was sent but could not be checked, and the request is not downgraded to keyless access. |
503 spot_unavailable, fx_unavailable, stream_unavailable | The failure is temporary. Retry later, and use Retry-After when it is present. |
| 500 | The operation failed internally. Retry later. |
| 400, 401, 403, 404, 422 | Do not retry the same request. Fix the symbol, parameters, key or plan first. |
A 200 can still carry old data. Every price has is_stale and computed_at, so check them before you show or store a price.
Availability and status
Measured uptime of the Gold Price API was 100% over the 30 days and 99.81% over the 90 days to 13 November 2026. That is a record of past performance, not a service level agreement. No plan has automatic credits, and any credit is at our discretion (Terms sections 9 and 16). Uptime history is public. Planned maintenance and incidents are announced on the status page and by email to paying customers.
GET /v1/health is a liveness probe and returns {"status":"ok"}. GET /v1/status is the readiness probe: it reports the deployed commit, Redis, how fresh each data collector is, and whether the gold, silver and copper bars are current. Neither needs a key.
curl "https://api.goldprice.dev/v1/health"{ "status": "ok"}Versioning and deprecation
Every route is under /v1, and the OpenAPI spec describes them. A change to a route, a field or an error shape is listed in the changelog, for example the error format change of 19 June 2026 and the retirement of the probability surface on 26 July 2026.
GET /v1/prices/history is deprecated. Use `/v1/bars` instead. Responses from the deprecated route carry Deprecation: true and a Link header with rel="successor-version" pointing at /v1/bars. No retirement date is set. A Sunset header will be sent once there is one.
curl -i "https://api.goldprice.dev/v1/prices/history?symbol=XAU-USD-SPOT&from=2026-11-01&to=2026-11-05"deprecation: truelink: <https://api.goldprice.dev/v1/bars>; rel="successor-version"Price changes take effect with 30 days' notice by email, and your current price holds through the notice period (Terms). Subscribers on older plans keep their plan, price and limits while they stay subscribed.