Bruxel Public API · v1
Integrate Bruxel into your stack
A REST API to generate SEO- and GEO-optimized product descriptions straight from your backend, PIM or e-commerce. Push your catalog, generate descriptions and export — without touching the UI.
Getting started
Create an API key, learn the base URL and make your first authenticated call.
Go to Quickstart →SecurityAuthentication
Per-organization bxk_ keys, scope-based permissions and one-time delivery.
See authentication →OpenAPIAPI reference
Explore the endpoints with their schemas, parameters and example responses.
See the reference →CodeExamples
End-to-end flows in curl, Python and JavaScript, ready to copy.
See examples →Quickstart
From zero to a generated description. Replace bxk_your_key with the one you created in the console.
# 1) Verifica que tu llave funciona
curl -s https://bruxel.ai/api/public/v1/me \
-H "Authorization: Bearer bxk_tu_llave"
# 2) Empuja productos a tu catálogo (upsert por SKU)
curl -s -X POST https://bruxel.ai/api/public/v1/products \
-H "Authorization: Bearer bxk_tu_llave" \
-H "Content-Type: application/json" \
-d '{"products":[{"sku":"MRN-CREW-01","name":"Suéter de lana merino"}]}'
# 3) Genera la descripción (síncrono — cobra créditos)
curl -s -X POST https://bruxel.ai/api/public/v1/generations \
-H "Authorization: Bearer bxk_tu_llave" \
-H "Content-Type: application/json" \
-d '{"product_id":"prod_8f2","languages":["es"]}'
# 4) Exporta tu catálogo timbrado a CSV
curl -s "https://bruxel.ai/api/public/v1/export/catalog.csv?language=es" \
-H "Authorization: Bearer bxk_tu_llave"import httpx
BASE = "https://bruxel.ai/api/public/v1"
headers = {"Authorization": "Bearer bxk_tu_llave"}
# Genera una descripción y léela
r = httpx.post(
f"{BASE}/generations",
headers=headers,
json={"product_id": "prod_8f2", "languages": ["es"]},
)
r.raise_for_status()
print(r.json()["description"])const BASE = "https://bruxel.ai/api/public/v1";
const headers = {
Authorization: "Bearer bxk_tu_llave",
"Content-Type": "application/json",
};
// Genera una descripción y léela
const res = await fetch(`${BASE}/generations`, {
method: "POST",
headers,
body: JSON.stringify({ product_id: "prod_8f2", languages: ["es"] }),
});
const data = await res.json();
console.log(data.description);Latest changes
What's new in the public API.
Video: ai_model_id and extra_ref_ids are retired
POST /v1/products/{id}/ai-video and its script no longer accept ai_model_id or extra_ref_ids (“the model in the cuts”): the provider rejects the model's face. Sending them returns 422 (X-Bruxel-Reason: params) without reserving credits. The shot-by-shot video, the one that accepts the model, is only available from the app for now.
Reports, now through the API too
With the new reports:read scope your BI reads the same reports as the section: GET /v1/reports gives the catalog with how many rows each one has in the period, GET /v1/reports/:id/rows a page as JSON (up to the first 10,000 rows) and GET /v1/reports/:id.csv the full file. The period goes with from, to and tz; brand and status filter where the report uses them.
Flows notify you: your turn, stopped, finished
Three new webhook events: flow.step.ready (products that reached a step), flow.run.blocked (journeys stopped by an error) and flow.run.completed (journeys that finished). They arrive grouped, one per step or per flow, with a count and a sample of up to 20; the full list comes from GET /v1/flows/:slug/steps/:key/products and GET /v1/flows/:slug/runs, with a cursor. Activating a flow wakes you once per step that has products.
Your systems' steps: notifications and calls
A flow step can also be marked done when Bruxel delivers a notification to a system (a webhook answered with a 2xx) or when that system's key reads or writes the product record (checking its journey or history doesn't count). In GET /v1/products/:id/journey, `cause` can be `delivery` or `api_call`, and `event_system` and `event_type` tell the system and what it was: the notification or the operation (`read` or `write`). An event without `system` takes the connected system the key is linked to.
Report what happens outside, and read a product's journey
POST /v1/events (scope `events:write`) takes up to 100 events per call: which system, what happened, whether it worked and when. A flow step can be marked done by that event. GET /v1/products/:id/journey (scope `catalog:read`) returns where the product is in each flow and what systems reported about it.
Derived attributes: `derived` and the `derived` 422
`GET /v1/attributes` returns `derived`: `true` when the system sets the value from another field (another attribute, the category or the brand) according to a customer rule. On a product where the derivation applies, `PUT /v1/products/{id}/attributes/{code}` answers 422 with `X-Bruxel-Reason: derived` and writes nothing: you change the source and the derived value is recalculated in the same write.
Shared lists: `value_list` on each attribute
`GET /v1/attributes` returns `value_list`: the code of the shared list the attribute's options come from, or `null`. A linked attribute's options are read and assigned as always; what changes is that they're edited in the list, from the app.
Kits: a product's composition, by SKU
`GET` and `PUT /v1/products/{product_id}/components` (scopes `catalog:read`/`catalog:write`) read and write a kit's composition: the products it is made of, with their quantity and in order, plus `part_of` (how many kits the product is in). An unknown SKU is a 422 that names it; a kit has a single level. The upsert and the import also build kits from `associated_skus` or `kit_components` (`sku=quantity`), and channels emit them with the `kit.components` column.
A value per channel: `is_channel_scoped` and `channel`
`GET /v1/attributes` says which attributes have their own value per channel (`is_channel_scoped`). For those, `PUT /v1/products/{id}/attributes/{code}` accepts `channel` (the channel's `slug`) to write THAT channel's value without touching the master, and `GET …/attributes?channel=` reads it with a fallback to the master: the same value the channel's CSV carries. An attribute that is not per channel or a channel that does not exist is a 422.
Each option's value in each channel: `map` and `strict`
`GET /v1/channels` now returns, per column, `map` (the value THAT file writes for each option of a list: “Rojo” → “Red” for Amazon, “001” for the ERP) and `strict` (an option with no value for the channel comes out empty and the product counts as incomplete in `readiness`). The key is the text the cell would have without the map, in the channel's language. With `null`/`false` the cell comes out as always.
Is this product complete for the channel? — per product
`GET /v1/products/{id}/channels/readiness` (scope `channels:read`) says, for each configured channel, whether the product would come out complete in its CSV: `published` (a sheet is published in the channel's language), `ready`, and `missing` with each missing column and its source (`output.{field}`, `attribute.{code}`, `media.*`). It is what a middleware checks before creating the product in the store. Same resolver as the file; no charge.
Upload and link images through the API (media:write)
New `media:write` scope and four routes: `POST /v1/media/presign` reserves quota and returns the S3 signed POST; `POST /v1/media/{asset_id}/complete` confirms by binary signature and real size; `POST /v1/products/{id}/media` links the asset with its role and `DELETE /v1/products/{id}/media/{asset_id}` unlinks it. It is the same DAM door the app uses: the server never sees the bytes, quota is reserved before signing and a file that lies about its type is discarded.
Write an attribute value in a language
`PUT /v1/products/{id}/attributes/{code}` accepts `language`: it writes the value IN THAT LANGUAGE for a localizable attribute; without it, the value goes to the global slot as always. `GET /v1/attributes` says up front which attributes accept it (`is_localizable`). An unsupported language, or a non-localizable attribute, is a 422 naming the code — and `GET …/attributes?language=xx` is now 422 too, instead of a 200 with the global values.
Attribute groups in GET /v1/attributes
Each attribute carries `group`: the code of the group (the section of the sheet) it belongs to, or `null`. It is the code and not an id because this API speaks in codes: an integrator building one team's row filters by it without a second trip. Groups are defined in the app or with the `group` column of the definitions CSV.
Filter the catalog by an attribute value
`GET /v1/products` accepts `attr=code:value` (repeatable, up to five, ANDed): the option text for a list, Yes/No for a boolean, exact text, number or date; an empty `code:` keeps the products WITHOUT a value. It is the same question a team's queue asks in the app — for instance `attr=ready_to_master_data:Sí&attr=ready_to_sap:` — and whatever cannot be applied is a 422 with the reason, never an empty list.
A change feed for product records, and a `product.updated` that says what changed
`GET /v1/products/deltas` walks the history of all your product records with a resumable cursor: for each version it tells you the product, the reason and WHAT changed (fields, attribute codes, categories, family), never the values; `attribute=` and `reason=` let you listen to a single flag or reason. And the `product.updated` webhook now fires on any real change to the record — including writing an attribute value, which never notified before — with `version`, `reason` and `changed`. Treat it as a wake-up call: re-read the record or the feed, don't rely on `changed` alone.
Product history records catalog changes
Each product's history now records what happens to it when the catalog changes: deleting an attribute, an option, a category or a family, or renaming an option or a code, leaves a version with `reason` `structure`. And `changes` can carry two new keys: `replaced` (the code now belongs to a different attribute) and `recoded` (the same attribute under another code).
Units of measure on attributes and channels
A measure attribute now states which measure it is and which units it takes: `GET /v1/attributes` returns `dimension` and `accepted_units`, and the value `PUT` accepts “1.7 fl oz” on an attribute in ml (it converts on its own). And each channel column can ask for a different unit, different decimals, or the bare number: `GET /v1/channels` exposes it in `columns[]` so you know how to read the cell.
Related products via API
You can now read and write which other products each one is offered with — related, cross-sell and upsell — by SKU, and download them in your channel CSV with the column names your store expects.
Publish, approve and unpublish via API
Three new endpoints with their own `content:manage` scope: publish a specific version, approve the editorial workflow candidate, and stop serving a listing. The scope is separate from `catalog:write` on purpose — writing the listing and deciding what your store serves are different things.
Product record history: what changed in a product, and when
GET /v1/products/{product_id}/revisions returns a record's versions, newest first, with what changed in each one: fields, attributes (by code and language), categories and family. From imported data it reports the names of the columns that changed, never their contents. For the integrator who syncs and wants to know what to look at without downloading the whole record again.
Channels: which columns are required and how many products are ready
GET /v1/channels now returns `columns[].required` and `skip_incomplete` (whether the channel drops incomplete rows from its CSV and feed). And GET /v1/channels/{slug}/readiness tells you how many products would come out complete in the channel, how many have no published content, and which required columns are missing most — with the same criterion the file uses.
The product's 3D object, in the same media endpoint
GET /v1/products/{id}/media can now return a fourth role: model_3d. Its url is the GLB and the new usdz_url field is the same model in USDZ, the only thing Quick Look opens on iPhone; on an image or a video it is null. Additive: if you already integrate, your contract does not change — but pick the base image by role, not by index 0.
The attribute's code in your store
GET /v1/attributes returns external_code: the attribute_code Adobe Commerce uses for each attribute, as entered in Bruxel. When present, /v1/export/catalog.csv names the column that way instead of code. null = no external code; nothing changes.
Video by API: clips up to 30 seconds, with a script
POST /v1/products/{id}/ai-video starts a clip from an image already saved to the product: five seconds at a flat rate, or up to 30 with audio charged per second × quality. With ai_model_id the same model appears in every cut. POST /v1/products/{id}/ai-video/script writes the shot-by-shot script with AI (3 credits, charged on success).
The look of each photo: hairstyle, makeup and wardrobe
POST /v1/products/{id}/ai-media accepts hairstyle, makeup and outfit to direct how the model looks in THAT photo, without touching her identity. All three default to as_model, and with that value nothing changes from before; the job returns them in look.
Clothing and expression in image generation
POST /v1/products/{id}/ai-media accepts subject (product | garment) so the model wears the garment instead of holding it, and expression (natural, smile, laugh, neutral, serious, playful) to pick her face. Both are optional and their defaults reproduce the previous behavior; the job returns them in subject and expression.
Images: change feed, webhook and generation by API
GET /v1/media/deltas tells you which products changed their gallery (deletions included); the media.updated event notifies in real time; GET /v1/products/{id}/media carries the real role (swatch shows up), the position and the provenance (origin, synthetic_performer); and with the new media:generate scope you open AI photo sessions from your system (POST /v1/products/{id}/ai-media, GET /v1/ai-media/jobs/{id}, GET /v1/ai-models).
Per-organization API keys
New bxk_ keys with scope-based permissions (catalog:read, generate, export…), one-time delivery and instant revocation from the console.
Async batches with idempotency
The POST /v1/batches endpoint for high-volume generation: credit reservation, a required Idempotency-Key and charge-per-result on read.
Catalog export by language
GET /v1/export/catalog.csv?language=es returns your generated catalog in the requested language, in Adobe Commerce format.