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
| Parameter | In | Required | Description |
|---|---|---|---|
api_key | query | one of two | Your key. Use this or the Authorization: Bearer header. |
sort_by | query | no | Field 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_genres | query | no | Genre ids. A comma means AND, a pipe character means OR. |
without_genres | query | no | Genre ids to exclude. |
with_cast, with_crew | query | no | Person ids. Find them with search. |
with_people | query | no | Person ids that appear as cast or crew. |
with_companies, without_companies | query | no | Production company ids to require or exclude. Comma and pipe work as in with_genres. |
with_keywords, without_keywords | query | no | Keyword ids to require or exclude. See keywords. |
with_origin_country | query | no | Uppercase 2 letter production country code such as FR. |
primary_release_year | query | no | Four digit year of the primary release date. |
year | query | no | Four 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.lte | query | no | Lower and upper bound for the primary release date, YYYY-MM-DD. |
release_date.gte, release_date.lte | query | no | Lower and upper bound for any release date in any country, YYYY-MM-DD. |
region | query | no | Uppercase 2 letter country code. Replaces release_date in each result with the date in that country. It does not remove titles. |
certification_country, certification | query | no | Age 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.lte | query | no | Minimum and maximum average score, 0 to 10. |
vote_count.gte, vote_count.lte | query | no | Minimum and maximum number of votes. |
with_runtime.gte, with_runtime.lte | query | no | Minimum and maximum runtime in minutes. |
with_original_language | query | no | Original language as a 2 letter code such as fr or ko. |
with_watch_providers | query | no | Provider ids. Needs watch_region, and is ignored without it. Ids come from genres and lookup lists. |
watch_region | query | no | Uppercase 2 letter country code for provider availability. |
include_adult | query | no | Accepted and ignored. Adult titles are never returned. |
language | query | no | TMDB tag such as en-US or pl-PL. 14 base languages are served. Default en-US. |
page | query | no | Page number from 1 to 500. Default 1. |
Response fields
| Field | Type | Notes |
|---|---|---|
page, total_pages, total_results | integer | Paging for this filter set. Broad filters report at most 1001 pages and 20001 results, but only pages 1 to 500 can be read. |
results | array | Up to 20 movies. Empty when no title matches every filter. |
id | integer | Movie id for movie details. |
title, original_title, overview | string | Localized text and the original title. |
original_language | string | Two letter code of the language the movie was made in. |
release_date | string | YYYY-MM-DD. |
genre_ids | array of integers | Ids only. Map them to names with /genre/movie/list. |
poster_path, backdrop_path | string or null | Append to https://img.amodb.dev/t/p/{size}. |
vote_average, vote_count, popularity | number | Score and ranking values. |
adult, video | boolean | adult is always false. |
Example response
{
"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.
| Goal | Request |
|---|---|
| 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.
| Parameter | Description |
|---|---|
first_air_date.gte, first_air_date.lte | Lower and upper bound for the first air date, YYYY-MM-DD. |
first_air_date_year | Four digit year of the first air date. |
air_date.gte, air_date.lte | Lower and upper bound for the dates a show has episodes airing, YYYY-MM-DD. |
with_networks | Network ids. Find them on companies and networks. |
with_status | Numeric status code: 0 returning series, 2 in production, 3 ended, 4 canceled. |
with_type | Numeric show type: 0 documentary, 1 news, 2 miniseries, 3 reality, 4 scripted, 5 talk show, 6 video. |
sort_by | popularity, 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_keywords | Same rules as for movies. |
vote_average.gte, vote_count.gte, with_runtime.gte, with_runtime.lte, with_original_language | Same rules as for movies. |
with_watch_providers, watch_region, language, page | Same 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
| HTTP | status_code | When |
|---|---|---|
| 401 | 7 | The key is missing or wrong. |
| 402 | 36 | The plan period has ended. |
| 405 | 37 | You called a write method. |
| 429 | 25 | Over your requests per minute. Wait for the Retry-After seconds. |
| 500 | 11 | A page outside 1 to 500. |
| 504 | 24 | The 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_resultsof 20001, which is a cap and not a real count. - Always pair
sort_by=vote_average.descwithvote_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.