Skip to main content
The CmsService class (accessed via client.cms) reads a store’s editorial content — blog posts and shoppable lookbooks — and records views against the shopper’s session, so a sale can later be attributed to the content that led to it.

Blog Posts

listPosts

Retrieve a paginated list of published blog posts, newest first. This is ideal for building a “News” or “Magazine” section on your storefront. Only published posts are returned — there is no status filter. Filter by category with the category slug (category: 'buying-guides').
Response
Post Metadata: The listing response includes light metadata like excerpt, featured_image, video (an optional cover video; featured_image stays its poster and fallback), slug, and product_count (how many products a shoppable post links to). For the full content, use getPost. ?fields= on the listing accepts the summary fields above. The article body (content) and its embedded products and lookbooks are fetched per post and belong to getPost; asking for one of them here returns 400 naming the field rather than omitting it silently. Each post carries two timestamps. created_at is when it was first written; updated_at is when it was last edited, by anyone and in any way — a merchant revising the text, or an automated change such as a regenerated cover image. An article page that shows a revision date reads as current when the text has been kept up to date, so rendering “Updated ” where updated_at is later than the publication date is usually worth doing. updated_at is always set: on a post that has never been revised it matches created_at.

getPost

Retrieve the full content of a specific published blog post by its slug, including any linked products for shoppable posts (each entry in the products array carries the product’s name, sku, price, images, and display_type). Returns 404 if no published post matches the slug. An embedded product also carries variant_id — pass that to client.cartWishlist.addToCart, which is keyed by variant rather than product, so a shoppable post can add to a basket without a second call. embed_id identifies the placement rather than the product. A productEmbed block in content is matched to its entry on product_id, which equals the block’s productId; block_id is the id of the content block an embed is anchored to and is null for embeds placed in the post editor. An embed whose product is no longer available is omitted, so a post can return fewer products than were authored. content is an Editor.js document: { time, version, blocks }, or null for a post with no body. Each block is { id, type, data }, and the fields in data depend on type. Headings are typed header. In a list, items holds plain strings in older posts and { content, items, meta } objects in current ones, where content is inline HTML and items nests a sub-list; both forms occur. A renderer skips any block whose type it does not recognise, because new block types can be added without a change to the API version. Inline HTML is limited to <b>, <i>, <u>, <a href>, <mark>, <code> and <br>. Content is returned as stored and is not sanitised on read, so a storefront sanitises inline HTML to that list before inserting it into a page, and accepts only http, https and mailto URLs in href. The example below uses DOMPurify for that step.
Response

getRelatedPosts

Posts related to a given one, ranked on the server. The ranking combines what the writing is about with the topics two posts share and how recently each was published, and nudges a post from a different category up between otherwise equal results. Returns 404 if no published post matches the slug. Render reason, not score. The list is ordered by a rank that includes the category nudge, so the scores are not always decreasing — a post scoring 0.458 can sit above one scoring 0.511. To a reader that looks like a sorting fault, while reason (“Closely related subject”) is something they can act on. The list is topped up to at least three entries when few posts qualify, since a shelf of one reads as a fault rather than a short list. Topped-up entries score strictly below the weakest genuine match and carry the reason “More from this blog”. degraded is true when the post has no stored relevance data yet — most often because it was just published — and the ranking used topics and recency alone; the list is still returned.
Response

getTrendingPosts

The posts being read most, ranked by reads per day over a recent window. The window widens — 7 days, then 21, 45 and 90 — until enough posts qualify, and the one that was used comes back as window_days. A blog that publishes occasionally has quiet weeks, and a list that is empty half the time is less useful than one describing a longer period honestly. Show the period beside the list rather than calling everything “this week”. reads_per_day is normalised by the window, so the figure stays comparable as the window widens. It orders the list; it is not a view counter and reads oddly if shown as one.
Response

Attributing content views

getPost and getLookbook both accept a visitor and a session identifier, and record the view against them. That is what lets the merchant see which posts and lookbooks drive sales rather than just traffic — and it is the first half of the content funnel: pass the same content_id (with content_type) in the order’s attribution at checkout and the sale is credited back to the content the shopper came from. Both are optional. When you omit them the view still counts, but it is attributed to a coarse fallback derived from the request, so repeat views by one shopper may not group together.

Lookbooks

Lookbooks are curated collections of lifestyle images linked to specific products, perfect for “Shop the Look” features.

listLookbooks

Retrieve a list of published lookbooks, newest first. Only published lookbooks are returned — there is no status filter.
Response
Lookbooks carry created_at and updated_at on both the listing and the detail response, with the same meaning they have on a post: when it was created, and when it was last changed. The gallery (images, with its shoppable hotspots) and collection_id are fetched per lookbook and belong to getLookbook; asking for one of them on the listing returns 400.

getLookbook

Retrieve the full details of a published lookbook by its slug, including its ordered image gallery and the shoppable hotspots placed on each image. Returns 404 if no published lookbook matches the slug. Each lookbook exposes status (always "published" from the public API) and collection_id — the product collection that backs the lookbook, or null if it is standalone. The gallery lives under images[], where each image has a url and a hotspots[] array tagging the products featured at specific points in the photo (your “Shop the Look” markers). A hotspot gives x and y as percentages of the image’s width and height from the top-left, so the marker lands correctly at any display size, plus an optional label and the resolved product. The product carries variant_id — pass that to client.cartWishlist.addToCart, which is keyed by variant rather than product, so a marker can be added to a basket without a second call. Send a shopper who wants a different size or colour to the product page instead. A hotspot whose product is no longer available is omitted from the response, so an image can return fewer hotspots than were authored.
Response
status is always "published" from the public API. collection_id is the backing product collection, or null when the lookbook is standalone (as above). Each images[] entry carries a hotspots[] array — empty here, otherwise one marker per tagged product.

Pro Discovery Tip: Use Lookbooks to increase Average Order Value (AOV) by encouraging customers to buy entire outfits or coordinated room sets featured in lifestyle photography.

Newsletter

subscribeNewsletter

Capture a newsletter signup from your storefront. The email address is the identity, so this works for both anonymous visitors and signed-in shoppers (a signed-in storefront prefills the email field). When the merchant has connected a marketing tool, subscribers are synced to their chosen list automatically.
Response
It is safe to call repeatedly — re-subscribing the same email returns 200 with already_subscribed: true and never creates a duplicate; a brand-new signup returns 201. This endpoint accepts both publishable and secret keys (it’s a storefront write, like cart), so you can call it directly from the browser.

Response Codes

The read endpoints (posts, lookbooks) are GET; subscribeNewsletter is a POST. All accept both publishable and secret keys. Slug validation is enforced — empty or whitespace-only slugs return 400 without hitting the database.