Building JSON APIs with PHP
Good APIs all feel the same: predictable URLs, real validation, honest status codes, sensible auth, pagination, safe retries, and error messages you can actually do something with. Here's how I build them in plain PHP.
Read and validate a JSON request
<?php
declare(strict_types=1);
header('Content-Type: application/json; charset=utf-8');
try {
$payload = json_decode(
file_get_contents('php://input'),
true,
512,
JSON_THROW_ON_ERROR
);
} catch (JsonException) {
http_response_code(400);
echo json_encode(['error' => ['code' => 'invalid_json', 'message' => 'Malformed JSON.']]);
exit;
}
$name = trim((string) ($payload['name'] ?? ''));
if ($name === '') {
http_response_code(422);
echo json_encode(['error' => ['code' => 'validation_failed', 'fields' => ['name' => 'Name is required.']]]);
exit;
}
Return a created resource
<?php
http_response_code(201);
header('Location: /api/v1/customers/' . $customerId);
echo json_encode([
'data' => [
'id' => $customerId,
'name' => $name,
'status' => 'active',
],
], JSON_THROW_ON_ERROR);
Consistent error shape
{
"error": {
"code": "trip_not_ready",
"message": "The trip cannot start until a vehicle and driver are assigned.",
"request_id": "req_7c761f23"
}
}
Give clients a stable error code their code can check, plus a message a person can read. Stack traces, SQL, credentials, and internal hostnames belong in your logs, never in the response.
Authentication options
| Method | Good fit | Considerations |
|---|---|---|
| Session cookie | Same-site browser application | Use CSRF protection for state changes. |
| Opaque API token | Simple integrations and service accounts | Store a hash, support expiration and revocation. |
| OAuth 2.0 / OpenID Connect | Third-party authorization or centralized identity | Use a mature library or identity provider. |
| Signed webhook | Inbound event delivery | Verify timestamp and signature over the raw body. |
Webhook signature verification
<?php
declare(strict_types=1);
$rawBody = file_get_contents('php://input');
$timestamp = $_SERVER['HTTP_X_WEBHOOK_TIMESTAMP'] ?? '';
$signature = $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'] ?? '';
if (!ctype_digit($timestamp) || abs(time() - (int) $timestamp) > 300) {
http_response_code(401);
exit;
}
$expected = hash_hmac('sha256', $timestamp . '.' . $rawBody, $_ENV['WEBHOOK_SECRET']);
if (!hash_equals($expected, $signature)) {
http_response_code(401);
exit;
}
Operational API features
- Version intentionally and document compatibility.
- Use idempotency keys for retryable create/payment operations.
- Paginate collections and set maximum page sizes.
- Apply per-identity and per-IP rate limits.
- Return request IDs and log execution duration.
- Set timeouts for every outbound HTTP request.
- Queue slow or unreliable work instead of holding the request open.
