Skip to content
Developers
Open app

Search media

POST
/v1/media/search
const url = 'https://api.focal.inc/v1/media/search';
const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: '{"space_id":"6916bc3d-e1ab-425b-ac25-eefada01a9a2","query":"trail runner at sunrise","type":"video","folder_id":"80539d38-3a8e-48f8-929b-8c0e9a22ab43","include_subfolders":true,"status":"Approved","uploaded_by":"cfebb6ef-f092-4591-a6e3-e6667ab5a252","attributes":[{"attribute_id":"846196ad-9fa2-40a8-8938-655a962220e1","values":["Spring Launch"],"match":"any"}],"created_after":"2027-03-01","created_before":"2027-03-31","sort":"relevance","order":"asc","limit":25,"cursor":"eyJvIjoyNX0","last_activity_after":"2027-03-01","last_activity_before":"2027-03-31","metric_filters":[{"metric":"spend","operator":"greater_than","value":1000}],"fields":["id"],"metrics":["spend","impressions"]}'
};
try {
const response = await fetch(url, options);
const data = await response.json();
console.log(data);
} catch (error) {
console.error(error);
}

Search a space’s media with the same engine that powers search inside Focal — so results and their ordering match what your team sees in the app.

query is ranked free text: it matches names, descriptions, folder names, tags and attribute values, and in spaces with Focal AI enabled it also matches text detected in the asset (OCR), speech transcripts, and the visual content itself — so “woman drinking coffee” can find a video nobody labelled that way. Filters (type, folder_id, status, uploaded_by, attributes, the created_* range) narrow the result set and combine with AND; a search with only filters and no query is ordered newest first.

Prefer GET /v1/media when you’re enumerating or syncing a space (stable keyset paging, cheaper), and this endpoint when you’re looking for something. Archived and still-uploading assets are excluded from both, and the response items are the same Media shape.

POST is used for the request-body filters; the call reads state and creates nothing — no Idempotency-Key applies.

Paging is an opaque next_cursor like the list endpoints, but over a ranked result set: it isn’t stable against assets entering or leaving the match set between pages, and relevance ordering gets approximate deep into the tail. Read the first pages, or sort by a column when you need to walk everything.

Pass fields to trim the response to the properties you actually need — e.g. "fields": ["name", "html_url"] returns just those (plus id, always). Unknown names are rejected rather than ignored.

Required scope: media:read. Rate-limit cost: 10.

Media typeapplication/json
object
space_id
required

Required. The single space to query, as a UUID. The API reads one space at a time — comma-separated lists and wildcards are not accepted, and there is no implicit default.

string format: uuid

Example

6916bc3d-e1ab-425b-ac25-eefada01a9a2
query

Free-text query, ranked by relevance (≤200 chars). Matches names, descriptions, folder names, tags and attribute values; in spaces with Focal AI enabled it also matches text detected in the asset (OCR), speech transcripts, and the visual content itself. A media id here matches that asset exactly.

string
<= 200 characters

Example

trail runner at sunrise
type

Restrict to one file type.

string
Allowed values: video image playable file
folder_id

Restrict to a folder in the searched space. Subfolders are included unless include_subfolders is false. Unknown folders are rejected rather than ignored.

string format: uuid

Example

80539d38-3a8e-48f8-929b-8c0e9a22ab43
include_subfolders

Whether to descend into subfolders. Default true. With folder_id, false restricts to that folder’s immediate contents; without it, false restricts to the space root.

boolean

Example

true
status

Restrict to media with this status — an id or name from the space’s catalog (the Space resource’s media_statuses), the same id-or-name contract as a status write.

string
>= 1 characters <= 255 characters

Example

Approved
uploaded_by

Restrict to media uploaded by this user (a GET /v1/users id).

string format: uuid

Example

cfebb6ef-f092-4591-a6e3-e6667ab5a252
attributes

Filter on media attribute (custom field) values. Media must satisfy every entry; within an entry, values is any-of. number and date attributes aren’t searchable yet and are rejected.

Array<object>
object
attribute_id
required

The media attribute (custom field) to filter on — an id from GET /v1/media-attributes.

string format: uuid

Example

846196ad-9fa2-40a8-8938-655a962220e1
values
required

Values to match, interpreted per the attribute’s type: option ids or names for a tags field, user ids for a user field, "true" / "false" for a checkbox field (exactly one), and the stored text for text / link. Several values match any of them. An option value matching none of the attribute’s options — and a user value that isn’t a user id — is rejected rather than ignored, so a filter never silently widens the result set.

Array<string>

Example

[
"Spring Launch"
]
match

any (default) keeps media matching at least one value; none excludes media matching any of them.

string
Allowed values: any none
created_after
Any of:
string format: date
created_before
Any of:
string format: date
sort

