Images et objets 3D
Les photos de chaque produit et son objet 3D, avec leur texte alternatif et les trois tailles.
Le scope
Cet endpoint exige le scope media:read, distinct de catalog:read. La séparation existe parce que les adresses renvoyées SONT l'accès au fichier : le CDN sert par chemin non devinable, donc qui a l'adresse a l'image. Une clé qui lit seulement la fiche ne les reçoit pas.
Les médias d'un produit
Renvoie les médias associés : le principal d'abord, puis par position, et l'objet 3D en dernier. L'ordre EST une donnée, mais depuis que l'objet 3D existe le tableau peut contenir autre chose qu'une image : choisissez l'image de base par role main — ou en filtrant le mime sur image/ —, pas par l'index 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 est l'ORIGINAL : c'est ce que vous importez dans votre boutique, qui génère ses propres tailles. medium_url (1024 px) et thumb_url (320 px) sont des dérivés WebP pour les interfaces — ne les utilisez pas comme image produit. Si un dérivé n'existe pas encore, repliez-vous sur l'original.
Rôle, position et provenance
role est le rôle RÉEL du lien : main (l'image de base), gallery (additionnelle) ou swatch (l'échantillon de couleur d'une variante, dessiné à partir du code hex de sa fiche ; il sert de color_swatch_link pour Google) ou model_3d (l'objet 3D du produit). position est la position explicite dans la galerie, pour ne pas dépendre de l'ordre de la liste. origin indique si l'image a été téléversée par le client (upload) ou générée par le Studio (ai), et synthetic_performer vaut true seulement si elle contient une personne synthétique : c'est ce qu'Amazon et Google exigent de déclarer depuis 2026, et l'original porte déjà la métadonnée XMP.
L'objet 3D du produit
L'objet 3D n'a pas d'endpoint à lui : il sort de ce même GET comme un média de plus, avec role model_3d et mime model/gltf-binary. Son url est le GLB — ce qu'ouvrent un visualiseur web ou Scene Viewer sur Android — et usdz_url est le même modèle en USDZ, la seule chose qu'ouvre Quick Look sur iPhone : deux formats du même objet, pas deux modèles. thumb_url et medium_url sont son aperçu en WebP, pour afficher la vignette sans télécharger le modèle. usdz_url vaut null sur une image ou une vidéo, et il y a au plus un model_3d par produit. Le modèle est TOUJOURS en dernier dans le tableau, pour que media[0] reste une image dans les intégrations qui existent déjà.
{
"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"
}Envoyer et lier via l'API
Avec le scope media:write, l'intégration dépose sa propre image sur le produit, en trois étapes identiques à celles de l'application. D'abord POST /v1/media/presign avec nom, type et taille : il valide le type contre la liste autorisée, réserve le quota et renvoie un POST signé S3 (url et fields) qui expire au bout d'expires_in secondes. Ensuite, l'intégration envoie ce formulaire à S3 avec le fichier dans le champ file ; le serveur de Bruxel ne voit jamais les octets. Enfin, POST /v1/media/ASSET/complete confirme auprès de S3 : il lit la signature binaire et le poids réel ; un fichier qui n'est pas du type déclaré est rejeté avec 415. Une fois prêt, POST /v1/products/ID/media le lie avec son rôle (main, gallery, swatch ou model_3d) et DELETE /v1/products/ID/media/ASSET le délie sans le supprimer de la bibliothèque. Lier et délier émettent 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} → 204Changements incrémentaux
GET /v1/media/deltas (scope export) indique quels produits ont modifié leur galerie depuis votre curseur : associer ou retirer une image, changer la principale, en mettre une à la corbeille, écrire son texte alternatif ou enregistrer une image générée. Il renvoie le produit, pas l'image : pour chacun, vous demandez la galerie complète avec GET /v1/products/{id}/media. C'est ainsi que les suppressions sont communiquées (le produit réapparaît et sa galerie ne contient plus l'image) et que quarante photos générées d'un même produit ne font qu'une entrée. Même sémantique que le flux de contenu : conservez next dès que la page contient des lignes, drainez tant que les pages arrivent pleines, et un since qui ne se parse pas est un 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
}Le webhook media.updated
Abonnez-vous à l'événement media.updated (Paramètres → Webhooks) et vous recevez un avis par produit chaque fois que sa galerie change, avec la charge utile légère habituelle : product_id et sku, jamais les adresses. Vous venez chercher la galerie avec votre clé. C'est at-least-once et coalescé : quinze changements du même produit en une minute font une seule livraison.
{
"event": "media.updated",
"occurred_at": "2026-09-03T15:04:05.123456Z",
"data": { "product_id": "…", "sku": "PLY-M-ROJ" }
}Générer des images par API
Avec le scope media:generate vous pouvez ouvrir une séance photo depuis votre système : kind packshot transforme UNE photo réelle du produit en sa photo principale de studio (sans personnes) ; kind product_photo combine un mannequin synthétique prêt (model_id) avec 1 à 3 photos réelles et renvoie jusqu'à deux propositions. La réponse est un 202 avec le job et réserve les crédits ; le prix est facturé au succès et remboursé en cas d'échec. Interrogez GET /v1/ai-media/jobs/{id} jusqu'à ce que status ne soit plus queued, running ou ingesting : c'est ce GET qui télécharge, valide, estampille les métadonnées et enregistre les propositions. Avec link_role elles sont liées au produit à la fin ; sans lui, elles restent dans la bibliothèque.
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 liste les mannequins synthétiques de l'organisation ; une séance avec mannequin en exige un en stage ready. Les mannequins se créent et s'acceptent dans l'application (casting), pas par API.
Vêtement et expression du mannequin
Une séance kind product_photo accepte deux champs optionnels, dont les valeurs par défaut reproduisent le comportement précédent. subject décide comment le produit apparaît : product (le mannequin le tient ou l'utilise) ou garment (le mannequin le porte ; la fidélité se concentre sur le tissu, l'imprimé, la coupe et la longueur). expression choisit son visage parmi natural, smile, laugh, neutral, serious et playful. Le packshot ignore les deux, et le job les renvoie dans subject et 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
}Le look de chaque photo
Trois champs facultatifs de plus, dans le même esprit : hairstyle (as_model, loose, ponytail, bun, braid, half_up, slicked_back), makeup (as_model, bare, natural, glam, editorial, bold_lip) et outfit (as_model, casual, business, evening, sporty, studio_neutral). Le casting fige l'identité du mannequin ; ceci est ce qui change d'une prise à l'autre. Avec les trois sur as_model — la valeur par défaut — le prompt est identique à celui d'avant.
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" }Vidéo IA
La vidéo part d'une image déjà enregistrée dans le produit : c'est elle la première image du clip, et c'est d'elle que vient le cadrage. short est le clip de cinq secondes à prix fixe ; extended va jusqu'à 30 secondes avec du son et se facture à la seconde × la qualité, donc la durée et la qualité sont obligatoires et doivent figurer parmi celles que propose votre environnement. Le polling se fait avec le même GET que les photos, et le job renvoie tier : c'est ce qui vous permet de rapprocher la facturation, car les deux types de clip arrivent comme 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/…" }]
}Avec ai_model_id, le clip long passe en reference-to-video : les cinq vues de votre mannequin partent comme référence d'identité, si bien que c'est la même personne dans tous les plans, et pas seulement sur la première image. C'est le seul cas où le cadrage se choisit. Les images supplémentaires du produit n'existent qu'avec un mannequin, et la dernière image seulement sans mannequin : avec des références, il n'y a ni première ni dernière image à fixer.
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 rend mieux avec des scénarios plan par plan horodatés, et personne n'écrit cela à la main. Cette route montre votre première image à Claude et renvoie le clip plan par plan, plus le prompt prêt à générer. Vous pouvez le modifier avant de l'envoyer ; ce qui part chez le fournisseur, c'est script.prompt, avec nos ancres de fidélité et de sécurité ajoutées à la fin.
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> }Synchroniser tout un catalogue
N'utilisez pas cet endpoint produit par produit. Le CSV du catalogue porte déjà les colonnes d'image avec les noms attendus par Adobe Commerce : un appel pour tout le catalogue au lieu d'un par produit.
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_imagesCe qui apparaît et ce qui n'apparaît pas
Uniquement les images prêtes et associées au produit. Ce que le client a mis à la corbeille n'apparaît pas, même si cela reste restaurable 30 jours ; un téléversement inachevé non plus.