A PHP script that serves an API sends JSON with the right headers and status code.
<?php
header("Content-Type: application/json; charset=utf-8");
echo json_encode(["status" => "ok", "time" => time()]);
Without the Content-Type header, the browser (or the app calling you) may treat the response as plain HTML.
A reusable helper
<?php
function jsonResponse(mixed $data, int $status = 200): string
{
// in a real script: http_response_code($status); header("Content-Type: application/json; charset=utf-8");
return json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR);
}
echo jsonResponse(["id" => 7, "name" => "Ana"]);
{"id":7,"name":"Ana"}
The real version also sends headers and stops the script.
<?php
declare(strict_types=1);
function sendJson(mixed $data, int $status = 200): never
{
http_response_code($status);
header("Content-Type: application/json; charset=utf-8");
header("X-Content-Type-Options: nosniff");
echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR);
exit;
}
The return type never says that this function does not come back. Use it like this:
<?php
require __DIR__ . "/api-helpers.php";
$product = findProduct((int) ($_GET["id"] ?? 0));
if ($product === null) {
sendJson(["error" => "Product not found"], 404);
}
sendJson($product);
Consistent error format
Clients can handle errors easily if every error looks the same.
<?php
function errorBody(string $code, string $message, array $details = []): array
{
$body = ["error" => ["code" => $code, "message" => $message]];
if ($details !== []) {
$body["error"]["details"] = $details;
}
return $body;
}
echo json_encode(errorBody("validation_failed", "The data is not valid.", ["email" => "Invalid email"]));
{"error":{"code":"validation_failed","message":"The data is not valid.","details":{"email":"Invalid email"}}}
Pair it with the right status code (see HTTP Status Codes): 400 or 422 for validation, 404 for missing, 500 for server errors. Never include stack traces or SQL in the message.
Lists with pagination info
<?php
$items = [["id" => 1], ["id" => 2]];
echo json_encode([
"data" => $items,
"meta" => ["page" => 1, "per_page" => 2, "total" => 11],
]);
{"data":[{"id":1},{"id":2}],"meta":{"page":1,"per_page":2,"total":11}}
Return an object at the top level ({"data": [...]}), not a bare array. It lets you add fields later without breaking clients.
Do not print anything else
Any stray output (a warning, an echo, a space before <?php) corrupts the JSON. Keep display_errors off in an API, and log errors instead.
CORS
When JavaScript on another website calls your API from a browser, the browser checks CORS headers. A simple setup for one trusted site:
<?php
$allowed = ["https://app.example.com"];
$origin = $_SERVER["HTTP_ORIGIN"] ?? "";
if (in_array($origin, $allowed, true)) {
header("Access-Control-Allow-Origin: " . $origin);
header("Vary: Origin");
header("Access-Control-Allow-Headers: Content-Type, Authorization");
header("Access-Control-Allow-Methods: GET, POST, PUT, DELETE");
}
if ($_SERVER["REQUEST_METHOD"] === "OPTIONS") { // the browser's "preflight" check
http_response_code(204);
exit;
}
Never use Access-Control-Allow-Origin: * together with cookies or private data. Allow only origins you trust.
Caching
Add Cache-Control for responses that can be cached (public, max-age=60), and no-store for private data.
Test with a client
curl -i https://yoursite.example/api.php, or a tool such as Postman. Look at the status code and the headers, not only the body.