Skip to content
Developers
Open app

Complete an upload

POST
/v1/media/uploads/{id}/complete
const url = 'https://api.focal.inc/v1/media/uploads/f216c4fe-23b1-4cc4-9b18-0f8b0c9f0da6/complete';
const options = {
method: 'POST',
headers: {
'Idempotency-Key': 'looker-sync-2027-03-04-0001',
Authorization: 'Bearer <token>'
}
};
try {
const response = await fetch(url, options);
const data = await response.json();
console.log(data);
} catch (error) {
console.error(error);
}

Finish a direct upload created via POST /v1/media/uploads (the size variant) after the file’s bytes have been sent to its upload_url. The stored object is verified against the declared size before the media is finished:

  • No object yet (the transfer is still in progress or never started) → 409; finish the transfer and call again.
  • Upload older than 12 hours → 400, and the upload is marked failed (the upload window has expired; any stored bytes are discarded) — create a new upload.
  • Size mismatch → 400, the stored bytes are discarded, and the upload is marked failed.
  • Verified → returns the upload with status: "finished" and the full media embedded. Calling again on a finished upload returns the same shape, so retries are safe.

URL imports complete automatically and reject this call with 409.

Required scope: media:write. Rate-limit cost: 10. An Idempotency-Key header is required — generate one key per upload’s complete call and reuse it only for retries; the key is what keeps a timed-out-and-retried complete from racing itself.

id
required
string format: uuid

Example

f216c4fe-23b1-4cc4-9b18-0f8b0c9f0da6
Idempotency-Key
required
string
<= 255 characters

Example

looker-sync-2027-03-04-0001

Client-generated key (≤255 chars) for safe retries. A retry carrying the same key replays the original response instead of re-applying the write; reusing a key with a different body returns 400, and a still-in-flight key returns 409. Keys are remembered for 24 hours. A UUID v4 is recommended — generate one per logical write and reuse it only for retries of that write.

OK

Media typeapplication/json
object
data
required
object
id
required

The created media’s id — use it with GET /v1/media/{id} once status is finished.

string format: uuid
status
required

pending while the URL is being fetched and stored, finished once the media is ready, failed if it could not be fetched or stored (see error).

string
Allowed values: pending finished failed
error
required

Failure reason when status is failed. null otherwise.

string
nullable
media
required

The created media, once status is finished. null while pending or failed.

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
upload_url
required

Direct uploads only, and only in the creation response: the Google Cloud Storage session URL to send the file’s bytes to (see POST /v1/media/uploads). null everywhere else — including status polls, so a lost URL can’t be recovered; create a new upload instead.

string
nullable
created_at
required
string format: date-time
updated_at
required
string format: date-time

Example

{
"data": {
"id": "f216c4fe-23b1-4cc4-9b18-0f8b0c9f0da6",
"status": "pending",
"error": null,
"media": {
"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"
},
"upload_url": "https://storage.googleapis.com/upload/storage/v1/b/focal-uploads/o?uploadType=resumable&upload_id=ABPtcPqk7Zr2vN8",
"created_at": "2027-03-04T10:15:00Z",
"updated_at": "2027-03-06T14:42:00Z"
}
}

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"
}
}

The request conflicts with the current state. Most commonly a concurrent request carrying the same Idempotency-Key is still in flight. error.code is conflict.

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": "conflict",
"message": "A request with this Idempotency-Key is already being processed"
}
}

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.