API reference contents

FrameThrower API

Curated film frames, searchable by concept, color, mood, composition and image similarity — for agents, creative tools and production pipelines.

Create your API key

Free to start — every new account gets credits, and the API is metered against the balance. No card, no plan to pick.

Introduction

The FrameThrower API gives you programmatic access to the same cinematography reference library that filmmakers use on the website — text search, image search, color matching, craft-attribute filtering, and a growing film catalog. It's designed to be read by humans, agents and LLMs alike.

Every response returns metadata, thumbnail URLs and deep links — never raw image bytes. Reference use, with attribution.

Base URL

https://framethrower.ai/api/v1

What you can build

The API is built for creative workflows. Four things it is already used for, each one a couple of calls:

Quickstart

1

Create a FrameThrower account

Sign up for free — Google or email. Takes 30 seconds.
2

Complete onboarding

Accept the Terms of Service and set your content preferences. This is required for API access because the API exposes the same content as the website.
3

Create an API token

Go to Settings → API and click + Create token. Copy the token — it's shown only once.
4

Make your first request

curl -X POST https://framethrower.ai/api/v1/search \
  -H "Authorization: Bearer ft_YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"query": "neon-lit rainy street at night", "limit": 5}'
Or from JavaScript, with the official SDK:
npm install framethrower-ai

Pricing

The API is included in the free plan. A new account gets $2 of credits, no card, and can spend them here straight away. There is no plan to upgrade to for access — usage is metered per call, and you buy more credits when you want more calls.

Most calls2 credits
Search, colour, craft, similar, frame detail, films.
Image search10 credits
Costs more because it embeds the image you send.
$11,000 credits
So $1 covers 500 searches. Credits never expire.
Top up$20 packs
In Settings → Billing, in whole multiples.

No rate limits. Deliberately — throttling the thing you are building against is not a business model. Run a balance down and the API answers with a message saying so, including where to top up, rather than failing silently.

Response format

Every endpoint returns JSON with this shape:

{
  "data": [ ... ] or { ... },   // the result(s)
  "meta": {                      // context about the request
    "total": 20,
    "query": "...",
    "mode": "hybrid",
    ...
  }
}

Every frame in a response carries:

idstring
Frame identifier, stable — use it with /similar and /frames.
imageUrlstring
Full-size original.
thumbUrlstring
1280px WebP thumbnail.
depthUrlstring | null
Precomputed depth map — see Depth maps. null when the frame has none; never the colour image.
deepLinkstring
Opens the frame on FrameThrower.
filmobject
Title, year, director, DP, genres.
metadataobject
Scene description, shot type, camera angle, lens character, lighting, moods, colors, era, setting, time of day.
promptstring
A composed AI image prompt. Frame detail only — see Frame detail.

Search results and frame detail are not the same shape. Two fields exist only on the single-frame call, which is the most common thing to get wrong:

FieldSearch, browse, similar/frames?id=
id, imageUrl, thumbUrl, deepLinkyesyes
depthUrlyesyes
film, metadatayesyes
metadata.sceneDescriptionyesyes
prompt (composed, ready to generate from)noyes
scoreyesno

So the usual shape of an integration is: search once to find candidates, then call /frames?id= on the handful you keep, to get the composed prompt.

Errors return { "error": "message" } with the appropriate HTTP status — 400, 401, 403, 404 or 500.

Depth maps

Every frame in the library has been through Depth-Anything V2 already, and the result is served alongside it. You do not request it, compute it, or pay for it — depthUrl is on every frame in every response, including search results.

{
  "id": "ac75d913-...",
  "imageUrl": "https://frames.shotspot.pro/core/ac75d913....jpg",
  "thumbUrl": "https://frames.shotspot.pro/thumb/ac75d913....jpg.webp",
  "depthUrl": "https://frames.shotspot.pro/depth/ac75d913....jpg.webp"
}

The map is greyscale WebP, roughly 9KB, and has the same pixel dimensions as the frame — so it drops straight into a depth ControlNet with no resizing. Near is bright, far is dark.

# Search, then feed the geometry straight into a generation
curl -s https://framethrower.ai/api/v1/search \
  -H "Authorization: Bearer $FT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"query":"low-angle staircase, hard shadows","limit":4}' \
| jq -r '.data[].depthUrl'

Fetching the map needs no authentication. It is a plain public file on our image host, not an API endpoint — do not send your Authorization header with it. Responses are cached for a year and support range requests.

