A PHP proxy with ext-curl takes two options: CURLOPT_PROXY for HOST:PORT and CURLOPT_PROXYUSERPWD for USERNAME:PASSWORD. Add CURLOPT_PROXYTYPE => CURLPROXY_SOCKS5_HOSTNAME for SOCKS5. In Guzzle 7, the same proxy goes into the proxy request option as a URL.
$ch = curl_init('https://api.ipify.org?format=json');
curl_setopt($ch, CURLOPT_PROXY, 'HOST:PORT');
curl_setopt($ch, CURLOPT_PROXYUSERPWD, 'USERNAME:PASSWORD');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
echo curl_exec($ch);
Every snippet on this page ran on 2026-09-30 with PHP 8.3.33 (libcurl 8.14.1) and Guzzle 7.15.5, against two local authenticating HTTP proxies and a SOCKS5 proxy that enforces username and password.
Before you start: copy your proxy details
- Open your order in the dashboard at https://app.proxyhive.io.
- Copy HOST, PORT, USERNAME and PASSWORD. Every ISP or datacenter IP is its own endpoint, and the order lists separate HTTP, HTTPS and SOCKS5 ports for each. Use the HTTP port unless a section below says otherwise.
- Make them available to PHP, for example in the web server's environment or a
.envfile your framework loads:
export PROXY_HOST=HOST PROXY_PORT=PORT PROXY_USER=USERNAME PROXY_PASS=PASSWORD
A single static ISP proxy is plenty for a price checker or a server that needs one fixed outbound address.
PHP cURL proxy with authentication
<?php
$ch = curl_init('https://api.ipify.org?format=json');
curl_setopt_array($ch, [
CURLOPT_PROXY => getenv('PROXY_HOST') . ':' . getenv('PROXY_PORT'),
CURLOPT_PROXYUSERNAME => getenv('PROXY_USER'),
CURLOPT_PROXYPASSWORD => getenv('PROXY_PASS'),
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CONNECTTIMEOUT => 5,
CURLOPT_TIMEOUT => 30,
]);
$body = curl_exec($ch);
if ($body === false) {
fwrite(STDERR, 'curl error ' . curl_errno($ch) . ': ' . curl_error($ch) . PHP_EOL);
exit(1);
}
echo curl_getinfo($ch, CURLINFO_HTTP_CODE), ' ', $body, PHP_EOL;
CURLOPT_PROXYUSERNAME and CURLOPT_PROXYPASSWORD take the values raw. We tested a password of p@ss:w/rd and it authenticated with no escaping. The shorter CURLOPT_PROXYUSERPWD => "$user:$pass" works too, but splits at the first colon, so it breaks on a username containing one. Both options are listed in the PHP curl_setopt reference.
HTTPS targets go through a CONNECT tunnel, so the proxy sees the hostname but not the path, headers or body. Leave CURLOPT_PROXYTYPE at its default (CURLPROXY_HTTP) for the HTTP port.
PHP SOCKS5 proxy: CURLPROXY_SOCKS5_HOSTNAME
Point CURLOPT_PROXY at the SOCKS5 port from the order and set the type:
curl_setopt_array($ch, [
CURLOPT_PROXY => getenv('PROXY_HOST') . ':SOCKS5_PORT',
CURLOPT_PROXYTYPE => CURLPROXY_SOCKS5_HOSTNAME,
CURLOPT_PROXYUSERPWD => getenv('PROXY_USER') . ':' . getenv('PROXY_PASS'),
]);
We watched what reached the proxy. CURLPROXY_SOCKS5_HOSTNAME sent api.ipify.org; CURLPROXY_SOCKS5 sent a bare IP resolved on our machine. The hostname variant keeps DNS lookups off your server and is usually what you want. A scheme prefix does the same job: CURLOPT_PROXY => 'socks5h://USERNAME:PASSWORD@HOST:SOCKS5_PORT'. HTTP vs SOCKS5 proxies covers when SOCKS5 is worth the switch.
Guzzle proxy: the proxy request option
Guzzle runs on the same libcurl, so every behaviour above carries over. Set a default on the client:
<?php
require __DIR__ . '/vendor/autoload.php';
use GuzzleHttp\Client;
$client = new Client([
'proxy' => getenv('PROXY_URL'),
'connect_timeout' => 5,
'timeout' => 30,
]);
$response = $client->get('https://api.ipify.org?format=json');
echo $response->getStatusCode(), ' ', $response->getBody(), PHP_EOL;
with PROXY_URL=http://USERNAME:PASSWORD@HOST:PORT. Percent-encode the password with rawurlencode() when it goes inside a URL. A socks5h:// URL works here as well.
Per-scheme proxies and the no list
The array form picks a proxy by the target's scheme and skips hosts in no:
$response = $client->get('http://httpbin.org/ip', [
'proxy' => [
'http' => 'http://USERNAME:PASSWORD@HOST1:PORT',
'https' => 'http://USERNAME:PASSWORD@HOST2:PORT',
'no' => ['localhost', '.internal.example'],
],
]);
Our logs showed the http:// request on the first proxy and an https:// request on the second. The Guzzle request options docs note one trap: Guzzle fills its defaults from HTTPS_PROXY, NO_PROXY and, from the command line only, HTTP_PROXY, but a proxy option you pass replaces them, no list included.
Rotate proxies across several IPs
Rotation is a different proxy per request. With Guzzle:
$endpoints = explode(',', getenv('PROXY_URLS'));
foreach (['https://httpbin.org/ip', 'https://httpbin.org/ip', 'https://httpbin.org/ip'] as $i => $url) {
$proxy = $endpoints[$i % count($endpoints)];
echo $client->get($url, ['proxy' => $proxy])->getBody(), PHP_EOL;
}
For parallel requests, curl_multi_init() or Guzzle's Pool run several handles at once, each with its own CURLOPT_PROXY. We ran two handles on two proxies through curl_multi_exec and both returned. Round-robin is the simple fair policy; rotating vs static proxies explains when a fixed IP per job is the better choice.
Common PHP proxy errors
| Symptom | Cause | Fix |
|---|---|---|
curl_errno 56, CONNECT tunnel failed, response 407 | Wrong credentials, HTTPS target | Re-copy from the order |
HTTP code 407 from curl_getinfo | Same, on an http:// target | Same fix |
Guzzle RequestException: cURL error 56 | Guzzle surfacing the same 407 on an HTTPS target | Same fix |
Guzzle ClientException ... 407 Proxy Authentication Required | The 407 on an http:// target | Same fix |
curl_errno 7, connection refused | Wrong host or port, or a port for another protocol | Match the port to CURLOPT_PROXYTYPE |
Username with : fails | CURLOPT_PROXYUSERPWD split it at the first colon | CURLOPT_PROXYUSERNAME and CURLOPT_PROXYPASSWORD |
For status codes from the target site, see proxy error codes.
Next steps
- Reproduce any failing request from the shell with cURL, the same library underneath.
- Same setup in Node.js or Python requests.
- Connection reference: the docs.