Subtle diagonal pinstripe pattern in light grey and cream, giving an impression of printed editorial paper

Fetch External Data Safely with wp_remote_get

WordPress can retrieve JSON, XML, HTML and other remote resources through wp_remote_get(), part of the WordPress HTTP API. It is usually preferable to calling PHP’s cURL functions directly because WordPress selects an available transport, applies familiar request arguments and provides a consistent response format across different hosting environments.

A basic request is easy to write, but production code needs more than a URL. External services can be slow, return a 404 or 500 response, send malformed JSON, enforce rate limits or become temporarily unavailable. A suitable timeout and explicit error handling prevent a remote problem from slowing down every page load or producing misleading content.

This approach is useful for weather widgets, stock availability, exchange rates, shipping estimates, product feeds and WordPress integrations. For an Australian site, the location of the remote service also matters: a Sydney or Melbourne server may respond quickly, while an overseas API can add latency for visitors in Perth, Adelaide or regional areas.

Use case Suggested timeout Caching approach Typical handling
Small JSON API request 5–10 seconds Cache for several minutes Check for WP_Error, status and JSON validity
Currency or weather data 10 seconds Cache for 5–30 minutes Use the last valid value when appropriate
Large feed or XML document 15–20 seconds Cache with scheduled refresh Limit body size and validate the format
Administrative import 30–60 seconds Store imported records Run with WP-Cron or a queue rather than during a page request
Optional page enhancement 3–5 seconds Short cache or no cache Fail silently so the page remains usable

Why the WordPress HTTP API is a good fit

wp_remote_get() accepts a URL and an optional array of arguments, then returns either a response array or a WP_Error object. The response array commonly contains headers, body, response and cookies. This abstraction means plugin and theme code does not need to manage transport selection, response parsing or low-level cURL options.

The simplest request looks like this:

$response = wp_remote_get(
    'https://api.example.com/items',
    array(
        'timeout' => 10,
    )
);

if ( is_wp_error( $response ) ) {
    return;
}

$body = wp_remote_retrieve_body( $response );

The default timeout may be unsuitable for a page that calls a slow third-party service. A visitor should not have to wait for an unrelated API, especially on mobile broadband in regional Australia or when a provider is experiencing an outage. Set a timeout based on the feature’s importance and expected response time rather than choosing a large value simply to make failures less visible.

Use wp_remote_retrieve_body() and wp_remote_retrieve_response_code() instead of reading array keys directly where practical. These helper functions make the intent clearer and reduce assumptions about the response structure. For headers, wp_remote_retrieve_header() can retrieve values such as content-type or retry-after.

Setting timeout and request arguments

A useful request often includes a timeout, redirection limit, SSL verification and a user agent. WordPress verifies SSL certificates by default, and that protection should remain enabled. Disabling sslverify can expose credentials and data to interception, so it is not a proper fix for an incorrectly configured certificate.

$args = array(
    'timeout'     => 8,
    'redirection' => 3,
    'sslverify'   => true,
    'headers'     => array(
        'Accept'     => 'application/json',
        'User-Agent' => 'Example WordPress Plugin/' . MY_PLUGIN_VERSION,
    ),
);

$response = wp_remote_get( $url, $args );

Do not send an API key in a query string unless the provider requires it. Query strings can appear in logs, browser history, proxy records and analytics systems. An authenticated API normally expects a header such as Authorization or X-API-Key. Keep secrets in server-side configuration and never print the complete request URL in a public error message.

$args['headers']['Authorization'] = 'Bearer ' . $api_token;

$response = wp_remote_get( $url, $args );

A timeout is measured in seconds and is not a guarantee that the complete transaction will finish within that exact period. DNS lookup, connection setup, redirects and transfer time can all contribute. A 5 second timeout may be suitable for an optional dashboard panel, whereas an overnight catalogue import can use a longer value when it runs through a scheduled task.

Avoid making several sequential remote requests during one front-end page load. Three requests with ten-second timeouts can create an unacceptable worst-case delay. Combine data at the provider if possible, cache each result, or move the work to WP-Cron. Australian stores that receive traffic across different time zones should also avoid relying on a visitor’s request to perform a daily refresh.

Checking errors and HTTP status codes

is_wp_error() must be checked before treating the return value as a response. A WP_Error indicates a transport or request-level problem, such as a DNS failure, connection refusal, timeout or invalid URL. It does not represent an HTTP status such as 404 or 500; those statuses are returned in a normal response array and require a separate check.

$response = wp_remote_get( $url, $args );

if ( is_wp_error( $response ) ) {
    error_log( 'Remote request failed: ' . $response->get_error_message() );
    return new WP_Error(
        'external_service_unavailable',
        'The external service could not be reached.'
    );
}

$status_code = wp_remote_retrieve_response_code( $response );

if ( $status_code < 200 || $status_code >= 300 ) {
    error_log( 'External service returned HTTP ' . $status_code );
    return new WP_Error(
        'external_service_http_error',
        'The external service returned an unexpected response.'
    );
}

