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:
Script → visual references
Search per scene by mood, lighting and setting, then build a lookbook from the results
Color palette matching
Start from a brand hex or a mood reference and find frames that carry those tones
Browse by craft
Filter on shot type, lens character, angle and lighting — how a shot was made, not what it shows
Image → more like this
Hand it a reference image and get frames that match it visually and semantically
Quickstart
Create a FrameThrower account
Complete onboarding
Create an API token
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}'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 creditsImage search10 credits$11,000 creditsTop up$20 packsNo 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/similar and /frames.imageUrlstringthumbUrlstringdepthUrlstring | nullnull when the frame has none; never the colour image.deepLinkstringfilmobjectmetadataobjectpromptstringSearch 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:
| Field | Search, browse, similar | /frames?id= |
|---|---|---|
| id, imageUrl, thumbUrl, deepLink | yes | yes |
| depthUrl | yes | yes |
| film, metadata | yes | yes |
| metadata.sceneDescription | yes | yes |
| prompt (composed, ready to generate from) | no | yes |
| score | yes | no |
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 URLstringhttps://framethrower.ai/api/v1AuthheaderBearer ft_your_tokenDocs URLstringhttps://framethrower.ai/developersPoint 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.
Text search
/api/v1/searchSearch the frame library with natural language. Describe a mood, scene, composition or visual — the search understands cinematography.
querystringrequiredneon-lit rainy street at night.limitintegermodestringhybrid (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"
}Image search
/api/v1/search/imageFind frames that match a reference image by visual similarity, color and semantic meaning.
imageUrlstringrequiredlimitinteger{ "imageUrl": "https://example.com/reference.jpg", "limit": 20 }Color search
/api/v1/search/colorFind frames dominated by specific colors. Pass one hex color or a palette of up to six.
colorstringcolors.colorsstring[]limitinteger// Single color
{ "color": "#e94560" }
// Palette (blended)
{ "colors": ["#e94560", "#1a1a2e", "#16213e"], "limit": 20 }Browse by craft
/api/v1/browseFilter 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
/api/v1/similarFind frames similar to one or more reference frames. The “more like this” flow.
frameIdstringframeIds.frameIdsstring[]modestringsemantic, visual or color.limitinteger// Single frame
{ "frameId": "abc123", "mode": "semantic", "limit": 20 }
// Multiple frames (averaged — finds the centroid)
{ "frameIds": ["abc", "def", "ghi"], "mode": "visual", "limit": 20 }Frame detail
/api/v1/frames?id=FRAME_IDFull 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
/api/v1/frames/random?limit=20Get a diverse random sample. Useful for seeding a UI, inspiration, or sampling the catalog.
Autocomplete
/api/v1/suggest?q=kubFuzzy-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
/api/v1/films?page=1&per_page=50&q=kubrickBrowse the full film catalog. Paginated, filterable by title or director name.
Film detail
/api/v1/films/:slugFull detail for a film — plot, runtime, MPAA rating, dominant color palette and sample frames.
GET /api/v1/films/blade-runner-2049-2017
Film frames
/api/v1/films/frames?slug=blade-runner-2049-2017Get 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")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));Legal
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