Imagens e objetos 3D
As fotos de cada produto e seu objeto 3D, com o texto alternativo e os três tamanhos.
O scope
Este endpoint exige o scope media:read, separado de catalog:read. A separação existe porque os endereços devolvidos SÃO o acesso ao arquivo: o CDN serve por rota não adivinhável, então quem tem o endereço tem a imagem. Uma chave que só lê a ficha não os recebe.
As mídias de um produto
Devolve as mídias atribuídas: a principal primeiro, depois por posição e o objeto 3D no fim. A ordem É um dado, mas desde que existe o objeto 3D o array pode trazer algo que não é uma imagem: escolha a imagem base por role main — ou filtrando o mime por image/ —, não pelo índice 0.
GET /v1/products/{id}/media
{
"product_id": "…",
"media": [
{
"id": "…",
"filename": "portada.jpg",
"mime": "image/jpeg",
"bytes": 482913,
"role": "main",
"position": 0,
"origin": "upload",
"synthetic_performer": false,
"alt": { "es": "Frasco blanco de crema, 50 ml", "en": "White 50 ml cream jar" },
"url": "https://media.bruxel.ai/orig/…/….jpg",
"medium_url": "https://media.bruxel.ai/deriv/…/…/medium.webp",
"thumb_url": "https://media.bruxel.ai/deriv/…/…/thumb.webp"
}
]
}url é o ORIGINAL: é o que você importa para a sua loja, que gera os próprios tamanhos. medium_url (1024 px) e thumb_url (320 px) são derivados em WebP para desenhar interfaces — não os use como imagem de produto. Se um derivado ainda não existir, recorra ao original.
Papel, posição e procedência
role é o papel REAL do vínculo: main (a imagem base), gallery (adicional) ou swatch (a amostra de cor de uma variante, desenhada a partir do hex da sua ficha; serve como color_swatch_link no Google) ou model_3d (o objeto 3D do produto). position é a posição explícita dentro da galeria, para não depender da ordem da lista. origin diz se a imagem foi enviada pelo cliente (upload) ou gerada pelo Estúdio (ai), e synthetic_performer é true só se ela traz uma pessoa sintética: é o que a Amazon e o Google exigem declarar desde 2026, e o original já traz o metadado XMP.
O objeto 3D do produto
O objeto 3D não tem endpoint próprio: sai deste mesmo GET como mais uma mídia, com role model_3d e mime model/gltf-binary. Sua url é o GLB — o que um visualizador web ou o Scene Viewer no Android abrem — e usdz_url é o mesmo modelo em USDZ, a única coisa que o Quick Look abre no iPhone: são dois formatos do mesmo objeto, não dois modelos. thumb_url e medium_url são a sua prévia em WebP, para pintar a miniatura sem baixar o modelo. usdz_url é null numa imagem ou num vídeo, e há no máximo um model_3d por produto. O modelo vem SEMPRE no fim do array, para que media[0] continue sendo uma imagem nas integrações que já existem.
{
"id": "…",
"filename": "objeto-3d-KOS-MUSE-50.glb",
"mime": "model/gltf-binary",
"bytes": 8134422,
"role": "model_3d",
"position": 3,
"origin": "ai",
"synthetic_performer": false,
"alt": {},
"url": "https://media.bruxel.ai/orig/…/….glb",
"medium_url": "https://media.bruxel.ai/deriv/…/…/medium.webp",
"thumb_url": "https://media.bruxel.ai/deriv/…/…/thumb.webp",
"usdz_url": "https://media.bruxel.ai/deriv/…/…/model.usdz"
}Enviar e vincular pela API
Com o scope media:write a integração coloca sua própria imagem no produto, nos mesmos três passos que o app usa. Primeiro POST /v1/media/presign com nome, tipo e tamanho: valida o tipo contra a lista permitida, reserva a cota e devolve um POST assinado do S3 (url e fields) que expira em expires_in segundos. Segundo, a integração envia esse formulário ao S3 com o arquivo no campo file; o servidor da Bruxel nunca vê os bytes. Terceiro, POST /v1/media/ASSET/complete confirma contra o S3: lê a assinatura binária e o peso real; um arquivo que não é do tipo declarado é descartado com 415. Pronto, POST /v1/products/ID/media o vincula com seu papel (main, gallery, swatch ou model_3d) e DELETE /v1/products/ID/media/ASSET o desvincula sem apagá-lo da biblioteca. Vincular e desvincular emitem media.updated.
POST /v1/media/presign
{ "filename": "frente.jpg", "mime": "image/jpeg", "bytes": 482113 }
→ { "asset_id": "…", "url": "https://s3…", "fields": { "key": "…", "Content-Type": "image/jpeg", … }, "expires_in": 900 }
POST {url} (multipart/form-data: cada campo de "fields", y el archivo en "file" al final)
POST /v1/media/{asset_id}/complete
→ { "asset_id": "…", "status": "ready", "mime": "image/jpeg", "bytes": 482113, "width": 1600, "height": 1600 }
POST /v1/products/{id}/media
{ "asset_id": "…", "role": "main" } → 204
DELETE /v1/products/{id}/media/{asset_id} → 204Mudanças incrementais
GET /v1/media/deltas (scope export) diz quais produtos mudaram a galeria desde o seu cursor: associar ou remover uma imagem, trocar a principal, mandar uma para a lixeira, escrever o texto alternativo ou salvar uma imagem gerada. Devolve o produto, não a imagem: para cada um você pede a galeria completa com GET /v1/products/{id}/media. Assim as exclusões são comunicadas (o produto reaparece e a galeria já não traz a imagem) e quarenta fotos geradas do mesmo produto viram uma única entrada. Mesma semântica do feed de conteúdo: guarde next sempre que a página trouxer linhas, drene enquanto vierem cheias, e um since que não parseia é 422.
GET /v1/media/deltas?since=<next anterior>&limit=100
{
"deltas": [
{ "product_id": "…", "sku": "PLY-M-ROJ", "updated_at": "2026-09-03T15:04:05.123456Z" }
],
"next": "2026-09-03T15:04:05.123456Z~…",
"limit": 100
}O webhook media.updated
Assine o evento media.updated (Configurações → Webhooks) e receba um aviso por produto sempre que a galeria dele mudar, com o payload enxuto de sempre: product_id e sku, nunca os endereços. Você busca a galeria com a sua chave. É at-least-once e coalescido: quinze mudanças do mesmo produto em um minuto são uma entrega.
{
"event": "media.updated",
"occurred_at": "2026-09-03T15:04:05.123456Z",
"data": { "product_id": "…", "sku": "PLY-M-ROJ" }
}Gerar imagens por API
Com o scope media:generate você abre uma sessão de fotos a partir do seu sistema: kind packshot transforma UMA foto real do produto na sua foto principal de estúdio (sem pessoas); kind product_photo combina um modelo sintético pronto (model_id) com 1 a 3 fotos reais e devolve até duas propostas. Responde 202 com o job e reserva os créditos; o preço é cobrado no sucesso e devolvido na falha. Faça poll em GET /v1/ai-media/jobs/{id} até status deixar de ser queued, running ou ingesting: esse GET é o que baixa, valida, carimba os metadados e salva as propostas. Com link_role elas ficam vinculadas ao produto ao terminar; sem ele, ficam na biblioteca.
POST /v1/products/{id}/ai-media (scope media:generate → 202)
{
"kind": "packshot",
"ref_asset_ids": ["<id de una foto REAL del producto>"],
"num_images": 1,
"aspect_ratio": "1:1",
"link_role": "main"
}
GET /v1/ai-media/jobs/{job_id} (poll: cuando termina, ESTE GET guarda las propuestas)
{
"id": "…", "type": "packshot", "status": "succeeded", "credits_charged": 5,
"results": [{ "asset_id": "…", "mime": "image/png", "url": "https://media.bruxel.ai/orig/…" }],
"error": "", "error_kind": ""
}GET /v1/ai-models lista os modelos sintéticos da organização; uma sessão com modelo precisa de um em stage ready. Os modelos são criados e aceitos no app (casting), não por API.
Roupa e expressão da modelo
Uma sessão kind product_photo aceita dois campos opcionais, e o padrão deles reproduz o comportamento anterior. subject decide como o produto aparece: product (a modelo segura ou usa) ou garment (a modelo veste; a fidelidade se concentra no tecido, na estampa, no corte e no comprimento). expression escolhe o rosto dela entre natural, smile, laugh, neutral, serious e playful. O packshot ignora os dois, e o job os devolve em subject e expression.
POST /v1/products/{id}/ai-media (scope media:generate → 202)
{
"kind": "product_photo",
"model_id": "<modelo en stage: ready>",
"ref_asset_ids": ["<foto REAL de la prenda>"],
"subject": "garment", // product (default) | garment
"expression": "smile", // natural (default) | smile | laugh | neutral | serious | playful
"num_images": 1
}O visual de cada foto
Mais três campos opcionais, no mesmo estilo: hairstyle (as_model, loose, ponytail, bun, braid, half_up, slicked_back), makeup (as_model, bare, natural, glam, editorial, bold_lip) e outfit (as_model, casual, business, evening, sporty, studio_neutral). O casting congela a identidade da modelo; isto é o que muda entre as tomadas. Com os três em as_model — o padrão — o prompt é idêntico ao de antes de existirem.
POST /v1/products/{id}/ai-media (scope media:generate → 202)
{
"kind": "product_photo",
"model_id": "<modelo en stage: ready>",
"ref_asset_ids": ["<foto REAL del producto>"],
"hairstyle": "ponytail", // as_model (default) | loose | ponytail | bun | braid | half_up | slicked_back
"makeup": "glam", // as_model (default) | bare | natural | glam | editorial | bold_lip
"outfit": "business", // as_model (default) | casual | business | evening | sporty | studio_neutral
"num_images": 1
}
# el job responde con lo que se pidió de verdad:
# "look": { "hairstyle": "ponytail", "makeup": "glam", "outfit": "business" }Vídeo com IA
O vídeo parte de uma imagem que já está salva no produto: essa imagem é o primeiro quadro, e dela sai o enquadramento. short é o clipe de cinco segundos com tarifa fixa; extended chega a 30 segundos com áudio e é cobrado por segundo × qualidade, então a duração e a qualidade são obrigatórias e precisam estar entre as que o seu ambiente oferece. Faça o poll no mesmo GET das fotos, e o job responde com tier: é o que permite conciliar a cobrança, porque os dois tipos de clipe chegam como video.
POST /v1/products/{id}/ai-video (scope media:generate → 202)
{
"ref_asset_id": "<una imagen YA guardada en el producto: es el primer cuadro>",
"tier": "extended", // short (default, 5 s) | extended (hasta 30 s con audio)
"duration_seconds": 15, // obligatorio en extended, y de la lista que ofrece tu ambiente
"resolution": "720p", // obligatorio en extended: 480p (mitad de precio) | 720p
"audio_mode": "ambient", // ambient (default) | none
"prompt": "gira despacio y termina en el logo"
}
GET /v1/ai-media/jobs/{job_id} (el MISMO poll de las fotos)
{
"id": "…", "type": "video", "tier": "extended", "status": "succeeded",
"credits_charged": 300,
"results": [{ "asset_id": "…", "mime": "video/mp4", "url": "https://media.bruxel.ai/orig/…" }]
}Com ai_model_id o clipe longo vai por reference-to-video: as cinco vistas do seu modelo entram como referência de identidade, então é a mesma pessoa em todas as tomadas e não só no primeiro quadro. Esse é o único caso em que o enquadramento é escolhido. As imagens extras do produto só existem com modelo, e o quadro final só sem modelo: com referências não há um primeiro nem um último para fixar.
POST /v1/products/{id}/ai-video
{
"ref_asset_id": "<primer cuadro>",
"tier": "extended", "duration_seconds": 15, "resolution": "720p",
"ai_model_id": "<modelo en stage: ready>", // la misma persona en TODOS los cortes
"aspect_ratio": "9:16", // con modelo SÍ se elige; sin él lo dicta la imagen
"extra_ref_ids": ["<otra imagen del producto>"] // solo con modelo, hasta 2
}
# sin modelo, y solo sin modelo, puedes fijar el ÚLTIMO cuadro:
# "end_asset_id": "<otra imagen del producto>"O Seedance rende com roteiros por tomadas, com marcas de tempo, e isso ninguém escreve à mão. Esta rota mostra o seu primeiro quadro ao Claude e devolve o clipe tomada por tomada mais o prompt pronto para gerar. Você pode editá-lo antes de enviar; o que viaja ao provedor é script.prompt, com as nossas âncoras de fidelidade e segurança coladas no final.
POST /v1/products/{id}/ai-video/script (3 créditos, se cobran AL ÉXITO)
{
"ref_asset_id": "<el mismo primer cuadro>",
"duration_seconds": 15, "resolution": "720p",
"platform": "reels", // tiktok | reels | shorts | amazon | web | other
"brief": "que se vea la textura" // opcional; NO nombres la red social aquí
}
# devuelve el guion toma por toma y el prompt listo:
# { "script": { "shots": [...], "prompt": "0-4s slow dolly-in…" },
# "credits_charged": 3, "balance": 8870 }
#
# y ese guion se manda tal cual al generar:
# POST …/ai-video { …, "script": <el objeto de arriba> }Sincronizar o catálogo inteiro
Não use este endpoint produto a produto. O CSV do catálogo já traz as colunas de imagem com os nomes que o Adobe Commerce espera: uma chamada para todo o catálogo em vez de uma por produto.
curl -s "https://bruxel.ai/api/public/v1/export/catalog.csv" \
-H "Authorization: Bearer bxk_…" -o catalogo.csv
# columnas: base_image, small_image, thumbnail, additional_imagesO que aparece e o que não
Só as imagens prontas e atribuídas ao produto. O que o cliente mandou para a lixeira não aparece, embora siga restaurável por 30 dias; um envio pela metade também não.