curl -s -o depth.webp \
  "https://frames.shotspot.pro/depth/ac75d913....jpg.webp"   # no token

From a browser, read it server-side. The image host sends no CORS headers, so an <img src> displays the map fine but fetch() is blocked, and drawing it to a canvas taints the canvas. If your pipeline runs in the browser, proxy the map through your own origin first.

Two things that will bite otherwise. depthUrl is null when no map exists — it never falls back to the colour image, because a photograph handed to a depth ControlNet produces a confidently wrong result with nothing to explain it. And a URL can 404 in the rare case where a map is missing; treat that the same as null and compute depth locally.

One quirk if you use Python: our CDN blocks the default Python-urllib user-agent, so urllib.request gets a 403 on any image URL, depth included. requests, httpx and anything with a normal user-agent are fine.

Depth costs no credits. Only the search that found the frame does.

Connect

Authentication

Use your API token as a Bearer token in the Authorization header. All endpoints accept JSON and return JSON.

Authorization: Bearer ft_your_token_here
Content-Type: application/json

From an AI agent (Claude, ChatGPT, Cursor…)

If your agent supports custom tools or API integrations, configure it with these three values. For a connection with no plumbing at all, use the MCP server instead.

Base URLstring
https://framethrower.ai/api/v1
Authheader
Bearer ft_your_token
Docs URLstring
https://framethrower.ai/developers

Point your agent at this page (/developers) — it can read the endpoint reference and examples to understand what's available. The agent makes requests on your behalf using your token, so it has the same access as you do on the website.

OpenAPI / tool definitions

For agents that consume OpenAPI specs, the machine-readable spec lives at /api/v1/openapi.json. To hand-write a tool schema instead, here is a minimal definition for search:

{
  "name": "framethrower_search",
  "description": "Search FrameThrower's film frame library by concept, mood, lighting, color, or composition. Returns thumbnails + metadata + deep links. Use for finding visual references.",
  "parameters": {
    "type": "object",
    "properties": {
      "query": { "type": "string", "description": "Natural language search (e.g. 'overhead dinner table warm candlelight')" },
      "limit": { "type": "integer", "minimum": 1, "maximum": 50, "default": 10 }
    },
    "required": ["query"]
  }
}

JavaScript SDK

framethrower-ai wraps every endpoint on this page — same token, no HTTP plumbing. TypeScript types are bundled.

npm install framethrower-ai
import FrameThrower from 'framethrower-ai'

const ft = new FrameThrower('ft_YOUR_TOKEN')

// Search by text, colour, or image
const frames = await ft.search('neon rain at night')
const warm   = await ft.searchByColor(['#e8a87c', '#1b4332'])
const like   = await ft.searchByImage('https://example.com/photo.jpg')

// Browse by craft, then follow a frame into its film
const shots = await ft.browse({ shot_type: 'close-up', lens_character: 'anamorphic' })
const detail = await ft.frame(frames[0].id)   // detail.prompt is a composed image prompt
const more   = await ft.similar(frames[0].id, { mode: 'visual' })

const films = await ft.films({ q: 'kubrick', sort: 'frames' })
const film  = await ft.film(films[0].slug)

Source and full reference: npmjs.com/package/framethrower-ai.

POST/api/v1/search

Search the frame library with natural language. Describe a mood, scene, composition or visual — the search understands cinematography.

querystringrequired
Natural language, e.g. neon-lit rainy street at night.
limitinteger
1–50. Defaults to 20.
modestring
hybrid (default) fuses visual and text embeddings. semantic uses image-derived embeddings only. description searches text metadata only.
{
  "query": "neon-lit rainy street at night",
  "limit": 20,
  "mode": "hybrid"
}
POST/api/v1/search/image

Find frames that match a reference image by visual similarity, color and semantic meaning.

imageUrlstringrequired
Publicly reachable URL of the reference image.
limitinteger
1–50. Defaults to 20.
{ "imageUrl": "https://example.com/reference.jpg", "limit": 20 }
POST/api/v1/search/color

Find frames dominated by specific colors. Pass one hex color or a palette of up to six.

colorstring
A single hex color. Use this or colors.
colorsstring[]
Up to six hex colors, blended into one target.
limitinteger
1–50. Defaults to 20.
// Single color
{ "color": "#e94560" }

// Palette (blended)
{ "colors": ["#e94560", "#1a1a2e", "#16213e"], "limit": 20 }

