Images API and image host

The images endpoints list the posters, backdrops, logos, profiles and stills of a title as file paths. You then load each file from img.amodb.dev in a fixed size, with no key and a 30 day edge cache. Paths and JSON match TMDB v3, so existing image code works after a base URL change.

Get images for a movie

GET /3/movie/{movie_id}/images lists every known image of one movie as three arrays: backdrops, logos and posters. Each entry has a file_path, its pixel size and a language tag. The file itself is not in the JSON. You load it from the image host in a second step.

The example asks for English and textless images, picks the first English poster and then fetches that file at 500 pixels wide.

curl -s "https://api.amodb.dev/3/movie/550/images?include_image_language=en,null" \
  -H "Authorization: Bearer $AMODB_KEY"

curl -s -o poster.jpg -w "%{http_code}\n" \
  "https://img.amodb.dev/t/p/w500/jSziioSwPVrOy9Yow3XhWIBDjq1.jpg"
const url = 'https://api.amodb.dev/3/movie/550/images?include_image_language=en,null';
const res = await fetch(url, { headers: { Authorization: `Bearer ${process.env.AMODB_KEY}` } });
if (!res.ok) throw new Error(`amodb returned ${res.status}`);
const { posters } = await res.json();
if (posters.length === 0) throw new Error('no posters');
const poster = posters.find((p) => p.iso_639_1 === 'en') ?? posters[0];
const posterUrl = `https://img.amodb.dev/t/p/w500${poster.file_path}`;
const img = await fetch(posterUrl);
console.log(img.status, img.headers.get('content-type'), posterUrl);
import os
import requests

r = requests.get(
    "https://api.amodb.dev/3/movie/550/images",
    params={"include_image_language": "en,null"},
    headers={"Authorization": f"Bearer {os.environ['AMODB_KEY']}"},
    timeout=10,
)
r.raise_for_status()
posters = r.json()["posters"]
if not posters:
    raise SystemExit("no posters")
poster = next((p for p in posters if p["iso_639_1"] == "en"), posters[0])
poster_url = "https://img.amodb.dev/t/p/w500" + poster["file_path"]
img = requests.get(poster_url, timeout=10)
print(img.status_code, img.headers.get("content-type"), poster_url)
<?php
$url = 'https://api.amodb.dev/3/movie/550/images?include_image_language=en,null';
$ch = curl_init($url);
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT => 10,
    CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('AMODB_KEY')],
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($status !== 200) {
    throw new RuntimeException("amodb returned $status");
}
$posters = json_decode($body, true)['posters'];
if (!$posters) {
    throw new RuntimeException('no posters');
}
$poster = $posters[0];
foreach ($posters as $p) {
    if ($p['iso_639_1'] === 'en') {
        $poster = $p;
        break;
    }
}
$posterUrl = 'https://img.amodb.dev/t/p/w500' . $poster['file_path'];
$img = curl_init($posterUrl);
curl_setopt_array($img, [CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 10]);
curl_exec($img);
echo curl_getinfo($img, CURLINFO_HTTP_CODE), ' ', curl_getinfo($img, CURLINFO_CONTENT_TYPE), ' ', $posterUrl, PHP_EOL;
curl_close($img);
package main

import (
	"encoding/json"
	"fmt"
	"net/http"
	"os"
	"time"
)

type Images struct {
	Posters []struct {
		FilePath string `json:"file_path"`
		Language string `json:"iso_639_1"`
	} `json:"posters"`
}

