Skip to content
Happy Programming Guide
Start learning
PHP

Working with APIs in PHP

Call a REST API from PHP with cURL or Guzzle, handle JSON and errors properly, add authentication, and build a small API of your own.

A view of the office desk with a monitor, water bottle, indoor plants, and cables

Calling an API from PHP means making an HTTP request and parsing the JSON that comes back. The built-in tools do this — file_get_contents for the simplest cases, cURL for everything else — and Guzzle makes it pleasant. This guide covers all three, plus the error handling that separates code that works from code that works reliably.

The simplest request#

PHP
<?php
$json = file_get_contents("https://api.example.com/products");

if ($json === false) {
    exit("Request failed.");
}

$products = json_decode($json, true);

Fine for a quick script. Its limits appear quickly: no control over timeouts, no access to the status code, and it depends on a PHP setting that some hosts disable. For anything real, use cURL.

cURL: the standard approach#

PHP
<?php
function apiGet(string $url, array $headers = []): array
{
    $ch = curl_init($url);

    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT        => 10,
        CURLOPT_CONNECTTIMEOUT => 5,
        CURLOPT_FOLLOWLOCATION => true,
        CURLOPT_HTTPHEADER     => array_merge(["Accept: application/json"], $headers),
    ]);

    $body = curl_exec($ch);
    $status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    $error = curl_error($ch);
    curl_close($ch);

    if ($body === false) {
        throw new RuntimeException("Request failed: $error");
    }

    if ($status >= 400) {
        throw new RuntimeException("API returned $status: " . substr($body, 0, 200));
    }

    $data = json_decode($body, true);

    if (json_last_error() !== JSON_ERROR_NONE) {
        throw new RuntimeException("Invalid JSON: " . json_last_error_msg());
    }

    return $data;
}

Four checks in that function, and each one catches a real failure: the request itself failing, an error status, invalid JSON, and a timeout. Code without them fails in ways that are hard to diagnose.

Sending data#

PHP
<?php
function apiPost(string $url, array $payload, array $headers = []): array
{
    $ch = curl_init($url);

    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_POST           => true,
        CURLOPT_POSTFIELDS     => json_encode($payload),
        CURLOPT_TIMEOUT        => 10,
        CURLOPT_HTTPHEADER     => array_merge([
            "Content-Type: application/json",
            "Accept: application/json",
        ], $headers),
    ]);

    $body = curl_exec($ch);
    $status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);

    if ($status >= 400) {
        throw new RuntimeException("API returned $status");
    }

    return json_decode($body, true) ?? [];
}

$result = apiPost("https://api.example.com/orders", [
    "product_id" => 42,
    "quantity"   => 2,
]);

For other methods, set CURLOPT_CUSTOMREQUEST to “PUT”, “PATCH” or “DELETE”.

Guzzle#

Terminal
composer require guzzlehttp/guzzle
PHP
<?php
require "vendor/autoload.php";

use GuzzleHttp\Client;
use GuzzleHttp\Exception\RequestException;

$client = new Client([
    "base_uri" => "https://api.example.com/",
    "timeout"  => 10,
    "headers"  => [
        "Accept"        => "application/json",
        "Authorization" => "Bearer " . getenv("API_TOKEN"),
    ],
]);

try {
    $response = $client->get("products", ["query" => ["page" => 1]]);
    $products = json_decode((string) $response->getBody(), true);
} catch (RequestException $e) {
    $status = $e->getResponse()?->getStatusCode();
    error_log("API call failed with status $status");
    $products = [];
}

Guzzle throws on error statuses by default, handles query strings and JSON bodies, and supports concurrent requests. For anything beyond a couple of calls it is worth the dependency.

Authentication#

PHP
<?php
// Bearer token
$headers = ["Authorization: Bearer " . getenv("API_TOKEN")];

// API key in a custom header
$headers = ["X-API-Key: " . getenv("API_KEY")];

// Basic authentication
curl_setopt($ch, CURLOPT_USERPWD, getenv("API_USER") . ":" . getenv("API_PASS"));

Retries and rate limits#

PHP
<?php
function apiGetWithRetry(string $url, int $attempts = 3): array
{
    $delay = 1;

    for ($i = 1; $i <= $attempts; $i++) {
        try {
            return apiGet($url);
        } catch (RuntimeException $e) {
            if ($i === $attempts) {
                throw $e;
            }
            sleep($delay);
            $delay *= 2;      // back off
        }
    }

    return [];
}

Doubling the delay each time is called exponential backoff. Retrying immediately in a tight loop makes an overloaded server worse and often gets you blocked.

Only retry on failures that might succeed later — timeouts, 429, 502, 503. A 400 or 401 will fail identically every time.

Caching#

PHP
<?php
function cachedGet(string $url, int $ttl = 300): array
{
    $file = sys_get_temp_dir() . "/api_" . md5($url) . ".json";

    if (file_exists($file) && (time() - filemtime($file)) < $ttl) {
        return json_decode(file_get_contents($file), true);
    }

    $data = apiGet($url);
    file_put_contents($file, json_encode($data));
    return $data;
}

Most API data does not change every second. Caching for even five minutes can cut your request count enormously and makes your pages noticeably faster.

Serving your own API#

PHP
<?php
declare(strict_types=1);

header("Content-Type: application/json; charset=utf-8");

if ($_SERVER["REQUEST_METHOD"] !== "GET") {
    http_response_code(405);
    header("Allow: GET");
    echo json_encode(["error" => "Method not allowed"]);
    exit;
}

$id = filter_input(INPUT_GET, "id", FILTER_VALIDATE_INT);

if ($id === false || $id === null) {
    http_response_code(400);
    echo json_encode(["error" => "A numeric id is required"]);
    exit;
}

$stmt = $pdo->prepare("SELECT id, name, price FROM products WHERE id = ?");
$stmt->execute([$id]);
$product = $stmt->fetch();

if (!$product) {
    http_response_code(404);
    echo json_encode(["error" => "Not found"]);
    exit;
}

echo json_encode($product, JSON_UNESCAPED_UNICODE);

Three habits in that snippet: set the content type, use the right status code for each outcome, and return errors as JSON rather than HTML. A client parsing your response should never receive an HTML error page.

Questions people ask#

cURL or Guzzle?

cURL if you want no dependencies and are making a handful of calls. Guzzle once you have several endpoints, need concurrency, or want retries and middleware without writing them yourself.

Why does json_decode return null?

The response was not valid JSON — often an HTML error page. Check json_last_error_msg() and print the first 200 characters of the body; the cause is usually obvious immediately.

Should json_decode use true for associative arrays?

Passing true gives arrays, which most PHP code handles more naturally. Without it you get objects and use arrow syntax. Pick one and be consistent.

How do I debug a failing request?

Print the status code, the response body and the request headers. Then reproduce it with the curl command line — that separates “my PHP is wrong” from “the API is rejecting this”.

Where to go next#

What is an API? The concepts behind the codeRead next

Keep reading

PHP

PHP PDO Explained With Examples

PDO is the standard way to talk to a database from PHP. Connecting, prepared statements that stop SQL injection, fetching rows, transactions…

5 min read

PHP

Introduction to PHP

What PHP is, how it differs from JavaScript, how a request becomes a page, and your first working script — with the…

5 min read

Keep going — pick your next guide

The fastest way to improve is to read one guide, then build the thing it describes. Start with the basics, or jump straight to a project.

Ask a question or share what worked

Your email address will not be published. Required fields are marked *