Browse by craft

POST/api/v1/browse

Filter frames by cinematography attributes — no text query needed. Combine any filters. This is the unique surface: search by how a shot was made, not just what it shows.

{
  "shot_type": "close-up",          // or "wide", "medium", "extreme-close-up"
  "lens_character": "anamorphic",   // or "spherical", "vintage_soft"
  "setting": "exterior",            // or "interior"
  "time_of_day": "night",           // or "day", "golden_hour", "blue_hour"
  "camera_angle": "low_angle",      // or "high_angle", "eye_level", "overhead"
  "visual_style": "noir",           // or "naturalistic", "stylized"
  "director": "Denis Villeneuve",
  "genre": "Science Fiction",
  "era": "2010s",
  "year_min": 2010,
  "year_max": 2025,
  "limit": 20
}

Similar frames

POST/api/v1/similar

Find frames similar to one or more reference frames. The “more like this” flow.

frameIdstring
A single frame id. Use this or frameIds.
frameIdsstring[]
Several ids, averaged — the result is the centroid of what you passed in.
modestring
semantic, visual or color.
limitinteger
1–50. Defaults to 20.
// Single frame
{ "frameId": "abc123", "mode": "semantic", "limit": 20 }

// Multiple frames (averaged — finds the centroid)
{ "frameIds": ["abc", "def", "ghi"], "mode": "visual", "limit": 20 }

Frame detail

GET/api/v1/frames?id=FRAME_ID

Full metadata for a single frame — film info, cinematography attributes, colors, mood, and a ready-to-use AI image prompt.

// Response shape
{
  "data": {
    "id": "abc123",
    "imageUrl": "https://frames.shotspot.pro/core/...",
    "thumbUrl": "https://frames.shotspot.pro/thumb/...",
    "deepLink": "https://framethrower.ai/#frame/abc123",
    "prompt": "A lone figure walks through dense orange haze toward a distant structure. Set in exterior, day. Diffused amber backlight with atmospheric haze. Desaturated warm tones dominated by deep orange and ochre. Naturalistic style with a melancholy, atmospheric, isolation mood.",
    "film": {
      "title": "Blade Runner 2049", "year": 2017,
      "director": "Denis Villeneuve", "dp": "Roger Deakins",
      "genres": ["Science Fiction", "Drama"],
      "tmdbId": 335984, "imdbId": "tt1856101"
    },
    "metadata": {
      "sceneDescription": "A lone figure walks through orange haze...",
      "shotType": "wide", "cameraAngle": "eye_level",
      "lensCharacter": "anamorphic",
      "lightingKey": "low_key",
      "lightingDescription": "Diffused amber backlight with atmospheric haze",
      "moods": ["melancholy", "atmospheric", "isolation"],
      "colorPalette": ["#c45a28", "#1a1a2e", "#e8a87c"],
      "era": "2010s", "setting": "exterior", "timeOfDay": "day"
    }
  }
}

Random frames

GET/api/v1/frames/random?limit=20

Get a diverse random sample. Useful for seeding a UI, inspiration, or sampling the catalog.

Autocomplete

GET/api/v1/suggest?q=kub

Fuzzy-match film titles and director/DP names. Returns up to 5 films and 3 people.

// Response
{
  "data": {
    "films": [
      { "type": "film", "title": "2001: A Space Odyssey", "year": 1968,
        "director": "Stanley Kubrick", "posterUrl": "...", "imdbId": "..." }
    ],
    "people": [
      { "type": "person", "name": "Stanley Kubrick", "slug": "stanley-kubrick",
        "portraitUrl": "...", "filmCount": 12 }
    ]
  }
}

Film catalog

GET/api/v1/films?page=1&per_page=50&q=kubrick

Browse the full film catalog. Paginated, filterable by title or director name.

Film detail

GET/api/v1/films/:slug

Full detail for a film — plot, runtime, MPAA rating, dominant color palette and sample frames.

GET /api/v1/films/blade-runner-2049-2017

Film frames

GET/api/v1/films/frames?slug=blade-runner-2049-2017

Get all frames for a specific film (up to 500). Each includes a thumbnail, deep link and scene description.

Full examples

curl — search and refine

# Search
curl -s -X POST https://framethrower.ai/api/v1/search \
  -H "Authorization: Bearer ft_YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"query": "overhead shot dinner table warm light"}' \
  | jq '.data[:3] | .[] | {title: .film.title, url: .deepLink}'