Result ordering. relevance (default) ranks by match quality — meaningful only with a query; a filter-only search falls back to newest first. Any other key sorts by that column and ignores relevance entirely.

string
Allowed values: relevance created_at updated_at name size spend impressions clicks reach installs watch_min_3_seconds watch_hook_rate_min_3_seconds last_activity
order

Sort direction. Defaults to asc for name and desc for the other columns. Ignored when sort is relevance (best matches always come first).

string
Allowed values: asc desc
limit

Max results per page (1–100). Default 25.

integer
>= 1 <= 100

Example

25
cursor

Pagination cursor returned as next_cursor from the previous page.

string

Example

eyJvIjoyNX0
last_activity_after
Any of:
string format: date
last_activity_before
Any of:
string format: date
metric_filters

Narrow to media whose ad-platform metrics satisfy every one of these (ANDed, like the other filters). Compares against lifetime totals — { metric: "spend", operator: "greater_than", value: 1000 } means “has spent over 1000 in total”, not “spent over 1000 in some window”. Media with no data for the metric never match.

Array<object>
object
metric
required

Which metric to compare — one of the keys that appear in a media’s metrics.insights.

string
Allowed values: spend impressions clicks reach installs watch_min_3_seconds watch_hook_rate_min_3_seconds
operator
required

How to compare — greater_than, less_than, or equals.

string
Allowed values: greater_than less_than equals
value
required

The bound to compare the metric’s lifetime total against.

number

Example

1000
fields

Return only these properties of each media asset, instead of the full object. id is always included. Omit for the complete object.

Array<string>
Allowed values: id name type format media_type organization_id space_id folder size width height aspect_ratio duration page_count description summary status creative_id concept_id variant_id attributes exports metrics created_at updated_at html_url
metrics

Performance metrics to include in each media’s metrics.insights. Default: the core set (spend, impressions, clicks, reach, installs, watch_min_3_seconds). all returns every metric with data — ad platforms report hundreds of conversion events, so expect a large response — or name keys like meta.omni_purchase. Only metrics with stored data come back, so an unknown key is silently absent rather than an error. Needs metrics to survive the fields selection.

Array<string>

Example

[
"spend",
"impressions"
]

OK

Media typeapplication/json
object
data
required
Array<object>
object
id
required
string format: uuid
name
required
string
type
required
string
Allowed values: video image playable file
format
required

File extension.

string
media_type
required

IANA media type (the spec’s current name for what was historically called MIME type) derived from format. Provided as a convenience so clients don’t need to maintain their own extension → media-type table when uploading to ad platforms.

Don’t confuse this with the top-level type field — type is Focal’s coarse asset bucket (image/video/playable/file), while media_type is the precise IANA identifier ad platforms expect (e.g. image/jpeg, video/mp4).

string
organization_id
required
string format: uuid
space_id
required
string format: uuid
folder
required

Parent folder summary, or null if the media is at the space root.

object
id
required
string format: uuid
name
required
string
size
required

File size in bytes.

integer
width
required

Pixel width for visual media. null when dimensions don’t apply (e.g. type=file) or couldn’t be extracted.

integer
nullable
height
required

Pixel height for visual media. null when dimensions don’t apply (e.g. type=file) or couldn’t be extracted.

integer
nullable
aspect_ratio
required

width / height for visual media. null when dimensions don’t apply (e.g. type=file) or couldn’t be extracted.

number
nullable
duration
required

Duration in seconds for video files. null for other types.

number
nullable
page_count
required

Total page count for multi-page formats (e.g. PDF). null for other types.

integer
nullable
description
required
string
nullable
summary
required

AI-generated summary of the media content, produced by Focal’s analysis pipeline. null until analysis has run (or for media types that aren’t analyzed). Read-only — for the customer-managed free-text field use description.

string
nullable
status
required

The media’s workflow status name (e.g. Approved), one of the space’s configured media statuses — see the Space resource’s media_statuses for the catalog. null when no status is set. Same read/write semantics as a tags attribute: reads return the name; writes accept the status id or name.

string
nullable
creative_id
required

Generated identifier produced by a Creative ID naming convention — the combined <concept_id>-<variant_id> string the customer configured. null when the media isn’t governed by a Creative ID configuration.

string
nullable
concept_id
required

Concept portion of the Creative ID — the stable text/date/counter prefix shared across variants. null when the media has no Creative ID.

string
nullable
variant_id
required

Variant number within a concept. Incremented per variant of the same concept. null when the media has no Creative ID.

integer
nullable
attributes
required

Custom-field values attached to this media — one entry per populated field. A multi-value field carries an array value; a single-value field a scalar. Empty array when no custom fields are populated.

Array<object>
object
attribute_id
required
string format: uuid
attribute_name
required
string
attribute_type
required

