Consuming APIs with PHP
Reviewed & published by Brayan K
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.
Part of the free PHP course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
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.
<?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";
?>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.
<?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";
?>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.
<?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";
?>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.
<?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);
?>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.
<?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
?>One more. A real client never decodes a response it hasn't checked. Add the condition that only proceeds on a successful 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
?>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 / Option | What 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_RETURNTRANSFER | Return the body instead of printing it |
| CURLOPT_HTTPHEADER | Set request headers (incl. auth token) |
| CURLOPT_POST / POSTFIELDS | Make a POST and set its JSON body |
| CURLOPT_TIMEOUT | Give 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 |
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.
<?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¤t=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
?>🎉 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
Practice quiz
What does json_decode($json, true) return?
- A stdClass object
- A JSON string
- An associative array
- A cURL handle
Answer: An associative array. Passing true as the second argument gives an associative array you can read by key, instead of a harder-to-use object.
What are the four steps of every cURL request, in order?
- init, setopt, exec, close
- open, read, write, close
- connect, send, receive, free
- create, configure, send, parse
Answer: init, setopt, exec, close. Every cURL request follows curl_init -> curl_setopt -> curl_exec -> curl_close.
What does CURLOPT_RETURNTRANSFER => true do?
- Sends the request body
- Sets a timeout
- Enables POST
- Returns the response as a string instead of printing it
Answer: Returns the response as a string instead of printing it. Without CURLOPT_RETURNTRANSFER, cURL prints the response; with it, curl_exec returns the body as a string.
How do you read the HTTP status code after a cURL request?
- curl_status($ch)
- curl_getinfo($ch, CURLINFO_HTTP_CODE)
- curl_code($ch)
- $ch->status
Answer: curl_getinfo($ch, CURLINFO_HTTP_CODE). curl_getinfo($ch, CURLINFO_HTTP_CODE) reads the status code (200, 404, 500, etc).
Which cURL option carries the JSON body of a POST request?
- CURLOPT_POSTFIELDS
- CURLOPT_BODY
- CURLOPT_HTTPHEADER
- CURLOPT_DATA
Answer: CURLOPT_POSTFIELDS. Set CURLOPT_POST => true and put the encoded JSON in CURLOPT_POSTFIELDS, with a Content-Type: application/json header.
Why does json_decode() sometimes return null?
- The body was too large
- cURL was not installed
- The input was not valid JSON (e.g. an HTML error page or empty body)
- The URL used https
Answer: The input was not valid JSON (e.g. an HTML error page or empty body). json_decode returns null on invalid JSON. Check the status code first, then json_last_error() to see what went wrong.
Where should API keys and tokens be stored?
- Hard-coded in the source file
- In environment variables, read with getenv()
- In a public config.js
- Committed to git for the team
Answer: In environment variables, read with getenv(). Never hard-code or commit secrets. Read them from the environment with getenv() and keep them in a .gitignored .env file.
How should you send a Bearer token for authentication?
- As a URL query parameter
- In CURLOPT_POSTFIELDS
- As a cookie only
- In an Authorization: Bearer <token> request header
Answer: In an Authorization: Bearer <token> request header. The common pattern is an Authorization: Bearer <token> header, with the token read from the environment.
What should you do when an API responds with HTTP 429?
- Immediately retry in a tight loop
- Read the Retry-After header and wait that many seconds before retrying
- Switch to file_get_contents
- Ignore it and continue
Answer: Read the Retry-After header and wait that many seconds before retrying. 429 means too many requests. Respect rate limits: read Retry-After and back off before trying again; cache to avoid re-requesting.
Why must you always set CURLOPT_TIMEOUT?
- It speeds up the request
- It enables HTTPS
- Without it, cURL waits indefinitely for a dead server and can freeze your app
- It validates the JSON
Answer: Without it, cURL waits indefinitely for a dead server and can freeze your app. With no timeout, cURL waits forever for a slow or dead server. Always set CURLOPT_TIMEOUT so a slow API fails fast.
Continue this course
- Previous: Security Best Practices
- Next: Advanced OOP Patterns: Factory, Singleton, Strategy & Repository — Apply GoF design patterns in production-quality PHP code
- Quick reference: PHP cheat sheet
- From the blog: Building Your First REST API with PHP
Frequently asked questions
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.
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.
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.
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.
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.