# PHP movie API tutorial

> Build one reusable PHP function and a tiny movie page. The function calls amodb with cURL, reads Retry-After on a 429, and caches each reply in a file for 6 hours. The page shows poster, cast and IMDb score. It needs PHP 8.2 or newer with the curl extension and takes about 20 minutes.

You will build two small files. `amodb.php` holds one function, `amodb_get()`, that calls the API with cURL and caches replies in files. `movie.php` is a page that shows a movie by id with poster, cast and IMDb score. You need PHP 8.2 or newer with the `curl` extension and about 20 minutes.

## Step 1: Get an API key

Buy a prepaid period on [pricing](https://amodb.dev/pricing/). There is no free tier, and every plan returns the full dataset at 20, 40 or 120 requests per minute. Create a key in your account dashboard. It is 32 lowercase hex characters. Export it before you start PHP:

```bash
export AMODB_KEY="YOUR_API_KEY"
```

The code reads it with `getenv('AMODB_KEY')`. Under PHP-FPM, workers do not see your shell, so set `env[AMODB_KEY]` in the pool configuration instead.

## Step 2: Prepare the project

Make a folder and check that cURL is available:

```bash
mkdir movie-page && cd movie-page
php -v
php -m | grep -i curl
```

The first command should report 8.2 or higher. The second must print `curl`.

## Step 3: Send the first request

A bare cURL call needs four things: the URL, a timeout, the `Authorization` header and a status check. In PHP 8 the handle frees itself, so there is no `curl_close()`.

```php
<?php
$ch = curl_init('https://api.amodb.dev/3/movie/550?language=en-US');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT => 10,
    CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('AMODB_KEY')],
]);
$body = curl_exec($ch);
echo curl_getinfo($ch, CURLINFO_RESPONSE_CODE), PHP_EOL;
```

## Step 4: Wrap it in a function

Calling cURL from every page repeats the same setup. `amodb_get($path, $query, $ttl)` builds the URL with `http_build_query`, sends the call and returns a decoded array. The movie page passes `append_to_response=credits,ratings`, so cast and scores arrive in the same request. Field details are on [movies](https://amodb.dev/movies/), and poster sizes are on [images](https://amodb.dev/images-api/).

## Step 5: Handle errors and rate limits

The function throws `AmodbException` for every failure, and the page catches it in one place.

| Status     | What the function does                                                                            |
| ---------- | ------------------------------------------------------------------------------------------------- |
| 401        | Throws at once. A bad or suspended key cannot be fixed by retrying.                               |
| 404        | Throws with the `status_message`, so the page can say the id is unknown.                          |
| 429        | Reads the `Retry-After` header, sleeps for that many seconds (1 to 30), then retries up to twice. |
| 5xx        | Throws with the status code and message.                                                          |
| cURL error | Throws `Network error` with the text from `curl_error()`.                                         |

The header arrives through `CURLOPT_HEADERFUNCTION`, a callback that cURL runs for each response header line. See [errors](https://amodb.dev/errors/) for the full code list.

## Step 6: Cache replies in files

The edge keeps details for 6 hours. A local file saves the request itself, and a cache hit costs nothing from your limit. Ages add up, so a file that is 6 hours old can hold data that was already hours old at the edge. Lower the TTL if that matters. The function hashes the final URL with `sha1`, looks for a file with that name and returns it when its modified time is newer than the TTL. Otherwise it fetches, writes the body with `LOCK_EX` and returns the data.

## Step 7: The complete program

Save the function as `amodb.php`.

```php
<?php
declare(strict_types=1);

const AMODB_BASE = 'https://api.amodb.dev/3';

final class AmodbException extends RuntimeException
{
}

function amodb_get(string $path, array $query = [], int $ttl = 21600): array
{
    $key = getenv('AMODB_KEY');
    if ($key === false || $key === '') {
        throw new AmodbException('Set the AMODB_KEY environment variable.');
    }

    $url = AMODB_BASE . $path . ($query ? '?' . http_build_query($query) : '');
    $dir = sys_get_temp_dir() . '/amodb-cache';
    $file = $dir . '/' . sha1($url) . '.json';
    if (is_file($file) && time() - filemtime($file) < $ttl) {
        return json_decode((string) file_get_contents($file), true, 512, JSON_THROW_ON_ERROR);
    }

    for ($attempt = 1; $attempt <= 3; $attempt++) {
        $retryAfter = 1;
        $ch = curl_init($url);
        curl_setopt_array($ch, [
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_CONNECTTIMEOUT => 5,
            CURLOPT_TIMEOUT => 10,
            CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $key, 'Accept: application/json'],
            CURLOPT_HEADERFUNCTION => static function ($handle, string $line) use (&$retryAfter): int {
                if (stripos($line, 'retry-after:') === 0) {
                    $retryAfter = (int) trim(substr($line, 12));
                }
                return strlen($line);
            },
        ]);
        $body = curl_exec($ch);
        $status = (int) curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
        $error = curl_error($ch);

        if ($body === false) {
            throw new AmodbException('Network error: ' . $error);
        }
        if ($status !== 429 || $attempt === 3) {
            break;
        }
        sleep(max(1, min($retryAfter, 30)));
    }

    if ($status === 401) {
        throw new AmodbException('401: the API key is invalid or suspended.');
    }
    if ($status !== 200) {
        $detail = json_decode((string) $body, true)['status_message'] ?? 'no message';
        throw new AmodbException("amodb returned $status: $detail");
    }

    if (!is_dir($dir)) {
        mkdir($dir, 0775, true);
    }
    file_put_contents($file, $body, LOCK_EX);

    return json_decode($body, true, 512, JSON_THROW_ON_ERROR);
}
```

Then save the page as `movie.php`. Every value goes through `htmlspecialchars` before it reaches the HTML.

```php
<?php
declare(strict_types=1);

require __DIR__ . '/amodb.php';

function h(?string $text): string
{
    return htmlspecialchars((string) $text, ENT_QUOTES, 'UTF-8');
}

$id = filter_input(INPUT_GET, 'id', FILTER_VALIDATE_INT) ?: 550;
$movie = null;
$error = null;
try {
    $movie = amodb_get("/movie/$id", ['language' => 'en-US', 'append_to_response' => 'credits,ratings']);
} catch (AmodbException $e) {
    http_response_code(502);
    $error = $e->getMessage();
}
$imdb = $movie['ratings']['imdb'] ?? null;
$cast = $movie ? array_slice(array_column($movie['credits']['cast'], 'name'), 0, 3) : [];
?>
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title><?= h($movie['title'] ?? 'Movie lookup') ?></title>
</head>
<body>
<?php if ($error !== null): ?>
  <p>Could not load the movie: <?= h($error) ?></p>
<?php else: ?>
  <h1><?= h($movie['title']) ?> (<?= h(substr((string) ($movie['release_date'] ?? ''), 0, 4)) ?>)</h1>
  <?php if (!empty($movie['poster_path'])): ?>
    <img src="https://img.amodb.dev/t/p/w342<?= h($movie['poster_path']) ?>" alt="Poster of <?= h($movie['title']) ?>" width="342">
  <?php endif; ?>
  <p><?= h($movie['overview'] ?? '') ?></p>
  <p>Cast: <?= h(implode(', ', $cast)) ?></p>
  <p>IMDb: <?= $imdb ? h((string) $imdb['value']) : 'not known' ?></p>
<?php endif; ?>
</body>
</html>
```

Start the built-in server with `php -S localhost:8000` and open `http://localhost:8000/movie.php?id=603`. The poster URL points at the image host, which needs no key.

## Next steps

- Move the cache from the temp folder to a directory you control, and delete files older than a day with a cron job.
- Add `language=ru-RU` or another of the [14 languages](https://amodb.dev/languages/) and cache each language separately, which the URL hash already does.
- Add `moods` to `append_to_response` and print the entry of `$movie['moods']['results']` with the highest `avg`. See [moods](https://amodb.dev/moods/).
- Read about [rate limits](https://amodb.dev/rate-limits/), or compare this with the [Go tutorial](https://amodb.dev/go-movie-api/).

## Questions and answers

### Which PHP extensions does the code need?

Only cURL. JSON support is built into PHP 8. Run php -m to check that curl is listed. The code runs on PHP 8.2 and newer and has no Composer dependencies, so you can drop the two files into any project.

### Where does the file cache keep its data?

In a folder named amodb-cache inside the system temp directory. Each request gets one JSON file named after a hash of its URL. A file is reused while its age is below the TTL, which is 6 hours by default.

### How do I pass the key under PHP-FPM?

Add env[AMODB_KEY] to the pool configuration, because workers do not inherit your shell. With the built-in server, the variable you exported before running php -S is enough. Never write the key into a file under the web root.

### What should the page do when amodb returns an error?

Show a short message and send a 502 so your own monitoring notices. Log the status_message on the server and never print the key. A 401 means every later call fails too, so fix the key before retrying.

### Is sleeping on a 429 safe inside a web request?

Keep it short. A request that sleeps for 30 seconds ties up a worker, so cap the wait low on pages, or serve a stale cached copy. With the file cache in place, repeated ids rarely reach a 429.

## Related pages

- [Movie details API](https://amodb.dev/movies/): Movie details API with the same paths and JSON as TMDB v3. Get one movie by id, add credits, ratings and moods with append_to_response, in 14 languages.
- [Images API and image host](https://amodb.dev/images-api/): Movie images API with TMDB v3 paths: list posters, backdrops, logos and stills per title, then load them from img.amodb.dev in 10 sizes with no key.
- [Authentication](https://amodb.dev/authentication/): How to send your amodb key: api_key in the query, an Authorization Bearer header, or apikey for OMDb calls. Key format, storage, rotation and the 401 errors.
- [Errors](https://amodb.dev/errors/): Every amodb error with its HTTP status, TMDB status_code and message, plus the OMDb error shape. What each one means and what your client should do next.
- [Python movie API tutorial](https://amodb.dev/python-movie-api/): Build a Python command line tool that searches a movie title and prints year, cast, IMDb rating and poster, with 429 retry and a file cache. Uses requests.
- [JavaScript movie API tutorial](https://amodb.dev/javascript-movie-api/): Write a Node.js script that loads details, credits and ratings for a list of movie ids in one request each, with a TTL cache, 429 retry and no dependencies.

Updated: 2026-10-18. Source: https://amodb.dev/php-movie-api/
