# 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.

## Endpoint

`GET /3/movie/{movie_id}/keywords`

```bash
curl "https://api.amodb.dev/3/movie/550/keywords" \
  -H "Authorization: Bearer $AMODB_KEY"
```

Response:

```json
{
  "id": 550,
  "keywords": [
    {
      "id": 818,
      "name": "based on novel or book"
    },
    {
      "id": 825,
      "name": "support group"
    },
    {
      "id": 851,
      "name": "dual identity"
    }
  ]
}
```

## 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](https://amodb.dev/discover/).

The example loads the keywords of a movie and then asks discover for the most popular movies that share its first keyword.

```bash
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"
```

```js
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));
}
```

```python
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
<?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;
}
```

```go
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

```json
{
  "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`.

```json
{
  "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](https://amodb.dev/trending-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](https://amodb.dev/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](https://amodb.dev/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](https://amodb.dev/errors/) for every code and [rate limits](https://amodb.dev/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_keywords` to 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.

## Questions and answers

### Why does the TV response use results while movies use keywords?

That is how TMDB v3 shapes them, and amodb keeps it so existing clients work. Movies return a keywords array. TV series return the same objects under results. Read data.keywords ?? data.results.

### How do I find the id of a keyword by its name?

Call the keyword search path with a query, for example /3/search/keyword?query=heist. It returns a paged list of id and name pairs. Pick the exact match and store the id.

### Can I use one keyword id for both movies and TV?

Yes. A keyword id is the same value in discover for movies and discover for TV, so with_keywords works on both paths. Store one id per theme and reuse it for both media types.

### How do I combine several keywords in one query?

Join ids in with_keywords with a comma to require all of them, or with a vertical bar to accept any of them. Use without_keywords to exclude ids. Keep the URL within your HTTP client length limit.

## Related pages

- [Discover API](https://amodb.dev/discover/): Discover API with the same filters as TMDB v3. List movies and TV shows by genre, cast, company, keyword, year, rating, runtime and provider, with sorting.
- [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.
- [TV series API](https://amodb.dev/tv-series/): TV series API with the same paths and JSON as TMDB v3. Get a show by id with seasons, networks and the next episode, plus content ratings and watch providers.
- [Genres and lookup lists](https://amodb.dev/genres/): Genre list API with TMDB v3 paths: movie and TV genre ids with translated names, plus countries, languages, jobs, certifications and watch provider lists.
- [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.

Updated: 2026-10-18. Source: https://amodb.dev/keywords/
