API Documentation
MediaCanon provides a free, open JSON API for movie and TV metadata. No API keys. No rate limits. Just data.
Base URL
https://mediacanon.org/api
All endpoints return JSON. The API is read-only.
Data Schemas
MCTitleLayer
Unified title response. For shows, includes season/episode metadata. For movies, includes runtime. title_id is the sole public identity.
{
"title_id": number,
"kind": "movie" | "show",
"imdb_id": string | null,
"display_name": string,
"poster_url": string | null,
"start_year": number | null,
"end_year": number | null,
"genres": string[],
"rating": number | null, // IMDb rating (0-10)
"vote_count": number | null, // IMDb vote count
"runtime_minutes": number | null, // Movies only
"show": MCShowInfo | null, // Shows only
"watch_providers": WatchProviders | null,
"networks": Network[] | null, // Shows only
"production_companies": ProductionCompany[] | null
}
The streaming decorator fields (watch_providers, networks, production_companies) are sourced from TMDB and populated lazily on first title view, then refreshed every 7 days. They may be null if the title has never been viewed, or {}/[] if TMDB has no data.
MCShowInfo
{
"season_count": number,
"episode_count": number, // Total across all seasons
"is_finished": boolean,
"seasons": [
{ "season_number": number, "episode_count": number }
]
}
WatchProviders
Region-keyed availability map from TMDB. Top-level keys are ISO 3166-1 alpha-2 country codes (e.g. "US", "GB", "JP"). Each region may contain any of the offer-type arrays below; absent keys mean "not available under that offer type in that region".
{
"US": {
"flatrate": WatchProvider[], // Subscription streaming
"rent": WatchProvider[], // Paid rental
"buy": WatchProvider[], // Paid purchase
"ads": WatchProvider[], // Free with ads
"free": WatchProvider[] // Free (no ads)
},
"GB": { ... },
...
}
WatchProvider
{
"provider_id": number,
"provider_name": string, // "Netflix", "Disney Plus", ...
"logo_path": string, // Prepend https://image.tmdb.org/t/p/original
"display_priority": number // Lower = higher priority
}
Network
Commissioning network or streamer for a show. Shows only — always null for movies. Use the id field to detect streaming "Originals" (see table below).
{
"id": number,
"name": string,
"logo_path": string | null, // Prepend https://image.tmdb.org/t/p/original
"origin_country": string // ISO 3166-1 alpha-2
}
Known streaming-service network IDs for "Netflix Original"-style detection:
| ID | Network |
|---|---|
213 | Netflix |
2739 | Disney+ |
2552 | Apple TV+ |
1024 | Prime Video |
49 | HBO |
453 | Hulu |
ProductionCompany
{
"id": number,
"name": string,
"logo_path": string | null, // Prepend https://image.tmdb.org/t/p/original
"origin_country": string
}
MCEpisodeLayer
{
"episode_number": number,
"display_name": string | null,
"image_url": string | null,
"synopsis": string | null,
"runtime_minutes": number | null,
"air_date": string | null // "YYYY-MM-DD" format
}
TitleSearchResult
Returned from search endpoints. title_id is the sole identity — no movie_id or show_id.
{
"title_id": number,
"type": "movie" | "show",
"display_name": string,
"start_year": number | null,
"end_year": number | null,
"imdb_id": string | null,
"image_url": string | null,
"num_votes": number | null,
"average_rating": number | null,
"original_language": string | null,
"release_date": string | null
}
Title & Episodes
The primary API. title_id is the sole public identity — no internal IDs exposed.
GET /api/titles/:titleId
Get a title by title_id. Returns MCTitleLayer.
Response (show):
GET /api/titles/311356
{
"title_id": 311356,
"kind": "show",
"imdb_id": "tt0903747",
"display_name": "Breaking Bad",
"poster_url": "https://image.tmdb.org/t/p/w500/...",
"start_year": 2008,
"end_year": 2013,
"genres": ["Crime", "Drama", "Thriller"],
"rating": 9.5,
"vote_count": 2581808,
"runtime_minutes": null,
"show": {
"season_count": 5,
"episode_count": 62,
"is_finished": true,
"seasons": [
{ "season_number": 1, "episode_count": 7 },
{ "season_number": 2, "episode_count": 13 },
{ "season_number": 3, "episode_count": 13 },
{ "season_number": 4, "episode_count": 13 },
{ "season_number": 5, "episode_count": 16 }
]
},
"watch_providers": {
"US": {
"flatrate": [
{
"provider_id": 8,
"provider_name": "Netflix",
"logo_path": "/pbpMk2JmcoNnQwx5JGpXngfoWtp.jpg",
"display_priority": 0
}
],
"buy": [
{
"provider_id": 2,
"provider_name": "Apple TV",
"logo_path": "/peURlLlr8jggOwK53fJ5wdQl05y.jpg",
"display_priority": 2
}
]
}
},
"networks": [
{
"id": 174,
"name": "AMC",
"logo_path": "/alqLicR1ZMHMaZGP3xRQxn9sq7p.png",
"origin_country": "US"
}
],
"production_companies": [
{
"id": 11073,
"name": "Sony Pictures Television",
"logo_path": "/A7TKdAOpfHCCvqzgJkm4EXHHFr0.png",
"origin_country": "US"
}
]
}
Response (movie):
GET /api/titles/105378
{
"title_id": 105378,
"kind": "movie",
"imdb_id": "tt0133093",
"display_name": "The Matrix",
"poster_url": "https://image.tmdb.org/t/p/w500/...",
"start_year": 1999,
"end_year": null,
"genres": ["Action", "Sci-Fi"],
"rating": 8.7,
"vote_count": 2233202,
"runtime_minutes": 136,
"show": null
}
GET /api/titles/:titleId/episodes?season=N
Get all episodes for a season. Returns MCEpisodeLayer[], ordered by episode number. Complete season snapshot — no pagination.
| Param | Type | Description |
|---|---|---|
season | number | Season number (required) |
Response:
GET /api/titles/311356/episodes?season=1
[
{
"episode_number": 1,
"display_name": "Pilot",
"image_url": "https://image.tmdb.org/t/p/w500/...",
"synopsis": null,
"runtime_minutes": 59,
"air_date": "2008-01-20"
},
{
"episode_number": 2,
"display_name": "Cat's in the Bag...",
"image_url": "https://image.tmdb.org/t/p/w500/...",
"synopsis": null,
"runtime_minutes": 49,
"air_date": "2008-01-27"
}
]
Errors
| Scenario | Status | Response |
|---|---|---|
| Unknown titleId | 404 | {"error": "title not found"} |
| Movie + ?season=N | 400 | {"error": "title is not a show"} |
| Season out of range | 404 | {"error": "season not found"} |
Search
Search across all movies and shows.
GET /api/titles
Search titles with optional filters. Paginated, up to 100 results per page.
| Param | Type | Description |
|---|---|---|
q | string | Search by display name (case-insensitive partial match) |
type | string | Filter by type: movie or show |
lang | string | Filter by original language (ISO 639-1 code, e.g. en, ja, ko) |
page | number | Page number (default: 1) |
per_page | number | Results per page, 1–100 (default: 100) |
Results are ordered by IMDb vote count (most popular first).
Response:
GET /api/titles?q=breaking&type=show
{
"titles": [
{
"title_id": 311356,
"type": "show",
"display_name": "Breaking Bad",
"start_year": 2008,
"end_year": 2013,
"imdb_id": "tt0903747",
"num_votes": 2200000,
"average_rating": 9.5,
"original_language": "en"
}
],
"total": 12,
"page": 1,
"per_page": 100,
"total_pages": 1,
"languages": [
{"code": "en", "count": 10},
{"code": "es", "count": 2}
]
}
The languages array shows the language distribution across all results matching q and type (ignoring the lang filter), useful for building faceted filters.
Discover
Browse titles with filters, sorting, and pagination. Returns only titles with poster images.
GET /api/discover/carousels
Get the carousel-based discover feed: interleaved show/movie collections with preview titles (up to 30 each). Paginated by number of carousels.
| Param | Type | Description |
|---|---|---|
page | number | Page number (default: 1) |
per_page | number | Carousels per page, 1–50 (default: 10) |
Response:
GET /api/discover/carousels?page=1&per_page=2
{
"carousels": [
{
"name": "Top Drama Shows",
"slug": "top-drama-shows",
"description": "Highest rated drama series",
"collection_id": 5,
"engagement_count": 42.0,
"total_count": 1250,
"titles": [...]
},
{
"name": "Best Action Movies",
"slug": "best-action-movies",
"collection_id": 8,
"engagement_count": 38.0,
"total_count": 890,
"titles": [...]
}
],
"total": 24,
"page": 1,
"per_page": 2
}
Each carousel's slug can be used with GET /api/collections/:slug to fetch all titles for that collection.
GET /api/discover
Discover titles with optional filters. Paginated, up to 100 results per page.
| Param | Type | Description |
|---|---|---|
type | string | Filter by type: movie or show |
genre | string | Filter by genre name (e.g. Action, Horror, Sci-Fi) |
country | string | Filter by origin country (ISO 3166-1 code, e.g. US, KR, JP) |
lang | string | Filter by original language (ISO 639-1 code, e.g. en, ja) |
sort | string | Sort order: most_rated, top_rated, newest, trending, popular, hidden_gems, a-z (default: most rated by IMDb vote count) |
year_min | number | Minimum year filter |
rating_min | number | Minimum average rating filter |
min_votes | number | Minimum IMDb vote count filter |
page | number | Page number (default: 1) |
limit | number | Results per page, 1–100 (default: 100) |
Response:
GET /api/discover?country=KR&type=movie&sort=top_rated&page=1
{
"titles": [
{
"title_id": 484052,
"type": "movie",
"display_name": "Parasite",
"start_year": 2019,
"image_url": "https://image.tmdb.org/t/p/w500/...",
"average_rating": 8.5,
"num_votes": 940000,
"tmdb_popularity": 19.4,
"genres": ["Comedy", "Drama", "Thriller"]
}
],
"total": 156,
"page": 1,
"per_page": 100
}
Collections
Curated lists of titles, designed for LLM-generated discovery.
GET /api/collections
List all active collections, ordered by pinned status and click popularity.
Response: Collection[]
GET /api/collections
[
{
"id": 1,
"name": "Classic Anime",
"slug": "classic-anime",
"description": "Beloved anime series and films",
"pinned": true,
"active": true,
"engagement_count": 42.0
}
]
GET /api/collections/:slug
Get a collection by slug or ID, including its matched titles.
Response:
GET /api/collections/classic-anime
{
"collection": {
"id": 1,
"name": "Classic Anime",
"slug": "classic-anime",
"description": "Beloved anime series and films",
"pinned": true,
"active": true,
"engagement_count": 42.0
},
"titles": [...],
"total": 25
}
Usage Examples
cURL
# Title & Episodes curl https://mediacanon.org/api/titles/311356 curl https://mediacanon.org/api/titles/311356/episodes?season=1 # Search curl https://mediacanon.org/api/titles?q=matrix curl https://mediacanon.org/api/titles?type=show&lang=ja&page=2 # Discover curl https://mediacanon.org/api/discover?genre=Horror&sort=top_rated curl https://mediacanon.org/api/discover?country=JP&type=movie&page=2 curl https://mediacanon.org/api/discover/carousels # Collections curl https://mediacanon.org/api/collections curl https://mediacanon.org/api/collections/classic-anime
JavaScript
// Get title details
const title = await fetch('https://mediacanon.org/api/titles/311356').then(r => r.json());
console.log(title.display_name, title.kind);
// Get episodes for season 1
const episodes = await fetch('https://mediacanon.org/api/titles/311356/episodes?season=1').then(r => r.json());
episodes.forEach(ep => console.log(ep.episode_number, ep.display_name));
Python
import requests
title = requests.get('https://mediacanon.org/api/titles/311356').json()
print(title['display_name'], title['kind'])
episodes = requests.get('https://mediacanon.org/api/titles/311356/episodes?season=1').json()
for ep in episodes:
print(ep['episode_number'], ep['display_name'])
Data Stats
- ~892,000 movies
- ~362,000 TV shows
- ~7.5 million episodes
Data sourced from IMDb datasets. Episode images, air dates, runtimes, and synopses from TMDB.
Rate Limits
None. Be reasonable.