The kind of custom field this value belongs to. Combined with multiple, tells you the JSON type of value (for a multi-value field it’s the type of each array element):

  • number → JSON number
  • checkbox → JSON true / false
  • date → ISO 8601 string
  • tags → option id string (option names in display_value)
  • user → user id string (display names in display_value)
  • everything else → JSON string (e.g. URLs for link)
string
Allowed values: text number tags checkbox date link user
multiple
required

Whether this field holds multiple values. When true, value is an array (one element per stored value); when false, value is a single scalar.

boolean
value
required
Any of:
Any of:
string
display_value
required
Any of:
string
exports
required

External identifiers for the ad platforms this media has been successfully exported to (YouTube, Meta, TikTok). One entry per export — re-exports and separate destinations each add an entry, so a media can appear more than once for the same channel. Empty array when the media hasn’t been exported, or only to channels without creative identifiers (e.g. Google Drive). In-progress and failed exports are excluded.

Array
Any of:
object
kind
required
string
Allowed values: youtube
type
required
string
Allowed values: video
video_id
required

YouTube video id — the watch id.

string
url
required

Canonical YouTube watch URL.

string
youtube_channel
required

Id of the YouTube channel the video was uploaded to (e.g. UC…).

string
exported_at
required

UTC ISO timestamp of when this export was created.

string format: date-time
metrics
required

Ad-platform performance for this media, rolled up by Focal’s metrics sync across every ad it has run in — where it ran (ad_accounts / campaigns / ad_sets), when it was last live (last_activity), and its lifetime totals (insights). null for media that has never run as an ad, or when the organization has no ad accounts connected. Use the metrics request parameter to choose which metrics come back.

object
last_activity
required

The most recent date the ad platforms reported any data for this media — i.e. when it was last live. null when no dated data has been synced.

string format: date-time
nullable
ad_accounts
required

Ad accounts this media has been running in.

Array<object>
object
id
required

Focal’s id for this ad account / campaign / ad set.

string
name
required

Its name as reported by the ad platform. null when the sync hasn’t recorded one.

string
nullable
campaigns
required

Campaigns this media has been running in.

Array<object>
object
id
required

Focal’s id for this ad account / campaign / ad set.

string
name
required

Its name as reported by the ad platform. null when the sync hasn’t recorded one.

string
nullable
ad_sets
required

Ad sets this media has been running in.

Array<object>
object
id
required

Focal’s id for this ad account / campaign / ad set.

string
name
required

Its name as reported by the ad platform. null when the sync hasn’t recorded one.

string
nullable
insights
required

Lifetime performance totals, one entry per metric. Which metrics appear is controlled by the metrics request parameter — by default the core set, and only those a media actually has data for. Core metrics come first, then the rest alphabetically.

Array<object>
object
metric
required

The metric key — a core name (spend, impressions, clicks, reach, installs, watch_min_3_seconds) or a platform-specific one (e.g. meta.omni_purchase). Pass these to the metrics parameter to select them.

string
value
required

The metric’s total across every ad this media has run in, over all time — not a windowed figure. Currency amounts are in the ad account’s own currency.

number
created_at
required
string format: date-time
updated_at
required
string format: date-time
html_url
required

Browser URL that opens this media in the Focal web app (its space’s library). Hand this to users as a clickable link to the exact asset.

string
next_cursor
required

Pass back as cursor to fetch the next page. null on the last page.

string
nullable
has_more
required

true when another page is available. Equivalent to next_cursor !== null.

boolean
total
required

Total number of media matching the search across all pages. Relevance-ranked results are approximate at the tail — treat this as the size of the match set, not a promise about how many pages you can walk.

integer

Example

