Gold Price API in PHP: cURL, Guzzle and a WordPress Shortcode
Use a gold price API in PHP with cURL or Guzzle, then add a cached [gold_price] shortcode to WordPress with transients and WP-Cron, or choose a no-code widget.
Gold price API in PHP: the basics
A gold price API gives your PHP code the current gold price as JSON, for product prices, a price line in a theme or a daily report. This tutorial covers plain PHP with cURL, the Guzzle HTTP client and WordPress, where a cached [gold_price] shortcode refreshed by WP-Cron puts the price into any post or page. It uses MetalAPI, a JSON REST API for precious metals, industrial metals, energy, currencies and crypto.
You need an API key from the MetalAPI dashboard. Keys start with mk_live_, and the Free plan includes 100 requests a month with daily price updates, without a credit card. Keep the key out of your code: in an environment variable for plain PHP, or in wp-config.php for WordPress.
Every endpoint is a GET request under https://metalapi.com/api/v1. Send the key in the X-API-Key header (or as Authorization: Bearer) rather than the api_key query parameter, so it stays out of access logs. A /latest response with the documentation's example values:
{
"success": true,
"timestamp": 1792834892,
"date": "2026-10-24",
"base": "USD",
"rates": {
"XAU": 0.0002501319,
"USDXAU": 3997.89,
"EUR": 0.8925,
"USDEUR": 1.1204
},
"rate_limit": { "...": "..." }
}rates.USDXAU is the price of one troy ounce of gold in dollars; rates.XAU is its inverse, ounces per dollar, so never show it as a price. rates.USDEUR is the price of one euro in dollars, which the WordPress plugin uses to convert. timestamp is the time of the price data in Unix seconds.
How to get the gold price in PHP with cURL
The cURL extension ships with almost every PHP installation. This function sends the key in a header, sets timeouts and turns network failures and API errors into exceptions:
<?php
// PHP 8+ with the curl extension. Run: METALAPI_KEY=mk_live_... php gold.php
function metalapi(string $path, array $params = []): array
{
$ch = curl_init('https://metalapi.com/api/v1/' . $path . '?' . http_build_query($params));
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CONNECTTIMEOUT => 5,
CURLOPT_TIMEOUT => 10,
CURLOPT_HTTPHEADER => ['X-API-Key: ' . getenv('METALAPI_KEY')],
]);
$body = curl_exec($ch);
if ($body === false) {
throw new RuntimeException('Network error: ' . curl_error($ch));
}
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
$data = json_decode($body, true);
if (empty($data['success'])) {
$code = $data['error']['code'] ?? $status;
$info = $data['error']['info'] ?? "HTTP $status";
throw new RuntimeException("MetalAPI $code: $info", $code);
}
return $data;
}
$data = metalapi('latest', ['base' => 'USD', 'currencies' => 'XAU']);
printf("Gold: %.2f USD per troy oz\n", $data['rates']['USDXAU']);
printf("Time: %s UTC\n", gmdate('Y-m-d H:i', $data['timestamp']));With the example values it prints Gold: 3997.89 USD per troy oz. The exception code is MetalAPI's error code: retry 104 (the per-minute limit) after the Retry-After seconds and 500 or 503 with backoff, but fix the cause of codes such as 102 (invalid key), 105 (monthly quota used up) or 106 (endpoint not in your plan). The error handling guide lists every code.
Using Guzzle instead of cURL
In a Composer project, Guzzle does the same in fewer lines. Keep the trailing slash on base_uri and request relative paths such as latest, because Guzzle resolves /latest against the domain root. And turn off http_errors, so error responses return MetalAPI's JSON body instead of throwing:
<?php
// composer require guzzlehttp/guzzle
require __DIR__ . '/vendor/autoload.php';
$client = new GuzzleHttp\Client([
'base_uri' => 'https://metalapi.com/api/v1/',
'timeout' => 10,
'http_errors' => false,
'headers' => ['X-API-Key' => (string) getenv('METALAPI_KEY')],
]);
$res = $client->get('latest', ['query' => ['base' => 'EUR', 'currencies' => 'XAU', 'unit' => 'gram']]);
$data = json_decode((string) $res->getBody(), true);
if (empty($data['success'])) {
$code = $data['error']['code'] ?? $res->getStatusCode();
throw new RuntimeException("MetalAPI $code: " . ($data['error']['info'] ?? $res->getReasonPhrase()), $code);
}
printf("Gold: %.2f EUR per gram\n", $data['rates']['EURXAU']);Timeouts and other network failures still throw a ConnectException, so catch it where a failure must not break the page.
Gold price in WordPress: a cached [gold_price] shortcode
Inside WordPress, use wp_remote_get() from the built-in HTTP API instead. Two design rules keep the shortcode fast and cheap:
- Don't call the API while a page renders. The plugin keeps prices in a transient that WP-Cron refreshes in the background. Visitors only trigger a request when the transient is missing, for example right after activation.
- Request every currency in one call. With base
USDandcurrencies=XAU,EUR,GBP, the response holds the dollar price of gold and of each currency from the same snapshot, so gold in euros isUSDXAU ÷ USDEUR, and grams follow from 31.1034768 grams per troy ounce. One request per refresh covers every currency and unit.
Transients can vanish before they expire, for example when a persistent object cache evicts them, so the plugin also keeps the last good prices in an option. If a refresh fails, it logs the error and serves that copy for five minutes. First add the key to wp-config.php, above the line that says to stop editing:
define( 'METALAPI_KEY', 'mk_live_your_key_here' );Then save the plugin as wp-content/plugins/metalapi-gold-price/metalapi-gold-price.php and activate it under Plugins:
<?php
/**
* Plugin Name: MetalAPI Gold Price
* Description: [gold_price] shortcode, cached in a transient and refreshed by WP-Cron.
* Requires PHP: 8.0
*/
defined( 'ABSPATH' ) || exit;
const MGP_INTERVAL = 600; // seconds. Standard: 600, Lite: 3600, Free: 86400
const MGP_CURRENCIES = [ 'EUR', 'GBP' ]; // besides USD, every currency your shortcodes use
const MGP_GRAMS = [ 'troy_oz' => 31.1034768, 'gram' => 1, 'kilogram' => 1000 ];
function mgp_refresh(): ?array {
if ( ! defined( 'METALAPI_KEY' ) ) {
return null;
}
$url = add_query_arg(
[ 'base' => 'USD', 'currencies' => implode( ',', [ 'XAU', ...MGP_CURRENCIES ] ) ],
'https://metalapi.com/api/v1/latest'
);
$res = wp_remote_get( $url, [ 'timeout' => 8, 'headers' => [ 'X-API-Key' => METALAPI_KEY ] ] );
$data = is_wp_error( $res ) ? null : json_decode( wp_remote_retrieve_body( $res ), true );
if ( empty( $data['success'] ) ) {
error_log( 'MetalAPI refresh failed: ' . ( is_wp_error( $res ) ? $res->get_error_message() : wp_remote_retrieve_body( $res ) ) );
$last = get_option( 'mgp_prices_last' );
if ( $last ) {
set_transient( 'mgp_prices', $last, 5 * MINUTE_IN_SECONDS ); // back off, keep the last good prices
}
return $last ?: null;
}
$per_oz = [ 'USD' => $data['rates']['USDXAU'] ];
foreach ( MGP_CURRENCIES as $cur ) {
if ( ! empty( $data['rates'][ "USD$cur" ] ) ) {
$per_oz[ $cur ] = $data['rates']['USDXAU'] / $data['rates'][ "USD$cur" ];
}
}
$prices = [ 'per_oz' => $per_oz, 'time' => $data['timestamp'] ];
set_transient( 'mgp_prices', $prices, 2 * MGP_INTERVAL ); // survives one late cron run
update_option( 'mgp_prices_last', $prices, false ); // fallback if the transient is evicted
return $prices;
}
function mgp_prices(): ?array {
return get_transient( 'mgp_prices' ) ?: mgp_refresh();
}
add_shortcode( 'gold_price', function ( $atts ): string {
$a = shortcode_atts( [ 'currency' => 'USD', 'unit' => 'troy_oz', 'decimals' => 2 ], $atts, 'gold_price' );
$cur = strtoupper( $a['currency'] );
$unit = strtolower( $a['unit'] );
$p = mgp_prices();
if ( ! $p || ! isset( $p['per_oz'][ $cur ] ) || ! array_key_exists( $unit, MGP_GRAMS ) ) {
return '';
}
$price = $p['per_oz'][ $cur ] / MGP_GRAMS['troy_oz'] * MGP_GRAMS[ $unit ];
return sprintf(
'<span class="gold-price">%s %s / %s</span> <small>%s</small>',
esc_html( number_format_i18n( $price, absint( $a['decimals'] ) ) ),
esc_html( $cur ),
esc_html( str_replace( '_', ' ', $unit ) ),
esc_html( wp_date( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), $p['time'] ) )
);
} );
// WP-Cron: refresh in the background once per interval.
add_filter( 'cron_schedules', function ( array $schedules ): array {
$schedules['mgp_interval'] = [ 'interval' => MGP_INTERVAL, 'display' => 'MetalAPI refresh' ];
return $schedules;
} );
add_action( 'mgp_refresh_event', 'mgp_refresh' );
register_activation_hook( __FILE__, function (): void {
if ( ! wp_next_scheduled( 'mgp_refresh_event' ) ) {
wp_schedule_event( time(), 'mgp_interval', 'mgp_refresh_event' );
}
} );
register_deactivation_hook( __FILE__, function (): void {
wp_clear_scheduled_hook( 'mgp_refresh_event' );
} );Use it in any post, page or Shortcode block as [gold_price] or [gold_price currency="EUR" unit="gram"]. With the example rates, the second renders 114.72 EUR / gram: 3997.89 ÷ 1.1204 gives 3,568.27 euros per troy ounce, and dividing by 31.1034768 gives the gram price. A currency missing from MGP_CURRENCIES prints nothing, so list every currency your shortcodes use.
Refreshing gold prices with WP-Cron
The activation hook schedules the refresh with the custom interval registered through cron_schedules, and deactivation removes it. Set MGP_INTERVAL to your plan's update interval, or longer when the quota requires it:
| Plan | MGP_INTERVAL | Requests per 31-day month | Monthly quota |
|---|---|---|---|
| Free (daily updates) | 86400 | 31 | 100 |
| Lite (30-minute updates) | 3600 | 744 | 1,000 |
| Standard (10-minute updates) | 600 | 4,464 | 10,000 |
WP-Cron is not a real scheduler: it runs due events when someone visits the site, so on a quiet site a refresh can be late; the transient's double lifetime and the stored copy absorb that. For exact timing, add define( 'DISABLE_WP_CRON', true ); to wp-config.php and run due events from the system cron with WP-CLI:
*/10 * * * * cd /var/www/html && wp cron event run --due-now > /dev/null 2>&1Plan for page caching too: a full-page cache stores the shortcode's output with the page, so visitors see the price from when the page was cached. Keep the cache lifetime of price pages at or below your refresh interval.
When a no-code widget is the better choice
The shortcode is the right tool when the price must be part of your HTML or when PHP needs the number, for example to reprice products. If you only want to show prices, a MetalAPI widget is simpler:
- No API key and no PHP. You build the widget in the dashboard and paste its embed code into a Custom HTML block. Later changes to symbols or layout need no edit on your site.
- No quota. Widget page views do not use your monthly requests, and every plan includes widgets: 1 on Free, 3 on Lite and 10 on Standard.
- Safe with page caching. Prices load inside an iframe, so a cached page never freezes them.
<div id="metalapi-widget-w_abc123"></div>
<script src="https://metalapi.com/js/widget.js" data-widget="w_abc123" async></script>If your host strips <script> tags, use the iframe version of the embed. How to add a live gold price widget covers the builder, WordPress placement and plan limits; the widget page shows the layouts.
Next steps
Create a free account, add your key to wp-config.php and install the plugin with MGP_INTERVAL set to 86400: a daily refresh uses 31 of the Free plan's 100 monthly requests. When you need fresher prices, set the interval to your new plan's update interval and compare plans on the pricing page. For Node.js backends, Gold price API in JavaScript shows the same patterns with Express and Next.js.
FAQ
Where should I store the API key in WordPress?
In wp-config.php as a constant, or in an environment variable read there. Never in theme files, which often end up in Git, or in JavaScript, which every visitor can read. PHP requests send no Origin or Referer header, so even a domain-restricted key works from the server.
Why does the shortcode show the same gold price all weekend?
Spot gold does not trade from Friday 17:00 to Sunday 17:00 New York time, so the API returns the last price until markets reopen. On the Free plan, prices also change only once a day.
How many API requests will the plugin use?
One per refresh, however many visitors, pages or shortcodes you have, because one call returns every currency and units are converted locally: about 4,500 a month at a 10-minute interval.
Can I use the gold price in WooCommerce product prices?
Yes. Call mgp_prices() from PHP, for example in a woocommerce_product_get_price filter, and multiply the price per gram by the product's weight and purity. Store the gold price used with each order, so invoices don't change later.
Do I need cURL or Guzzle in WordPress?
No. wp_remote_get() uses the WordPress HTTP API, which picks an available transport and honours proxy settings. Keep cURL or Guzzle for plain PHP and frameworks such as Laravel or Symfony.
Try it with your own data
Create a free account and make your first request in under two minutes. 100 requests a month, no credit card.
Keep reading
XAU/USD Meaning: What XAU, XAG, XPT and XPD Stand For
XAU/USD meaning explained: why gold has an ISO 4217 currency code, what XAG, XPT and XPD stand for, and how to read quote direction in prices and API data.
Free Gold Price API: What 100 Requests a Month Can Do
A free gold price API, honestly explained: what MetalAPI's Free plan includes, how to stretch 100 requests a month with caching and batching, when to upgrade.
Gold Price API in Python: Fetch, Cache and Plot Prices
Use a gold price API in Python: get live gold prices with requests, handle errors and retries, cache responses, and plot a pandas time series with matplotlib.