NightFox

NightFox

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

MethodGood fitConsiderations
Session cookieSame-site browser applicationUse CSRF protection for state changes.
Opaque API tokenSimple integrations and service accountsStore a hash, support expiration and revocation.
OAuth 2.0 / OpenID ConnectThird-party authorization or centralized identityUse a mature library or identity provider.
Signed webhookInbound event deliveryVerify 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.

Last Modified: October 12, 2026, 11:46 am