Discover API

Discover lists movies at GET /3/discover/movie and TV shows at GET /3/discover/tv, filtered and sorted by the parameters you choose: genre, cast, crew, company, keyword, dates, rating, votes, runtime, language and streaming provider. The JSON matches TMDB v3, and results are cached for 30 minutes.

Discover movies

GET /3/discover/movie returns a paged list of movies that match every filter you send, in the order you choose. It is the endpoint for browse pages, "best of" lists and recommendations by facet, and it needs no query text. When you have a title typed by a user, use search instead.

The example lists science fiction movies (genre id 878) with at least 1000 votes, best rated first.

curl -sSf --get "https://api.amodb.dev/3/discover/movie" \
  --data-urlencode "with_genres=878" \
  --data-urlencode "sort_by=vote_average.desc" \
  --data-urlencode "vote_count.gte=1000" \
  --data-urlencode "language=en-US" \
  -H "Authorization: Bearer $AMODB_KEY"
const params = new URLSearchParams({
  with_genres: '878',
  sort_by: 'vote_average.desc',
  'vote_count.gte': '1000',
  language: 'en-US',
});
const res = await fetch(`https://api.amodb.dev/3/discover/movie?${params}`, {
  headers: { Authorization: `Bearer ${process.env.AMODB_KEY}` },
});
if (!res.ok) throw new Error(`amodb returned ${res.status}`);
const { results, total_pages } = await res.json();
console.log(`page 1 of ${total_pages}`);
for (const m of results.slice(0, 5)) console.log(m.vote_average, m.title, m.release_date);
import os
import requests

r = requests.get(
    "https://api.amodb.dev/3/discover/movie",
    params={
        "with_genres": 878,
        "sort_by": "vote_average.desc",
        "vote_count.gte": 1000,
        "language": "en-US",
    },
    headers={"Authorization": f"Bearer {os.environ['AMODB_KEY']}"},
    timeout=10,
)
r.raise_for_status()
data = r.json()
print("page 1 of", data["total_pages"])
for m in data["results"][:5]:
    print(m["vote_average"], m["title"], m["release_date"])
<?php
$url = 'https://api.amodb.dev/3/discover/movie?' . http_build_query([
    'with_genres' => 878,
    'sort_by' => 'vote_average.desc',
    'vote_count.gte' => 1000,
    'language' => 'en-US',
]);
$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");
}
$data = json_decode($body, true);
foreach (array_slice($data['results'], 0, 5) as $m) {
    echo $m['vote_average'], ' ', $m['title'], PHP_EOL;
}
package main

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

type Page struct {
	TotalPages int `json:"total_pages"`
	Results    []struct {
		Title       string  `json:"title"`
		ReleaseDate string  `json:"release_date"`
		VoteAverage float64 `json:"vote_average"`
	} `json:"results"`
}

