How to consume external APIs with wp_remote_get and cache in transients in WordPress

Contents

Introduction

Consuming external APIs from WordPress is a common need: you might pull weather data, remote posts, currency rates, or any third-party JSON/XML. Making live HTTP requests on every page load is slow and fragile. WordPress provides a built-in HTTP API (wp_remote_get, wp_remote_post, etc.) and a simple caching mechanism (transients) which together let you fetch remote data reliably and cache it to reduce latency and API usage.

What this guide covers

  • How to safely use wp_remote_get to call external APIs
  • How to cache responses with transients (and site transients for multisite)
  • Error handling, response validation and sanitization
  • Advanced patterns: conditional requests (ETag / If-Modified-Since), stale-while-revalidate and background refresh using WP-Cron
  • Best practices for keys, TTLs, and avoiding common pitfalls
  • Full code examples you can drop into a plugin or theme

Why use wp_remote_get transients?

  • wp_remote_get is the WordPress HTTP API wrapper. It normalizes transport (cURL, fsockopen) and provides error handling helpers like is_wp_error and wp_remote_retrieve_ helpers.
  • Transients provide a simple, standardized way to cache arbitrary data in the database (or object cache if available). Use set_transient, get_transient and delete_transient.
  • Combining them means fewer external calls, better page performance, and fewer rate-limit or outage problems with third-party APIs.

Basic pattern

High-level approach:

  1. Construct a unique transient key for the request (URL query args).
  2. Try get_transient(key). If a value exists, return it.
  3. If no cached value, call wp_remote_get with reasonable args (timeout, headers).
  4. Validate the response: check for is_wp_error, response code, content-type and body.
  5. Decode the body (json_decode or wp_json_decode), optionally sanitize the result.
  6. Store the decoded value in set_transient with an appropriate expiration (TTL).
  7. Return the data (or a WP_Error / fallback if the call failed).

Minimal, production-ready example

This function fetches a JSON endpoint, caches it for 10 minutes, and returns decoded data or WP_Error. Use it as the basis for your API calls.

 10,
        redirection => 5,
        headers     => array(
            Accept => application/json,
            User-Agent => MySiteName/1.0 ( https://example.com),
        ),
        sslverify   => true,
    )
    args = wp_parse_args( args, defaults )

    response = wp_remote_get( url, args )

    if ( is_wp_error( response ) ) {
        // Log the error server-side do not expose detailed errors to end-users
        error_log( Remote request failed:  . response->get_error_message() )
        return response
    }

    code = wp_remote_retrieve_response_code( response )
    if ( 200 !== (int) code ) {
        return new WP_Error( http_error, Unexpected HTTP status:  . code, array( response => response ) )
    }

    body = wp_remote_retrieve_body( response )
    if ( empty( body ) ) {
        return new WP_Error( empty_body, Remote API returned empty body )
    }

    // Decode JSON safely
    data = json_decode( body, true )
    if ( null === data  JSON_ERROR_NONE !== json_last_error() ) {
        return new WP_Error( json_error, Invalid JSON:  . json_last_error_msg() )
    }

    // Optionally sanitize data here depending on your schema

    // Cache the data
    set_transient( transient_key, data, expiration )

    return data
}
?>

Notes on the example

  • esc_url_raw secures the URL before use.
  • Use a predictable prefix for transient keys and generate the rest with md5 of a stable encoding of URL args.
  • Return WP_Error for consumer code to handle dont echo raw errors to users.
  • Tune timeout and redirection according to the API. A long timeout blocks the request a short timeout may cause false failures.

Handling headers and authentication

Many APIs require API keys or Bearer tokens. Pass them in headers or use the query string according to the APIs spec. Never hard-code secrets in a template file use constants or options with appropriate capability checks.

 array(
            Authorization => Bearer  . token,
            Accept        => application/json,
        ),
    )
    return my_get_remote_json( url, args )
}
?>

Conditional requests (ETag / If-Modified-Since)

