bruxel.ai

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.

Hasta septiembre de 2026 el rol se derivaba de la posición (la primera era main y las demás gallery), así que swatch nunca salía por aquí, y model_3d es todavía más nuevo. Si tu integración asumía solo dos valores, prepárala: ante un rol que no conozcas, mira el mime antes de publicar nada — un model/gltf-binary colgado en una galería de imágenes rompe la tienda.

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"
}
El GLB no viaja en las columnas de imagen del CSV y nunca lo hará: base_image y additional_images esperan imágenes, y un modelo ahí rompería el import de la tienda. Este endpoint es el único camino público al objeto 3D. Generarlo se hace hoy desde la aplicación; por API solo se lee. El archivo lleva dentro la declaración de que se generó con IA, igual que el XMP de las fotos.

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
}
Los cambios aparecen con unos minutos de asentamiento (el feed retiene el borde caliente para no perder escrituras de transacciones largas). El camino en tiempo real es el webhook media.updated; el feed es la recuperación.

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
}
En modo garment basta UNA foto real de la prenda, y no importa si en esa foto la trae puesta otra persona: solo se toma la prenda. Si tu integración manda más de una cuando el try-on dedicado está activo, la respuesta es 422 sin reservar créditos.

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" }
El rostro, el largo, el color y la textura del cabello y la complexión no cambian nunca: son del modelo, no de la foto. outfit se ignora con subject garment, porque ahí la prenda de tu producto es el vestuario, y la generación de video no acepta ninguno de los tres (anima una imagen que ya los tiene). El job devuelve look con solo lo que se pidió de verdad.
Las referencias deben ser fotos REALES de ese producto: una generada, ajena o en la papelera responde 422 sin reservar nada. El espacio de la biblioteca se valida antes de cobrar (413 sin cobro), y el tope de gasto y el cap diario de IA aplican igual que desde la app. Las imágenes generadas salen etiquetadas (origin ai y, con modelo, synthetic_performer true): decide tú si las publicas.

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/…" }]
}
Un clip de 30 segundos en 720p son 600 créditos. Prueba la idea en 480p, que cuesta la mitad, y genera en 720p solo lo que vas a publicar. El tope es de seis clips extendidos al día por organización y cuenta INTENTOS: si uno falla te devolvemos los créditos, pero ese intento ya se usó.

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> }
Cobra 3 créditos y se cobran al éxito: si el modelo no devuelve un guion usable la respuesta es 503 y no se te cobró. Pídelo con la duración y el modo ya elegidos, porque cambiar después la duración no lo reescribe. Y no nombres la red social en el «brief»: el filtro de personas reales reacciona a «tiktok» o «instagram» y rechaza la petición sin cobrar; para eso está «platform».

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_images
El CSV va con el scope export, no con media:read: es el catálogo completo, la misma clasificación de dato.

Qué 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.

Si el almacenamiento de imágenes no está configurado en la cuenta, la respuesta es una lista vacía con 200 — nunca direcciones que darían 404 en tu catálogo.
Imágenes y objetos 3D — Bruxel