func main() {
	url := "https://api.amodb.dev/3/movie/550/images?include_image_language=en,null"
	req, _ := http.NewRequest("GET", url, nil)
	req.Header.Set("Authorization", "Bearer "+os.Getenv("AMODB_KEY"))
	client := &http.Client{Timeout: 10 * time.Second}
	res, err := client.Do(req)
	if err != nil {
		panic(err)
	}
	defer res.Body.Close()
	if res.StatusCode != http.StatusOK {
		panic(res.Status)
	}
	var images Images
	if err := json.NewDecoder(res.Body).Decode(&images); err != nil || len(images.Posters) == 0 {
		panic("no posters")
	}
	poster := images.Posters[0]
	for _, p := range images.Posters {
		if p.Language == "en" {
			poster = p
			break
		}
	}
	posterURL := "https://img.amodb.dev/t/p/w500" + poster.FilePath
	img, err := client.Get(posterURL)
	if err != nil {
		panic(err)
	}
	defer img.Body.Close()
	fmt.Println(img.StatusCode, img.Header.Get("Content-Type"), posterURL)
}

Parameters

ParameterInRequiredDescription
movie_idpathyesNumeric movie id, for example 550.
api_keyqueryone of twoYour key. Use this or the Authorization: Bearer header.
include_image_languagequerynoComma separated ISO 639-1 codes. Add null to include textless images, for example en,null. Only the listed languages come back. Without it you get every language.
languagequerynoAccepted but ignored on this endpoint. Use include_image_language to pick languages.

Response fields

FieldTypeNotes
idintegerThe movie id.
backdrops, logos, postersarrayOne array per image kind, in that key order. Each can be empty.
file_pathstringStarts with a slash. Append it to a size on the image host. Posters and backdrops are .jpg, logos are .png or .svg.
width, heightintegerPixel size of the original file. 0 when the size is not known.
aspect_rationumberWidth divided by height. Posters sit near 0.667, backdrops near 1.778. 0 when the size is not known.
iso_639_1string or nullLanguage of the text on the image. null means no text.
vote_average, vote_countnumber, integerAlways 0 for movie and series images, so they cannot rank images. Keep the order the API returns.

Example response

Two entries per array are shown.

JSON
{
  "id": 550,
  "backdrops": [
    {
      "aspect_ratio": 1.778,
      "height": 1440,
      "iso_639_1": null,
      "file_path": "/c6OLXfKAk5BKeR6broC8pYiCquX.jpg",
      "vote_average": 0,
      "vote_count": 0,
      "width": 2560
    },
    {
      "aspect_ratio": 1.778,
      "height": 1080,
      "iso_639_1": "en",
      "file_path": "/b9HyPoxwxjxkWEUL5ErZdhApQe2.jpg",
      "vote_average": 0,
      "vote_count": 0,
      "width": 1920
    }
  ],
  "logos": [
    {
      "aspect_ratio": 4.638,
      "height": 389,
      "iso_639_1": "en",
      "file_path": "/7Uqhv24pGJs4Ns31NoOPWFJGWNG.png",
      "vote_average": 0,
      "vote_count": 0,
      "width": 1804
    },
    {
      "aspect_ratio": 0.848,
      "height": 1295,
      "iso_639_1": "en",
      "file_path": "/4XkF0Rf7gvSfea8fYLFbU5tmuJw.png",
      "vote_average": 0,
      "vote_count": 0,
      "width": 1098
    }
  ],
  "posters": [
    {
      "aspect_ratio": 0.667,
      "height": 3000,
      "iso_639_1": "en",
      "file_path": "/jSziioSwPVrOy9Yow3XhWIBDjq1.jpg",
      "vote_average": 0,
      "vote_count": 0,
      "width": 2000
    },
    {
      "aspect_ratio": 0,
      "height": 0,
      "iso_639_1": null,
      "file_path": "/qkrH7rjYKU4KOwwIl6wF6H3ISvm.jpg",
      "vote_average": 0,
      "vote_count": 0,
      "width": 0
    }
  ]
}

The second poster shows an entry whose size is not known. The file still loads.

Build an image URL

Join three parts: https://img.amodb.dev/t/p/, a size segment and the file_path. The path already starts with a slash, so https://img.amodb.dev/t/p/w500 plus /jSziioSwPVrOy9Yow3XhWIBDjq1.jpg is a complete URL.