To reduce bandwidth you can use ETag/Last-Modified. Store the ETag or Last-Modified received with the response and include it in subsequent requests. If the server responds 304 Not Modified, reuse the cached data instead of re-downloading.

 ..., etag => ..., last_modified => ... )

    headers = array( Accept => application/json )
    if ( is_array( cached_entry ) ) {
        if ( ! empty( cached_entry[etag] ) ) {
            headers[If-None-Match] = cached_entry[etag]
        }
        if ( ! empty( cached_entry[last_modified] ) ) {
            headers[If-Modified-Since] = cached_entry[last_modified]
        }
    }

    response = wp_remote_get( url, array( headers => headers, timeout => 15 ) )

    if ( is_wp_error( response ) ) {
        // Return cached data if available, otherwise the error
        if ( ! empty( cached_entry[data] ) ) {
            return cached_entry[data]
        }
        return response
    }

    code = wp_remote_retrieve_response_code( response )

    if ( 304 === (int) code  ! empty( cached_entry[data] ) ) {
        // Resource not modified - reuse cached data
        // Refresh the transient TTL to avoid losing it if desired
        set_transient( transient_key, cached_entry, expiration )
        return cached_entry[data]
    }

    if ( 200 !== (int) code ) {
        // Fall back to cached data if present
        if ( ! empty( cached_entry[data] ) ) {
            return cached_entry[data]
        }
        return new WP_Error( http_error, Unexpected HTTP status:  . code )
    }

    body = wp_remote_retrieve_body( response )
    data = json_decode( body, true )

    if ( null === data  JSON_ERROR_NONE !== json_last_error() ) {
        if ( ! empty( cached_entry[data] ) ) {
            return cached_entry[data]
        }
        return new WP_Error( json_error, json_last_error_msg() )
    }

    // Save ETag and Last-Modified for future conditional requests
    etag = wp_remote_retrieve_header( response, etag )
    last_modified = wp_remote_retrieve_header( response, last-modified )

    store = array(
        data          => data,
        etag          => etag,
        last_modified => last_modified,
    )

    set_transient( transient_key, store, expiration )

    return data
}
?>

Important notes about ETag / If-Modified-Since

  • Not all APIs return ETag or Last-Modified headers. Check the API documentation.
  • When using conditional requests, you may want to increase TTL but rely on conditional checks to avoid re-downloading unchanged data.
  • Always normalize header names (WordPress returns headers lowercased) and test your logic thoroughly.

Stale-while-revalidate (serve stale, refresh in background)

To provide fast responses even when the remote API is down, use a stale-while-revalidate pattern:

  1. Keep the cached value for a long period but record a freshness timestamp or a separate short TTL.
  2. If the cache is stale, immediately return the stale data to the user, but schedule a background refresh (WP-Cron) to update the transient.
  3. When the update finishes, the next request will get fresh data.

Stale-while-revalidate example using wp_schedule_single_event

 ..., timestamp => ... )

    now = time()

    if ( false === cached_entry ) {
        // No cache - synchronous fetch (first request)
        data = my_fetch_and_store_swr( url, transient_key, revalidate_age )
        return data
    }

    // If cached and fresh
    if ( ! empty( cached_entry[timestamp] )  ( now - cached_entry[timestamp] ) <= max_age ) {
        return cached_entry[data]
    }

    // Cached but stale: return stale data immediately and schedule background refresh
    // Schedule only if not already scheduled
    if ( ! wp_next_scheduled( my_swr_refresh_hook, array( url, transient_key, revalidate_age ) ) ) {
        wp_schedule_single_event( time()   1, my_swr_refresh_hook, array( url, transient_key, revalidate_age ) )
    }

    return cached_entry[data]
}

// Hook receiver to perform background refresh
add_action( my_swr_refresh_hook, my_swr_refresh_worker, 10, 3 )
function my_swr_refresh_worker( url, transient_key, revalidate_age ) {
    my_fetch_and_store_swr( url, transient_key, revalidate_age )
}

function my_fetch_and_store_swr( url, transient_key, revalidate_age ) {
    response = wp_remote_get( url, array( timeout => 15, headers => array( Accept => application/json ) ) )
    if ( is_wp_error( response ) ) {
        error_log( SWR refresh failed:  . response->get_error_message() )
        return false
    }
    code = wp_remote_retrieve_response_code( response )
    if ( 200 !== (int) code ) {
        error_log( SWR refresh HTTP status:  . code )
        return false
    }
    body = wp_remote_retrieve_body( response )
    data = json_decode( body, true )
    if ( null === data  JSON_ERROR_NONE !== json_last_error() ) {
        error_log( SWR refresh JSON error:  . json_last_error_msg() )
        return false
    }

    store = array(
        data      => data,
        timestamp => time(),
    )
    // Store longer than revalidate_age to keep stale fallback available
    set_transient( transient_key, store, revalidate_age  2 )
    return data
}
?>

Why schedule background refresh?

  • WP-Cron is triggered by page loads, so the refresh may not run exactly on time on low-traffic sites thats acceptable for many use cases.
  • Alternatively, you can implement a separate real cron job that calls wp-cron externally if precise timing is needed.

Using site transients on multisite

For network-wide caching in a multisite WordPress install, use set_site_transient, get_site_transient and delete_site_transient. The API is the same but the storage is shared across the network.

Key naming, size and storage considerations

  • Transient keys must be 172 characters or less when using set_transient (WordPress prefixes your key) shorter keys are safer. Using md5 or sha1 on url args ensures a stable key length.
  • Transients are serialized and stored in the options table by default. If you use an object cache (Redis, Memcached) the transients are stored there instead. Some object cache backends impose value size limits — avoid storing very large payloads.
  • If you need to store huge payloads, consider storing only the minimal subset needed, or use a custom table / file cache and manage it carefully.

