The REST API was about allowing inbound connections (third parties connecting with your server). The HTTP API is about outbound connections – your server sending HTTP requests to third parties. It’s the official WordPress way of sending HTTP requests. You don’t use cURL or file_get_contents(). You use this API.
It’s a rather simple API so we shouldn’t spend as much time here as we did on the REST API. We’ll cover making requests for every method and then we’ll discuss accessing responses.
GET
To GET data from an external service, you should use the wp_remote_get( $url, $args ) function. $args is just an array of arguments. A full list of accepted keys and values is available in the WP_Http::request() documentation, but some of the most important for GET are:
- timeout – how long to wait before aborting the connection. Default 5.
- redirection – how many times to follow redirections. Default 5.
- httpversion – either “1.0” or “1.1”. Default “1.0”.
- user-agent – User-agent value sent. Default: ‘WordPress/’ . get_bloginfo( ‘version’ ) . ‘; ‘ . get_bloginfo( ‘url’ ).
- blocking – if false, the request will be sent asynchronously (PHP execution will continue without waiting for the response – use only if your code doesn’t need the response). Default true.
- headers – array or string of headers to send with the request.
- cookies – array of cookies to send with the request.
Oh, and if you need to include query parameters in the URL, especially if they are included dynamically (e.g., read from the database), you will find the add_query_arg() function very useful. It’s a generic WordPress helper for adding query params to URLs. Sending a GET request in practice is as simple as doing:
$url = add_query_arg( 'key', 'value', 'https://example.com' );
$response = wp_remote_get( $url );POST
POSTing data is almost as easy as GETting data. You use the wp_remote_post( $url, $args ) function. There’s one $args key I’ve deliberately not mentioned when discussing GET (because it shouldn’t be used with GET requests) – ‘body‘. This key accepts either a string or an associative array and defaults to null. This will be our payload. An example of a post request:
$args = [
'body' => [
'email' => 'mail@example.com',
'message' => 'I hope you read the security chapter!;DROP TABLE wp_posts;'
]
];
wp_remote_post( 'https://example.com/contact', $args );If you need to pass the data as JSON, you will have to encode it yourself using wp_encode_json() and pass it as a string to the ‘body’ argument. Remember to also set the content-type header to “application/json”.
Unfortunately, it’s not clear to me what the content-type is by default. The documentation doesn’t mention it and I couldn’t find it in code. I’m assuming it’s application/x-www-form-urlencoded if you’re passing an array. Keep it in mind if you have problems with POST requests and maybe set it directly or do some more testing.
Other Methods
All of the request methods actually use WP_Http::request() underneath. That’s why their arguments are the same. One of the most crucial keys you can pass into $args that I’ve not yet mentioned is ‘method‘. Although you could use it with the _get and _post helpers (which would be very confusing), it’s more correct to include it when using the more generic wp_remote_request( $url, $args ) function.
The list of accepted ‘method’ values:
- ‘GET’
- ‘POST’
- ‘HEAD’
- ‘PUT’
- ‘DELETE’
- ‘TRACE’
- ‘OPTIONS’
- ‘PATCH’
Using this generic method, along with all the other options available to you in the $args parameter, you can send literally any HTTP request. Oh, and there’s also a helper function for sending HEAD requests that might come in handy – wp_remote_head( $url, $args ).
Responses
Because all the functions use the same underlying method for making the requests, their response structure looks exactly the same. This is convenient because I can cover responses only once instead of having to discuss them for every method individually. I made a GET request with wp_remote_get() to https://jsonplaceholder.typicode.com/todos/1 and printed the $response with print_r(). Here’s what it looks like (truncated):
Array
(
[headers] => WpOrg\Requests\Utility\CaseInsensitiveDictionary Object
(
[data:protected] => Array
(
[date] => Tue, 23 Sep 2025 17:52:09 GMT
[content-type] => application/json; charset=utf-8
[server] => cloudflare
[... truncated headers ...]
)
)
[body] => {
"userId": 1,
"id": 1,
"title": "delectus aut autem",
"completed": false
}
[response] => Array
(
[code] => 200
[message] => OK
)
[cookies] => Array
(
[0] => WP_Http_Cookie Object
(
[name] => example_cookie_i_set_with_$args
[value] => example_value
[expires] =>
[path] => /
[domain] =>
[port] =>
[host_only] => 1
)
)
[filename] =>
[http_response] => WP_HTTP_Requests_Response Object
(
[data] =>
[headers] =>
[status] =>
[response:protected] => WpOrg\Requests\Response Object
(
[body] => {
"userId": 1,
"id": 1,
"title": "delectus aut autem",
"completed": false
}
[raw] => HTTP/1.1 200 OK
Date: Tue, 23 Sep 2025 17:52:09 GMT
Content-Type: application/json; charset=utf-8
Transfer-Encoding: chunked
Connection: close
Server: cloudflare
[... truncated headers ...]
{
"userId": 1,
"id": 1,
"title": "delectus aut autem",
"completed": false
}
[headers] => WpOrg\Requests\Response\Headers Object
(
[data:protected] => Array
(
[date] => Array
(
[0] => Tue, 23 Sep 2025 17:52:09 GMT
)
[content-type] => Array
(
[0] => application/json; charset=utf-8
)
[server] => Array
(
[0] => cloudflare
)
[... truncated headers ...]
)
)
[status_code] => 200
[protocol_version] => 1.1
[success] => 1
[redirects] => 0
[url] => https://jsonplaceholder.typicode.com/todos/1
[history] => Array
(
)
[cookies] => WpOrg\Requests\Cookie\Jar Object
(
[cookies:protected] => Array
(
[example_cookie_i_set_with_$args] => WpOrg\Requests\Cookie Object
(
[name] => example_cookie_i_set_with_$args
[value] => example_value
[attributes] => Array
(
)
[flags] => Array
(
[creation] => 1758650522
[last-access] => 1758650522
[persistent] =>
[host-only] => 1
)
[reference_time] => 1758650522
)
)
)
)
[filename:protected] =>
)
)As you can see, the response is actually just a big array. There are different data types inside of this array, such as CaseInsensitiveDictionary, WP_Http_Cookie, WP_HTTP_Requests_Response, and more. But why am I showing you this? So that you understand why we don’t just read the data directly from this array and why you don’t have to remember its structure.
You read data from the response array by using wp_remote_retrieve_*() functions. These are helper functions designed to interact with the structure of this array and return only the data you care about in a predictable format.
Here’s a list of these functions. Check out their individual documentations when you need to use them:
- wp_remote_retrieve_body()
- wp_remote_retrieve_cookie()
- wp_remote_retrieve_cookies()
- wp_remote_retrieve_cookie_value()
- wp_remote_retrieve_header()
- wp_remote_retrieve_headers()
- wp_remote_retrieve_response_code()
- wp_remote_retrieve_response_message()
The response array is of course only returned if the request succeeds. But what if it fails? What if the timeout is exceeded? Or the server’s network is so overloaded it starts rejecting requests at the TCP level? In those cases, the response is a WP_Error object. That’s why you should always check if the response is an error before proceeding, like this:
$response = wp_remote_get( 'https://example.com/contact' );
if ( is_wp_error( $response ) ) {
// handle error
} else {
// proceed as if successful
}Remember that WP_Error is only returned when there was an actual error with getting the response. If the response was received successfully but the status code was 4xx or 5xx, that would not return a WP_Error but a normal response array.
A nice touch of the HTTP API is that all wp_remote_retrieve_*() functions work even if you pass a WP_Error as the argument, i.e., they expect either an array or a WP_Error. Of course their behavior will be different if passed an error object (usually returning an empty string).
Security
There are two $args entries that relate to security. You should pay some attention to them, which is why I’m writing this section.
The first one is ‘reject_unsafe_urls‘. It expects a boolean value. It’s false by default, but if you set it to true, the URL of the request will be passed through the wp_http_validate_url() function. This function will abort the request if the URL is deemed unsafe. Some examples of unsafe URLs are:
- ftp://example.com/caniload.php – Invalid protocol – only http and https are allowed.
- http:///example.com/caniload.php – Malformed URL.
- http://user:pass@example.com/caniload.php – Login information.
- http://example.invalid/caniload.php – Invalid hostname, as the IP cannot be looked up in DNS.
- http://192.168.0.1/caniload.php – IPs from LAN networks.
- http://198.143.164.252:81/caniload.php – By default, only 80, 443, and 8080 ports are allowed.
This function is explicitly designed to secure you from Server-Side Request Forgery (SSRF) attacks. It’s like CSRF but for servers (an attacker tricks your server into performing an action you didn’t mean to perform).
You might be wondering if you need this if you’re hard-coding the URLs. The answer is yes, and I hope to explain why in just one question: are you 100% sure the URL you hard-coded is not going to redirect (status code 3xx) to a different URL? I can answer that for you – no, you are not. with ‘reject_unsafe_urls’, not only is the first URL validated, but every redirect hop is validated as well.
As a matter of fact, this is such a big deal that there’s an entire family of helper functions designed just so that they set this argument to true by default. That’s the only way they differ from their aforementioned counterparts. These are:
- wp_safe_remote_get()
- wp_safe_remote_post()
- wp_safe_remote_head()
- wp_safe_remote_request()
The other important security argument in $args is ‘sslverify‘. Thankfully, it defaults to true. If set and the request is sent using HTTPS, the SSL certificate will be verified against a bundled CA Root Certificates file (which you can change with the ‘sslcertificates’ argument by the way). If the certificate of the recipient fails verification, a WP_Error will be returned.
Hooks
This is one of the biggest reasons you should use the HTTP API instead of raw-dogging cURL. As with everything in WordPress, the functions and methods in the HTTP API provide many actions and filters allowing for seamless extensions. I’m not going to list all the hooks now – you have the documentation and the code for that.
That being said, some notable hooks include “http_request_args”, “pre_http_request”, and “http_api_debug”. The first one allows you to filter $args for every request, which makes it easy to change the user agent or enforce ‘reject_unsafe_urls’ (or anything else your soul wants to do). “pre_http_request” filters the request before it is sent. This, coupled with “http_api_debug” which runs after the request, allows you to introduce caching of responses.