Skip to content

Recommendations

A recommendation is one profile saying a catalog item is worth buying. Curated shelves on United Pet Club are all built from them: the main store is what the United Pet Club profile (@unitedpetclub, C1B00000-0000-4000-A000-000000000002) recommends, and a professional’s store page is what they recommend.

Recommendations are public reads. No token is required.

GET /marketplace/v1/recommendations/by-profile/{profileId}
[
{
"id": "019f...",
"recommenderProfileId": "22222222-2222-2222-2222-222222222222",
"catalogItemId": "019fce24-6c6e-7298-a79c-b38cc7e7d259",
"offerId": null,
"offerStorefrontId": null,
"offerStatusCode": null,
"noteText": "Third season on the same pair.",
"sortOrder": 10,
"isActiveFlag": true,
"isResolvable": true,
"effectiveFromUtc": null,
"effectiveUntilUtc": null,
"updatedAtUtc": "2026-08-05T09:12:00+00:00"
}
]

Rows come back in sortOrder — the recommender’s own running order. Preserve it. A curated shelf re-sorted by price or name is no longer a recommendation.

A catalog item, always. An offer, sometimes.

PayloadMeansWhat you should render
offerId: null”I recommend this product.”Resolve the item’s live offers yourself and pick a winner.
offerId set”I recommend buying it here.”Use that offer. Do not second-guess it, even if another is cheaper.

The second shape is how services work: a named trainer’s course is inseparable from the trainer. offerStorefrontId tells you whose it is without a second read.

Never render a row where isResolvable is false

Section titled “Never render a row where isResolvable is false”
{ "offerId": "…", "offerStatusCode": "WITHDRAWN", "isResolvable": false }

isResolvable is computed by the Marketplace BC and folds in three things: the row is active, it is inside its effective window, and — when pinned — the pinned offer is still ACTIVE. The common false case is a pin to an offer that has since been withdrawn, and rendering it would put a buy button on something nobody can sell.

Do not re-derive the rule from the other fields. It is one flag precisely so every surface agrees on the answer.

by-catalog-item already filters to resolvable rows; by-profile does not, because a caller may legitimately want to show an author their own paused picks:

GET /marketplace/v1/recommendations/by-profile/{profileId}?includeInactive=true

Only send includeInactive for a view the recommender themselves is looking at. A paused pick is not published content.

When nothing is pinned, get every live offer on the item in one call:

POST /marketplace/v1/offers/by-catalog-items?status=ACTIVE
{ "ids": ["019fce24-…", "019fce2a-…"] }

Then pick, in this order:

  1. Purchasable beats external. An offer you can transact beats an affiliate link to a retailer, even when the retailer is cheaper.
  2. In stock beats out of stock.
  3. Cheapest wins.
  4. Break ties deterministically (offer id works) so repeated renders agree.

Price offers per owning storefront — POST /price/v1/pricepoints/resolve/preview takes one storefrontId per call, and the storefront decides which price list applies. Pricing a rival’s offer against your own storefront quotes the wrong list.

A price renders only when resolutionReasonCode is RESOLVED_SUCCESSFULLY or RESOLVED_WITH_FALLBACK. A miss is HTTP 200 with zeros; showing it advertises a free product.

GET /marketplace/v1/recommendations/by-catalog-item/{catalogItemId}

Resolvable rows only. Use it for a “recommended by” line on a product page. Resolving each recommenderProfileId to a name costs a Profile read, so name a few and count the rest rather than resolving dozens for one sentence.

noteText is the recommender’s own words, in whatever language they wrote them, capped at 280 characters. There is no locale variant of it and there never will be — machine translating a personal endorsement puts words in someone’s mouth. Render it verbatim, attributed, and do not feed it to a translation layer.

Writes need a token and act as the acting profile. There is no recommender field on any request body: you can only curate your own shelf. Editing or deleting another profile’s recommendation answers 404, not 403.

POST /marketplace/v1/recommendations
{ "catalogItemId": "019fce24-…", "offerId": null, "noteText": "…" }

Idempotent per catalog item — a profile recommends a given product at most once, so a second POST for the same item replaces the first rather than adding a row. It replaces rather than merges: omitting offerId means “no pin”, not “keep the old one”. For partial edits use PATCH /recommendations/{id}, where clearOfferPin and clearNote exist because a null cannot express “remove” on a patch.

Reordering is one call for the whole shelf:

POST /marketplace/v1/recommendations/reorder
{ "orderedIds": ["019f…a", "019f…b", "019f…c"] }

All-or-nothing. An id you do not own rejects the entire request rather than applying a partial order — a half-reordered shelf is worse than an unchanged one, because nothing tells you which half landed.

See also: Product variants, Quote requests.