bruxel.ai

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.

Jusqu'en septembre 2026 le rôle se déduisait de la position (la première était main et les autres gallery), donc swatch ne sortait jamais par ici, et model_3d est encore plus récent. Si votre intégration ne connaissait que ces deux valeurs, préparez-la : face à un rôle inconnu, regardez le mime avant de publier quoi que ce soit — un model/gltf-binary accroché dans une galerie d'images casse la boutique.

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"
}
Le GLB ne voyage pas dans les colonnes d'image du CSV et ne le fera jamais : base_image et additional_images attendent des images, et un modèle à cet endroit casserait l'import de la boutique. Cet endpoint est le seul chemin public vers l'objet 3D. Le générer se fait aujourd'hui depuis l'application ; par l'API il est en lecture seule. Le fichier porte en lui la déclaration qu'il a été généré par IA, comme le XMP des photos.

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}  → 204
Le quota de base de la bibliothèque est celui de l'organisation (500 Mo aujourd'hui) : presign répond 413 quand ça ne rentre pas, et l'espace s'agrandit depuis Crédits. L'envoi depuis une URL distante n'est pas dans cette phase : c'est celui qui a le fichier qui l'envoie.

Changements 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
}
Les changements apparaissent après quelques minutes de tassement (le flux retient le bord chaud pour ne jamais perdre les écritures de transactions longues). Le chemin temps réel est le webhook media.updated ; le flux est la récupération.

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
}
En mode garment, UNE seule photo réelle du vêtement suffit, et peu importe si quelqu'un d'autre le porte sur cette photo : seul le vêtement est repris. Si votre intégration en envoie plusieurs alors que l'essayage dédié est actif, la réponse est 422 sans réserver de crédits.

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" }
Le visage, la longueur, la couleur et la texture des cheveux et la morphologie ne changent jamais : ils appartiennent au mannequin, pas à la photo. outfit est ignoré avec subject garment, car le vêtement de votre produit EST la tenue, et la génération vidéo n'accepte aucun des trois (elle anime une image qui les a déjà). Le job renvoie look avec uniquement ce qui a été demandé.
Les références doivent être des photos RÉELLES de ce produit : une image générée, étrangère ou à la corbeille répond 422 sans rien réserver. L'espace de la bibliothèque est validé avant de facturer (413, sans débit), et le plafond de dépense et le cap quotidien d'IA s'appliquent comme depuis l'application. Les images générées sortent étiquetées (origin ai et, avec mannequin, synthetic_performer true) : c'est vous qui décidez de les publier.

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/…" }]
}
Un clip de 30 secondes en 720p fait 600 crédits. Testez l'idée en 480p, qui coûte moitié prix, et ne générez en 720p que ce que vous allez publier. Le plafond est de six clips longs par jour et par organisation, et il compte les TENTATIVES : si l'une échoue, nous vous remboursons les crédits, mais cette tentative est déjà consommée.

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> }
Elle coûte 3 crédits, facturés au succès : si le modèle ne renvoie pas de scénario utilisable, la réponse est un 503 et rien ne vous a été facturé. Demandez-le avec la durée et le mode déjà choisis, car changer la durée ensuite ne le réécrit pas. Et ne nommez pas le réseau social dans brief : le filtre qui empêche de demander des personnes réelles réagit à tiktok ou instagram et refuse la demande sans facturer ; c'est à cela que sert platform.

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_images
Le CSV relève du scope export, pas de media:read : c'est le catalogue complet, la même classification de données.

Ce 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.

Si le stockage d'images n'est pas configuré sur le compte, la réponse est une liste vide avec 200 — jamais des adresses qui donneraient 404 dans votre catalogue.
Images et objets 3D — Bruxel