curl --request POST \
--url https://api.moda.app/v1/brand-kits/{brand_kit_id}/images \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"file_id": "file_01HT9WK8N3M2J4A5Z6P7Q8R9TV",
"role": "logo",
"label": "Dark mode logo"
}
'import requests
url = "https://api.moda.app/v1/brand-kits/{brand_kit_id}/images"
payload = {
"file_id": "file_01HT9WK8N3M2J4A5Z6P7Q8R9TV",
"role": "logo",
"label": "Dark mode logo"
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({
file_id: 'file_01HT9WK8N3M2J4A5Z6P7Q8R9TV',
role: 'logo',
label: 'Dark mode logo'
})
};
fetch('https://api.moda.app/v1/brand-kits/{brand_kit_id}/images', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.moda.app/v1/brand-kits/{brand_kit_id}/images",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'file_id' => 'file_01HT9WK8N3M2J4A5Z6P7Q8R9TV',
'role' => 'logo',
'label' => 'Dark mode logo'
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.moda.app/v1/brand-kits/{brand_kit_id}/images"
payload := strings.NewReader("{\n \"file_id\": \"file_01HT9WK8N3M2J4A5Z6P7Q8R9TV\",\n \"role\": \"logo\",\n \"label\": \"Dark mode logo\"\n}")
req, _ := http.NewRequest("POST", url, payload)
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(string(body))
}HttpResponse<String> response = Unirest.post("https://api.moda.app/v1/brand-kits/{brand_kit_id}/images")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"file_id\": \"file_01HT9WK8N3M2J4A5Z6P7Q8R9TV\",\n \"role\": \"logo\",\n \"label\": \"Dark mode logo\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.moda.app/v1/brand-kits/{brand_kit_id}/images")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"file_id\": \"file_01HT9WK8N3M2J4A5Z6P7Q8R9TV\",\n \"role\": \"logo\",\n \"label\": \"Dark mode logo\"\n}"
response = http.request(request)
puts response.read_body{}{
"type": "invalid_request",
"code": "<string>",
"message": "<string>",
"doc_url": "<string>",
"request_id": "<string>",
"causes": [
"<unknown>"
],
"details": {},
"retry_after_ms": 123,
"retryable": true
}{
"type": "invalid_request",
"code": "<string>",
"message": "<string>",
"doc_url": "<string>",
"request_id": "<string>",
"causes": [
"<unknown>"
],
"details": {},
"retry_after_ms": 123,
"retryable": true
}{
"type": "invalid_request",
"code": "<string>",
"message": "<string>",
"doc_url": "<string>",
"request_id": "<string>",
"causes": [
"<unknown>"
],
"details": {},
"retry_after_ms": 123,
"retryable": true
}{
"type": "invalid_request",
"code": "<string>",
"message": "<string>",
"doc_url": "<string>",
"request_id": "<string>",
"causes": [
"<unknown>"
],
"details": {},
"retry_after_ms": 123,
"retryable": true
}{
"type": "invalid_request",
"code": "<string>",
"message": "<string>",
"doc_url": "<string>",
"request_id": "<string>",
"causes": [
"<unknown>"
],
"details": {},
"retry_after_ms": 123,
"retryable": true
}{
"type": "invalid_request",
"code": "<string>",
"message": "<string>",
"doc_url": "<string>",
"request_id": "<string>",
"causes": [
"<unknown>"
],
"details": {},
"retry_after_ms": 123,
"retryable": true
}{
"type": "invalid_request",
"code": "<string>",
"message": "<string>",
"doc_url": "<string>",
"request_id": "<string>",
"causes": [
"<unknown>"
],
"details": {},
"retry_after_ms": 123,
"retryable": true
}{
"type": "invalid_request",
"code": "<string>",
"message": "<string>",
"doc_url": "<string>",
"request_id": "<string>",
"causes": [
"<unknown>"
],
"details": {},
"retry_after_ms": 123,
"retryable": true
}Add Brand Kit Image
Attach an uploaded image to a brand kit (ENG-2466).
Multipart-upload flow: the caller first POST /v1/uploads their file
and receives a file_* id (the response’s id field), then calls this
endpoint with that id as file_id to associate the image with the brand
kit (logo / reference / asset). A bare UUID is also accepted for back-compat
with older integrations.
Returns 404 not_found if the brand kit does not exist, has been
deleted, or the caller lacks team access — collapsed like the sibling
endpoints so the write path does not leak existence of brand kits in
other teams. An unusable file_id (missing, deleted, or outside the
kit’s team) is a 400.
The attach is idempotent (ENG-3043): re-posting a file_id already on
the kit returns 201 with the brand kit unchanged rather than adding a
second image, so retries and full re-syncs are safe. Dedupe is per storage
bucket, and reference and asset share one bucket — re-attaching a
reference image as an asset succeeds without recording the new
role. Re-posting the same role with a different label relabels
the existing image in place (ENG-5988) and returns 201: the ref keeps
its id, notes and position, and no second image is added. The role has to
match because the scan is bucket-scoped: re-posting under the other bucket’s
role finds nothing to relabel and appends instead. role is required
here, so a caller always states one — the transports that default it carry a
louder warning. It was a 400 until then, on the
reasoning that the no-op path could not carry a new label without writing
and that returning success over a dropped one would be undetectable — both
true, and both answered by writing the label instead of dropping it.
curl --request POST \
--url https://api.moda.app/v1/brand-kits/{brand_kit_id}/images \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"file_id": "file_01HT9WK8N3M2J4A5Z6P7Q8R9TV",
"role": "logo",
"label": "Dark mode logo"
}
'import requests
url = "https://api.moda.app/v1/brand-kits/{brand_kit_id}/images"
payload = {
"file_id": "file_01HT9WK8N3M2J4A5Z6P7Q8R9TV",
"role": "logo",
"label": "Dark mode logo"
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({
file_id: 'file_01HT9WK8N3M2J4A5Z6P7Q8R9TV',
role: 'logo',
label: 'Dark mode logo'
})
};
fetch('https://api.moda.app/v1/brand-kits/{brand_kit_id}/images', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.moda.app/v1/brand-kits/{brand_kit_id}/images",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'file_id' => 'file_01HT9WK8N3M2J4A5Z6P7Q8R9TV',
'role' => 'logo',
'label' => 'Dark mode logo'
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.moda.app/v1/brand-kits/{brand_kit_id}/images"
payload := strings.NewReader("{\n \"file_id\": \"file_01HT9WK8N3M2J4A5Z6P7Q8R9TV\",\n \"role\": \"logo\",\n \"label\": \"Dark mode logo\"\n}")
req, _ := http.NewRequest("POST", url, payload)
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(string(body))
}HttpResponse<String> response = Unirest.post("https://api.moda.app/v1/brand-kits/{brand_kit_id}/images")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"file_id\": \"file_01HT9WK8N3M2J4A5Z6P7Q8R9TV\",\n \"role\": \"logo\",\n \"label\": \"Dark mode logo\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.moda.app/v1/brand-kits/{brand_kit_id}/images")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"file_id\": \"file_01HT9WK8N3M2J4A5Z6P7Q8R9TV\",\n \"role\": \"logo\",\n \"label\": \"Dark mode logo\"\n}"
response = http.request(request)
puts response.read_body{}{
"type": "invalid_request",
"code": "<string>",
"message": "<string>",
"doc_url": "<string>",
"request_id": "<string>",
"causes": [
"<unknown>"
],
"details": {},
"retry_after_ms": 123,
"retryable": true
}{
"type": "invalid_request",
"code": "<string>",
"message": "<string>",
"doc_url": "<string>",
"request_id": "<string>",
"causes": [
"<unknown>"
],
"details": {},
"retry_after_ms": 123,
"retryable": true
}{
"type": "invalid_request",
"code": "<string>",
"message": "<string>",
"doc_url": "<string>",
"request_id": "<string>",
"causes": [
"<unknown>"
],
"details": {},
"retry_after_ms": 123,
"retryable": true
}{
"type": "invalid_request",
"code": "<string>",
"message": "<string>",
"doc_url": "<string>",
"request_id": "<string>",
"causes": [
"<unknown>"
],
"details": {},
"retry_after_ms": 123,
"retryable": true
}{
"type": "invalid_request",
"code": "<string>",
"message": "<string>",
"doc_url": "<string>",
"request_id": "<string>",
"causes": [
"<unknown>"
],
"details": {},
"retry_after_ms": 123,
"retryable": true
}{
"type": "invalid_request",
"code": "<string>",
"message": "<string>",
"doc_url": "<string>",
"request_id": "<string>",
"causes": [
"<unknown>"
],
"details": {},
"retry_after_ms": 123,
"retryable": true
}{
"type": "invalid_request",
"code": "<string>",
"message": "<string>",
"doc_url": "<string>",
"request_id": "<string>",
"causes": [
"<unknown>"
],
"details": {},
"retry_after_ms": 123,
"retryable": true
}{
"type": "invalid_request",
"code": "<string>",
"message": "<string>",
"doc_url": "<string>",
"request_id": "<string>",
"causes": [
"<unknown>"
],
"details": {},
"retry_after_ms": 123,
"retryable": true
}Authorizations
API key from Settings > Developer > REST API
Headers
Calendar-dated API version pin. New integrations should pin 2026-05-01 to opt into the newest response shapes. For back-compat the server also accepts requests with no header and resolves them to the current default (today: 2026-04-12); that default advances on each sunset date. Any unsupported value returns 400 unsupported_version.
2026-04-12, 2026-05-01 "2026-05-01"
Path Parameters
Prefixed bk_ wire ID (Crockford base32 body) — the canonical, recommended form. For back-compat, a bare UUID string is also accepted in both path parameters and JSON request bodies (older integrations that stored raw UUIDs keep working), as is the prefix over a UUID body (bk_00000000-0000-4000-8000-000000000000). All three are permanent, supported inputs; only the canonical form is ever emitted.
^bk_[0-9A-HJKMNP-TV-Za-hjkmnp-tv-z]{26}$"bk_01HT9WK8N3M2J4A5Z6P7Q8R9TV"
Body
Request body for POST /v1/brand-kits/{id}/images (ENG-2466).
The file_ ID returned as id by a prior POST /v1/uploads response. A bare UUID (the form older integrations stored) is also accepted for back-compat.
^file_[0-9A-HJKMNP-TV-Za-hjkmnp-tv-z]{26}$"file_01HT9WK8N3M2J4A5Z6P7Q8R9TV"
How the image is used. 'logo' gets special downstream treatment; 'reference' is a style hint visible to the design agent; 'asset' is includable in designs.
logo, reference, asset "logo"
Optional human-readable label.
"Dark mode logo"
Response
Successful Response
The response is of type Response Addbrandkitimage · object.