# Movie database API for your own catalog

> Use amodb as the source for your own movie database. Movies, people, companies, collections, keywords and genres share numeric ids, so rows join cleanly. Seed with discover paging in release date windows, refresh rows by age, and match your own ids through find by external id. TMDB v3 JSON, 20 to 120 requests per minute.

## Build your own movie database from an API

amodb is an independent API that speaks the TMDB v3 protocol. Movies, people, companies, collections, keywords and genres all have numeric ids and point at each other, so you can fill SQL tables from it and keep them current. Seed the tables with discover paging, refresh them by age, and match your own identifiers with `find`. amodb has no changes feed, so a refresh is a plain refetch of the rows you keep.

What you get:

- Six linked entity types with stable numeric ids.
- Discover paging for the first load: up to 20 results per page, 500 pages per query.
- Release date windows on discover, to slice the first load and to list new releases.
- External id lookups for `imdb_id`, `tvdb_id`, `wikidata_id` and `kinopoisk_id`.

## What data you get

This is the data model. Each row is one table in a relational copy.

| Entity     | Read with                           | Key             | Links to                                                                                                  |
| ---------- | ----------------------------------- | --------------- | --------------------------------------------------------------------------------------------------------- |
| Movie      | `GET /3/movie/{movie_id}`           | `id`, `imdb_id` | `genres[].id`, `production_companies[].id`, `belongs_to_collection.id`                                    |
| Person     | `GET /3/person/{person_id}`         | `id`, `imdb_id` | Movies through `/3/movie/{movie_id}/credits`                                                              |
| Company    | `GET /3/company/{company_id}`       | `id`            | Movies through `with_companies` in discover. `parent_company` is always `null` and `description` is empty |
| Collection | `GET /3/collection/{collection_id}` | `id`            | `parts[]`, a list of movies                                                                               |
| Keyword    | `GET /3/keyword/{keyword_id}`       | `id`            | Movies through `/3/keyword/{keyword_id}/movies` or `with_keywords`                                        |
| Genre      | `GET /3/genre/movie/list`           | `id`            | `genres[].id` on movies                                                                                   |

