How to get live gold prices in PHP

Call the goldprice.dev API from PHP with native cURL, BCMath for exact math, retry-on-429, and a 60-second cache — no Composer.

Updated

To get live gold prices in PHP, send a GET to api.goldprice.dev/v1/spot/XAU-USD-SPOT with your API key in the X-API-Key header using native cURL — no Composer required. Decode the JSON, keep prices as strings, and use BCMath for exact decimal math. Cache per symbol for 60 seconds. The free tier covers 1,000 calls/month, no credit card. Tested June 2026 against PHP 8.3.

  1. 1.Get your goldprice.dev API key

    Sign up for a free account at goldprice.dev/onboarding. After email confirmation, your dashboard shows a key starting with goldprice_live_. Set it as an environment variable:

    export GOLDPRICE_API_KEY=goldprice_live_replace_with_your_key_here

    The free tier includes 1,000 calls/month and 13 supported currencies. No credit card required.

    Get your free API key1,000 calls/mo, no credit card
    Sign up free →
  2. 2.Confirm the required extensions

    No Composer packages are needed — curl and json ship with PHP 8, and bcmath is a standard extension for exact decimal math. Confirm all three are enabled:

    BASH · shell
    php -m | grep -E '^(curl|json|bcmath)$'
    # curl
    # json
    # bcmath   <- if missing, install php8.3-bcmath (or enable in php.ini)
  3. 3.Write the goldprice client

    Save this as goldprice.php. spot() caches per symbol for 60 seconds in a static array, retries up to 3 times on 429 with exponential backoff, and returns the decoded array with prices left as strings so BCMath can do exact arithmetic.

    PHP · goldprice.php
    <?php
    // php: goldprice.php — native cURL + BCMath, no Composer
    // curl-equivalent: https://api.goldprice.dev/v1/spot/XAU-USD-SPOT
    declare(strict_types=1);
    
    const API_BASE = 'https://api.goldprice.dev';
    const TIMEOUT  = 5;   // seconds
    const TTL      = 60;  // seconds
    
    final class RateLimitedException extends RuntimeException {}
    
    final class GoldpriceClient
    {
        private string $apiKey;
    
        /** @var array<string, array{data: array, expires: int}> */
        private static array $cache = [];
    
        public function __construct()
        {
            $key = getenv('GOLDPRICE_API_KEY');
            if ($key === false || $key === '') {
                throw new RuntimeException('GOLDPRICE_API_KEY not set');
            }
            $this->apiKey = $key;
        }
    
        /**
         * Fetch live spot price. Cached 60s per symbol, retries 3x on 429.
         * Prices stay as strings for BCMath.
         */
        public function spot(string $symbol = 'XAU-USD-SPOT'): array
        {
            $now = time();
            if (isset(self::$cache[$symbol]) && self::$cache[$symbol]['expires'] > $now) {
                return self::$cache[$symbol]['data'];
            }
    
            for ($attempt = 0; $attempt < 3; $attempt++) {
                try {
                    $data = $this->fetch($symbol);
                    self::$cache[$symbol] = ['data' => $data, 'expires' => $now + TTL];
                    return $data;
                } catch (RateLimitedException) {
                    sleep(1 << $attempt); // 1s, 2s, 4s
                }
            }
            throw new RateLimitedException('goldprice: 429 after 3 retries');
        }
    
        private function fetch(string $symbol): array
        {
            $ch = curl_init(API_BASE . '/v1/spot/' . rawurlencode($symbol));
            curl_setopt_array($ch, [
                CURLOPT_RETURNTRANSFER => true,
                CURLOPT_TIMEOUT        => TIMEOUT,
                CURLOPT_HTTPHEADER     => ['X-API-Key: ' . $this->apiKey],
            ]);
            $body = curl_exec($ch);
            $code = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
            curl_close($ch);
    
            if ($code === 429) {
                throw new RateLimitedException('rate limited');
            }
            if ($code !== 200 || $body === false) {
                throw new RuntimeException("goldprice: unexpected status {$code}");
            }
    
            return json_decode((string) $body, true, 512, JSON_THROW_ON_ERROR);
        }
    }
    
    $client = new GoldpriceClient();
    $r = $client->spot('XAU-USD-SPOT');
    
    // Prices are strings; use BCMath for exact math (e.g. price for 5 oz):
    $fiveOz = bcmul($r['price'], '5', 4);
    
    printf("%s-%s: %s (computed %s)\n", $r['symbol'], $r['quote_currency'], $r['price'], $r['computed_at']);
    printf("5 troy oz = %s %s\n", $fiveOz, $r['quote_currency']);
  4. 4.Run it

    From the same shell where you exported GOLDPRICE_API_KEY, run the file with the CLI. The first call hits the API; further spot() calls in the same process within 60 seconds return the cached array. The static cache lives per process — back it with APCu or Redis to share across requests under PHP-FPM.

    BASH · shell
    php goldprice.php
    # XAU-USD: 4726.01 (computed 2026-06-26T04:49:01.706844+00:00)
    # 5 troy oz = 23630.0500 USD
  5. 5.Switch currency or metal

    Symbols are METAL-CURRENCY-SPOT. Pass any of the 13 supported currencies to spot(). Each unique symbol caches independently in the static array, so a page rendering 5 symbols makes at most 5 uncached calls and reuses them on the next render within the window.

    PHP · goldprice.php · multi-currency
    $client = new GoldpriceClient();
    
    // USD spot — default
    $client->spot('XAU-USD-SPOT');
    
    // INR — Indian gold market (largest retail gold-search globally)
    $client->spot('XAU-INR-SPOT');
    
    // EUR — European pricing surfaces
    $client->spot('XAU-EUR-SPOT');
    
    // Silver in USD
    $client->spot('XAG-USD-SPOT');