A successful transport does not mean the requested operation succeeded. A 200 response may contain an error object, an HTML login page or an empty body. Check the status code before decoding data, and consider accepting only the statuses documented by the provider. A 204 response, for example, has no body and should not be passed to code that expects a JSON document.

Log enough information to investigate failures without recording personal information or authentication data. A private log can include the host, endpoint name, status code, elapsed time and WordPress error code. Do not expose raw provider messages to site visitors, because they may reveal internal URLs, tokens or implementation details.

For an optional feature, return a safe fallback rather than breaking the page:

if ( is_wp_error( $response ) ) {
    return array(
        'available' => false,
        'items'     => array(),
    );
}

A required checkout or fulfilment operation needs stricter behaviour. It should stop before creating an order if a live shipping quote cannot be confirmed. A non-essential homepage exchange-rate widget can display a cached value or remain hidden. Error policy should follow the business impact of stale or missing data.

Decoding and validating external data

After checking the response status, retrieve the body and decode it according to the provider’s documented format. For JSON, use json_decode() with the true argument when an associative array is convenient. Check json_last_error() or use JSON_THROW_ON_ERROR when the supported PHP version and surrounding error strategy make exceptions appropriate.

$body = wp_remote_retrieve_body( $response );

$data = json_decode( $body, true );

if ( JSON_ERROR_NONE !== json_last_error() || ! is_array( $data ) ) {
    error_log( 'External service returned invalid JSON.' );

    return new WP_Error(
        'invalid_external_data',
        'The external service returned invalid data.'
    );
}

if ( empty( $data['items'] ) || ! is_array( $data['items'] ) ) {
    return new WP_Error(
        'unexpected_external_schema',
        'The external response did not have the expected structure.'
    );
}

Validate individual fields before using them. Confirm that prices are numeric, identifiers have an expected type, dates can be parsed and URLs use an allowed scheme. Remote content should never be trusted merely because it arrived over HTTPS. If text is displayed in HTML, escape it at output with functions such as esc_html(), esc_attr() or esc_url().

Where the provider supplies a Content-Type header, inspect it as an additional sanity check. It is not a substitute for parsing and validation, since providers sometimes configure headers incorrectly. A response claiming to be JSON can still be malformed, incomplete or structurally different after an API upgrade.

For feeds containing customer, health or account information, collect and retain only what the site needs. The Privacy Act 1988 and the Australian Privacy Principles can apply to personal information handled by an Australian organisation. Sending names, addresses or order data to an overseas provider may require a review of privacy notices, contracts, consent and cross-border disclosure obligations.

Caching, security and production use

Caching is the most effective way to reduce repeated remote calls. The WordPress Transients API is suitable for data that can expire naturally, such as weather, exchange rates and public product availability. Store the validated result, not an untrusted raw response, and choose a key that identifies the request parameters.

$cache_key = 'partner_items_v1';
$cached    = get_transient( $cache_key );

if ( false !== $cached ) {
    return $cached;
}

$response = wp_remote_get( $url, $args );

if ( is_wp_error( $response ) ) {
    return array();
}

$status_code = wp_remote_retrieve_response_code( $response );

if ( 200 !== $status_code ) {
    return array();
}

$data = json_decode( wp_remote_retrieve_body( $response ), true );

if ( ! is_array( $data ) ) {
    return array();
}

set_transient( $cache_key, $data, 15 * MINUTE_IN_SECONDS );

return $data;

Do not let anonymous visitors submit arbitrary URLs that your server then fetches. That pattern can create a server-side request forgery vulnerability, allowing access to internal services or cloud metadata endpoints. Use a fixed endpoint or an allowlist of approved hostnames, and validate any variable path or identifier before constructing the URL.

For recurring imports, schedule a WP-Cron event or use a real system cron job on higher-traffic sites. WP-Cron runs when WordPress receives traffic, so a low-traffic regional site may refresh later than expected. A managed host in Sydney, a WooCommerce store serving Melbourne customers and a national retailer with a server in Singapore may each have different latency and scheduling characteristics. Store the last successful result so a brief provider outage does not erase usable catalogue or shipping data.

Use exponential backoff only for appropriate failures and avoid aggressive retries during a normal page request. A short first retry can help with a transient network issue, but repeated attempts multiply latency and may trigger provider rate limits. Respect Retry-After when supplied, and apply a clear maximum. Australian providers and international APIs may enforce quotas based on account, IP address or region.

Finally, consider how external data affects pricing, tax and consumer communications. A WooCommerce price or delivery promise should not silently rely on stale information when Australian Consumer Law obligations may be relevant. For products subject to GST, shipping rules or availability commitments, save the source timestamp and make failures visible to administrators. With bounded timeouts, validated responses, sensible caching and a deliberate fallback, wp_remote_get() becomes a dependable part of a WordPress integration rather than a single point of failure.