Where to Watch a Movie by IMDb ID: Python API Guide

Published October 15, 2026 · UpdatedOctober 16, 2026

tt0111161 is the field. A watchlist row that already stores that IMDb id does not need the English title, the release year, or another search to ask where the movie streams in one country. Hand the id to one lookup, choose a market, and read that market's offers. Title search is only for rows that never received an identity. An empty region is still a result.

The Streaming Availability API returns metadata and streaming-service title pages — not video, not a subscription, and not a way around regional locks. Opening this page or copying the script does not send a request. Offers move; using tt0111161 here is not a promise that any service currently carries the film.

Sign in and copy an authorized platform key from the dashboard. The script reads OPENDATA_API_KEY from the server environment. Leave YOUR_API_KEY as a placeholder in any URL you paste until you deliberately run a call. The key travels on the query string, so keep production traffic server-side and redact URLs from logs, tickets, and screenshots.

tt0111161 is the join key

get-show accepts one id. The IMDb form is tt plus digits — tt0111161 in this guide, or the documented Titanic sample tt0120338. TMDB movie ids look like movie/597; series ids look like tv/1396. A show id this API already returned also works. Do not send an IMDb webpage URL or a bare TMDB number.

Store id, imdbId, and tmdbId from the returned object so later jobs join on identity instead of the display title. country=us selects which key you read under streamingOptions; it does not change which movie you asked for. Omit country only when you intentionally want the documented multi-market payload. Confirm lowercase two-letter codes with get-countries rather than assuming every ISO code is covered. get-country lists services and add-ons in one market — capability flags, not this title's offers.

output_language is en, es, tr, or fr. It is copy language, not availability. For series, pass series_granularity=show when you only need title-level options; this operation otherwise defaults to episode.

Install requests and export the key privately. Run the script when you mean to spend credits. HTTP 4xx/5xx means the call did not run — auth, quota, or network. The gateway envelope is {code, message, data}. Gateway code 200 is business success; HTTP 200 is not. On success, data is one show object. It is not data.shows and not a search array.

Group that country's options by type. Keep subscription, free, rent, buy, and addon apart. Subscription still needs a suitable account; add-ons may need another. Print price only when present — missing is not free. Do not fetch the link; it is a service title page, not a stream.

import os
from collections import defaultdict

import requests

BASE = "https://api.opendata-api.com/api/project/streaming-availability"
COUNTRY = "us"
SHOW_ID = "tt0111161"


def get_show(show_id):
    try:
        response = requests.get(
            f"{BASE}/get-show",
            params={
                "key": os.environ["OPENDATA_API_KEY"],
                "id": show_id,
                "country": COUNTRY,
                "output_language": "en",
                "series_granularity": "show",
            },
            timeout=30,
        )
    except requests.RequestException:
        raise RuntimeError("Request failed; inspect redacted diagnostics") from None
    if not 200 <= response.status_code < 300:
        raise RuntimeError(f"HTTP failure: {response.status_code}")
    try:
        envelope = response.json()
    except ValueError:
        raise RuntimeError("Expected a JSON response") from None
    if not isinstance(envelope, dict) or str(envelope.get("code")) != "200":
        raise RuntimeError("Gateway business failure; inspect redacted code/message")
    show = envelope.get("data")
    if not isinstance(show, dict) or not show.get("id"):
        raise RuntimeError("Expected one show object")
    return show


def group_options(show):
    markets = show.get("streamingOptions") or {}
    if not isinstance(markets, dict):
        raise RuntimeError("Expected country-keyed streamingOptions")
    options = markets.get(COUNTRY) or []
    if not isinstance(options, list):
        raise RuntimeError("Expected a viewing-option array")
    groups = defaultdict(list)
    for option in options:
        if not isinstance(option, dict):
            raise RuntimeError("Unexpected viewing-option entry")
        service = option.get("service") or {}
        groups[option.get("type") or "unclassified"].append(
            {
                "service": service.get("name") or service.get("id"),
                "link": option.get("link"),
                "price": option.get("price"),
                "addon": option.get("addon"),
            }
        )
    return options, groups


show = get_show(SHOW_ID)
print(show.get("title"), show.get("releaseYear"), show.get("imdbId"))

options, groups = group_options(show)
if not options:
    print("No reported viewing options for this country.")
for access_type, entries in sorted(groups.items()):
    print(access_type)
    for entry in entries:
        print(" ", entry)

Optional fields on the show — overview, genres, directors, cast, rating, runtime, imageSet — stay nullable. Optional fields on an option — audio, subtitles, quality, add-on, dates — stay nullable. Do not invent defaults.

Title search only when the id is missing

When an import has only a name, search-shows-by-title is the fallback. It requires title and country. The documented title sample is Titanic with country=us and optional show_type=movie. data is a raw array: no shows wrapper, no nextCursor. Compare releaseYear, showType, cast, imdbId, and tmdbId before you keep a row. Do not take the first candidate. A title hit can still have an empty local streamingOptions list — identity and availability are separate.

Once you accept a candidate, persist its IMDb id. Every later where-to-watch call uses get-show. Re-searching the display title on every page view is how remakes land on the wrong card.

search-shows-by-filters browses a market (country required, optional catalogs=netflix). get-changes polls additions and removals inside a documented window. Those are discovery and diffs. They do not mint a join key.

A region with no offer is still an answer

A missing streamingOptions map, a missing us key, or an empty array is a finished lookup: no reported offer in that market. It is not a transport failure, a deleted movie, or proof the title is gone worldwide. Keep the title card and show an unavailable state. A non-200 gateway code, a non-JSON body, and a structurally broken data object are errors; do not render them as "not streaming."

get-country service flags say what a platform can sell in a market, not whether this id is on it. Directory homePage values are not title links. Do not build a watch URL from a service name.

This lookup does not grant media rights, bypass subscriptions, or unblock regions. It is not an official catalog of any streaming service. Filtered discovery can still return other services on a matched title — display what the option list actually contains. Genre ids for later catalog work come from get-genres; do not submit translated labels.

Do not put the platform key in a client-side player, a public notebook, or a shared curl history. Query URLs leak it. Do not retry an empty option list as if the gateway failed. Do not treat HTTP 200 as "the movie is on Netflix." Credits are per request; a watchlist enricher should call get-show for stored ids, not recrawl catalogs to rediscover the same films. Parameter notes live on the show lookup and the project page; per-request cost is on pricing.