Expected output

The API returns this shape:

JSON · GET /v1/spot/XAU-USD-SPOT
{
  "symbol": "XAU",
  "quote_currency": "USD",
  "unit": "troy_ounce",
  "contract_type": "spot",
  "price": "4726.01",
  "bid": "4726.68",
  "ask": "4725.33",
  "is_stale": false,
  "divergence_flag": false,
  "computed_at": "2026-06-26T04:49:01.706844+00:00",
  "sources": [
    {
      "source": "wgc.fsapi.usd",
      "display_name": "Continuous spot reference (live spot)",
      "price": "4726.01",
      "is_stale": false,
      "timestamp": "2026-06-26T04:47:24+00:00"
    }
    /* + cmc.paxg + cmc.xaut entries — full sources[] in live response */
  ],
  "value_stale": false,
  "price_gram_24k": "151.9447",
  "open_price": "4681.305",
  "high_price": "4729.56",
  "low_price": "4672.927",
  "prev_close_price": "4681.302",
  "ch": "44.6260",
  "chp": "0.9533",
  "open_time": 1782259200
}

The CLI prints two lines: the symbol/currency/price/timestamp, and the BCMath product for 5 troy ounces. The decoded array is what your downstream code consumes — prices stay as strings.

Common errors

CodeSymptomFix
401RuntimeException: goldprice: unexpected status 401API key missing or invalid. Confirm the variable reaches PHP: php -r 'echo getenv("GOLDPRICE_API_KEY");'. Under PHP-FPM, CLI env vars are not shared — set the key via fastcgi_param or your process manager. If empty, re-copy it from your dashboard.
429Three retries with backoff all return 429, then RateLimitedException is thrownYou hit the per-minute cap. The 60-second cache prevents this for steady traffic; under PHP-FPM the static cache is per request, so promote it to APCu (apcu_fetch / apcu_store) to share across requests. For sustained volume, see /pricing for Basic and Pro tiers.
N/ARounding errors when you cast prices to float for mathNever cast the price to (float) before arithmetic — PHP floats lose precision on summation. Keep the value as the string the API returns and use bcadd, bcmul, and bccomp with an explicit scale. Enable the bcmath extension if those functions are undefined.

FAQ

How often does the price update?

The spot endpoint refreshes every 60 seconds (live oracle + continuous spot reference + futures settlement aggregation). The 60-second TTL above matches that cadence — repeated spot() calls within the window return the cached array without an HTTP round-trip.

Will this fit the free tier with auto-refresh?

It depends on your traffic. A page that calls the API on every uncached request can exceed the 1,000/month free tier quickly under load — share the cache via APCu so one fetch serves many requests. Low-traffic sites calling a few times an hour stay well inside the free tier. For continuous high-volume use, see /pricing for Basic and Pro tiers.

Can I use this commercially?

The Free tier is for personal use. For commercial use (apps you ship, services you sell, internal production systems), upgrade to Basic or Pro — see /pricing.

Why BCMath instead of float?

PHP floats are IEEE-754 doubles: 0.1 + 0.2 is 0.30000000000000004, and errors accumulate across many operations. Gold prices carry 6+ decimal places, so summing thousands of values in float drifts by basis points. BCMath works on the decimal strings directly with a fixed scale, so totals stay exact.

Do I really not need Composer or Guzzle?

Correct — curl and json are bundled with PHP 8, so this client has zero install step beyond the API key. If you already run Guzzle, swap curl_* for a Guzzle client with a retry middleware; the request shape (a GET with the X-API-Key header) is identical.

What metals are supported?

Gold (XAU), silver (XAG), copper (HG), platinum (XPT), palladium (XPD). Substitute the metal code in the symbol, e.g. $client->spot("XAG-USD-SPOT").

Can I have Claude write this for me?

Yes. Open Claude Desktop with the goldprice.dev MCP server installed and ask: Build a PHP 8 client for the goldprice.dev /v1/spot endpoint. Use native cURL (no Composer), a 60-second per-symbol static cache, retry-on-429 with exponential backoff, and keep prices as strings for BCMath. Read the API key from GOLDPRICE_API_KEY. The MCP server gives Claude the live schema, so generated code uses correct field names on the first try.

Going further

  • Promote the static cache to apcu_store / apcu_fetch so one fetch serves every PHP-FPM worker for the full 60 seconds
  • Add a history() method for /v1/history/XAU-USD-SPOT?days=30 and chart the rows with your template engine
  • Wrap spot() in a thin Laravel or Symfony service and bind it in the container for dependency injection
  • Add an If-None-Match / ETag check if you poll the same symbol from many workers, to skip unchanged payloads
  • Format prices for display with bcdiv against troy-ounce-to-gram factors for a per-gram retail view

Next steps

Try the same setup in a different platform:

Browse all 8 tutorials →