cURL is a library for making HTTP requests, and PHP has an extension for it (ext-curl). It is the standard tool for calling APIs.
Check that it is available:
<?php
var_dump(extension_loaded("curl"));
bool(true)
If it is false, enable extension=curl in php.ini.
A GET request
<?php
$ch = curl_init("https://api.example.com/products?limit=5");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true, // return the body as a string
CURLOPT_TIMEOUT => 10, // total time allowed (seconds)
CURLOPT_CONNECTTIMEOUT => 5, // time allowed to connect
CURLOPT_FOLLOWLOCATION => false, // do not follow redirects blindly
CURLOPT_HTTPHEADER => ["Accept: application/json"],
]);
$body = curl_exec($ch);
if ($body === false) {
$error = curl_error($ch); // e.g. "Could not resolve host"
throw new RuntimeException("Request failed: " . $error);
}
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$data = json_decode($body, true, 512, JSON_THROW_ON_ERROR);
The steps are always: curl_init(), set options, curl_exec(), check for failure, read the status. Since PHP 8.0 a cURL handle is an object (CurlHandle) that closes itself automatically, so curl_close() is no longer needed.
The options you will use most
| Option | Meaning |
|---|---|
CURLOPT_RETURNTRANSFER | Return the response instead of printing it |
CURLOPT_TIMEOUT | Maximum seconds for the whole request |
CURLOPT_CONNECTTIMEOUT | Maximum seconds to connect |
CURLOPT_HTTPHEADER | An array of header lines |
CURLOPT_POST | Send a POST request |
CURLOPT_POSTFIELDS | The request body |
CURLOPT_CUSTOMREQUEST | Set PUT, PATCH or DELETE |
CURLOPT_FOLLOWLOCATION | Follow redirects (and CURLOPT_MAXREDIRS) |
CURLOPT_USERAGENT | The User-Agent string |
CURLOPT_SSL_VERIFYPEER | Keep true. Never turn certificate checks off in production |
A POST with JSON
<?php
$payload = json_encode(["name" => "Pen", "price" => 12.5], JSON_THROW_ON_ERROR);
$ch = curl_init("https://api.example.com/products");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 10,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $payload,
CURLOPT_HTTPHEADER => [
"Content-Type: application/json",
"Accept: application/json",
"Authorization: Bearer " . getenv("API_TOKEN"),
],
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
A reusable function
<?php
function apiRequest(string $method, string $url, ?array $json = null, array $headers = []): array
{
$ch = curl_init($url);
$options = [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 10,
CURLOPT_CONNECTTIMEOUT => 5,
CURLOPT_CUSTOMREQUEST => $method,
CURLOPT_HTTPHEADER => array_merge(["Accept: application/json"], $headers),
];
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("cURL error: " . curl_error($ch));
}
return [
"status" => curl_getinfo($ch, CURLINFO_HTTP_CODE),
"body" => $body,
];
}
Reading the response
<?php
function describeResponse(int $status, string $body): string
{
$data = json_decode($body, true);
if ($status >= 200 && $status < 300) {
return "OK: " . ($data["name"] ?? "no name");
}
return "Error $status: " . ($data["message"] ?? "unknown error");
}
echo describeResponse(200, '{"name":"Pen"}') . "\n";
echo describeResponse(404, '{"message":"Not found"}');
OK: Pen
Error 404: Not found
Common cURL problems
| Message | Likely cause |
|---|---|
Could not resolve host | Wrong host name, or no internet from the server |
Operation timed out | The service is slow or blocked. Check timeouts and firewalls |
SSL certificate problem | Missing CA certificates (configure curl.cainfo in php.ini). Do not disable verification |
| Empty body, status 0 | The request failed. Read curl_error() |
Never disable certificate checks
You will find code online with CURLOPT_SSL_VERIFYPEER => false. It “fixes” certificate errors by turning off the protection that makes HTTPS safe. Fix the certificate configuration instead.
Guzzle and other clients
The popular Guzzle library (installed with Composer), Symfony’s HTTP client, and PHP’s own PSR-18 clients wrap cURL in a cleaner API with retries, async requests and middleware. For anything beyond a few calls, use one of them.
Timeouts are not optional
CURLOPT_TIMEOUT, a hanging service can hold your PHP process (and your visitor’s page) for as long as the server allows. Always set both timeouts.