PHP cURL
What Is cURL? In simple terms: cURL lets your PHP code act like a browser or an API client: it can request web pages, call APIs, submit data, download files, and read the responses. Payment gateways,
What Is cURL?
In simple terms: cURL lets your PHP code act like a browser or an API client: it can request web pages, call APIs, submit data, download files, and read the responses. Payment gateways, SMS services, shipping providers, and social login all require your server to send HTTP requests to other servers.
Technically: The curl extension is a PHP binding for libcurl, a widely used library that supports HTTP(S) and many other protocols. You create a handle with curl_init(), configure it with options (curl_setopt() / curl_setopt_array()), run the request with curl_exec(), and inspect results with curl_getinfo(), curl_errno(), and curl_error(). Since PHP 8.0, handles are CurlHandle objects that are freed automatically.
Checking That cURL Is Available
<?php
var_dump(extension_loaded('curl'));
echo curl_version()['version'] . "\n";
Output (the version depends on your system)
bool(true)
8.5.0
On Debian/Ubuntu, the extension is installed with sudo apt install php-curl (or the version-specific package, such as php8.4-curl).
A Basic GET Request
The examples use JSONPlaceholder (jsonplaceholder.typicode.com), a free fake API for testing that returns fixed sample data.
<?php
$ch = curl_init('https://jsonplaceholder.typicode.com/todos/1');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 10,
CURLOPT_HTTPHEADER => ['Accept: application/json'],
]);
$body = curl_exec($ch);
if ($body === false) {
throw new RuntimeException('Request failed: ' . curl_error($ch));
}
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
$todo = json_decode($body, true, flags: JSON_THROW_ON_ERROR);
echo "Status: $status\n";
echo "Title: {$todo['title']}\n";
var_dump($todo['completed']);
Output
Status: 200
Title: delectus aut autem
bool(false)
How It Works
curl_init($url)creates a handle for the URL.CURLOPT_RETURNTRANSFER => truemakescurl_exec()return the response body as a string instead of printing it — almost always what you want.CURLOPT_TIMEOUTlimits the whole request to 10 seconds, so a slow remote server cannot block your page indefinitely.curl_exec()returns the body, orfalseif the request could not be completed (network failure, timeout, DNS error).curl_getinfo(..., CURLINFO_RESPONSE_CODE)returns the HTTP status code.
Note:
curl_close()has had no effect since PHP 8.0 and is deprecated as of PHP 8.5. The handle is released automatically when the variable goes out of scope; older tutorials still showcurl_close($ch).
Transport Errors vs HTTP Errors
| Situation | curl_exec() Returns | How to Detect |
|---|---|---|
| Server unreachable, DNS failure, timeout, TLS error | false | curl_errno() / curl_error() |
Server responded with 404, 422, 500, … | The response body (a string) | Check the status code |
A response with status 500 is still a successful transfer from cURL's point of view. Always check both.
Important Options
| Option | Purpose |
|---|---|
CURLOPT_RETURNTRANSFER | Return the body as a string |
CURLOPT_CONNECTTIMEOUT | Maximum seconds to establish the connection |
CURLOPT_TIMEOUT | Maximum seconds for the whole request |
CURLOPT_HTTPHEADER | Request headers, as an array of "Name: value" strings |
CURLOPT_POST / CURLOPT_POSTFIELDS | Send a POST request with a body |
CURLOPT_CUSTOMREQUEST | Other methods: PUT, PATCH, DELETE |
CURLOPT_FOLLOWLOCATION / CURLOPT_MAXREDIRS | Follow redirects, with a limit |
CURLOPT_USERAGENT | Identify your application |
CURLOPT_SSL_VERIFYPEER / CURLOPT_SSL_VERIFYHOST | Certificate verification — leave enabled (the default) |
CURLOPT_HEADERFUNCTION | Callback that receives each response header line |
CURLOPT_FILE | Write the response directly to a file (downloads) |
Warning: Disabling certificate verification (
CURLOPT_SSL_VERIFYPEER => false) is a common "fix" for certificate errors found online. It allows attackers to intercept and modify the traffic. Fix the underlying problem instead — usually an outdated CA certificate bundle on the server.
Sending JSON With POST
<?php
$payload = ['title' => 'Learning cURL', 'body' => 'PHP HTTP requests', 'userId' => 1];
$ch = curl_init('https://jsonplaceholder.typicode.com/posts');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode($payload, JSON_THROW_ON_ERROR),
CURLOPT_HTTPHEADER => [
'Content-Type: application/json; charset=utf-8',
'Accept: application/json',
],
CURLOPT_CONNECTTIMEOUT => 5,
CURLOPT_TIMEOUT => 15,
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
$created = json_decode($body, true, flags: JSON_THROW_ON_ERROR);
echo "Status: $status\n";
echo "New post ID: {$created['id']}\n";
echo "Title: {$created['title']}\n";
Output
Status: 201
New post ID: 101
Title: Learning cURL
- The body is a JSON string (
json_encode()), and theContent-Typeheader tells the server how to read it. - If an array is passed to
CURLOPT_POSTFIELDS, cURL sends it asmultipart/form-datainstead — like an HTML form with file uploads. For a regular form encoding, passhttp_build_query($array). - JSONPlaceholder answers with
201 Createdand simulates an ID (101); nothing is actually stored.
Handling Failures
<?php
$ch = curl_init('https://nonexistent.invalid/api');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CONNECTTIMEOUT => 3,
]);
$body = curl_exec($ch);
if ($body === false) {
echo "cURL error " . curl_errno($ch) . ": " . curl_error($ch) . "\n";
}
Output
cURL error 6: Could not resolve host: nonexistent.invalid
| Error Code | Constant | Meaning |
|---|---|---|
| 6 | CURLE_COULDNT_RESOLVE_HOST | DNS lookup failed |
| 7 | CURLE_COULDNT_CONNECT | Connection refused |
| 28 | CURLE_OPERATION_TIMEDOUT | Timeout reached |
| 60 | CURLE_PEER_FAILED_VERIFICATION | Certificate verification failed |
Network errors and timeouts are temporary failures that may succeed on retry; 4xx responses usually are not (see Consuming REST APIs).
A Reusable Request Function
Repeating the same option setup for every call is error-prone. A small function centralizes timeouts, headers, and error handling:
<?php
function httpRequest(string $method, string $url, ?array $json = null, array $headers = []): array
{
$ch = curl_init($url);
$responseHeaders = [];
$options = [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => strtoupper($method),
CURLOPT_CONNECTTIMEOUT => 5,
CURLOPT_TIMEOUT => 20,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_MAXREDIRS => 3,
CURLOPT_USERAGENT => 'GoCourseClient/1.0',
CURLOPT_HTTPHEADER => array_merge(['Accept: application/json'], $headers),
CURLOPT_HEADERFUNCTION => function ($handle, string $line) use (&$responseHeaders): int {
$parts = explode(':', $line, 2);
if (count($parts) === 2) {
$responseHeaders[strtolower(trim($parts[0]))] = trim($parts[1]);
}
return strlen($line);
},
];
if ($json !== null) {
$options[CURLOPT_POSTFIELDS] = json_encode($json, JSON_THROW_ON_ERROR);
$options[CURLOPT_HTTPHEADER][] = 'Content-Type: application/json';
}
curl_setopt_array($ch, $options);
$body = curl_exec($ch);
if ($body === false) {
throw new RuntimeException('HTTP request failed: ' . curl_error($ch), curl_errno($ch));
}
return [
'status' => curl_getinfo($ch, CURLINFO_RESPONSE_CODE),
'headers' => $responseHeaders,
'body' => $body,
];
}
$response = httpRequest('GET', 'https://jsonplaceholder.typicode.com/users/1');
echo $response['status'] . "\n";
echo $response['headers']['content-type'] . "\n";
echo json_decode($response['body'], true)['name'] . "\n";
Output
200
application/json; charset=utf-8
Leanne Graham
How It Works
CURLOPT_CUSTOMREQUESTsets the method, so the same function handlesGET,POST,PUT,PATCH, andDELETE.CURLOPT_HEADERFUNCTIONreceives every response header line; the callback stores them in an array with lowercase names (HTTP header names are case-insensitive). It must return the number of bytes it handled.- Transport failures become exceptions with the cURL error code; HTTP status codes are returned for the caller to evaluate.
- Sensible defaults (timeouts, redirect limit, user agent) apply to every request.
Downloading a File
<?php
$target = fopen(__DIR__ . '/report.pdf', 'w');
$ch = curl_init('https://example.com/files/report.pdf');
curl_setopt_array($ch, [
CURLOPT_FILE => $target,
CURLOPT_FAILONERROR => true,
CURLOPT_TIMEOUT => 60,
]);
$ok = curl_exec($ch);
fclose($target);
if (!$ok) {
unlink(__DIR__ . '/report.pdf');
throw new RuntimeException('Download failed: ' . curl_error($ch));
}
CURLOPT_FILE streams the response directly into the file, so large downloads do not need to fit in memory. CURLOPT_FAILONERROR treats HTTP status codes of 400 and above as failures.
cURL vs Alternatives
| Tool | When to Use |
|---|---|
curl_* functions | Full control, no dependencies |
file_get_contents() with a stream context | Very simple GET requests (less control over errors and timeouts) |
| Guzzle or Symfony HttpClient | Applications: clean API, middleware, retries, async/parallel requests, PSR-18 compatibility |
Libraries use cURL internally; understanding cURL makes their options and errors easier to understand.
Common Mistakes
Forgetting CURLOPT_RETURNTRANSFER
Effect: The response is printed directly into your page.
Fix: Always set it to true.
No Timeouts
Effect: A slow external service makes your pages hang until PHP's time limit.
Fix: Set CURLOPT_CONNECTTIMEOUT and CURLOPT_TIMEOUT.
Ignoring HTTP Status Codes
Effect: Error responses are treated as valid data.
Fix: Check the status code in addition to curl_exec()'s result.
Disabling SSL Verification
Effect: Traffic can be intercepted and modified.
Fix: Keep verification enabled; update CA certificates.
Fetching User-Supplied URLs Without Checks
Effect: Server-Side Request Forgery (SSRF) against internal services.
Fix: Allow-list domains or validate resolved IP addresses (see PHP Filters Advanced).
Best Practices
- Set
CURLOPT_RETURNTRANSFER, connection and total timeouts, and a user agent on every request. - Check transport errors (
false) and HTTP status codes separately. - Send JSON as an encoded string with a
Content-Typeheader. - Keep certificate verification enabled.
- Wrap cURL in a reusable function or use an HTTP client library for larger applications.
Official reference: https://www.php.net/manual/en/book.curl.php