Manage product images and documents
A product page needs a picture that loads fast, and a buyer's purchasing team wants the datasheet. At the end of this guide, a product carries a main image and a datasheet, the image is served at the size each page asks for, and the datasheet opens through a link that expires. If your catalog came in through an import, it has no media yet; step 8 covers attaching media to a whole catalog.
This is for developers and integrators who fill a catalog or build a storefront. New to the platform? Prerequisites covers Cockpit, your tenant slug, installing an app and a test tenant. You need:
- a tenant (your company's isolated workspace on revenexx) with the Products app installed, which holds the catalog, and Storage, the object store behind every file;
- its tenant slug, the short name you send as
X-Revenexx-Tenant; - an API key with two scopes (see which scope covers which app):
storagefor the file calls andproducts.*for the catalog calls.products.*includes writes, which every step below needs. You create the key in Cockpit, the admin interface at app.revenexx.com; - a product to attach media to, and the product must have a family (a family decides which attributes its products carry; see Data model);
- one image and one PDF to try.
Every call goes through the API gateway:
export REVENEXX_TENANT="<TENANT_SLUG>"
export REVENEXX_API_KEY="rvxk_..."
How the pieces fit
Three records are involved, and keeping them apart is most of the work. The word "asset" names two of them, so this guide says file for the Storage object and catalog asset for the catalog row:
| Record | Lives in | What it is |
|---|---|---|
| The file | Storage | The bytes, a visibility (public or private) and an id like ast_…. Storage's own API calls it an asset |
| The catalog asset | the catalog | A row that says what the file is: its class (an asset family), a code, and the file's id |
| The reference | the product | The catalog asset's code, stored in an asset_collection attribute of the product |
There is no call that links a product to an asset. A product points at a catalog asset by writing its code into an attribute. The steps below set up that attribute and then create the three records in order.
The two APIs shape their answers differently. Storage wraps a file in data (read .data.id), the catalog returns a single row as it is (read .id) and a list as items next to page and filter (read .items[0]), and a signed link comes back unwrapped. The jq filters below follow that.
1. Create an asset family
An asset family is a class of media, such as product photos or datasheets. Catalog assets in an asset family share a code namespace. Create one for each class and keep the id each answer returns:
curl -s -X POST https://api.revenexx.com/v1/products/asset_families \
-H "X-Revenexx-Tenant: $REVENEXX_TENANT" \
-H "X-Revenexx-Api-Key: $REVENEXX_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "code": "packshots", "labels": { "en": "Packshots" } }'
curl -s -X POST https://api.revenexx.com/v1/products/asset_families \
-H "X-Revenexx-Tenant: $REVENEXX_TENANT" \
-H "X-Revenexx-Api-Key: $REVENEXX_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "code": "datasheets", "labels": { "en": "Datasheets" } }'
A duplicate code answers 409. In the steps below, <PACKSHOTS_FAMILY_ID> and <DATASHEETS_FAMILY_ID> are these two ids.
2. Add media attributes to the product's family
A product's attributes come from its family: the family lists them, and the product form and the completeness check read that list. Create two attributes of type asset_collection, one for images and one for datasheets, and add both to the product's family.
Find the product's id and its family's id by its SKU:
curl -s "https://api.revenexx.com/v1/products?sku=ACME-4711" \
-H "X-Revenexx-Tenant: $REVENEXX_TENANT" \
-H "X-Revenexx-Api-Key: $REVENEXX_API_KEY" \
| jq '.items[0] | { id, family_id }'
<PRODUCT_ID> and <PRODUCT_FAMILY_ID> below are these two values. Then create the attributes. config.asset_family names the asset family by its code; Cockpit shows and edits it as the attribute's asset family:
curl -s -X POST https://api.revenexx.com/v1/products/attributes \
-H "X-Revenexx-Tenant: $REVENEXX_TENANT" \
-H "X-Revenexx-Api-Key: $REVENEXX_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "code": "images", "type": "asset_collection", "config": { "asset_family": "packshots" }, "labels": { "en": "Images" } }'
curl -s -X POST https://api.revenexx.com/v1/products/attributes \
-H "X-Revenexx-Tenant: $REVENEXX_TENANT" \
-H "X-Revenexx-Api-Key: $REVENEXX_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "code": "datasheets", "type": "asset_collection", "config": { "asset_family": "datasheets" }, "labels": { "en": "Datasheets" } }'
Each answer is the stored attribute; keep its id (<IMAGES_ATTRIBUTE_ID> and <DATASHEETS_ATTRIBUTE_ID>). A 409 means an attribute with that code exists already. Find it with GET /v1/products/attributes?code=images and reuse it if it is an asset_collection. The API does not check the codes you write later against config.asset_family.
Add both attributes to the family:
curl -s -X POST https://api.revenexx.com/v1/products/family_attributes \
-H "X-Revenexx-Tenant: $REVENEXX_TENANT" \
-H "X-Revenexx-Api-Key: $REVENEXX_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "family_id": "<PRODUCT_FAMILY_ID>", "attribute_id": "<IMAGES_ATTRIBUTE_ID>" }'
curl -s -X POST https://api.revenexx.com/v1/products/family_attributes \
-H "X-Revenexx-Tenant: $REVENEXX_TENANT" \
-H "X-Revenexx-Api-Key: $REVENEXX_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "family_id": "<PRODUCT_FAMILY_ID>", "attribute_id": "<DATASHEETS_ATTRIBUTE_ID>" }'
A 409 here means the attribute is in the family already, which is the state you want. To make images the main image that grids and pickers show, set it on the family with PUT /v1/products/families/<PRODUCT_FAMILY_ID> and the body { "image_attribute": "images" }. Send only that field: the update leaves the family's other fields alone.
3. Upload the files
Upload is a Storage call, not a catalog one. Choose the visibility with care: a public file can be fetched by anyone who has its address, and a private one only through a signed link (step 7). Make the image public and the datasheet private:
curl -s -X POST https://api.revenexx.com/v1/storage/assets \
-H "X-Revenexx-Tenant: $REVENEXX_TENANT" \
-H "X-Revenexx-Api-Key: $REVENEXX_API_KEY" \
-F "file=@./ACME-4711-front.jpg" \
-F "visibility=public" \
-F "alt_text=Cordless drill, front"
curl -s -X POST https://api.revenexx.com/v1/storage/assets \
-H "X-Revenexx-Tenant: $REVENEXX_TENANT" \
-H "X-Revenexx-Api-Key: $REVENEXX_API_KEY" \
-F "file=@./ACME-4711-datasheet.pdf" \
-F "visibility=private" \
-F "display_name=Datasheet ACME-4711"
{ "data": { "id": "ast_01hxyz…", "status": "pending_processing", "visibility": "public", "url": "https://…" } }
Keep the id of each: <IMAGE_FILE_ID> and <DATASHEET_FILE_ID> below. The status starts as pending_processing and becomes available once processing finishes; poll GET /v1/storage/assets/<FILE_ID> until it does. A file that fails becomes processing_failed and can be retried with POST /v1/storage/assets/<FILE_ID>/reprocess. A private file has url: null, because it has no public address.
Two refusals to expect: 413 when the file is too large or the tenant is over its quota, and 415 when the type is not allowed (executables and scripts are blocked). Upload and manage files covers the folder, rename and delete calls, and who may see a file is set out in Access control and visibility.
4. Register the files as catalog assets
Create one catalog asset per file, pointing at the file's id. The code is what a product will store, and it only has to be unique inside its asset family:
curl -s -X POST https://api.revenexx.com/v1/products/assets \
-H "X-Revenexx-Tenant: $REVENEXX_TENANT" \
-H "X-Revenexx-Api-Key: $REVENEXX_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "asset_family_id": "<PACKSHOTS_FAMILY_ID>", "code": "ACME-4711_front", "source": "storage", "storage_asset_id": "<IMAGE_FILE_ID>" }'
curl -s -X POST https://api.revenexx.com/v1/products/assets \
-H "X-Revenexx-Tenant: $REVENEXX_TENANT" \
-H "X-Revenexx-Api-Key: $REVENEXX_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "asset_family_id": "<DATASHEETS_FAMILY_ID>", "code": "ACME-4711_datasheet", "source": "storage", "storage_asset_id": "<DATASHEET_FILE_ID>" }'
storage_asset_id is the stable identifier: it survives a rename or a move of the file. delivery_path is optional and only a cached copy of the address, so never join on it. A file on someone else's host is registered with source: "external" and an external_url instead, which the tenant must allow (Assets has the fields and the rules). An asset family may carry a naming_convention, but nothing reads it today.
5. Reference the catalog assets from a product
Write the codes into the attributes from step 2, images and datasheets. Their values go in the common bucket, because neither attribute is localizable or scopable; Products explains the four buckets of attribute_values:
PUT replaces attribute_values as a whole. A body that carries only images and datasheets deletes every other value the product holds. Read the product first and send the merged object back, as below.Read the product, merge the two codes into what it already holds, and send the result. The merge is in jq, so nothing else in attribute_values is touched:
BODY=$(curl -s "https://api.revenexx.com/v1/products/<PRODUCT_ID>" \
-H "X-Revenexx-Tenant: $REVENEXX_TENANT" \
-H "X-Revenexx-Api-Key: $REVENEXX_API_KEY" \
| jq '{ attribute_values: ((.attribute_values // {}) * { common: { images: ["ACME-4711_front"], datasheets: ["ACME-4711_datasheet"] } }) }')
curl -s -X PUT "https://api.revenexx.com/v1/products/<PRODUCT_ID>" \
-H "X-Revenexx-Tenant: $REVENEXX_TENANT" \
-H "X-Revenexx-Api-Key: $REVENEXX_API_KEY" \
-H "Content-Type: application/json" \
-d "$BODY"
The merged body keeps the product's other values next to the two new ones:
{ "attribute_values": { "common": { "images": ["ACME-4711_front"], "datasheets": ["ACME-4711_datasheet"] } } }
The update changes only the fields in the body and leaves the others alone, but attribute_values is one field: keys you leave out are gone. The jq merge sets images and datasheets to the lists shown, so if the product already holds codes there, put them in those lists too. A code that does not resolve is accepted without complaint, so a typo shows up later as a missing picture, not as an error.
6. Serve the image at the size the page needs
A storefront goes from the product to the picture in three reads. Cache the result rather than repeating it per page view; the catalog asset list is cached on the platform side too, and that cache is cleared on every write to a catalog asset.
# 1. the code is on the product: attribute_values.common.images[0]
# 2. the catalog asset for that code, to get the file id
curl -s "https://api.revenexx.com/v1/products/assets?asset_family_id=<PACKSHOTS_FAMILY_ID>&code=ACME-4711_front" \
-H "X-Revenexx-Tenant: $REVENEXX_TENANT" -H "X-Revenexx-Api-Key: $REVENEXX_API_KEY" \
| jq -r '.items[0].storage_asset_id'
# 3. the file, to get its url
curl -s "https://api.revenexx.com/v1/storage/assets/<IMAGE_FILE_ID>" \
-H "X-Revenexx-Tenant: $REVENEXX_TENANT" -H "X-Revenexx-Api-Key: $REVENEXX_API_KEY" \
| jq -r '.data.url'
Pass the asset family id along with the code, because a code is unique only inside its asset family. A query key that is not a column of the entity is dropped without an error and the list comes back unfiltered. The list answer echoes the filter it applied, so check that both keys are in it:
{
"items": [ { "id": "…", "asset_family_id": "…", "code": "ACME-4711_front", "source": "storage", "storage_asset_id": "ast_01hxyz…" } ],
"page": { "limit": 50, "offset": 0, "total": 1, "returned": 1, "hasMore": false },
"filter": { "asset_family_id": "…", "code": "ACME-4711_front" }
}
The url is the original. To get a derivative, add parameters to it. The image is resized and re-encoded when it is asked for, and the answer is marked cacheable for a year:
<url from the file>?w=400&fm=webp
w and h are pixels (up to 5000), fm is webp, avif, jpg, png or gif, q is the quality from 1 to 100, and fit and gravity control cropping. Upload the largest version you have and generate every smaller size this way; do not store thumbnails. Serve and transform images lists every parameter.
The url follows the file's folder and name, and names are lowercased, so renaming or moving a file changes it. Two live files in one folder never share a name: a second upload of front.jpg is stored as front_2.jpg. If you delete a file and upload another under its name, the old address serves the new bytes, and copies of the old ones may stay cached for a long time. Re-read the url after you move a file instead of keeping the old one.
7. Hand out the private datasheet
A private file opens through a signed link, and anyone who holds the link can open it until it expires. Mint one when a buyer asks for the file, not when the page renders:
curl -s -X POST "https://api.revenexx.com/v1/storage/assets/<DATASHEET_FILE_ID>/sign" \
-H "X-Revenexx-Tenant: $REVENEXX_TENANT" \
-H "X-Revenexx-Api-Key: $REVENEXX_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "ttl_seconds": 900 }'
{ "url": "https://…?expires=1750680000&signature=…", "expires_in": 900 }
ttl_seconds defaults to one hour and goes up to seven days. A link cannot be revoked before it expires, so keep it short and sign on demand. After it expires the link answers 403. Your API key does the signing, so mint the link on your server, never in the browser (Signed URLs and access).
8. Load a whole catalog
A catalog import does not carry media today. A BMECAT file (the XML standard for exchanging B2B product catalogs) lists a product's images and documents in MIME entries; the import reads them and creates nothing from them in the catalog: no catalog asset, no reference. Import the products first, then attach media with a script of your own over a list of SKU, file and kind: upload each file (step 3), create its catalog asset (step 4), and write the codes with the product update (step 5). Step 2 is done once per family, not per product.
For a large folder of files, an SFTP sync rule pulls them from your SFTP server into a Storage folder on a schedule. The other route is the Direct Media Import from ZIP Integration Studio template, a ready-made workflow you install in Integration Studio, which takes a ZIP you drop in by hand (it needs an API credential with the storage scope). Make the script safe to run again: creating a catalog asset with an existing code in the asset family answers 409, which means the code is taken. Before you treat that as done, check that the existing catalog asset points at the file you meant, because a code that points at the wrong file looks the same as a code that is already right. Import a BMECAT catalog covers getting the products in.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
The image url answers 404 | The file is not available: still pending_processing (also right after a reprocess), or processing_failed | Read the file's status; retry a failed one with reprocess (step 3) |
Upload answers 413 or 415 | Too large, over the tenant's quota, or a blocked type | Reduce the file, free quota, or use an allowed type |
| A product shows no image | The code on the product does not match a catalog asset's code; nothing checked it | Compare the codes (step 5) |
| The picture disappeared after a move | The url follows the folder and name | Read the url again (step 6) |
A signed link answers 403 | It expired | Mint a new one (step 7) |
Creating a catalog asset answers 409 | The code exists in that asset family | Check it points at the right file, then keep it or pick another code (step 8) |
| A product update dropped other attribute values | The update replaced attribute_values as a whole | Send the full object you want to keep (step 5) |
Where to go next
Do one product by hand, then script steps 3 to 5 for the catalog. Then read:
- Assets and Data model: the records and the attribute that holds the codes.
- Serve and transform images and Signed URLs and access: the Storage side in full.
- Import and export: the bulk routes for the products themselves.
- Import a BMECAT catalog: getting the products in first.