Skip to main content
    Courses/PHP/Consuming APIs

    Lesson 14 • Expert

    Consuming APIs with PHP 🌐

    By the end of this lesson you'll be able to call any external API from PHP — fetch data with file_get_contents and cURL, send authenticated POST requests, parse the JSON you get back, and handle errors, status codes, and timeouts like a professional.

    What You'll Learn in This Lesson

    • Fetch a remote URL with file_get_contents and a stream context
    • Make full HTTP requests with cURL: init, setopt, exec, close
    • Send headers, a JSON POST body, and a Bearer auth token
    • Parse the JSON response with json_decode into a PHP array
    • Check status codes and handle errors and timeouts safely
    • Respect rate limits and keep your API keys out of your code

    1️⃣ The Quick Way: file_get_contents

    The fastest way to read a remote URL is file_get_contents() — the same function you'd use to read a file, except you hand it a web address. It downloads the response body as one big string. Most APIs answer in JSON (a text format for structured data), so you then call json_decode($json, true) to turn that text into a PHP associative array you can read by key. The true matters: it gives you an array, not a harder-to-use object.

    Fetch JSON with file_get_contents
    <?php
    // The simplest way to fetch a URL: file_get_contents().
    // It downloads the body of the page (or API response) as a string.
    
    $url  = "https://api.github.com/repos/php/php-src";
    $json = file_get_contents($url);   // returns the raw JSON text, or false on failure
    
    // json_decode turns that JSON text into PHP data.
    // The second argument 'true' gives you an ASSOCIATIVE ARRAY (not an object).
    $data = json_decode($json, true);
    
    echo "Repo:  " . $data["full_name"] . "\n";   // dig into the decoded array by key
    echo "Stars: " . $data["stargazers_count"] . "\n";
    echo "Lang:  " . $data["language"] . "\n";
    ?>
    Output
    Repo:  php/php-src
    Stars: 38000
    Lang:  C
    This is real code — run it for free atonecompiler.com/phpor in your own editor.

    Two lines do the real work: one fetches the text, one decodes it. The catch is that file_get_contents on a URL is bare-bones — it sends no useful headers, has no obvious timeout, and on failure returns false with only a warning. For real APIs you need to configure the request, which is the next section.

    2️⃣ Adding Headers & a Timeout: Stream Context

    A stream context is an options bundle you pass as the third argument to file_get_contents. It lets you set the HTTP method, request headers (many APIs reject a request with no User-Agent), and a timeout so a dead server can't hang your script forever. After the call, PHP auto-fills $http_response_header with the response status line so you can see what the server actually said.

    file_get_contents with headers + timeout
    <?php
    // Many APIs reject a request with no User-Agent, or need a header/timeout.
    // file_get_contents accepts a "stream context" so you can set those.
    
    $options = [
        "http" => [
            "method"  => "GET",
            "header"  => "User-Agent: my-app\r\n" .   // GitHub REQUIRES a User-Agent
                         "Accept: application/json\r\n",
            "timeout" => 5,                            // give up after 5 seconds
            "ignore_errors" => true,                   // still read the body on 404/500
        ],
    ];
    $context = stream_context_create($options);        // build the context object
    
    $url  = "https://api.github.com/repos/php/php-src";
    $body = file_get_contents($url, false, $context);  // pass the context as 3rd arg
    
    // $http_response_header is auto-filled by PHP with the response status line.
    echo $http_response_header[0] . "\n";              // e.g. HTTP/1.1 200 OK
    
    $data = json_decode($body, true);
    echo "Open issues: " . $data["open_issues_count"] . "\n";
    ?>
    Output
    HTTP/1.1 200 OK
    Open issues: 350
    This is real code — run it for free atonecompiler.com/phpor in your own editor.

    Setting ignore_errors => true is important: without it, a 404 or 500 makes file_get_contents return false and throw away the body — but error responses usually contain a helpful JSON message you'll want to read. This works, but cURL gives you cleaner control, so it's what professionals reach for.

    3️⃣ The Standard Way: cURL

    cURL is PHP's built-in library for HTTP requests and the tool you'll use most. Every cURL request follows the same four steps: curl_init() creates a request handle, curl_setopt() configures it (one option at a time), curl_exec() sends it, and curl_close() frees the handle. The single most important option is CURLOPT_RETURNTRANSFER => true — without it, cURL prints the response instead of returning it as a string.

    A GET request with cURL
    <?php
    // cURL is the standard, fully-featured way to talk to APIs in PHP.
    // Four steps every time: init -> setopt -> exec -> close.
    
    $ch = curl_init("https://api.github.com/repos/php/php-src");  // 1) init with the URL
    
    // 2) setopt — configure the request
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);   // return the body as a string (don't print it)
    curl_setopt($ch, CURLOPT_TIMEOUT, 10);            // hard limit: 10 seconds total
    curl_setopt($ch, CURLOPT_HTTPHEADER, [            // request headers, one string each
        "User-Agent: my-app",
        "Accept: application/json",
    ]);
    
    $body   = curl_exec($ch);                                  // 3) exec — actually send the request
    $status = curl_getinfo($ch, CURLINFO_HTTP_CODE);           // read the HTTP status code (200, 404…)
    curl_close($ch);                                           // 4) close — free the handle
    
    echo "HTTP status: " . $status . "\n";
    
    $data = json_decode($body, true);                          // JSON text -> PHP array
    echo "Repo:  " . $data["full_name"] . "\n";
    echo "Forks: " . $data["forks_count"] . "\n";
    ?>
    Output
    HTTP status: 200
    Repo:  php/php-src
    Forks: 7600
    This is real code — run it for free atonecompiler.com/phpor in your own editor.

    Notice curl_getinfo($ch, CURLINFO_HTTP_CODE) — that's how you read the HTTP status code (200 = OK, 404 = not found, 500 = server error). Always set a CURLOPT_TIMEOUT so one slow API can't freeze your whole app.

    4️⃣ POST, JSON Bodies & Bearer Auth

    To send data — create a record, log in, trigger an action — you make a POST request. Set CURLOPT_POST => true, put your JSON in CURLOPT_POSTFIELDS, and add a Content-Type: application/json header so the server knows how to read it. Most APIs require authentication: the common pattern is a Bearer token, a secret string you send in an Authorization: Bearer <token> header. Read that token from the environment with getenv() — never write it into your code.

    Authenticated POST with full error handling
    <?php
    // A POST request with a Bearer token, JSON body, and full error handling.
    
    $token   = getenv("API_TOKEN");                  // read the secret from the environment
    $payload = ["title" => "Buy milk", "done" => false];
    
    $ch = curl_init("https://api.example.com/v1/todos");
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_POST, true);            // make this a POST
    curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload));  // the JSON body
    curl_setopt($ch, CURLOPT_HTTPHEADER, [
        "Authorization: Bearer " . $token,           // bearer-token auth
        "Content-Type: application/json",            // tell the server we're sending JSON
    ]);
    curl_setopt($ch, CURLOPT_TIMEOUT, 10);
    
    $body   = curl_exec($ch);
    $status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    
    if ($body === false) {                           // network/timeout failure
        echo "Request failed: " . curl_error($ch) . "\n";
    } elseif ($status >= 200 && $status < 300) {     // 2xx = success
        $data = json_decode($body, true);
        echo "Created todo #" . $data["id"] . " (status " . $status . ")\n";
    } else {                                          // 4xx / 5xx = the server said no
        echo "API error: HTTP " . $status . "\n";
    }
    curl_close($ch);
    ?>
    Output
    Created todo #42 (status 201)
    This is real code — run it for free atonecompiler.com/phpor in your own editor.

    This is the production shape of an API call: send the request, then branch on the result. First check whether the request failed at the network level ($body === false), then check the status code for 2xx success, and only decode the JSON when you know you have a real response. Skipping these checks is the number-one source of API bugs.

    5️⃣ Status Codes, Timeouts & Rate-Limit Etiquette

    An API can refuse you for many reasons, and it tells you which with a status code: 200/201 success, 401 bad token, 404 not found, 429 too many requests, 500 server error. Always branch on the code before trusting the body. Equally, always set a timeout so a hanging server can't take your app down with it.

    Being a good API citizen — rate-limit etiquette — keeps you from getting blocked. Don't request the same data twice; cache it. Don't hammer an endpoint in a tight loop; add a small delay. And if you get a 429, read the Retry-After header and wait that many seconds before retrying. Now you try the two core skills — fetching and guarding.

    Fill in each ___ using the 👉 hint, then run it and check it against the Output panel.

    🎯 Your turn: decode the response
    <?php
    // 🎯 YOUR TURN — fetch a JSON API and pull two values out of the response.
    // Fill in each blank marked ___ , then run it.
    
    $body = file_get_contents("https://api.github.com/repos/php/php-src", false,
        stream_context_create(["http" => ["header" => "User-Agent: my-app\r\n"]]));
    
    // 1) Turn the JSON text into a PHP associative array
    $data = json_decode($body, ___);     // 👉 pass true so you get an ARRAY, not an object
    
    // 2) Read the "full_name" key out of $data
    echo "Repo: " . ___ . "\n";          // 👉 e.g.  $data["full_name"]
    
    // ✅ Expected output:
    //    Repo: php/php-src
    ?>
    Output
    Repo: php/php-src
    Fill the two ___ blanks: pass true to json_decode, then read $data["full_name"]. Run it on a machine with internet.

    One more. A real client never decodes a response it hasn't checked. Add the condition that only proceeds on a successful status code.

    🎯 Your turn: guard on the status code
    <?php
    // 🎯 YOUR TURN — only call json_decode when the request actually succeeded.
    // A 404 or a timeout returns junk, so guard on the status code FIRST.
    
    $ch = curl_init("https://api.github.com/repos/php/php-src");
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_HTTPHEADER, ["User-Agent: my-app"]);
    $body   = curl_exec($ch);
    $status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);
    
    // Only decode on a successful 2xx response
    if (___) {                            // 👉 check $status is 200 (or "< 300")
        $data = json_decode($body, true);
        echo "OK: " . $data["full_name"] . "\n";
    } else {
        echo "Request failed with status " . $status . "\n";
    }
    
    // ✅ Expected output (when the call succeeds):
    //    OK: php/php-src
    ?>
    Output
    OK: php/php-src
    Replace the ___ with a check such as $status === 200 (or $status < 300) so JSON is only decoded on success.

    Common Errors (and the fix)

    • No error handling — your script crashes on a bad response — you decoded $body without checking it first, so a failed call feeds null into the rest of your code. Always check $body === false (network failure) and the status code before json_decode.
    • Ignoring status codes — you trust a 404 or 500 body — a request that "worked" at the network level can still be an error page. Read curl_getinfo($ch, CURLINFO_HTTP_CODE) and only treat 2xx as success; on 429 back off, on 401 fix your token.
    • Blocking timeouts — your whole app hangs — with no timeout, cURL waits forever for a dead server. Always set CURLOPT_TIMEOUT (total) and CURLOPT_CONNECTTIMEOUT (connect) so a slow API fails fast instead of freezing your page.
    • Leaking API keys — your token ends up in git — hard-coding "Bearer sk-12345" in a file means anyone with the code has your secret. Read it from the environment with getenv("API_TOKEN") and keep the value in a .env file that's in .gitignore.
    • "json_decode(): returns null" — the response wasn't valid JSON (often an HTML error page or an empty body). After decoding, check json_last_error() !== JSON_ERROR_NONE and log json_last_error_msg() to see why.

    Pro Tips

    • 💡 Default to cURL. Use file_get_contents only for a quick public GET; reach for cURL the moment you need auth, a POST, or a reliable status code.
    • 💡 Cache responses. If you fetched a list a minute ago, don't fetch it again — store it (file, Redis, database) to stay fast and under rate limits.
    • 💡 Consider Guzzle for big projects. The Guzzle HTTP client wraps cURL in a much cleaner API with retries and middleware — but knowing raw cURL first means you understand what it's doing.

    📋 Quick Reference — Consuming APIs

    Function / OptionWhat It Does
    file_get_contents($url)Quick GET — returns the body as a string
    stream_context_create([...])Add headers/timeout to file_get_contents
    curl_init($url)Create a cURL request handle
    curl_setopt($ch, OPT, val)Configure one request option
    CURLOPT_RETURNTRANSFERReturn the body instead of printing it
    CURLOPT_HTTPHEADERSet request headers (incl. auth token)
    CURLOPT_POST / POSTFIELDSMake a POST and set its JSON body
    CURLOPT_TIMEOUTGive up after N seconds
    curl_exec($ch)Send the request, get the response
    curl_getinfo($ch, CURLINFO_HTTP_CODE)Read the HTTP status code
    json_decode($body, true)JSON string → PHP associative array

    Frequently Asked Questions

    Q: Should I use file_get_contents() or cURL to call an API?

    file_get_contents() is fine for a quick GET when you just need to read a public URL and the allow_url_fopen setting is enabled. For anything real — POST/PUT/DELETE, custom headers, bearer-token auth, fine-grained timeouts, or reading the HTTP status code reliably — use cURL. It is the de-facto standard in PHP, works everywhere, and gives you full control over the request and the response.

    Q: Why does json_decode() return null?

    json_decode() returns null when the input is not valid JSON. The usual causes are an empty or failed response (the request errored, so check the status code first), an HTML error page returned instead of JSON, or a trailing/leading character. After decoding, call json_last_error() — if it is not JSON_ERROR_NONE, log json_last_error_msg() to see exactly what went wrong instead of letting null propagate.

    Q: Where should I store my API keys and tokens?

    Never hard-code them in your source files and never commit them to git. Put them in environment variables (read with getenv()) or a .env file that is listed in .gitignore, and load them at runtime. This keeps secrets out of your repository, lets you use different keys per environment, and means a leaked code file does not leak your credentials.

    Q: How do I avoid hitting an API's rate limit?

    Read the response headers — most APIs send X-RateLimit-Remaining and a Retry-After header. Cache responses you have already fetched so you do not re-request the same data, batch requests where the API supports it, and back off: if you get a 429 Too Many Requests, wait the number of seconds in Retry-After before trying again. Adding a small delay between calls in a loop is polite and keeps you under the limit.

    Q: Why is my cURL request hanging or timing out?

    Without a timeout, cURL will wait indefinitely for a slow or dead server, which can freeze your whole script. Always set CURLOPT_TIMEOUT (total time) and CURLOPT_CONNECTTIMEOUT (time to connect). If a request still fails, check $body === false and read curl_error() — common causes are a wrong URL, an SSL certificate problem, or no network access from the server.

    Mini-Challenge: Weather Lookup Client

    No code is filled in this time — just a brief and an outline. Write it yourself, run it on a machine with internet, then check your result against the expected output in the comments. This is exactly the fetch-check-decode loop you'll use against every real API.

    🎯 Mini-Challenge: fetch and read a weather API
    <?php
    // 🎯 MINI-CHALLENGE: A tiny weather lookup client.
    // No code is filled in — work from the steps below, then run it.
    //
    // 1. Put your API key in an env var and read it:  $key = getenv("WEATHER_KEY");
    // 2. Build the URL for a real free API, e.g.
    //      "https://api.open-meteo.com/v1/forecast?latitude=51.5&longitude=-0.1&current=temperature_2m"
    //    (open-meteo needs no key — great for testing).
    // 3. Use cURL: init -> set CURLOPT_RETURNTRANSFER + a 10s CURLOPT_TIMEOUT -> exec.
    // 4. Read the status with curl_getinfo(..., CURLINFO_HTTP_CODE).
    // 5. If status is 2xx, json_decode the body (true) and echo the temperature.
    //    Otherwise echo "Lookup failed: HTTP <status>".
    //
    // ✅ Expected output (example):
    //    London is currently 18°C
    
    // your code here
    ?>
    Build a cURL request to a real API (open-meteo needs no key), set a timeout, guard on the status code, decode the JSON, and print one value from the response.

    🎉 Lesson Complete!

    • file_get_contents fetches a URL fast; a stream context adds headers and a timeout
    • cURL is the standard: init → setopt → exec → close, with CURLOPT_RETURNTRANSFER to get the body back
    • ✅ Send a JSON body with CURLOPT_POST + CURLOPT_POSTFIELDS, and auth with an Authorization: Bearer header
    • json_decode($body, true) turns the JSON response into a PHP array
    • ✅ Always check the status code and $body === false before trusting the response, and set a timeout
    • ✅ Keep keys in the environment, cache responses, and back off on 429 to respect rate limits
    • Next lesson: Advanced OOP Patterns — structure production-quality, reusable code

    Sign up for free to track which lessons you've completed and get learning reminders.

    Previous

    Cookie & Privacy Settings

    We use cookies to improve your experience, analyze traffic, and show personalized ads. You can manage your preferences below.

    By clicking "Accept All", you consent to our use of cookies for analytics and personalized advertising. You can customize your preferences or reject non-essential cookies.

    Privacy PolicyTerms of Service