# Browse by craft
curl -s -X POST https://framethrower.ai/api/v1/browse \
  -H "Authorization: Bearer ft_YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"shot_type": "close-up", "lens_character": "anamorphic", "limit": 5}' \
  | jq '.data | length'

# Autocomplete
curl -s "https://framethrower.ai/api/v1/suggest?q=nolan" \
  -H "Authorization: Bearer ft_YOUR_TOKEN" \
  | jq '.data.films[].title'

Reference to generation, end to end

The flow most image pipelines want: find a frame, take its geometry, take its prompt. Note the second call — the composed prompt only comes from frame detail.

import requests

TOKEN = "ft_YOUR_TOKEN"
H = {"Authorization": f"Bearer {TOKEN}"}

# 1. Find candidate references.
hits = requests.post(f"https://framethrower.ai/api/v1/search", headers=H,
    json={"query": "low-angle staircase, hard shadows", "limit": 5}).json()["data"]

frame = hits[0]

# 2. The composed prompt lives on frame detail, not on search results.
detail = requests.get(f"https://framethrower.ai/api/v1/frames",
    headers=H, params={"id": frame["id"]}).json()["data"]

prompt = detail["prompt"]          # scene + people + lighting + colour + mood
scene  = frame["metadata"]["sceneDescription"]   # just the description, no extra call

# 3. The depth map is a plain public file — no token, and do not send one.
depth = frame["depthUrl"]
if depth:
    r = requests.get(depth, timeout=30)     # NOT urllib: our CDN 403s its user-agent
    if r.ok:
        open("depth.webp", "wb").write(r.content)
    else:
        depth = None                        # rare miss — fall back to computing it

# 4. Hand both to whatever you generate with.
print(prompt)
print("depth map:", "depth.webp" if depth else "compute locally")
Depth controls geometry, not identity. If you are placing a character into a frame's composition, run the character on its own conditioning — an IP-Adapter or a character LoRA — alongside the depth ControlNet rather than expecting depth to carry both. Dropping the depth weight to around 0.6–0.8 also stops it fighting whatever is holding the character.

Python

import requests

TOKEN = "ft_YOUR_TOKEN"
H = {"Authorization": f"Bearer {TOKEN}"}

# Search by concept
r = requests.post(f"https://framethrower.ai/api/v1/search", headers=H,
    json={"query": "anamorphic close-up blue hour", "limit": 5})
for f in r.json()["data"]:
    print(f'{f["film"]["title"]} — {f["deepLink"]}')

# Find by color
r = requests.post(f"https://framethrower.ai/api/v1/search/color", headers=H,
    json={"colors": ["#c45a28", "#1a1a2e"]})
print(f'{len(r.json()["data"])} frames match that palette')

# Browse craft
r = requests.post(f"https://framethrower.ai/api/v1/browse", headers=H,
    json={"director": "Wong Kar-wai", "time_of_day": "night"})
for f in r.json()["data"]:
    print(f'{f["film"]["title"]} — {f["metadata"]["lensCharacter"]}')

JavaScript / TypeScript

const TOKEN = "ft_YOUR_TOKEN";
const api = (path, body) =>
  fetch(`https://framethrower.ai/api/v1${path}`, {
    method: body ? "POST" : "GET",
    headers: { "Authorization": `Bearer ${TOKEN}`, "Content-Type": "application/json" },
    body: body ? JSON.stringify(body) : undefined,
  }).then(r => r.json());

// Script → visual references workflow
async function findRefsForScene(sceneDescription) {
  // 1. Search by scene text
  const { data: results } = await api("/search", {
    query: sceneDescription, limit: 10
  });

  // 2. Pick the best match, find more like it
  if (results.length > 0) {
    const { data: similar } = await api("/similar", {
      frameId: results[0].id, mode: "visual", limit: 5
    });
    return [...results.slice(0, 5), ...similar];
  }
  return results;
}

// Use it
const refs = await findRefsForScene(
  "candlelit dinner party in a grand ballroom, wide shot, warm"
);
refs.forEach(f => console.log(f.film.title, f.thumbUrl));
The API surfaces frames as reference material — metadata, thumbnail URLs and deep links back to FrameThrower. It never returns raw image bytes.

By using the API you agree to the Terms of Service and Intended Use Policy. Bulk redistribution and automated scraping are prohibited. Rights to the original films remain with their respective rights holders.

Questions? contact@framethrower.ai