A size is w plus a width in pixels, h plus a height in pixels, or original for the stored file. The host serves ten sizes: w92, w154, w185, w300, w342, w500, w780, w1280, h632 and original. Any other size, such as w400, answers 404. /3/configuration lists the sizes that fit each image kind.

KindConfig fieldSizes
Posterposter_sizesw92, w154, w185, w342, w500, w780, original
Backdropbackdrop_sizesw300, w780, w1280, original
Logologo_sizesw92, w154, w185, w300, w500, original
Profileprofile_sizesw185, h632, original
Stillstill_sizesw92, w185, w300, original

The host does not check the kind, so a poster also loads at w1280. Stay with the sizes listed for the kind, so your code matches the configuration and any TMDB client.

The image host needs no key, and the edge keeps each file for 30 days. Because no key is sent, image loads do not use your requests per minute. Responses carry Access-Control-Allow-Origin: *, so a browser can load them from any page. An unknown size or an unknown file answers 404 with a JSON body.

Responsive images with srcset

Pick the smallest size that still covers the displayed width times the screen density. A poster shown 171 CSS pixels wide needs w342 on a 2x screen. This helper builds a srcset from a stored path.

JavaScript
const WIDTHS = [185, 342, 500, 780];

function posterSrcset(filePath) {
  return WIDTHS.map((w) => `https://img.amodb.dev/t/p/w${w}${filePath} ${w}w`).join(', ');
}

const path = '/jSziioSwPVrOy9Yow3XhWIBDjq1.jpg';
console.log(`<img src="https://img.amodb.dev/t/p/w342${path}" srcset="${posterSrcset(path)}" sizes="(max-width: 600px) 50vw, 342px" alt="Fight Club poster">`);

Other image endpoints

All paths below use the base https://api.amodb.dev/3 and return the arrays named in the second column.

Method and pathArrays returnedParameters
GET /tv/{series_id}/imagesbackdrops, logos, postersinclude_image_language
GET /tv/{series_id}/season/{season_number}/imagesposters, always emptynone
GET /tv/{series_id}/season/{season_number}/episode/{episode_number}/imagesstills, one entry with width and height set to 0none
GET /person/{person_id}/imagesprofilesnone

For movies and series you can also add append_to_response=images to a details call. include_image_language works there too. The appended block has no id of its own.

Person profiles are the one place where vote_average carries a real score. The id of a season or episode response is the season or episode id. See people and TV series for the parent records.

amodb does not offer images paths for collections, companies and networks. Their records already hold one poster_path, backdrop_path or logo_path, described on collections and companies and networks.

The configuration endpoint

GET /3/configuration returns an images block with secure_base_url set to https://img.amodb.dev/t/p/, base_url (the plain http form), and the size lists backdrop_sizes, logo_sizes, poster_sizes, profile_sizes and still_sizes. Read it once at startup, use the secure form, and build URLs from it instead of hard coding sizes. The edge caches it for 24 hours.

Errors

HTTPstatus_codeWhen
4017The key is missing or wrong on a JSON call.
40236The plan period has ended.
40434No person, season or episode with that id, or an unknown size or file on the image host.
40537You used a write method on an images path.
42925You went over your requests per minute. Wait for the Retry-After seconds.

A movie or series id that does not exist is not an error here: the images path answers 200 with empty arrays, while the details path answers 404. See errors for the full list. Image file requests are plain HTTP loads, so check the status code of the response instead of parsing a JSON body.

Practical notes

  • Store file_path in your database and build the URL at render time. A new size or host then costs one code change.
  • Pass include_image_language with your display language plus null, for example ru,null, so posters with foreign text do not appear.
  • An empty array is a normal 200. Fall back to the poster_path on the movie details record.
  • Use w1280 for full width hero backdrops and w780 for cards. original is the stored file at full resolution and is much larger than w780.
  • Edge copies live for 30 days and are shared, so a file that one client requested is served from cache to the next.