{
"data": [
{
"id": "786beeda-55ac-4025-bf6e-43b2918e84a9",
"name": "spring-hero-4x5.mp4",
"type": "video",
"format": "mp4",
"media_type": "video/mp4",
"organization_id": "b46d9033-24b2-4441-87ad-2151088fde01",
"space_id": "6916bc3d-e1ab-425b-ac25-eefada01a9a2",
"folder": {
"id": "80539d38-3a8e-48f8-929b-8c0e9a22ab43",
"name": "Spring 2027"
},
"size": 48213504,
"width": 1080,
"height": 1350,
"aspect_ratio": 0.8,
"duration": 18.5,
"page_count": null,
"description": "Hero cut for Meta and TikTok placements, 4:5.",
"summary": "A trail runner crests a ridge at sunrise in Alder & Pine gear, then the logo and Spring 2027 title card appear.",
"status": "Approved",
"creative_id": "SPR27-0042-3",
"concept_id": "SPR27-0042",
"variant_id": 3,
"attributes": [
{
"attribute_id": "0f35c2a2-a71a-4423-b1f2-3b5d795c6332",
"attribute_name": "Usage rights",
"attribute_type": "text",
"multiple": false,
"value": "Paid social only, 12 months",
"display_value": "Paid social only, 12 months"
}
],
"exports": [
{
"kind": "youtube",
"type": "video",
"video_id": "u8Lk2VnTq4c",
"url": "https://www.youtube.com/watch?v=u8Lk2VnTq4c",
"youtube_channel": "UCa1Dp9Qw3nXr7LbT5vHkZ2g",
"exported_at": "2027-03-05T09:30:00Z"
}
],
"metrics": {
"last_activity": "2027-03-14T00:00:00Z",
"ad_accounts": [
{
"id": "act_2048117733",
"name": "Alder & Pine – Paid Social"
}
],
"campaigns": [
{
"id": "120211482993410331",
"name": "Spring 2027 – Prospecting"
}
],
"ad_sets": [
{
"id": "120211482993520447",
"name": "Trail runners 25–44"
}
],
"insights": [
{
"metric": "spend",
"value": 1842.5
},
{
"metric": "impressions",
"value": 412930
},
{
"metric": "clicks",
"value": 9318
}
]
},
"created_at": "2027-03-04T10:15:00Z",
"updated_at": "2027-03-06T14:42:00Z",
"html_url": "https://app.focal.inc/spaces/6916bc3d-e1ab-425b-ac25-eefada01a9a2/library?file=786beeda-55ac-4025-bf6e-43b2918e84a9&folder=80539d38-3a8e-48f8-929b-8c0e9a22ab43"
}
],
"next_cursor": "eyJ0IjoiMjAyNy0wMy0wNFQxMDoxNTowMC4wMDBaIiwiaSI6Ijc4NmJlZWRhLTU1YWMtNDAyNS1iZjZlLTQzYjI5MThlODRhOSJ9",
"has_more": true,
"total": 128
}

The request was malformed (invalid cursor, unknown type, validation failure, etc.). error.code is bad_request.

Media typeapplication/json

Canonical error envelope. code is stable across releases and intended for programmatic handling; message is human-readable. details only appears for validation failures and lists the offending fields.

object
error
required
object
code
required
string
Allowed values: unauthenticated forbidden not_found bad_request conflict rate_limited internal_error
message
required
string
details
Array<object>
object
field
string
issue
required
string

Example

{
"error": {
"code": "bad_request",
"message": "Invalid request body",
"details": [
{
"field": "space_id",
"issue": "Invalid UUID"
}
]
}
}

Missing, invalid, or disabled token. error.code is unauthenticated.

Media typeapplication/json

Canonical error envelope. code is stable across releases and intended for programmatic handling; message is human-readable. details only appears for validation failures and lists the offending fields.

object
error
required
object
code
required
string
Allowed values: unauthenticated forbidden not_found bad_request conflict rate_limited internal_error
message
required
string
details
Array<object>
object
field
string
issue
required
string

Example

{
"error": {
"code": "unauthenticated",
"message": "Bearer token is missing or invalid"
}
}

The token doesn’t have the scope this endpoint requires. error.code is forbidden.

Media typeapplication/json

Canonical error envelope. code is stable across releases and intended for programmatic handling; message is human-readable. details only appears for validation failures and lists the offending fields.

object
error
required
object
code
required
string
Allowed values: unauthenticated forbidden not_found bad_request conflict rate_limited internal_error
message
required
string
details
Array<object>
object
field
string
issue
required
string

Example

{
"error": {
"code": "forbidden",
"message": "Token doesn't have the required scope"
}
}

Resource doesn’t exist or is outside the token’s space allow-list. The two cases share a response shape so a restricted token can’t probe for the existence of out-of-scope spaces. error.code is not_found.

Media typeapplication/json

Canonical error envelope. code is stable across releases and intended for programmatic handling; message is human-readable. details only appears for validation failures and lists the offending fields.

object
error
required
object
code
required
string
Allowed values: unauthenticated forbidden not_found bad_request conflict rate_limited internal_error
message
required
string
details
Array<object>
object
field
string
issue
required
string

Example

{
"error": {
"code": "not_found",
"message": "Not found"
}
}

Rate limit exceeded. error.code is rate_limited. Wait the number of seconds in the Retry-After header before retrying. Quotas are per-token; see the “Rate limiting” section at the top of this document.

Media typeapplication/json

Canonical error envelope. code is stable across releases and intended for programmatic handling; message is human-readable. details only appears for validation failures and lists the offending fields.

object
error
required
object
code
required
string
Allowed values: unauthenticated forbidden not_found bad_request conflict rate_limited internal_error
message
required
string
details
Array<object>
object
field
string
issue
required
string

Example

{
"error": {
"code": "rate_limited",
"message": "Rate limit of 1000 units per 60s exceeded (this request cost 5)"
}
}
Retry-After
integer
Example
12

Seconds to wait before retrying.