Error handling and fallbacks

  • Always check for is_wp_error after wp_remote_get and check the HTTP response code.
  • Log errors server-side with error_log or a logger but avoid showing sensitive info to the user.
  • If the API fails, return cached data if available. If not, return a graceful fallback (empty array, message) or a WP_Error that your presentation layer understands.

HTTP args you should know

  • timeout — maximum seconds to wait for a response (default 5 in WP core but override based on expected latency).
  • redirection — how many redirects to follow.
  • headers — array of headers (Accept, Authorization, If-None-Match, etc.).
  • sslverify — TRUE to verify certificates. Keep TRUE in production do not disable unless absolutely necessary and understood.
  • blocking — TRUE by default set to FALSE for asynchronous requests (useful with wp_remote_post to trigger background tasks).

Example: caching remote tweets (hypothetical)

Example showing a complete flow: fetch JSON, handle errors, store in transient, and provide a manual flush function.

 8,
        headers => array(
            Accept => application/json,
            User-Agent => MySiteTweets/1.0,
        ),
    ) )

    if ( is_wp_error( response ) ) {
        return response
    }

    code = wp_remote_retrieve_response_code( response )
    if ( 200 !== (int) code ) {
        return new WP_Error( http_error, HTTP status:  . code )
    }

    body = wp_remote_retrieve_body( response )
    data = json_decode( body, true )

    if ( null === data  JSON_ERROR_NONE !== json_last_error() ) {
        return new WP_Error( json_error, json_last_error_msg() )
    }

    // Light sanitization for tweet output (example)
    foreach ( data as tweet ) {
        if ( isset( tweet[text] ) ) {
            tweet[text] = wp_kses_post( tweet[text] ) // allow safe formatting
        }
    }

    // Cache for 15 minutes
    set_transient( transient_key, data, 15  MINUTE_IN_SECONDS )

    return data
}

// Manual flush helper for admin or webhook
function my_flush_tweet_cache( username, count = 5 ) {
    url = https://api.example.com/users/ . rawurlencode( sanitize_text_field( username ) ) . /tweets?count= . intval( count )
    transient_key = mr_tweets_ . md5( url )
    delete_transient( transient_key )
}
?>

When to use object cache directly instead of transients

  • Transients are convenience wrappers that use object cache if present. If your site has a persistent object cache (Redis/ Memcached), transients will use it automatically (via object-cache drop-in) and performance is good.
  • If you need advanced invalidation, tagging, or extremely high-performance ephemeral storage, consider using a dedicated caching layer (a plugin or direct object cache calls like wp_cache_set/get) and keep transients for compatibility.

Security and privacy considerations

  • Never store API secrets in transients because transients may be stored in shared caches accessible to other sites on the same object cache if misconfigured.
  • Store secrets in wp-config.php constants or in options with proper capability checks, or use environment variables in your hosting setup.
  • Sanitize URLs and request parameters (esc_url_raw, sanitize_text_field, intval, etc.). Escape any output with esc_html or similar before rendering in templates.

Common pitfalls and troubleshooting

  • Timeouts — increase timeout if the API is slow. But avoid excessively long timeouts in user-facing requests.
  • SSL verification — do not disable SSL verify in production. If it fails, fix the server trust chain or system CA store.
  • Large payloads — avoid caching very large responses in transients if your object cache or DB has size limits.
  • Transient key collisions — always prefix keys and use hashing of URL args to avoid collisions.
  • WP-Cron not firing — on low-traffic sites, scheduled background refreshes may not run reliably consider external cron if you need consistent refreshes.

Useful function summary

  • wp_remote_get( url, args ) — perform HTTP GET via WP HTTP API.
  • is_wp_error( obj ) — check for errors from the HTTP API.
  • wp_remote_retrieve_body( response ) — get response body string.
  • wp_remote_retrieve_response_code( response ) — get HTTP status code.
  • wp_remote_retrieve_header( response, header ) — get a header value (e.g. etag).
  • set_transient( key, value, expiration ) — store transient.
  • get_transient( key ) — retrieve transient, false if not present.
  • delete_transient( key ) — delete transient manually.
  • set_site_transient / get_site_transient — multisite network transients.

Performance tips

  • Cache small, useful subsets instead of entire responses when possible.
  • Reuse transient keys across components to avoid duplicate requests for the same external resource.
  • Keep TTLs conservative for rapidly changing data longer for rarely changing data.
  • Combine conditional requests with a cached payload to minimize bandwidth and still respect freshness.

Where to put this code

  • For site-wide utility functions, create a simple plugin or include in a mu-plugin. Avoid placing heavy logic directly in theme templates.
  • For theme-specific features you can include the functions.php, but plugins are better for portability and separation of concerns.

Further references

This guide provides complete patterns, code samples and explanations so you can fetch external APIs reliably in WordPress while minimizing latency and API usage. Use the examples as templates and adapt headers, TTLs and sanitization to the requirements of the API you consume.



Acepto donaciones de BAT's mediante el navegador Brave 🙂



Leave a Reply

Your email address will not be published. Required fields are marked *