# Python movie API tutorial

> Build a small Python command line tool that takes a movie title, searches for it, then fetches details, cast and ratings in one extra call. You need Python 3.10 or newer, the requests library and an amodb key. It takes about 15 minutes and ends with a complete script.

You will build `movie.py`, a command line tool. You pass it a title, it searches, picks the first match and prints year, runtime, genres, top cast, the IMDb rating and a poster URL. It needs Python 3.10 or newer, the `requests` library and an amodb key. Plan on 15 minutes.

## Step 1: Get an API key

amodb is prepaid with no free tier, so pick a plan on [pricing](https://amodb.dev/pricing/) first. The 20, 40 and 120 requests per minute plans all return the same data. Then create a key in your account dashboard. A key is 32 lowercase hex characters.

Export it as an environment variable so it stays out of your source code:

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

On Windows PowerShell the same line is `$env:AMODB_KEY = "YOUR_API_KEY"`.

## Step 2: Set up the project

Create a virtual environment and install `requests`:

```bash
python -m venv .venv
source .venv/bin/activate
pip install requests
```

On Windows run `.venv\Scripts\activate` instead of the second line.

## Step 3: Send the first request

The search endpoint is `GET /3/search/movie` with a `query` parameter. This snippet prints the HTTP status and the id of the best match. Always pass a timeout, because `requests` waits forever by default.

```python
import os
import requests

res = requests.get(
    "https://api.amodb.dev/3/search/movie",
    params={"query": "fight club", "language": "en-US"},
    headers={"Authorization": f"Bearer {os.environ['AMODB_KEY']}"},
    timeout=10,
)
print(res.status_code, res.json()["results"][0]["id"])
```

The reply has the TMDB shape: a `results` array sorted by relevance, plus `page` and `total_results`. See [search](https://amodb.dev/search/) for every filter.

## Step 4: Fetch details, cast and ratings

Search results are short. Use the id with `GET /3/movie/{movie_id}` and add `append_to_response=credits,ratings` to get cast and the IMDb, Kinopoisk, Metacritic, Rotten Tomatoes and TMDB scores in the same request. Each score is an object with `value` and `votes`, or `null` when the source is not known, so test before you print. Details are on the [movies](https://amodb.dev/movies/) and [ratings](https://amodb.dev/ratings/) pages. In the final script `session` is a `requests.Session` that already carries the Authorization header.

```python
params = {"language": "en-US", "append_to_response": "credits,ratings"}
movie = session.get(f"{BASE}/movie/{movie_id}", params=params, timeout=10).json()
imdb = (movie.get("ratings") or {}).get("imdb")
```

## Step 5: Handle errors and rate limits

Check the status before you parse the body. Errors use the TMDB body with `status_code` and `status_message`.

| Status          | Meaning                       | What the script does                                                 |
| --------------- | ----------------------------- | -------------------------------------------------------------------- |
| 401             | Invalid or suspended key      | Stops and says to check `AMODB_KEY`. Retrying cannot help.           |
| 404             | No such id                    | Prints the `status_message`.                                         |
| 429             | Over your requests per minute | Sleeps for `Retry-After` seconds (cap 30), then retries up to twice. |
| 504             | Backend timeout               | Printed as an error. Run the command again.                          |
| Network failure | No connection or timeout      | Caught as `requests.RequestException` and reported.                  |

The retry loop is short:

```python
for attempt in range(3):
    res = session.get(url, params=params, timeout=10)
    if res.status_code != 429 or attempt == 2:
        break
    time.sleep(min(int(res.headers.get("Retry-After", "1")), 30))
```

More codes are listed on [errors](https://amodb.dev/errors/), and the window rules are on [rate limits](https://amodb.dev/rate-limits/).

## Step 6: Cache responses on disk

The edge keeps details for 6 hours and search for 10 minutes. Your own cache saves the round trip and spends no request from your limit. The final script stores each reply in a file named after a hash of the path and parameters, and reuses the file while it is younger than the TTL.

## Step 7: The complete program

Save this as `movie.py` and run `python movie.py "fight club"`.

```python
import hashlib
import json
import os
import sys
import time
from pathlib import Path

import requests

BASE = "https://api.amodb.dev/3"
POSTER = "https://img.amodb.dev/t/p/w342"
CACHE_DIR = Path.home() / ".cache" / "amodb-movie-cli"
SEARCH_TTL = 10 * 60
DETAILS_TTL = 6 * 60 * 60


class ApiError(Exception):
    pass


def error_message(res):
    try:
        return res.json()["status_message"]
    except (ValueError, KeyError):
        return res.reason


def get(session, path, params, ttl):
    CACHE_DIR.mkdir(parents=True, exist_ok=True)
    stamp = f"{path}?{json.dumps(params, sort_keys=True)}"
    cache_file = CACHE_DIR / (hashlib.sha256(stamp.encode()).hexdigest() + ".json")
    if cache_file.exists() and time.time() - cache_file.stat().st_mtime < ttl:
        return json.loads(cache_file.read_text(encoding="utf-8"))

    for attempt in range(3):
        try:
            res = session.get(BASE + path, params=params, timeout=10)
        except requests.RequestException as exc:
            raise ApiError(f"network error: {exc}") from exc
        if res.status_code != 429 or attempt == 2:
            break
        time.sleep(min(int(res.headers.get("Retry-After", "1")), 30))

    if res.status_code == 401:
        raise ApiError("401: the key is invalid or suspended. Check AMODB_KEY.")
    if res.status_code != 200:
        raise ApiError(f"{res.status_code}: {error_message(res)}")
    cache_file.write_text(res.text, encoding="utf-8")
    return res.json()


def search_movie(session, title):
    data = get(session, "/search/movie", {"query": title, "language": "en-US"}, SEARCH_TTL)
    if not data["results"]:
        raise ApiError(f"no movie found for {title!r}")
    return data["results"][0]["id"]


def movie_details(session, movie_id):
    params = {"language": "en-US", "append_to_response": "credits,ratings"}
    return get(session, f"/movie/{movie_id}", params, DETAILS_TTL)


def print_card(movie):
    year = (movie.get("release_date") or "")[:4] or "unknown year"
    genres = ", ".join(g["name"] for g in movie["genres"])
    cast = ", ".join(p["name"] for p in movie["credits"]["cast"][:3])
    imdb = (movie.get("ratings") or {}).get("imdb")
    print(f"{movie['title']} ({year}), {movie.get('runtime') or '?'} min")
    print(f"Genres: {genres or 'none listed'}")
    print(f"Cast: {cast or 'none listed'}")
    print(f"IMDb: {imdb['value']} ({imdb['votes']} votes)" if imdb else "IMDb: not known")
    if movie.get("poster_path"):
        print(f"Poster: {POSTER}{movie['poster_path']}")
    print(movie.get("overview") or "No overview in this language.")


def main():
    if len(sys.argv) < 2:
        sys.exit('Usage: python movie.py "fight club"')
    key = os.environ.get("AMODB_KEY")
    if not key:
        sys.exit("Set AMODB_KEY first.")
    session = requests.Session()
    session.headers["Authorization"] = f"Bearer {key}"
    try:
        movie_id = search_movie(session, " ".join(sys.argv[1:]))
        print_card(movie_details(session, movie_id))
    except ApiError as exc:
        sys.exit(f"error: {exc}")


if __name__ == "__main__":
    main()
```

## Next steps

- Print a poster in another size. The image host takes any of the sizes listed on [images](https://amodb.dev/images-api/), and needs no key.
- Add a `year` filter to the search call, or switch to [discover](https://amodb.dev/discover/) for filtered lists.
- Show a mood meter next to the rating by adding `moods` to `append_to_response`. Each entry in `moods.results` has a `name` and an `avg` on a 0 to 10 scale.
- Read the [quickstart](https://amodb.dev/quickstart/) for the same flow in curl, then try the [JavaScript tutorial](https://amodb.dev/javascript-movie-api/) for a batch version.

## Questions and answers

### Which Python version and libraries do I need?

Python 3.10 or newer and the requests library. Everything else comes from the standard library. The key travels in an Authorization Bearer header, so it never appears in URLs, shell history or server logs.

### How do I stay under the rate limit?

Cache responses and honor Retry-After. Each key gets 20, 40 or 120 requests per minute in a sliding 60 second window. The script retries a 429 twice, waits the number of seconds in the header and caps the wait at 30.

### Can I reuse code written for a TMDB v3 client?

Yes for read calls. Paths, parameters and JSON match TMDB v3, so you change the base URL to https://api.amodb.dev/3 and send an amodb key. Write methods such as rating a movie return 405 with status_code 37.

### Should the key go in api_key or in a header?

Both work. The header keeps the key out of URLs, proxy logs and request lines, so this tutorial uses Authorization: Bearer. The api_key query parameter stays available for tools that cannot set headers.

### How long should I cache search and details?

Use 10 minutes for search and 6 hours for details, the same lifetimes as the edge cache. A shorter local cache only adds requests, because the edge copy is shared across keys and already holds those lifetimes.

## Related pages

- [Search API](https://amodb.dev/search/): Search API with the same paths and JSON as TMDB v3. Find movies, TV shows, people, collections, companies and keywords by text, with year filters and paging.
- [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.
- [Movie and TV ratings](https://amodb.dev/ratings/): Movie ratings API that returns IMDb, Kinopoisk, Metacritic, Rotten Tomatoes and TMDB scores for a title in one call, with null for any source not known.
- [Quickstart](https://amodb.dev/quickstart/): Make your first amodb call: get a key, request movie details in curl, JavaScript, Python, PHP or Go, then load the poster from img.amodb.dev. Under 2 minutes.
- [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.
- [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.

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