Add tag options
CURL *hnd = curl_easy_init();
curl_easy_setopt(hnd, CURLOPT_CUSTOMREQUEST, "POST");curl_easy_setopt(hnd, CURLOPT_URL, "https://api.focal.inc/v1/media-attributes/846196ad-9fa2-40a8-8938-655a962220e1/options");
struct curl_slist *headers = NULL;headers = curl_slist_append(headers, "Idempotency-Key: looker-sync-2027-03-04-0001");headers = curl_slist_append(headers, "Authorization: Bearer <token>");headers = curl_slist_append(headers, "Content-Type: application/json");curl_easy_setopt(hnd, CURLOPT_HTTPHEADER, headers);
curl_easy_setopt(hnd, CURLOPT_POSTFIELDS, "{ \"options\": [ { \"name\": \"Evergreen\", \"description\": \"Always-on assets that aren't tied to a single campaign.\" } ] }");
CURLcode ret = curl_easy_perform(hnd);using System.Net.Http.Headers;var client = new HttpClient();var request = new HttpRequestMessage{ Method = HttpMethod.Post, RequestUri = new Uri("https://api.focal.inc/v1/media-attributes/846196ad-9fa2-40a8-8938-655a962220e1/options"), Headers = { { "Idempotency-Key", "looker-sync-2027-03-04-0001" }, { "Authorization", "Bearer <token>" }, }, Content = new StringContent("{ \"options\": [ { \"name\": \"Evergreen\", \"description\": \"Always-on assets that aren't tied to a single campaign.\" } ] }") { Headers = { ContentType = new MediaTypeHeaderValue("application/json") } }};using (var response = await client.SendAsync(request)){ response.EnsureSuccessStatusCode(); var body = await response.Content.ReadAsStringAsync(); Console.WriteLine(body);}package main
import ( "fmt" "strings" "net/http" "io")
func main() {
url := "https://api.focal.inc/v1/media-attributes/846196ad-9fa2-40a8-8938-655a962220e1/options"
payload := strings.NewReader("{ \"options\": [ { \"name\": \"Evergreen\", \"description\": \"Always-on assets that aren't tied to a single campaign.\" } ] }")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Idempotency-Key", "looker-sync-2027-03-04-0001") req.Header.Add("Authorization", "Bearer <token>") req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close() body, _ := io.ReadAll(res.Body)
fmt.Println(res) fmt.Println(string(body))
}HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://api.focal.inc/v1/media-attributes/846196ad-9fa2-40a8-8938-655a962220e1/options")) .header("Idempotency-Key", "looker-sync-2027-03-04-0001") .header("Authorization", "Bearer <token>") .header("Content-Type", "application/json") .method("POST", HttpRequest.BodyPublishers.ofString("{ \"options\": [ { \"name\": \"Evergreen\", \"description\": \"Always-on assets that aren't tied to a single campaign.\" } ] }")) .build();HttpResponse<String> response = HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString());System.out.println(response.body());OkHttpClient client = new OkHttpClient();
MediaType mediaType = MediaType.parse("application/json");RequestBody body = RequestBody.create(mediaType, "{ \"options\": [ { \"name\": \"Evergreen\", \"description\": \"Always-on assets that aren't tied to a single campaign.\" } ] }");Request request = new Request.Builder() .url("https://api.focal.inc/v1/media-attributes/846196ad-9fa2-40a8-8938-655a962220e1/options") .post(body) .addHeader("Idempotency-Key", "looker-sync-2027-03-04-0001") .addHeader("Authorization", "Bearer <token>") .addHeader("Content-Type", "application/json") .build();
Response response = client.newCall(request).execute();import axios from 'axios';
const options = { method: 'POST', url: 'https://api.focal.inc/v1/media-attributes/846196ad-9fa2-40a8-8938-655a962220e1/options', headers: { 'Idempotency-Key': 'looker-sync-2027-03-04-0001', Authorization: 'Bearer <token>', 'Content-Type': 'application/json' }, data: { options: [ { name: 'Evergreen', description: 'Always-on assets that aren\'t tied to a single campaign.' } ] }};
try { const { data } = await axios.request(options); console.log(data);} catch (error) { console.error(error);}const url = 'https://api.focal.inc/v1/media-attributes/846196ad-9fa2-40a8-8938-655a962220e1/options';const options = { method: 'POST', headers: { 'Idempotency-Key': 'looker-sync-2027-03-04-0001', Authorization: 'Bearer <token>', 'Content-Type': 'application/json' }, body: '{"options":[{"name":"Evergreen","description":"Always-on assets that aren\'t tied to a single campaign."}]}'};
try { const response = await fetch(url, options); const data = await response.json(); console.log(data);} catch (error) { console.error(error);}val client = OkHttpClient()
val mediaType = MediaType.parse("application/json")val body = RequestBody.create(mediaType, "{ \"options\": [ { \"name\": \"Evergreen\", \"description\": \"Always-on assets that aren't tied to a single campaign.\" } ] }")val request = Request.Builder() .url("https://api.focal.inc/v1/media-attributes/846196ad-9fa2-40a8-8938-655a962220e1/options") .post(body) .addHeader("Idempotency-Key", "looker-sync-2027-03-04-0001") .addHeader("Authorization", "Bearer <token>") .addHeader("Content-Type", "application/json") .build()
val response = client.newCall(request).execute()use serde_json::json;use reqwest;
#[tokio::main]pub async fn main() { let url = "https://api.focal.inc/v1/media-attributes/846196ad-9fa2-40a8-8938-655a962220e1/options";
let payload = json!({"options": ( json!({ "name": "Evergreen", "description": "Always-on assets that aren't tied to a single campaign." }) )});
let mut headers = reqwest::header::HeaderMap::new(); headers.insert("Idempotency-Key", "looker-sync-2027-03-04-0001".parse().unwrap()); headers.insert("Authorization", "Bearer <token>".parse().unwrap()); headers.insert("Content-Type", "application/json".parse().unwrap());
let client = reqwest::Client::new(); let response = client.post(url) .headers(headers) .json(&payload) .send() .await;
let results = response.unwrap() .json::<serde_json::Value>() .await .unwrap();
dbg!(results);}curl --request POST \ --url https://api.focal.inc/v1/media-attributes/846196ad-9fa2-40a8-8938-655a962220e1/options \ --header 'Authorization: Bearer <token>' \ --header 'Content-Type: application/json' \ --header 'Idempotency-Key: looker-sync-2027-03-04-0001' \ --data '{ "options": [ { "name": "Evergreen", "description": "Always-on assets that aren'\''t tied to a single campaign." } ] }'wget --quiet \ --method POST \ --header 'Idempotency-Key: looker-sync-2027-03-04-0001' \ --header 'Authorization: Bearer <token>' \ --header 'Content-Type: application/json' \ --body-data '{ "options": [ { "name": "Evergreen", "description": "Always-on assets that aren'\''t tied to a single campaign." } ] }' \ --output-document \ - https://api.focal.inc/v1/media-attributes/846196ad-9fa2-40a8-8938-655a962220e1/optionsAppend options to a tags field and return the updated definition (including the new options’ ids). Purely additive: existing options are never renamed, reordered, or removed, and a name that already exists is skipped rather than duplicated — so the call is safe to repeat.
Rejected with 400 on a non-tags field.
Required scope: media:write — unlike the other definition writes, adding an option can’t invalidate a stored value, so it rides on the same scope as writing values and needs no confirmation over MCP.
A field with allow_create: false (the setting that also stops the app from adding options while tagging) is closed to that scope, and needs spaces:write instead — the scope for the space’s own settings, with the same admin/owner requirement PATCH /v1/media-attributes/{id} applies when the credential acts for a specific user. A credential holding neither gets a 400. Defining options on a closed field this way is still purely additive: prefer it over PATCH, which replaces the option set.
Rate-limit cost: 10. Requires an Idempotency-Key.
Authorizations
Section titled “Authorizations”Parameters
Section titled “Parameters”Path Parameters
Section titled “Path Parameters”Example
846196ad-9fa2-40a8-8938-655a962220e1Header Parameters
Section titled “Header Parameters”Example
looker-sync-2027-03-04-0001Client-generated key (≤255 chars) for safe retries. A retry with the same key replays the original response instead of re-applying the write; reusing a key with a different body → 400, an in-flight key → 409.
Request Bodyrequired
Section titled “Request Bodyrequired”object
Options to append, in order. Names that already exist on the field (case-insensitive) are skipped rather than duplicated, so the call is safe to repeat. At most 100 per request, and the field cannot exceed 499 options in total — a request that would go past that is rejected rather than partly applied.
object
The option’s label. Must be unique within the field (case-insensitive).
Example
Spring LaunchHelp text shown next to the option in Focal when picking tags. At most 500 characters; send an empty string to clear it.
Example
Assets for the Spring 2027 launch campaign.Example
[ { "name": "Evergreen", "description": "Always-on assets that aren't tied to a single campaign." }]Responses
Section titled “Responses”OK
object
object
The attribute definition id — referenced by media payloads as attributes[].attribute_id.
Display name of the field, as media payloads carry in attributes[].attribute_name.
The field’s kind — same enum as attributes[].attribute_type on media payloads. tags fields carry selectable options; fetch them via GET /v1/media-attributes/{id}. user fields hold user ids — resolve them via GET /v1/users.
Whether the field holds multiple values per media (e.g. multi-select). Mirrors attributes[].multiple.
For tags fields, the selectable options in display order. null for field types that have no options concept.
object
Stable option id — the value media payloads carry in attributes[].value for this option. Option renames keep this id; match on it rather than on name when tracking options over time.
Human-readable option label. This is the string media payloads carry in attributes[].display_value for this option.
The option’s help text, shown next to it in Focal when picking tags — what this option means, or when to use it. Empty string when unset; never null, so it’s safe to render directly. Not carried on media payloads, which reference the option by id/display_value only.
The field’s AI autofill configuration, or null when autofill is off — which is also what field types that can’t autofill (everything but text and tags) always report. For a tags field the model is additionally given each option’s name and description, so option descriptions act as per-option guidance on top of this instruction.
object
The instruction the AI follows, as plain text. A prompt written in the app with @mentions of other fields is flattened to @Label here.
The media kinds this runs on, or null for all of them.
Whether the AI sees only the filename rather than analyzing the media itself.
Example
{ "data": { "id": "846196ad-9fa2-40a8-8938-655a962220e1", "name": "Campaign", "type": "text", "multiple": true, "space_id": "6916bc3d-e1ab-425b-ac25-eefada01a9a2", "organization_id": "b46d9033-24b2-4441-87ad-2151088fde01", "created_at": "2027-03-04T10:15:00Z", "updated_at": "2027-03-06T14:42:00Z", "options": [ { "id": "80f1e4d3-cbab-42a0-a397-318f348f5f93", "name": "Spring Launch", "description": "Assets for the Spring 2027 launch campaign." } ], "autofill": { "prompt": "Pick the campaign this asset belongs to, based on its content and filename.", "allowed_media_types": [ "videos" ], "name_only": false } }}The request was malformed (invalid cursor, unknown type, validation failure, etc.). error.code is bad_request.
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
object
object
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.
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
object
object
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.
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
object
object
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.
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
object
object
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.
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
object
object
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.
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
object
object
Example
{ "error": { "code": "rate_limited", "message": "Rate limit of 1000 units per 60s exceeded (this request cost 5)" }}Headers
Section titled “Headers”Example
12Seconds to wait before retrying.