Many to many links need join tables: `movie_genre`, `movie_company`, `movie_keyword` and `movie_credit`. Use `credit_id` from [credits](https://amodb.dev/credits/) as the key of the last one. Field lists are on [movies](https://amodb.dev/movies/), [people](https://amodb.dev/people/) and [collections](https://amodb.dev/collections/).

## Start in 3 steps

1. Compare the three request rates on [pricing](https://amodb.dev/pricing/). A bigger rate shortens the first load, nothing else changes.
2. Create a key in your account dashboard and keep it in an environment variable.
3. Call `/3/discover/movie` for a one day window of release dates, then fetch each id on the list.

## Example request

The PHP script lists the movies released yesterday and today through discover and upserts each record into a SQLite table. It sleeps 3 seconds between detail reads so it stays under 20 requests per minute, retries a 429 after `Retry-After`, and deletes rows whose id now returns 404.

```bash
curl -s "https://api.amodb.dev/3/discover/movie?primary_release_date.gte=2026-10-17&primary_release_date.lte=2026-10-18&sort_by=primary_release_date.asc&page=1" \
  -H "Authorization: Bearer $AMODB_KEY"
```

```php
<?php
const BASE = 'https://api.amodb.dev/3';

function amodb(string $path, array $query = []): ?array
{
    $url = BASE . $path . '?' . http_build_query($query);
    for ($attempt = 0; $attempt < 3; $attempt++) {
        $retryAfter = 1;
        $ch = curl_init($url);
        curl_setopt_array($ch, [
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_TIMEOUT => 10,
            CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('AMODB_KEY')],
            CURLOPT_HEADERFUNCTION => function ($handle, $line) use (&$retryAfter) {
                if (stripos($line, 'Retry-After:') === 0) {
                    $retryAfter = max(1, (int) trim(substr($line, 12)));
                }
                return strlen($line);
            },
        ]);
        $body = curl_exec($ch);
        $status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        curl_close($ch);
        if ($status === 429) {
            sleep($retryAfter);
            continue;
        }
        if ($status === 404) {
            return null;
        }
        if ($status !== 200) {
            throw new RuntimeException("amodb returned $status for $path");
        }
        return json_decode($body, true);
    }
    throw new RuntimeException("Still rate limited on $path");
}

$db = new PDO('sqlite:movies.sqlite');
$db->exec('CREATE TABLE IF NOT EXISTS movie (id INTEGER PRIMARY KEY, imdb_id TEXT, title TEXT, release_date TEXT, raw TEXT)');
$save = $db->prepare('INSERT OR REPLACE INTO movie (id, imdb_id, title, release_date, raw) VALUES (?, ?, ?, ?, ?)');
$drop = $db->prepare('DELETE FROM movie WHERE id = ?');

$from = date('Y-m-d', strtotime('-1 day'));
$to = date('Y-m-d');
$page = 1;
do {
    $list = amodb('/discover/movie', [
        'primary_release_date.gte' => $from,
        'primary_release_date.lte' => $to,
        'sort_by' => 'primary_release_date.asc',
        'page' => $page,
    ]);
    foreach ($list['results'] as $row) {
        $movie = amodb('/movie/' . $row['id'], ['language' => 'en-US']);
        if ($movie === null) {
            $drop->execute([$row['id']]);
            continue;
        }
        $save->execute([$movie['id'], $movie['imdb_id'] ?? null, $movie['title'], $movie['release_date'] ?? null, json_encode($movie)]);
        sleep(3);
    }
    $page++;
} while ($page <= min($list['total_pages'], 500));
```

Discover returns list rows, not full records. This is 2 of the 20 results on page 1. Read `/3/movie/{id}` for the rest of the fields.

```json
{
  "page": 1,
  "results": [
    {
      "adult": false,
      "backdrop_path": "/siepLRGVjM8yLKppApkD3uSh8Y0.jpg",
      "genre_ids": [
        35
      ],
      "id": 1800227,
      "original_language": "en",
      "original_title": "Sly's Pack",
      "overview": "Nesmith, Gary, & Tony are smoking weed on the roof of a parking garage, but a...",
      "popularity": 0.0071,
      "poster_path": "/3HpIWZUI7jmpKZkfZNdchS2WsvN.jpg",
      "release_date": "2026-10-17",
      "title": "Sly's Pack",
      "video": false,
      "vote_average": 0,
      "vote_count": 0
    },
    {
      "adult": false,
      "backdrop_path": null,
      "genre_ids": [
        99
      ],
      "id": 1800168,
      "original_language": "en",
      "original_title": "The Good Life",
      "overview": "The Good LIfe is based on the best-selling book by Drs. Robert Waldinger and...",
      "popularity": 0.0143,
      "poster_path": null,
      "release_date": "2026-10-17",
      "title": "The Good Life",
      "video": false,
      "vote_average": 0,
      "vote_count": 0
    }
  ],
  "total_pages": 15,
  "total_results": 299
}
```

## Fill and refresh the tables

| Job               | Call                                                                                                      | Limits                                                                                                                        |
| ----------------- | --------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
| First load        | `GET /3/discover/movie` with `sort_by` and a date window                                                  | `page` 1 to 500, up to 20 results per page, so 10,000 per query. Slice by `primary_release_date.gte` and `.lte` to go beyond. |
| New releases      | `GET /3/discover/movie` with `primary_release_date.gte` set to your last run, and `GET /3/movie/upcoming` | Cached for 30 minutes.                                                                                                        |
| Refresh rows      | `GET /3/movie/{movie_id}` for rows older than 6 hours                                                     | There is no changes feed. Refresh the titles users open, and the rest less often.                                             |
| Join your own ids | `GET /3/find/{external_id}`                                                                               | One id per call, source chosen with `external_source`.                                                                        |
| Lazy fill         | `GET /3/movie/{movie_id}` when a user opens a title                                                       | Store the reply with a timestamp.                                                                                             |

The paging window is the main design choice. When `total_pages` in a reply is above 500, the window holds more than 10,000 rows, so cut it in halves until it fits. A recent year needs months, and one day of releases is a few hundred titles. Store the slice you finished so a crashed run can resume. Matching rules for the join are on [find by external id](https://amodb.dev/find-by-external-id/).

## Sizing the sync

Each row you refresh costs one detail read. A plan of 20 requests per minute gives 1,200 calls per hour, 40 gives 2,400 and 120 gives 7,200. A discover page lists 20 ids for one call. Run the refresh at off-peak hours if your app shares the key, because the limit is per key, not per script. See [rate limits](https://amodb.dev/rate-limits/) for the headers that report what is left in the window.

## Caching in two layers

| Layer       | Holds                                | TTL                                                            |
| ----------- | ------------------------------------ | -------------------------------------------------------------- |
| Edge cache  | Details and credits                  | 6 hours                                                        |
| Edge cache  | Discover, trending and popular lists | 30 minutes                                                     |
| Edge cache  | Search                               | 10 minutes                                                     |
| Edge cache  | Configuration                        | 24 hours                                                       |
| Edge cache  | Images                               | 30 days                                                        |
| Your tables | Rows you keep                        | Until your age rule says to refetch, never sooner than 6 hours |

The edge cache is shared across keys, but the rate limit runs before it, so a cache hit still uses one request from your minute. Rereading a row more often than every 6 hours returns the same copy and wastes quota. Let an age rule decide when to refetch.

## How it compares

| Capability           | Scrape pages yourself       | Static dataset files                | amodb                                                |
| -------------------- | --------------------------- | ----------------------------------- | ---------------------------------------------------- |
| Freshness            | As fresh as your last crawl | As fresh as the last published file | Details cached 6 hours, refetch by age               |
| Incremental updates  | Re-crawl and compare        | Download a new file                 | Discover with a release date window, no changes feed |
| Ids across entities  | Page URLs                   | Defined by the publisher            | Numeric ids, `imdb_id` and `/3/find`                 |
| What you run         | Crawlers and storage        | Storage and an import job           | Storage and a sync script                            |
| Rate limit           | Set by the sites you crawl  | Not applicable                      | 20, 40 or 120 requests per minute, counted per key   |
| Languages per record | Whatever each page offers   | Depends on the files                | 14, chosen with `language`                           |

## Pricing

The first load is the expensive part, and it is bounded by your requests per minute. Choose 20, 40 or 120 per minute on [pricing](https://amodb.dev/pricing/). Pay for 1, 3, 6, 12 or 24 months at 0, 10, 20, 30 or 40 percent off. Nothing renews automatically, there is no free tier, and every plan reads the same data.

## Questions and answers

### How do I keep a local copy fresh?

amodb has no changes feed, so refresh by age. Details are cached at the edge for 6 hours, so refetch a row no sooner than that. Pick up new releases with discover and primary_release_date.gte set to your last run.

### How do I load the first copy?

Walk /3/discover/movie with page 1 to 500 (up to 20 results each, so 10,000 per query) and split by primary_release_date windows to go further. March 2025 alone holds about 4,400 titles. Then read /3/movie/{id} for each id you keep.

### How long does a first load take at my rate limit?

At 20 requests per minute you can make 1,200 calls an hour, at 40 it is 2,400 and at 120 it is 7,200. Divide the number of detail reads you need by that figure to plan the run.

### Can I join amodb ids with the ids I already store?

Yes. /3/find/{external_id} accepts imdb_id, tvdb_id, wikidata_id and kinopoisk_id and returns the matching amodb record. Movie records also carry imdb_id, so the join works in both directions.

### Is there a free tier for building a database?

No. Access is prepaid for 1, 3, 6, 12 or 24 months and does not renew by itself. Every plan reads the same data, so a larger plan only makes the first load and the refreshes run faster.

## 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.
- [Person details API](https://amodb.dev/people/): Person details API with the same paths and JSON as TMDB v3. Get an actor or crew member by id, with biography, filmography, external ids and the popular list.
- [Collections API](https://amodb.dev/collections/): Movie collections API with TMDB v3 paths: fetch a franchise by id with all its parts, read belongs_to_collection on a movie, and search collections by name.
- [Companies and networks](https://amodb.dev/companies-networks/): Look up production companies and TV networks with TMDB v3 paths. Read name, country and logo by id, then filter discover by company or network id.
- [Find by external id](https://amodb.dev/find-by-external-id/): Look up a movie, series or person by IMDb, TVDB, Wikidata or Kinopoisk id with the TMDB find endpoint, and read external ids and Wikidata facts for a title.
- [Rate limits](https://amodb.dev/rate-limits/): amodb limits each key by requests per minute in a sliding 60 second window: 20, 40 or 120 by plan. Read the rate limit headers and handle 429 with Retry-After.

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