Add an image to a product
Product imagery is content-addressed. You never upload bytes through the API:
you ask for a presigned upload target, PUT the file straight to object
storage, finalize the asset so the platform confirms the object landed,
then attach the resulting ref to a product. The public catalog resolves
that ref to a fetchable image_url for every consumer.
This guide uses an fdk_ merchant credential. The tenant is carried by the
key (never in a path or body), and two scopes are involved: manage:own_media
for the upload/finalize steps and manage:own_catalog to attach the image.
The examples use the origin https://api.ruo.pro and the tenant slug
nova-peptide. Your credential decides the real tenant; the slug only
appears again when you read the resulting public catalog.
Before you start
- An
fdk_credential withmanage:own_mediaandmanage:own_catalog. - A product to attach to (see Add products to a catalog).
- The image file’s SHA-256, lowercase hex. On macOS:
shasum -a 256 photo.png.
Steps
Presign an upload
Declare the file you are about to upload. The platform records a pending, tenant-scoped asset row and hands back a presigned target.
curl -X POST https://api.ruo.pro/v1/merchant/media/uploads \
-H "Authorization: Bearer fdk_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"kind": "product_image",
"content_type": "image/png",
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"byte_size": 20481
}'A 201 returns the pending asset and the upload target:
{
"asset_id": "ast_9f2c…",
"asset_ref": "asset:nova-peptide/e3b0…b855.png",
"storage_key": "stores/nova-peptide/assets/e3b0…b855.png",
"cdn_url": "https://assets.ruo.pro/stores/nova-peptide/assets/e3b0…b855.png",
"status": "pending",
"replayed": false,
"upload": {
"url": "https://…/stores/nova-peptide/assets/e3b0…b855.png?X-Amz-Signature=…",
"method": "PUT",
"fields": {
"content-type": "image/png",
"x-amz-checksum-sha256": "47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU="
}
}
}Keep the asset_id (to finalize) and the asset_ref (to attach). Because the
ref is derived from the content hash, re-presigning the same bytes is
idempotent — replayed: true means you already have this asset.
Upload the bytes with a PUT
Send the file directly to upload.url using upload.method (PUT). Every
entry in upload.fields is a request header you must echo back — the
content-type and the base64 x-amz-checksum-sha256 the store verifies the
object against.
curl -X PUT "<upload.url from the previous step>" \
-H "content-type: image/png" \
-H "x-amz-checksum-sha256: 47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=" \
--data-binary @photo.pngNo byte flows through the commerce API — the upload goes straight to storage.
Finalize the asset
Tell the platform the object landed. This confirms the upload and flips the
asset from pending to ready; the optional dimensions are stored as metadata.
curl -X POST https://api.ruo.pro/v1/merchant/media/assets/ast_9f2c…/finalize \
-H "Authorization: Bearer fdk_your_key_here" \
-H "Content-Type: application/json" \
-d '{ "width": 1200, "height": 1200, "byte_size": 20481 }'A 200 returns the asset with status: "ready". A 409 means no object is at
the presigned key yet — upload before finalizing.
Attach the ref to a product
Set the product’s image to the asset_ref from step one. The ref must match
asset:<storeKey>/<sha256>.<ext> or the call is refused 400
(invalid_asset_ref).
curl -X PUT https://api.ruo.pro/v1/merchant/catalog/products/prod_123/image \
-H "Authorization: Bearer fdk_your_key_here" \
-H "Content-Type: application/json" \
-d '{ "image_asset_ref": "asset:nova-peptide/e3b0…b855.png" }'The response is { "product": { … } }. Send "image_asset_ref": null to clear
the image.
Consume the resolved image_url on the storefront
The public catalog carries both fields: image_asset_ref (the raw ref you set)
and image_url (that ref resolved against the platform’s asset origin). Clients
should prefer image_url — it is null only when no image is set or the origin
is unconfigured, never a broken URL.
import { createStorefrontClient } from "@dscodotco/sdk";
const store = createStorefrontClient({
apiUrl: "https://api.ruo.pro",
tenant: "nova-peptide",
storefrontToken: process.env.FLIGHTDECK_STOREFRONT_TOKEN!, // server-side only
});
const { products } = await store.products.list({ limit: 24 });
for (const p of products) {
// p.image_url is ready to drop into an <img src>; null means no image.
console.log(p.name, p.image_url);
}Error shape
Every failure is the nested envelope { "error": { "code", "message" } } — for
example invalid_asset_ref on a malformed ref, or 404 when the asset or
product does not belong to your tenant.
Related
- Media reference — the write-side pipeline, provider port, and audited asset rows.
- Add products to a catalog — create the products you attach images to.
- Import a catalog — bring an existing catalog in, images included.