Imágenes y objetos 3D
Las fotos de cada producto y su objeto 3D, con el texto alternativo y las tres medidas.
El scope
Este endpoint pide el scope media:read, aparte de catalog:read. Va separado porque las direcciones que devuelve SON el acceso al archivo: el CDN sirve por ruta no adivinable, así que quien tiene la dirección tiene la imagen. Una llave que solo lee la ficha no la recibe.
Los medios de un producto
Devuelve los medios asignados: la principal primero, luego por posición y el objeto 3D al final. El orden ES un dato, pero desde que existe el objeto 3D el array puede traer algo que no es una imagen: elige la imagen base por role main —o filtrando el mime por image/—, no por el í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 es el ORIGINAL: es lo que se importa a tu tienda, que genera sus propios tamaños. medium_url (1024 px) y thumb_url (320 px) son derivados en WebP para pintar interfaces — no los uses como imagen de producto. Si un derivado aún no existe, cae al original.
Rol, posición y procedencia
role es el rol REAL del vínculo: main (la imagen base), gallery (adicional) o swatch (la muestra de color de una variante, dibujada del hex de su ficha; sirve como color_swatch_link en Google) o model_3d (el objeto 3D del producto). position es la posición explícita dentro de la galería, para no depender del orden de la lista. origin dice si la imagen la subió el cliente (upload) o la generó el Estudio (ai), y synthetic_performer es true solo si lleva una persona sintética: es lo que Amazon y Google piden declarar desde 2026, y el original ya trae el metadato XMP estampado.
El objeto 3D del producto
El objeto 3D no tiene endpoint propio: sale de este mismo GET como un medio más, con role model_3d y mime model/gltf-binary. Su url es el GLB —lo que abre un visor web o Scene Viewer en Android— y usdz_url es el mismo modelo en USDZ, que es lo único que abre Quick Look en iPhone: son dos formatos del mismo objeto, no dos modelos. thumb_url y medium_url son su vista previa en WebP, para pintar la miniatura sin bajar el modelo. usdz_url es null en una imagen o un video, y hay como máximo un model_3d por producto. El modelo va SIEMPRE al final del array, para que media[0] siga siendo una imagen en las integraciones que ya existen.
{
"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"
}Cambios incrementales
GET /v1/media/deltas (scope export) dice qué productos movieron su galería desde tu cursor: asociar o quitar una imagen, cambiar la principal, mandar una a la papelera, escribir su texto alternativo o guardar una imagen generada. Devuelve el producto, no la imagen: por cada uno pides la galería completa con GET /v1/products/{id}/media. Así los borrados sí se comunican (el producto reaparece y su galería ya no trae la imagen) y cuarenta fotos generadas del mismo producto son una sola entrada. Misma semántica que el feed de contenido: guarda next siempre que la página traiga filas, drena mientras vengan llenas, y un since que no parsea es 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
}El webhook media.updated
Suscríbete al evento media.updated (Ajustes → Webhooks) y recibes un aviso por producto cada vez que su galería cambia, con el payload delgado de siempre: product_id y sku, nunca las direcciones. Vienes por la galería con tu llave. Es at-least-once y coalesced: quince cambios del mismo producto en un minuto son una entrega.
{
"event": "media.updated",
"occurred_at": "2026-09-03T15:04:05.123456Z",
"data": { "product_id": "…", "sku": "PLY-M-ROJ" }
}Generar imágenes por API
Con el scope media:generate puedes abrir una sesión de fotos desde tu sistema: kind packshot convierte UNA foto real del producto en su foto principal de estudio (sin personas); kind product_photo combina un modelo sintético listo (model_id) con 1 a 3 fotos reales y devuelve hasta dos propuestas. Responde 202 con el job y reserva los créditos; el precio se cobra al éxito y se devuelve al fallo. Haz poll a GET /v1/ai-media/jobs/{id} hasta que status deje de ser queued, running o ingesting: ese GET es el que descarga, valida, estampa los metadatos y guarda las propuestas. Con link_role quedan vinculadas al producto al terminar; sin él, en la 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 los modelos sintéticos de la organización; una sesión con modelo necesita uno en stage ready. Los modelos se crean y aceptan desde la app (casting), no por API.
Ropa y expresión de la modelo
En una sesión kind product_photo hay dos campos opcionales, y su default reproduce el comportamiento anterior. subject decide cómo aparece el producto: product (la modelo lo sostiene o lo usa) o garment (la modelo se pone la prenda; la fidelidad se cuida en tela, estampado, corte y largo). expression elige su cara entre natural, smile, laugh, neutral, serious y playful. El packshot ignora los dos, y el job los devuelve en subject y 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
}El look de cada foto
Tres campos opcionales más, del mismo estilo: hairstyle (as_model, loose, ponytail, bun, braid, half_up, slicked_back), makeup (as_model, bare, natural, glam, editorial, bold_lip) y outfit (as_model, casual, business, evening, sporty, studio_neutral). El casting congela la identidad del modelo; esto es lo que cambia entre tomas. Con los tres en as_model —el default— el prompt es idéntico al de antes de que existieran.
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" }Video con IA
El video parte de una imagen que ya está guardada en el producto: esa imagen es el primer cuadro, y de ahí sale el encuadre. «short» es el clip de cinco segundos con tarifa plana; «extended» llega a 30 segundos con audio y se cobra por segundo × calidad, así que la duración y la calidad son obligatorias y tienen que estar entre las que ofrece tu ambiente. Se poléa con el mismo GET de las fotos, y el job responde con «tier»: es lo que te deja conciliar el cobro, porque los dos tipos de clip llegan 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/…" }]
}Con «ai_model_id» el clip largo va por reference-to-video: las cinco vistas de tu modelo entran como referencia de identidad, así que es la misma persona en todos los cortes y no solo en el primer cuadro. Ese es el único caso en el que el encuadre se elige. Las imágenes extra del producto solo existen con modelo, y el último cuadro solo sin modelo: con referencias no hay un primero ni un último que fijar.
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>"Seedance rinde con guiones por tomas con marcas de tiempo, y eso nadie lo escribe a mano. Esta ruta le enseña tu primer cuadro a Claude y devuelve el clip toma por toma más el prompt listo para generar. Puedes editarlo antes de mandarlo; lo que viaja al proveedor es «script.prompt», con nuestras anclas de fidelidad y seguridad pegadas al 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 el catálogo completo
No uses este endpoint producto por producto. El CSV del catálogo ya trae las columnas de imagen con los nombres que Adobe Commerce espera: una llamada para todo el catálogo, en vez de una por producto.
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_imagesQué aparece y qué no
Solo las imágenes listas y asignadas al producto. Lo que el cliente mandó a la papelera no aparece, aunque siga siendo restaurable durante 30 días; una subida a medias tampoco.