func main() {
	q := url.Values{
		"with_genres":    {"878"},
		"sort_by":        {"vote_average.desc"},
		"vote_count.gte": {"1000"},
		"language":       {"en-US"},
	}
	req, _ := http.NewRequest("GET", "https://api.amodb.dev/3/discover/movie?"+q.Encode(), 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 p Page
	if err := json.NewDecoder(res.Body).Decode(&p); err != nil {
		panic(err)
	}
	fmt.Println("pages:", p.TotalPages)
	for _, m := range p.Results {
		fmt.Println(m.VoteAverage, m.Title, m.ReleaseDate)
	}
}

Parameters

ParameterInRequiredDescription
api_keyqueryone of twoYour key. Use this or the Authorization: Bearer header.
sort_byquerynoField and direction such as vote_average.desc. Fields: popularity, vote_average, vote_count, primary_release_date, revenue, title, original_title, each with .asc or .desc. title and original_title both sort by the original title. An unknown value falls back to popularity.desc, which is also the default.
with_genresquerynoGenre ids. A comma means AND, a pipe character means OR.
without_genresquerynoGenre ids to exclude.
with_cast, with_crewquerynoPerson ids. Find them with search.
with_peoplequerynoPerson ids that appear as cast or crew.
with_companies, without_companiesquerynoProduction company ids to require or exclude. Comma and pipe work as in with_genres.
with_keywords, without_keywordsquerynoKeyword ids to require or exclude. See keywords.
with_origin_countryquerynoUppercase 2 letter production country code such as FR.
primary_release_yearquerynoFour digit year of the primary release date.
yearquerynoFour digit year. Matches any release date in that year, so it returns more titles than primary_release_year.
primary_release_date.gte, primary_release_date.ltequerynoLower and upper bound for the primary release date, YYYY-MM-DD.
release_date.gte, release_date.ltequerynoLower and upper bound for any release date in any country, YYYY-MM-DD.
regionquerynoUppercase 2 letter country code. Replaces release_date in each result with the date in that country. It does not remove titles.
certification_country, certificationquerynoAge rating filter, for example US and PG. certification is ignored without a country. certification.gte and certification.lte give a range.
vote_average.gte, vote_average.ltequerynoMinimum and maximum average score, 0 to 10.
vote_count.gte, vote_count.ltequerynoMinimum and maximum number of votes.
with_runtime.gte, with_runtime.ltequerynoMinimum and maximum runtime in minutes.
with_original_languagequerynoOriginal language as a 2 letter code such as fr or ko.
with_watch_providersquerynoProvider ids. Needs watch_region, and is ignored without it. Ids come from genres and lookup lists.
watch_regionquerynoUppercase 2 letter country code for provider availability.
include_adultquerynoAccepted and ignored. Adult titles are never returned.
languagequerynoTMDB tag such as en-US or pl-PL. 14 base languages are served. Default en-US.
pagequerynoPage number from 1 to 500. Default 1.

Response fields

FieldTypeNotes
page, total_pages, total_resultsintegerPaging for this filter set. Broad filters report at most 1001 pages and 20001 results, but only pages 1 to 500 can be read.
resultsarrayUp to 20 movies. Empty when no title matches every filter.
idintegerMovie id for movie details.
title, original_title, overviewstringLocalized text and the original title.
original_languagestringTwo letter code of the language the movie was made in.
release_datestringYYYY-MM-DD.
genre_idsarray of integersIds only. Map them to names with /genre/movie/list.
poster_path, backdrop_pathstring or nullAppend to https://img.amodb.dev/t/p/{size}.
vote_average, vote_count, popularitynumberScore and ranking values.
adult, videobooleanadult is always false.

Example response

JSON
{
  "page": 1,
  "results": [
    {
      "adult": false,
      "backdrop_path": "/wLWvHcQz7N13DvSOTV7bHgwGXpT.jpg",
      "genre_ids": [878, 12],
      "id": 687163,
      "original_language": "en",
      "original_title": "Project Hail Mary",
      "overview": "Science teacher Ryland Grace wakes up on a spaceship light years from home with no recollection of who he i...",
      "popularity": 101.0341,
      "poster_path": "/yihdXomYb5kTeSivtFndMy5iDmf.jpg",
      "release_date": "2026-03-15",
      "title": "Project Hail Mary",
      "video": false,
      "vote_average": 8.635,
      "vote_count": 8354
    },
    {
      "adult": false,
      "backdrop_path": "/Ys8UIGWJpd2TMuQk8fU77W3mBz.jpg",
      "genre_ids": [12, 18, 878],
      "id": 157336,
      "original_language": "en",
      "original_title": "Interstellar",
      "overview": "The adventures of a group of explorers who make use of a newly discovered wormhole to surpass the limitatio...",
      "popularity": 74.3949,
      "poster_path": "/yQvGrMoipbRoddT0ZR8tPoR7NfX.jpg",
      "release_date": "2014-11-04",
      "title": "Interstellar",
      "video": false,
      "vote_average": 8.491,
      "vote_count": 41526
    },
    {
      "adult": false,
      "backdrop_path": "/tanwf7KOwQ6dxZzehKsXkffIJOd.jpg",
      "genre_ids": [878, 28, 12],
      "id": 969681,
      "original_language": "en",
      "original_title": "Spider-Man: Brand New Day",
      "overview": "Fighting crime full-time as Spider-Man in a world that doesn't remember him...",
      "popularity": 663.5004,
      "poster_path": "/bjiS5ipwxb9JFy3XRRN4OAilSeX.jpg",
      "release_date": "2026-07-29",
      "title": "Spider-Man: Brand New Day",
      "video": false,
      "vote_average": 8.468,
      "vote_count": 5684
    }
  ],
  "total_pages": 39,
  "total_results": 772
}

The reply was fetched from the live API for the example request. It is cut to 3 of the 20 entries on page 1, and long overview strings are shortened.

Combine filters

Separate parameters combine with AND. Inside one parameter, a comma means AND and a pipe character means OR. In a raw URL the pipe is written %7C, and clients that encode query values do this for you.

GoalRequest
Best rated sci-fi with enough votes/discover/movie?with_genres=878&sort_by=vote_average.desc&vote_count.gte=1000
Action or adventure/discover/movie?with_genres=28%7C12
Action but not animation/discover/movie?with_genres=28&without_genres=16
One actor with one director/discover/movie?with_cast=6193&with_crew=525
Long films from one year/discover/movie?primary_release_year=2010&with_runtime.gte=150
Releases in a date window/discover/movie?primary_release_date.gte=2026-10-01&primary_release_date.lte=2026-10-31
Films in one original language/discover/movie?with_original_language=fr
Streaming in one country/discover/movie?with_watch_providers=8&watch_region=US
Rated R in the US/discover/movie?certification_country=US&certification=R
Without a keyword/discover/movie?with_keywords=818&without_keywords=4565

with_original_language filters by the language a title was made in, and it accepts codes outside the 14 served languages, for example ja or ko. The language parameter still decides the language of the text you get back. In the provider row, 8 is Netflix. Read provider ids from /watch/providers/movie.

Discover TV shows

GET /3/discover/tv uses the same envelope and paging. Entries hold name, original_name, first_air_date, origin_country and genre_ids, and genre ids come from /genre/tv/list, which holds the merged TV ids such as 10765 for Sci-Fi and Fantasy.

ParameterDescription
first_air_date.gte, first_air_date.lteLower and upper bound for the first air date, YYYY-MM-DD.
first_air_date_yearFour digit year of the first air date.
air_date.gte, air_date.lteLower and upper bound for the dates a show has episodes airing, YYYY-MM-DD.
with_networksNetwork ids. Find them on companies and networks.
with_statusNumeric status code: 0 returning series, 2 in production, 3 ended, 4 canceled.
with_typeNumeric show type: 0 documentary, 1 news, 2 miniseries, 3 reality, 4 scripted, 5 talk show, 6 video.
sort_bypopularity, vote_average, vote_count, first_air_date, name or original_name, each with .asc or .desc. name and original_name both sort by the original name.
with_genres, without_genres, with_companies, with_keywords, without_keywordsSame rules as for movies.
vote_average.gte, vote_count.gte, with_runtime.gte, with_runtime.lte, with_original_languageSame rules as for movies.
with_watch_providers, watch_region, language, pageSame rules as for movies.

A request such as /discover/tv?first_air_date.gte=2025-01-01&with_genres=18&vote_count.gte=100&sort_by=popularity.desc lists drama series that started in 2025 or later. Read a result in detail on TV series.

Errors

HTTPstatus_codeWhen
4017The key is missing or wrong.
40236The plan period has ended.
40537You called a write method.
42925Over your requests per minute. Wait for the Retry-After seconds.
50011A page outside 1 to 500.
50424The backend did not answer in time. Retry once.

A filter set that matches nothing is not an error. You get HTTP 200 with an empty results array and total_results of 0. See errors and rate limits.

Practical notes

  • The edge caches each distinct filter set for 30 minutes and shares it across keys. The cache key uses the sorted query, so the order of your parameters does not matter.
  • A page holds 20 entries and the last page is 500, so one filter set reaches 10,000 entries. Slice by date windows to go deeper. A broad filter reports total_results of 20001, which is a cap and not a real count.
  • Always pair sort_by=vote_average.desc with vote_count.gte. Without it, titles with a few votes crowd the top.
  • Ids in filters are numeric. Get person ids from search, and genre and provider ids from genres and lookup lists.
  • For ready-made lists such as popular or now playing, see trending and lists.