Products in images
Access via client.assets.
A merchant can mark the products shown in an image in the media library: the sofa, the rug and the lamp in a room photo. Each mark is a hotspot, a point on the image and the product it shows. Use them for an image feed on the product page ("see it at home"), a shoppable campaign image, or a "products in this image" overlay.
Hotspots are read separately from the product. The product page renders without them, and you load them afterwards, so they never delay the first paint.
Images where a product appears
Pass every variant id of the product page, so an image tagged with any variant is included:
const product = await client.products.getByHandle('valen-sofa')
const group = product.variantGroupId ? await client.products.getVariantGroup(product.variantGroupId) : [product]
const variantIds = group.map((p) => p.productId)
const { data, pagination } = await client.assets.list({ productIds: variantIds, limit: 12 })
// → data: [{ id, url, path, name, width, height, hotspots: [{ id, x, y, productId, sku, label }] }]
Results are newest first. Load the next page with startingAfter: pagination.nextCursor while pagination.hasMore is true.
Hotspots on specific images
To check whether the product's own gallery images have hotspots, pass their asset ids:
const { data } = await client.assets.list({ ids: (product.assets ?? []).flatMap((a) => (a.assetId ? [a.assetId] : [])) })
An image passed by id is returned also when it has no hotspots (hotspots: []). Both parameters can be combined; at most 50 ids each.
Name and price
A hotspot carries the product's productId and sku, never its name or price: those differ per market and customer and change without the image changing. Fetch them in one call, and drop hotspots whose product does not come back (not sold in the market, or outside a signed-in customer's catalog):
const ids = [...new Set(data.flatMap((img) => img.hotspots.map((h) => h.productId)))]
const products = await client.products.getBatchByIds(ids)
const byId = new Map(products.map((p) => [p.productId, p]))
const images = data.map((img) => ({
...img,
hotspots: img.hotspots.filter((h) => byId.has(h.productId)),
}))
Show label instead of the product name when it is set.
Placing the dots
x and y run from 0 to 1 and are measured from the top-left corner of the original image, so they hold in every rendition:
<div class="relative inline-block">
<img :src="`${img.url}?width=1200&fm=webp`" :width="img.width" :height="img.height" class="block w-full h-auto" />
<button
v-for="h in img.hotspots"
:key="h.id"
class="absolute -translate-x-1/2 -translate-y-1/2"
:style="{ left: `${h.x * 100}%`, top: `${h.y * 100}%` }"
/>
</div>
When the image is cropped with object-fit: cover, map the point through the crop first: the original width and height are in the response for that.
Caching and visibility
- Only images that are public, published and finished processing are returned.
- The response does not depend on the customer and is cached at the edge for up to ten minutes. A moved hotspot can therefore take a few minutes to show.
- A hotspot whose product has been deleted is left out of the response.
The REST endpoint is GET /v1/storefront/{marketId}/assets, see the Storefront API.