http clients and apis
cURL
cURL is PHP's lower-level HTTP client extension. It gives detailed control over methods, headers, bodies, redirects, TLS, timeouts, and response metadata.
cURL is common in older PHP applications, small integrations, and codebases that do not use a framework HTTP client. Even when you use Guzzle or Symfony HttpClient, understanding cURL helps with debugging because many clients use cURL underneath.
Configure a request handle
cURL requests start with a handle from curl_init(). You then set options and execute the handle.
<?php
declare(strict_types=1);
if (!function_exists('curl_init')) {
echo 'cURL extension unavailable' . PHP_EOL;
return;
}
$ch = curl_init('https://api.example.test/products');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 5,
CURLOPT_HTTPHEADER => ['Accept: application/json'],
]);
echo 'cURL request configured' . PHP_EOL;
curl_close($ch);
// Prints:
// cURL request configured
CURLOPT_RETURNTRANSFER tells cURL to return the response body from curl_exec() instead of printing it. Most API clients should set it.
Send a JSON POST
For JSON APIs, encode the payload and set both Content-Type and Accept headers.
<?php
declare(strict_types=1);
$payload = ['name' => 'Keyboard', 'price' => 99.99];
$json = json_encode($payload, JSON_THROW_ON_ERROR);
$headers = [
'Accept: application/json',
'Content-Type: application/json',
];
echo 'POST body: ' . $json . PHP_EOL;
echo 'Headers: ' . implode(', ', $headers) . PHP_EOL;
// Prints:
// POST body: {"name":"Keyboard","price":99.99}
// Headers: Accept: application/json, Content-Type: application/json
In real cURL code, those values become CURLOPT_POSTFIELDS and CURLOPT_HTTPHEADER.
Transport errors and HTTP errors are different
curl_exec() can fail because of a transport problem: DNS failure, timeout, TLS issue, refused connection, and similar errors.
An HTTP response with status 404 or 500 is not the same kind of failure. The server responded; it responded with an error status.
<?php
declare(strict_types=1);
function classifyCurlResult(bool|string $body, int $statusCode, string $curlError): string
{
if ($body === false) {
return 'transport error: ' . $curlError;
}
if ($statusCode >= 400) {
return 'http error: ' . $statusCode;
}
return 'success';
}
echo classifyCurlResult('{"ok":true}', 200, '') . PHP_EOL;
echo classifyCurlResult('Not found', 404, '') . PHP_EOL;
echo classifyCurlResult(false, 0, 'Operation timed out') . PHP_EOL;
// Prints:
// success
// http error: 404
// transport error: Operation timed out
Good API code handles both categories deliberately.
Timeouts are required
Always set timeouts for external calls. Without timeouts, your PHP workers can be tied up waiting on slow services.
Useful options include:
CURLOPT_CONNECTTIMEOUT: time allowed to establish the connectionCURLOPT_TIMEOUT: total request timeCURLOPT_RETURNTRANSFER: return the body instead of printing itCURLOPT_HEADER: include response headers in the output if you need themCURLOPT_HTTPHEADER: request headersCURLOPT_CUSTOMREQUEST: method such asPATCHorDELETE
TLS verification
Do not disable TLS verification to "fix" certificate problems in production. Options like CURLOPT_SSL_VERIFYPEER => false make HTTPS much less meaningful.
If certificates fail, fix the certificate chain, CA bundle, environment, hostname, or proxy configuration.
A small request wrapper shape
This example avoids a real network call, but shows the result shape a wrapper should return:
<?php
declare(strict_types=1);
/**
* @return array{status: int, body: string, error: string|null}
*/
function fakeCurlResponse(bool $transportOk, int $status): array
{
if (!$transportOk) {
return ['status' => 0, 'body' => '', 'error' => 'Connection timed out'];
}
return ['status' => $status, 'body' => '{"ok":true}', 'error' => null];
}
$response = fakeCurlResponse(true, 200);
echo $response['status'] . ' ' . ($response['error'] ?? 'no transport error') . PHP_EOL;
// Prints:
// 200 no transport error
Returning status, body, and transport error separately makes the caller's decision clearer.
What to check in a project
Check every cURL request has connection and total timeouts.
Check transport errors are handled separately from HTTP status codes.
Check JSON requests set the correct headers and use json_encode().
Check secrets are sent in headers or bodies as the API expects, not logged or placed in URLs.
Check TLS verification has not been disabled.
Check whether repeated cURL setup should be moved into a small client class or replaced by a maintained HTTP client library.
What you should be able to do
After this lesson, you should be able to configure a cURL request, send JSON payloads, set timeouts, distinguish transport failures from HTTP error statuses, and spot unsafe TLS or logging choices.
Practice
Task: Classify cURL Outcomes
Write a small PHP script that classifies the outcome of a cURL-style request.
Requirements
- Use
declare(strict_types=1);. - Accept a response body, HTTP status code, and cURL error string.
- Return
successfor a 2xx response with no transport error. - Return
http_errorfor 4xx or 5xx statuses. - Return
transport_errorwhen the body isfalse. - Include all three cases in the output.
Check Your Work
Run the script and confirm a timeout is not treated the same as a 404 response.
Show solution
This solution models the decision you make after curl_exec().
<?php
declare(strict_types=1);
function classifyCurlOutcome(bool|string $body, int $statusCode, string $curlError): string
{
if ($body === false) {
return 'transport_error: ' . $curlError;
}
if ($statusCode >= 400) {
return 'http_error: ' . $statusCode;
}
return 'success: ' . $statusCode;
}
echo classifyCurlOutcome('{"ok":true}', 200, '') . PHP_EOL;
echo classifyCurlOutcome('Not found', 404, '') . PHP_EOL;
echo classifyCurlOutcome(false, 0, 'Operation timed out') . PHP_EOL;
// Prints:
// success: 200
// http_error: 404
// transport_error: Operation timed out
In real cURL code, $body comes from curl_exec($ch), the status comes from curl_getinfo($ch, CURLINFO_RESPONSE_CODE), and the error comes from curl_error($ch).
Why This Works
The examples separate network failure from server responses. That distinction matters for retries, alerts, logs, and user-facing error messages.