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

  1. Open your order in the dashboard at https://app.proxyhive.io.
  2. 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.
  3. Make them available to PHP, for example in the web server's environment or a .env file 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

SymptomCauseFix
curl_errno 56, CONNECT tunnel failed, response 407Wrong credentials, HTTPS targetRe-copy from the order
HTTP code 407 from curl_getinfoSame, on an http:// targetSame fix
Guzzle RequestException: cURL error 56Guzzle surfacing the same 407 on an HTTPS targetSame fix
Guzzle ClientException ... 407 Proxy Authentication RequiredThe 407 on an http:// targetSame fix
curl_errno 7, connection refusedWrong host or port, or a port for another protocolMatch the port to CURLOPT_PROXYTYPE
Username with : failsCURLOPT_PROXYUSERPWD split it at the first colonCURLOPT_PROXYUSERNAME and CURLOPT_PROXYPASSWORD

For status codes from the target site, see proxy error codes.

Next steps