Consuming APIs (cURL & HTTP)
Most non-trivial applications eventually need to talk to some other service over HTTP — checking an exchange rate, verifying a payment, sending a message through a third-party provider, looking up a shipping rate. PHP's built-in way of doing this is the cURL extension, a thin wrapper around the widely used libcurl library that gives fine-grained control over an HTTP request: which method to use, which headers to send, how to authenticate, and how to handle timeouts and errors. It's verbose compared to HTTP clients in some other languages, but understanding the raw cURL pattern makes it much easier to reason about what a higher-level library like Guzzle is actually doing underneath.
The curl_init / curl_setopt / curl_exec pattern
Every cURL request in PHP follows the same three-step shape: curl_init() creates a request "handle" (a resource representing the request being built), curl_setopt() is called once per option to configure that handle — the URL, the method, headers, and so on — and curl_exec() actually sends the request and returns the response body. A final curl_close() releases the handle once it's no longer needed.
A basic GET request
<?php
$ch = curl_init('https://api.example.com/status');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
$response = curl_exec($ch);
curl_close($ch);
echo $response;CURLOPT_RETURNTRANSFER is easy to forget but changes the behavior completely: without it, curl_exec() prints the response body directly and returns true/false instead of giving the body back as a string, which is almost never what a script actually wants. CURLOPT_TIMEOUT caps how long PHP will wait for the whole request to complete — without a timeout, a script can hang indefinitely if the remote API stalls, which is a real production risk for anything handling incoming user requests on a web server.
Sending headers and an auth token
Most real APIs require an Authorization header carrying an API key or bearer token, and often expect a specific Content-Type for the request body. CURLOPT_HTTPHEADER takes an array of raw header strings, and CURLOPT_POSTFIELDS supplies the request body for a POST, PUT, or PATCH request. Setting CURLOPT_CUSTOMREQUEST is how the HTTP method itself is changed away from the default GET.
A POST request with a bearer token and a JSON body
<?php
$payload = json_encode([
'amount' => 2500,
'currency' => 'usd',
]);
$ch = curl_init('https://api.example.com/v1/charges');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Authorization: Bearer ' . getenv('API_TOKEN'),
'Content-Type: application/json',
]);
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
$response = curl_exec($ch);
$statusCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
echo "Status: {$statusCode}\n";
echo $response;Status: 201
{"id":"ch_1a2b3c","amount":2500,"currency":"usd","status":"succeeded"}Decoding the response and handling errors
A successful response body is usually a JSON string that still needs json_decode() to become a usable PHP array or object. But curl_exec() returning a string doesn't necessarily mean the request succeeded — a curl_exec() return value of false indicates a transport-level failure (DNS resolution failed, the connection timed out, the remote host refused the connection), which is completely different from an HTTP-level failure like a 404 or 500 response, which cURL treats as a perfectly normal completed request. Checking both layers separately is the difference between an API client that fails silently and one that reports useful errors.
Checking curl_errno() and the HTTP status separately
<?php
$ch = curl_init('https://api.example.com/v1/charges/ch_1a2b3c');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
$response = curl_exec($ch);
if (curl_errno($ch)) {
// Transport-level failure: DNS, connection refused, timeout, etc.
$error = curl_error($ch);
curl_close($ch);
throw new RuntimeException("Request failed: {$error}");
}
$statusCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($statusCode >= 400) {
// The request reached the server, but it rejected it.
throw new RuntimeException("API returned HTTP {$statusCode}: {$response}");
}
$data = json_decode($response, true);
echo $data['status'];Guzzle: the ecosystem-standard HTTP client
Raw cURL works, but the option-setting boilerplate above gets repetitive fast across a real application with many outgoing API calls. Guzzle is the HTTP client the PHP ecosystem has broadly standardized on for this — it wraps the same underlying transport cURL uses, but exposes a far more ergonomic API: JSON bodies, headers, and query parameters are passed as plain array options, responses come back as proper objects with helper methods, and it integrates with the PSR-7 HTTP message interfaces so it plays nicely with any other PSR-7-aware library or framework middleware.
The same POST request, using Guzzle
<?php
require __DIR__ . '/vendor/autoload.php';
use GuzzleHttp\Client;
use GuzzleHttp\Exception\RequestException;
$client = new Client(['timeout' => 10]);
try {
$response = $client->post('https://api.example.com/v1/charges', [
'headers' => [
'Authorization' => 'Bearer ' . getenv('API_TOKEN'),
],
'json' => [
'amount' => 2500,
'currency' => 'usd',
],
]);
$data = json_decode($response->getBody()->getContents(), true);
echo $data['status'];
} catch (RequestException $e) {
error_log('Charge request failed: ' . $e->getMessage());
}Guzzle also throws an exception by default on a 4xx or 5xx response, which removes the need to manually branch on the status code for the common case — errors surface as exceptions the same way any other PHP failure would, which fits much more naturally into normal try/catch error handling than manually checking a status code after every call.
Every raw cURL request follows the same pattern:
curl_init(), one or morecurl_setopt()calls,curl_exec(), thencurl_close().CURLOPT_RETURNTRANSFERmust be set totrueto get the response body back as a string instead of it being printed directly.CURLOPT_HTTPHEADERsends custom headers likeAuthorization: Bearer ...;CURLOPT_POSTFIELDSsends the request body.curl_errno()catches transport-level failures (DNS, timeout, refused connection);CURLINFO_HTTP_CODEviacurl_getinfo()catches HTTP-level failures like 404 or 500 — check both.Guzzle wraps the same underlying transport with a far more ergonomic API, PSR-7 integration, and exceptions thrown automatically on error responses.