---
title: Building batch & collection tools
description: Build a metadata, list, or library-matching tool that reads across a whole catalog.
section: Guides
order: 2
---

# Building batch & collection tools

Build a tool that reads metadata, lists, or library matches for many titles at once, the way a media-server metadata agent or a collection manager does.

## 1. Create an API key with the read scope

A batch tool that only reads catalog data, lists, and sync state needs the `read` scope, not `write`. See [Authentication](/dev/authentication) for how the key attaches to a request.

## 2. Pull catalog metadata for the titles you already matched

Once your tool has a TMDB ID for a title (from your own matching step or from a library's existing metadata), pull its full record from the catalog.

:::tabs
```bash
curl "https://flicklist.tv/api/v3/movie/550?api_key=$API_KEY"
```
```javascript
const res = await fetch(
  `https://flicklist.tv/api/v3/movie/550?api_key=${apiKey}`
);
const movie = await res.json();
```
```python
import requests
r = requests.get(
    "https://flicklist.tv/api/v3/movie/550",
    params={"api_key": api_key},
)
movie = r.json()
```
:::

See [Catalog](/dev/catalog) for the rest of the read endpoints, including discover and search for titles you haven't matched yet.

## 3. Resolve mixed external IDs to one identifier

Most libraries mix ID types. Some items only have an IMDb ID, others only a TVDB ID. Use `find` to resolve any of them to a FlickList catalog record before you do anything else with the title.

:::tabs
```bash
curl "https://flicklist.tv/api/v3/find/tt0111161?api_key=$API_KEY&external_source=imdb_id"
```
```javascript
const res = await fetch(
  `https://flicklist.tv/api/v3/find/tt0111161?api_key=${apiKey}&external_source=imdb_id`
);
const result = await res.json();
```
```python
r = requests.get(
    "https://flicklist.tv/api/v3/find/tt0111161",
    params={"api_key": api_key, "external_source": "imdb_id"},
)
result = r.json()
```
:::

If you're writing data back later (marking things watched, rating them), the same mixed-ID problem shows up in reverse. [The ids object](/dev/ids) covers the precedence FlickList uses when a write request carries more than one ID for the same item.

## 4. Read public and community lists

List reads don't require a credential when the list is public. Browse the community list surface, or pull a specific list's items, without an API key at all.

:::tabs
```bash
curl "https://flicklist.tv/api/v3/lists/community"
```
```javascript
const res = await fetch('https://flicklist.tv/api/v3/lists/community');
const lists = await res.json();
```
```python
r = requests.get("https://flicklist.tv/api/v3/lists/community")
lists = r.json()
```
:::

See [Lists](/dev/lists) for search, tags, a user's public lists, and fetching one list's items.

## 5. Walk pagination all the way through

Catalog endpoints paginate in the response body, TMDB style: every page carries `page`, `total_pages`, and `total_results`, so read `total_pages` from the first response and loop. (List and history endpoints paginate through `X-FlickList-*` response headers instead; [Pagination](/dev/pagination) has the full split.)

:::tabs
```bash
curl "https://flicklist.tv/api/v3/discover/movie?api_key=$API_KEY&page=1"
```
```javascript
const res = await fetch(
  `https://flicklist.tv/api/v3/discover/movie?api_key=${apiKey}&page=1`
);
const { total_pages: totalPages } = await res.json();
```
```python
r = requests.get(
    "https://flicklist.tv/api/v3/discover/movie",
    params={"api_key": api_key, "page": 1},
)
total_pages = r.json()["total_pages"]
```
:::

```python
page = 1
while page <= total_pages:
    r = requests.get(
        "https://flicklist.tv/api/v3/discover/movie",
        params={"api_key": api_key, "page": page},
    )
    process(r.json()["results"])
    page += 1
```

See [Pagination](/dev/pagination) for the full set of headers and both query parameters.

## 6. Follow batch etiquette

> [!REQUIRED]
> Stay under 1,000 requests per hour per credential. On a `429`, read `Retry-After` and wait that many seconds before retrying, rather than retrying immediately.

A tool that syncs on a schedule should poll `GET /sync/last_activities` first and only re-fetch the collections whose timestamps actually moved. Re-walking every list and every collection on every run wastes your rate budget on data that hasn't changed.
