Keywords API
The keywords endpoints return the plot tags attached to a movie or TV series, each as an id and a name. You can look up one keyword, list the movies that carry it, or pass the ids to discover with with_keywords to build theme pages. Movies reply under keywords, TV series reply under results.
Get keywords for a movie
GET /3/movie/{movie_id}/keywords returns the plot tags of one movie. A keyword is a short phrase such as a theme, a setting or a source type, and it has a numeric id you can reuse in discover.
The example loads the keywords of a movie and then asks discover for the most popular movies that share its first keyword.
curl -s "https://api.amodb.dev/3/movie/550/keywords" \
-H "Authorization: Bearer $AMODB_KEY"
curl -s "https://api.amodb.dev/3/discover/movie?with_keywords=818&sort_by=popularity.desc" \
-H "Authorization: Bearer $AMODB_KEY"const base = 'https://api.amodb.dev/3';
const headers = { Authorization: `Bearer ${process.env.AMODB_KEY}` };
const kw = await fetch(`${base}/movie/550/keywords`, { headers });
if (!kw.ok) throw new Error(`keywords failed with ${kw.status}`);
const { keywords } = await kw.json();
console.log(keywords.map((k) => `${k.id}:${k.name}`).join(', '));
if (keywords.length > 0) {
const first = keywords[0];
const found = await fetch(`${base}/discover/movie?with_keywords=${first.id}&sort_by=popularity.desc`, { headers });
if (!found.ok) throw new Error(`discover failed with ${found.status}`);
const page = await found.json();
console.log(first.name, page.results.slice(0, 5).map((m) => m.title));
}import os
import requests
base = "https://api.amodb.dev/3"
headers = {"Authorization": f"Bearer {os.environ['AMODB_KEY']}"}
r = requests.get(f"{base}/movie/550/keywords", headers=headers, timeout=10)
r.raise_for_status()
keywords = r.json()["keywords"]
print(", ".join(f"{k['id']}:{k['name']}" for k in keywords))
if keywords:
first = keywords[0]
d = requests.get(
f"{base}/discover/movie",
params={"with_keywords": first["id"], "sort_by": "popularity.desc"},
headers=headers,
timeout=10,
)
d.raise_for_status()
print(first["name"], [m["title"] for m in d.json()["results"][:5]])<?php
function amodb_get(string $url): array {
$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");
}
return json_decode($body, true);
}
$base = 'https://api.amodb.dev/3';
$keywords = amodb_get("$base/movie/550/keywords")['keywords'];
if ($keywords) {
$first = $keywords[0];
$found = amodb_get("$base/discover/movie?with_keywords={$first['id']}&sort_by=popularity.desc");
$titles = array_column(array_slice($found['results'], 0, 5), 'title');
echo $first['name'], ': ', implode(', ', $titles), PHP_EOL;
}package main
import (
"encoding/json"
"fmt"
"net/http"
"os"
"time"
)
var client = &http.Client{Timeout: 10 * time.Second}
func get(path string, out any) error {
req, _ := http.NewRequest("GET", "https://api.amodb.dev/3"+path, nil)
req.Header.Set("Authorization", "Bearer "+os.Getenv("AMODB_KEY"))
res, err := client.Do(req)
if err != nil {
return err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return fmt.Errorf("amodb returned %s", res.Status)
}
return json.NewDecoder(res.Body).Decode(out)
}
type Keywords struct {
Keywords []struct {
ID int `json:"id"`
Name string `json:"name"`
} `json:"keywords"`
}
type Page struct {
Results []struct {
Title string `json:"title"`
} `json:"results"`
}
func main() {
var kw Keywords
if err := get("/movie/550/keywords", &kw); err != nil {
panic(err)
}
if len(kw.Keywords) == 0 {
return
}
first := kw.Keywords[0]
var page Page
if err := get(fmt.Sprintf("/discover/movie?with_keywords=%d&sort_by=popularity.desc", first.ID), &page); err != nil {
panic(err)
}
fmt.Println(first.Name)
for i, m := range page.Results {
if i == 5 {
break
}
fmt.Println(" ", m.Title)
}
}Parameters
| Parameter | In | Required | Description |
|---|---|---|---|
movie_id | path | yes | Numeric movie id, for example 550. |
api_key | query | one of two | Your key. Use this or the Authorization: Bearer header. |
Response fields
| Field | Type | Notes |
|---|---|---|
id | integer | The movie id. |
keywords | array | Objects with id and name. Empty when the movie has none. |
keywords[].id | integer | Keyword id. Use it in with_keywords and in the keyword paths below. |
keywords[].name | string | The keyword text. |
Example response
{
"id": 550,
"keywords": [
{ "id": 818, "name": "based on novel or book" },
{ "id": 825, "name": "support group" },
{ "id": 851, "name": "dual identity" }
]
}The real reply holds 14 keywords. This one is cut to the first 3. Keyword names are not translated, so language has no effect here.
Keywords for a TV series
GET /3/tv/{series_id}/keywords takes the same key and returns the same objects, but the array is named results. The id in the path is the series id, for example 1396.
{
"id": 1396,
"results": [
{ "id": 1508, "name": "new mexico" },
{ "id": 2231, "name": "drug dealer" }
]
}The real reply holds 29 entries and this one is cut to the first 2. A client that serves both media types can read data.keywords ?? data.results and treat the two the same way.
Keyword records and their movies
| Method and path | Returns | Parameters |
|---|---|---|
GET /keyword/{keyword_id} | { id, name } | none |
GET /keyword/{keyword_id}/movies | Paged movie list with id, page, results, total_pages, total_results | language, page |
Both paths use the numeric id from a keyword array. The movie list gives the same item shape as other movie lists, including genre_ids and poster_path, plus the keyword id at the top. include_adult is accepted and ignored. A page past total_pages returns an empty results array. There is no list of series for a keyword, so use /discover/tv?with_keywords= for TV.
Combine keywords with discover
For anything beyond a plain list, filter in discover. It sorts, pages and combines keywords with other filters, and it covers TV as well as movies.
| Goal | Query string |
|---|---|
| Movies with one keyword | /discover/movie?with_keywords=818 |
| Movies with both of two keywords | /discover/movie?with_keywords=818,4565 |
| Movies without a keyword | /discover/movie?without_keywords=818 |
| Series with a keyword | /discover/tv?with_keywords=9712 |
| Series with either of two keywords | /discover/tv?with_keywords=818%7C9712 |
| One keyword inside a genre | /discover/movie?with_keywords=818&with_genres=18 |
A comma between ids means all of them. Write a vertical bar between ids, as in with_keywords=818|4565, to match any of them. Discover results are cached at the edge for 30 minutes.
Find a keyword id
To turn text into an id, call GET /3/search/keyword?query=heist. It returns a paged list of id and name pairs, 20 per page. See search for the full set of search paths. A keyword search that matches nothing currently answers 500 with status_code 11 instead of an empty list. Search answers are cached for 10 minutes, but every call still counts against your requests per minute, so debounce a typeahead box.
Errors
| HTTP | status_code | When |
|---|---|---|
| 401 | 7 | The key is missing or wrong. |
| 402 | 36 | The plan period has ended. |
| 404 | 34 | No keyword with that id on /keyword/{keyword_id} and its movie list. |
| 405 | 37 | You used a write method on a keyword path. |
| 429 | 25 | You went over your requests per minute. Wait for the Retry-After seconds. |
| 500 | 11 | A /search/keyword query that matches nothing. |
An unknown movie or series id on the keywords paths is not a 404. You get HTTP 200 with the id and an empty array.
See errors for every code and rate limits for plan limits.
Practical notes
- Store the keyword
id, not the name. A name can be reworded, an id does not change. - Keyword lists are cached with the details of the title, for 6 hours. A weekly refresh for tag pages is plenty.
- A single title has a short tag list. For theme pages, start from a keyword id and list its titles in discover, not the other way round.
- Use
without_keywordsto remove noise, for example a tag you never want on a family page. - An empty array is a valid answer for obscure titles. Hide the tag row instead of showing an error.