The HTTP API

wp_remote_get and wp_remote_post make outbound requests through WordPress's transport layer instead of raw cURL.

Syntaxwp_remote_get( string $url, array $args = array() ): array|WP_Error

The HTTP API is WordPress's wrapper for outbound requests. wp_remote_get( $url, $args ), wp_remote_post(), wp_remote_head() and the general wp_remote_request() all return either a response array or a WP_Error. Check with is_wp_error() before anything else — the array keys do not exist on failure.

Reading the response

The array has headers, body, response, cookies and http_response. Read them through the helpers rather than by index: wp_remote_retrieve_body() returns the body or an empty string, wp_remote_retrieve_response_code() returns the status as an int, and wp_remote_retrieve_header() pulls one header. They never throw on a WP_Error, which keeps error handling in one place.

Arguments

Defaults worth knowing: timeout is 5 seconds, redirection is 5, httpversion is '1.0', sslverify is true, and blocking is true. A 5-second timeout is short for a slow third-party API and is the cause of most mysterious http_request_failed errors. Setting blocking to false fires and forgets — you get no body back.

Why not cURL

The API picks a working transport, so it still runs where the cURL extension is missing. It also honours site configuration your raw call would ignore: proxy constants, the WP_HTTP_BLOCK_EXTERNAL allowlist, and the http_request_args and pre_http_request filters that let tests and plugins intercept requests.

Example

Example · php
<?php
/**
 * Fetch the current stock level for a SKU, cached for five minutes.
 *
 * @return array|WP_Error Decoded payload, or WP_Error on any failure.
 */
function scx_fetch_stock( string $sku ) {
	$key    = 'scx_stock_' . md5( $sku );
	$cached = get_transient( $key );

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

	$response = wp_remote_get(
		add_query_arg( 'sku', rawurlencode( $sku ), 'https://api.example.com/v1/stock' ),
		array(
			// The default is 5 seconds, which most third-party APIs will exceed.
			'timeout' => 15,
			'headers' => array(
				'Accept'        => 'application/json',
				'Authorization' => 'Bearer ' . SCX_API_TOKEN,
			),
		)
	);

	// Transport-level failure only: DNS, TLS, timeout.
	if ( is_wp_error( $response ) ) {
		return $response;
	}

	// A 404 arrives here as a perfectly successful request, so check the code.
	$code = wp_remote_retrieve_response_code( $response );
	if ( 200 !== $code ) {
		return new WP_Error( 'scx_bad_status', 'Stock API returned ' . $code );
	}

	$data = json_decode( wp_remote_retrieve_body( $response ), true );
	if ( ! is_array( $data ) ) {
		return new WP_Error( 'scx_bad_json', 'Unparseable response body.' );
	}

	set_transient( $key, $data, 5 * MINUTE_IN_SECONDS );

	return $data;
}

When to use it

  • Call a third-party JSON API from a shortcode and cache the result in a transient.
  • Send a webhook when an order is created without blocking the checkout response.
  • Stop a staging site making outbound calls to a live payment provider.
  • Stub an external API in integration tests without touching the network.

More examples

POSTing form data and POSTing JSON

The array-versus-string body distinction accounts for a large share of "the API says my request is empty" bug reports. wp_remote_post() is shorthand for wp_remote_request() with method set to POST; wp_remote_get() and wp_remote_head() are the same wrapper for GET and HEAD, so anything you can do with one you can do with wp_remote_request().

Example · php
// Array body: WordPress form-encodes it and sets the content type for you.
$response = wp_remote_post(
	'https://api.example.com/v1/orders',
	array(
		'timeout' => 20,
		'body'    => array(
			'sku'      => $sku,
			'quantity' => 2,
		),
	)
);

// String body: you must set Content-Type yourself, or the endpoint
// will read your JSON as form data and reject it.
$response = wp_remote_post(
	'https://api.example.com/v1/orders',
	array(
		'timeout' => 20,
		'headers' => array( 'Content-Type' => 'application/json; charset=utf-8' ),
		'body'    => wp_json_encode(
			array(
				'sku'      => $sku,
				'quantity' => 2,
			)
		),
	)
);

// Any other verb goes through wp_remote_request().
$response = wp_remote_request(
	'https://api.example.com/v1/orders/42',
	array( 'method' => 'DELETE', 'timeout' => 20 )
);

Applying a longer timeout to one host only

These two filters are the practical reason to use the HTTP API rather than cURL: they give you a single place to adjust or intercept every request in the site, including ones made by plugins you did not write. pre_http_request returning false means "carry on", and returning an array means "use this instead" — which makes integration tests possible without a network.

Example · php
/**
 * http_request_args runs on every outbound request, so filter narrowly.
 *
 * @param array  $args Request arguments.
 * @param string $url  The URL being requested.
 */
function scx_slow_partner_timeout( array $args, string $url ): array {
	if ( str_contains( wp_parse_url( $url, PHP_URL_HOST ) ?? '', 'slow-partner.example' ) ) {
		$args['timeout'] = 30;
	}

	return $args;
}
add_filter( 'http_request_args', 'scx_slow_partner_timeout', 10, 2 );

/**
 * pre_http_request short-circuits entirely: return a response array and
 * no network call happens. This is how you stub an API in tests.
 */
add_filter(
	'pre_http_request',
	static function ( $preempt, array $args, string $url ) {
		if ( ! defined( 'SCX_STUB_HTTP' ) || ! str_contains( $url, 'api.example.com' ) ) {
			return $preempt; // false: let the real request proceed
		}

		return array(
			'headers'  => array(),
			'body'     => '{"stock":7}',
			'response' => array( 'code' => 200, 'message' => 'OK' ),
			'cookies'  => array(),
		);
	},
	10,
	3
);

Locking down outbound requests

WP_HTTP_BLOCK_EXTERNAL stops a staging clone from emailing real customers or hitting a production API; only hosts in WP_ACCESSIBLE_HOSTS get through. The blocking => false form is for webhooks where you do not care about the reply — but be clear that you cannot check the status code afterwards, so never use it for anything you need to confirm.

Example · php
// wp-config.php on a staging site: block all outbound HTTP except an allowlist.
define( 'WP_HTTP_BLOCK_EXTERNAL', true );
define( 'WP_ACCESSIBLE_HOSTS', 'api.example.com,*.wordpress.org' );

// Reading response headers. Header names are matched case-insensitively.
$response = wp_remote_head( 'https://example.com/big.zip', array( 'timeout' => 10 ) );

if ( ! is_wp_error( $response ) ) {
	$length = wp_remote_retrieve_header( $response, 'content-length' );
	$type   = wp_remote_retrieve_header( $response, 'content-type' );
}

// Fire-and-forget: returns immediately, body is empty by design.
wp_remote_post(
	'https://hooks.example.com/notify',
	array(
		'blocking' => false,
		'timeout'  => 1,
		'body'     => array( 'event' => 'order.created' ),
	)
);

Discussion

  • Be the first to comment on this lesson.