Building Custom Reviews Widgets with the Yotpo APIs (v1 and V3)

A best-practices playbook for building your own star ratings, review lists, search, galleries, and review forms on the Yotpo v1 and V3 APIs.

Overview

This playbook shows how to build your own review widgets on top of the Yotpo Reviews APIs. It covers the full path: reading ratings and reviews, rendering them on your storefront, collecting new reviews, and going live safely.

Who it is for

  • Brands that want full control over the look, markup, and behavior of reviews on their site, and prefer their own components over the Yotpo widgets.
  • Brands that need a widget Yotpo does not offer out of the box, such as a custom carousel, a comparison block, or reviews inside a native app or kiosk.
  • Agencies and developers building headless storefronts, where a script-based widget does not fit the front-end stack.

What you should know first

You should be comfortable with REST APIs, JSON, and your storefront's front-end framework. Steps that need a secret key also require a server or serverless function you control.

How to use this playbook

  1. Read v1 vs V3 and Prerequisites once. They decide which endpoints you call and where your code runs.
  2. Follow Steps 1 to 4 in order. Each step is a working widget on its own, so you can ship after any of them.
  3. Use the SEO, performance, and integrity sections as your quality bar before launch.
  4. Run the go-live checklist at the end.

Every request in this playbook is shown as plain HTTP so it maps to any language. Replace values in {curly_braces} with your own.

v1 vs V3: which API to use

Use V3 to read and display reviews. Use v1 for everything V3 does not cover yet: review submission, votes, Q&A, and Visual UGC. Most custom widgets call both.

V3 is the next generation of the UGC API and is rolling out in stages, starting with storefront reviews (V3 recipes). v1 stays fully supported, and future changes to it will be announced in the Changelog in advance (v1 Welcome).

How the two versions differ

v1V3
ScopeFull UGC API: reviews (read and write), votes, Q&A, Visual UGC, metadata, SEOStorefront reviews, read-only
Base URLhttps://api-cdn.yotpo.com for storefront reads, https://api.yotpo.com for writes and merchant callshttps://api-cdn.yotpo.com/v3/storefront
Store identifierstore_id (your app key) in the path, or appkey in the bodystoreId in the path
Naming stylesnake_case (per_page, average_score)camelCase (perPage, averageScore)
Response shapeWrapped: { status, response: { ... } }Product reviews are flat: { bottomline, pagination, reviews }
Product review sort optionsdate, votes_up, votes_down, time, rating, reviewer_typesmart, date, badge, images, rating
Localized review contentNot available on the product reviews endpointlang query parameter

Pick by widget

You want to buildUse
Star rating on a product pageV3 product rating bottomline
Star ratings across a category or collection pageV3 bottomline scores
Reviews list with pagination and sortingV3 product reviews
Search box, star filter, photo filter, topic filterV3 search product reviews, plus V3 product filters
AI review summary and smart topicsV3 product data
Customer photo and video gallery from reviewsV3 reviews media
Featured reviews carouselV3 top product reviews or top store reviews
Site (store) reviews pageV3 store reviews
Write-a-review form with photo uploadv1 create review, v1 upload images
Helpful / not helpful votesv1 vote on reviews
Questions and answersv1 Q&A endpoints
Instagram and album galleriesv1 Visual UGC endpoints
Review content readable by search and AI crawlersV3 LLM schema HTML

Rule of thumb for new builds: read with V3, write with v1. If you already run a v1 widget, it keeps working. Move your read calls to V3 when you want Smart Sorting, localized content, or the newer filters.

Because the two versions name fields differently, map both into one internal review model in your code. Step 2 shows the model this playbook uses.

Prerequisites and authentication

You need three things before you write code: your store ID, your API secret, and product IDs that match the ones Yotpo already holds. Only the store ID may appear in browser code.

What to collect

  1. Store ID (app key). Identifies your store in every request. See Finding your Yotpo app key and API secret.
  2. Product IDs. Use the same external product ID your store already sends to Yotpo, for example, the Shopify product ID or your SKU. If the IDs differ, requests return an empty result or a 404. In v1, product_id supports only letters, digits, _ and -.
  3. A server-side runtime. Any server, edge function, or serverless function you control. We recommend it for review submission (see Architecture).

Know your three credentials

CredentialWhat it doesWhere it may live
Store ID (app key)Identifies the store in storefront URLs and in the create-review bodyBrowser, server, mobile app
API secretGenerates access tokensServer only, in a secrets manager or environment variable
Access tokenAuthorizes merchant (non-public) API callsServer only

Treat the API secret and the access token like passwords. Never put them in front-end JavaScript, mobile app bundles, public repositories, or URLs that end up in logs. Anyone holding a token can read private account data.

Generate an access token

POST https://api.yotpo.com/oauth/token
Content-Type: application/json

{
  "client_id": "{store_id}",
  "client_secret": "{api_secret}",
  "grant_type": "client_credentials"
}

The response returns your access_token. Store it and reuse it across requests. When a call returns 401, generate a new token and retry once (Yotpo authentication, and Authenticate and make your first call for the full walkthrough).

Send the token in the X-Yotpo-Token header on any call that requires it:

X-Yotpo-Token: {access_token}

Two exceptions: the Insights API takes the token as Authorization: Bearer {access_token}, and Custom albums takes it as a utoken query parameter.

Which calls need a token

Call typeExamplesToken
Storefront readsV3 storefront endpoints, v1 widget endpoints on api-cdn.yotpo.comNo. The store ID in the path identifies the store. Every storefront endpoint answers without a token.
Merchant callsv1 retrieve all reviews, metadata updates, Visual UGC importsYes

Respect the intended use

Storefront endpoints exist to display reviews to shoppers. They are not meant for bulk export, data harvesting, or server-side aggregation. For those jobs, use the merchant endpoints or the App Developer API (About reviews, storefront).

Architecture: where each call should run

Use a hybrid setup. Render the first view of ratings and reviews on your server from a cache, let the browser call Yotpo directly for shopper interactions, and send all writes through an endpoint you own.

flowchart LR
  B[Shopper browser] -->|1. page load| S[Your server or SSR layer]
  S -->|cached read| C[(Your cache)]
  C -->|on miss| R[Yotpo CDN<br/>api-cdn.yotpo.com]
  B -->|2. paginate, sort, filter| R
  B -->|3. submit review, vote| W[Your write endpoint]
  W -->|validated request| A[Yotpo API<br/>api.yotpo.com]

Read the diagram as three paths: a cached server path for the first paint, a direct browser path for interactions, and a guarded server path for writes.

Path 1: server-side reads with a cache

Use this for anything that must be in the initial HTML: the star rating, the first page of reviews, structured data, and the LLM schema snippet.

  • Why: shoppers see reviews without a loading state, and crawlers can read the content from the page source.
  • Caching is required here. Storefront rate limits are counted per IP address. When your server makes the calls, all of your traffic arrives from a few IPs. Cache each response and serve shoppers from the cache.
  • Cache lifetime: Storefront data already lags by up to 3 hours, so a lifetime of 1 to 3 hours costs you no freshness. Responses are sent with Cache-Control: no-store, so this cache has to be yours. For the LLM schema snippet, Yotpo asks for a refresh at least once a day.
  • Fail open. Set a short timeout (the Yotpo examples use 3 seconds). If the call fails or times out, render the page without reviews. A reviews outage should never break a product page.

Path 2: direct browser reads

Use this for interactions after the page has loaded: next page, sort change, star filter, search.

  • Why: the storefront endpoints are built for client-side display and are served from a CDN. Each shopper calls from their own IP, so per-IP limits are not a concern.
  • Only the store ID appears in these requests. No secret or token is involved.
  • Debounce search input and cancel in-flight requests when the shopper changes a filter, so stale responses never overwrite newer ones.

Path 3: writes through your own endpoint

Send review submissions and votes to an endpoint on your server, which validates the request and forwards it to Yotpo.

  • Why: the create-review endpoint accepts requests with only the store ID, which is public. Your endpoint is where you add input validation, spam and bot protection, and per-shopper rate limiting.
  • It also keeps your options open. Features that sign requests with the API secret, such as Trusted Vendors, can only run on a server.

When a simpler setup is fine

A browser-only widget is acceptable for a low-traffic page where SEO does not matter, such as a reviews carousel on a landing page. Keep writes on the server even then.

Plan for data delay

Storefront responses lag behind reality. Product reviews are returned with a delay of up to 3 hours, and new reviews pass through your moderation settings first. Design your UX around that: after a shopper submits a review, show a confirmation message. Do not promise that the review will appear immediately.

Step 1: Show star ratings

Build the star rating first. It is the smallest widget, and it proves that your store ID and product IDs match what Yotpo holds.

1a. Rating on a product page

Call the product rating bottomline. It returns the average score and the review count without any review bodies, so it is the lightest call for a stars-and-count summary.

GET https://api-cdn.yotpo.com/v3/storefront/store/{storeId}/product/{productId}/ratings
{
  "bottomline": {
    "totalReview": 128,
    "averageScore": 4.6
  }
}

The v1 equivalent is GET https://api.yotpo.com/products/{store_id}/{product_id}/bottomline, which returns response.bottomline.average_score and response.bottomline.total_reviews. If you also want the star histogram (how many 5-star, 4-star reviews, and so on), the v1 product reviews response includes bottomline.star_distribution.

Render it properly

  1. Round only for display. Show one decimal (4.6). Keep the raw value for structured data.
  2. Fill stars by percentage. averageScore / 5 gives the fill width (4.6 becomes 92%). Clip a filled star row over an empty one. This avoids misleading half-star rounding.
  3. Handle zero reviews. When the count is 0, hide the stars and show a prompt such as "Be the first to review". Never display or mark up a rating of 0.
  4. Make it accessible. Give the star graphic role="img" and a text label. Screen readers should hear "Rated 4.6 out of 5 stars", not five unlabeled icons.
  5. Link the count to the reviews section on the same page.
<a class="rating" href="#reviews">
  <span class="stars" role="img" aria-label="Rated 4.6 out of 5 stars">
    <span class="stars-fill" style="width: 92%"></span>
  </span>
  <span class="rating-count">128 reviews</span>
</a>

1b. Ratings across a category or collection page

Do not call the product endpoint once per product card. A page with 48 cards would fire 48 requests. Use the bottomline scores endpoint, which returns ratings for many products per call.

GET https://api-cdn.yotpo.com/v3/storefront/stores/{store_id}/ratings?page=1&perPage=10
{
  "status": { "code": 200, "message": "OK" },
  "response": {
    "pagination": { "page": 1, "perPage": 10, "total": 342 },
    "bottomlines": [
      {
        "productId": "884XXXXXXXXX75",
        "totalReviews": 117,
        "averageScore": 4.6153846,
        "totalOrganicReviews": 0,
        "organicAverageScore": 0
      }
    ]
  }
}

Recommended pattern

  1. Run a scheduled job on your server that pages through this endpoint. page and perPage are both required, and perPage can be at most 10 (higher values return 400). Stop when you have fetched pagination.total products or a page comes back empty.
  2. Store each product's rating in your own cache or database, keyed by productId.
  3. Have your category template read ratings from that store. The shopper's page load makes no call to Yotpo.
  4. Refresh every few hours. Yotpo's storefront data already carries a delay of up to 3 hours, so a faster schedule adds load without fresher data.

Total or organic: choose once. totalReviews and averageScore include every source, such as syndicated reviews. totalOrganicReviews and organicAverageScore cover reviews collected by your own store only. Pick one pair and use it everywhere: product page, category page, and structured data. Mixed numbers on the same site look like an error to shoppers.

This path uses the plural stores, while the product endpoints use the singular store and product. Both are correct as written.

Step 2: Fetch and render product reviews

One GET returns a page of reviews together with the rating summary. Map the response into your own review model first, then render from that model, so v1 and V3 feed the same components.

2a. Request a page of reviews

V3

GET https://api-cdn.yotpo.com/v3/storefront/store/{storeId}/product/{productId}/reviews?page=1&perPage=10&sort=smart
ParameterValuesNotes
pageInteger, starting at 1
perPageIntegerDefault 5, maximum 150. Request only what you render.
sortsmart, date, badge, images, ratingsmart is AI-powered Smart Sorting (Pro and Premium plans)
directionasc, desc
langLanguage codeLocalizes review content
prefilterFields, presortingFieldsStringNot documented yet. Leave them out of your requests.

The response has three top-level blocks: bottomline (totalReview, averageScore), pagination (page, perPage, total), and reviews.

v1

GET https://api-cdn.yotpo.com/v1/widget/{store_id}/products/{product_id}/reviews.json?page=1&per_page=10&sort=date&direction=desc

v1 accepts per_page (default 10, maximum 150), page, star (1 to 5), sort (date, votes_up, votes_down, time, rating, reviewer_type), and direction. The payload sits under response and also includes bottomline.star_distribution and a products array.

2b. Normalize into one review model

Your components should never read raw API fields. A single model protects your UI from naming differences between v1 and V3 and from future API changes.

interface Review {
  id: number;
  score: number;                 // 1 to 5
  title: string;
  content: string;
  createdAt: string;             // ISO 8601
  reviewerName: string;
  verifiedBuyer: boolean;
  incentivized: boolean;
  incentiveType: string | null;
  syndicated: boolean;
  votesUp: number;
  votesDown: number;
  images: { id: number; thumbUrl: string; originalUrl: string }[];
  customFields: { title: string; fieldType: string; value: string | number }[];
  merchantReply: { content: string; createdAt: string } | null;
}

function fromV1(raw: V1Review): Review {
  return {
    id: raw.id,
    score: raw.score,
    title: raw.title ?? "",
    content: raw.content ?? "",
    createdAt: raw.created_at,
    reviewerName: raw.user?.display_name ?? "Anonymous",
    verifiedBuyer: Boolean(raw.verified_buyer),
    incentivized: Boolean(raw.is_incentivized),
    incentiveType: raw.incentive_type ?? null,
    syndicated: raw.source_review_id != null,
    votesUp: raw.votes_up ?? 0,
    votesDown: raw.votes_down ?? 0,
    images: (raw.images_data ?? []).map((i) => ({
      id: i.id,
      thumbUrl: i.thumb_url,
      originalUrl: i.original_url,
    })),
    customFields: Object.values(raw.custom_fields ?? {}).map((f) => ({
      title: f.title,
      fieldType: f.field_type,
      value: f.value,
    })),
    merchantReply: raw.comment
      ? { content: raw.comment.content, createdAt: raw.comment.created_at }
      : null,
  };
}

Write a matching fromV3 mapper from the response example in the V3 product reviews reference. Treat every field as optional in both mappers. custom_fields, comment, and images_data are absent or null on many reviews.

2c. Render each review

  1. Escape all review text. Titles, content, reviewer names, custom answers, and merchant replies are user-generated. Insert them as text (textContent, or your framework's default escaping). Never pass them to innerHTML or dangerouslySetInnerHTML.
  2. Show the trust signals the API gives you. Display a verified-buyer badge when verified_buyer is true. Display an incentive disclosure when is_incentivized is true.
  3. Show merchant replies. When comment is present, render it under the review, labeled as a response from the store.
  4. Use semantic dates. Wrap the date in <time datetime="..."> and format it for the shopper's locale.
  5. Load images responsibly. Use thumb_url in the list and original_url in the lightbox. Addloading="lazy", fixed dimensions to prevent layout shift, and alt text such as "Photo from John D.'s review".
  6. Render custom questions by type. field_type tells you the shape: Rating holds a number, SingleChoice and MultipleChoice hold labels, and the free-text types hold sentences.
  7. Design the empty state. With zero reviews, invite the shopper to write the first one.

2d. Paginate

  • Build paging from the pagination block. Do not raise perPage to avoid paging: large pages slow the first render, and most shoppers read only a few reviews.
  • Prefer a "Load more" button or numbered pages over infinite scroll. Shoppers can still reach the footer, and keyboard users keep control.
  • After new reviews load, move focus to the first new review or announce the update through an aria-live region.
  • pagination.total is the total number of reviews, not the number of pages, in both v1 and V3.

2e. Sort

Map a sort dropdown straight to the sort and direction parameters, and reset page to 1 on every change. In V3, smart applies Yotpo's AI-powered Smart Sorting, which puts the most important reviews first. Smart Sorting is part of the Pro and Premium plans. If you offer "Most helpful" in V1, use sort=votes_up.

2f. Filter and search

In V3, filtering is a POST to the same reviews path. Filters go in the JSON body. Paging and sorting stay in the query string.

POST https://api-cdn.yotpo.com/v3/storefront/store/{storeId}/product/{productId}/reviews?page=1&perPage=10
Content-Type: application/json

{
  "freeTextSearch": "comfortable",
  "scores": [4, 5],
  "pictured": true
}

The response adds foundFilteredReviews, which is true when the filters matched reviews and null when nothing matched. An unsupported sortings.sortBy value also returns no results. The reference also lists filters for topics, metadata, variants, and country.

  1. Build the filter UI from data. Call GET .../store/{storeId}/product/{productId}/productFilters and show only the filters that exist for that product. A filter that always returns zero results is worse than no filter.
  2. Keep the body identical across pages. When the shopper pages through filtered results, change only page. If the body changes, the result set shifts under them.
  3. Debounce search input by about 300 ms, and cancel the previous request with AbortController when a new one starts.
  4. Show the state. Display the result count, list the active filters, and offer a single "Clear all" action. When foundFilteredReviews is not true, say that no reviews match and keep the filters visible.

The v1 equivalent is POST https://api-cdn.yotpo.com/v1/reviews/{store_id}/filter.json. Its body takes domain_key (the product ID), scores, topic_names, pictured, free_text_search, crfs (custom question answers), orders (order and customer metadata), and sortings. Free-text search supports English, French, German, Italian, Norwegian, Portuguese, Spanish, Russian, Swedish, and Turkish (Search reviews).

Step 3: Widgets beyond the main reviews list

Every widget in this step reuses the review model and rendering rules from Step 2. Only the endpoint changes.

3a. Customer photo and video gallery

GET https://api-cdn.yotpo.com/v3/storefront/stores/{storeId}/products/{productId}/reviewsMedia?page=1&perPage=12

This returns the media attached to a product's reviews, so you can build a gallery without paging through review bodies.

  • Show thumbnails in a grid or a horizontal strip near the top of the reviews section.
  • On click, open a lightbox that shows the full image next to the review it came from. A photo without its review loses most of its value as proof.
  • Make the lightbox keyboard-friendly: trap focus while it is open, close on Esc, and return focus to the thumbnail on close.
  • Note that this path uses the plural stores and products form.

3b. Featured reviews carousel

GET https://api-cdn.yotpo.com/v3/storefront/store/{storeId}/product/{productId}/top_reviews?reviewType=product

reviewType accepts site, product, or both. lang localizes the content. For a store-wide version, for example on the home page, use Retrieve top store reviews.

  • Do not auto-rotate. If the design requires rotation, add a visible pause control and stop on hover and on focus.
  • Keep the star rating, the reviewer name, and the verified-buyer badge on every slide. A quote with no attribution reads as marketing copy.
  • Render the first slide on the server so the carousel is not an empty box while scripts load.

3c. Site (store) reviews

GET https://api-cdn.yotpo.com/v3/storefront/store/{storeId}/reviews?page=1&perPage=10&sort=date

Site reviews are about the shopping experience, not about a product. The parameters match product reviews: page, perPage, sort, direction, lang. To filter or search them, use Search store reviews.

  • Put them where trust matters most: a dedicated reviews page, the cart, or the footer.
  • Label them clearly as store reviews. Never blend them into a product's list or count them in a product's rating.
  • In v1, site reviews use the reserved product ID yotpo_site_reviews (Retrieve widget site reviews).

3d. AI summary and smart topics

GET https://api-cdn.yotpo.com/v3/storefront/stores/{store_id}/products/{product_id}/productData?include=topics,reviewsSummary

include is required. Without it, the call returns 400.

Product-level data includes three things: Smart Topics (AI-generated filters), an AI review summary for the product, and Summary Highlights taken from the top 3 reviews (Product-Level Data).

  • Label the summary as AI-generated, and place it above the reviews list, not in place of it.
  • Render topics as filter chips. A click should run the search from Step 2f with that topic.
  • Hide the whole block when the response has no summary. Products with few reviews may not have one.

3e. Questions and answers

Q&A lives in v1 only. Use Retrieve Q&A for a product to display, Create question to collect, and Vote on answers for helpfulness.

  • Apply the same rules as reviews: escape all text, paginate, and send writes through your own endpoint.
  • Mark store answers and shopper answers differently, so shoppers can tell an official reply from a community one.
  • Choose the question flow on purpose. One endpoint sends the shopper a confirmation email and one does not. Read About Q&A before you pick.

3f. Visual UGC galleries

Instagram content, imported media, and curated albums come from the v1 Visual UGC endpoints: get media, custom albums, photos by album, and product images. Use them for shoppable galleries that are not tied to reviews. Use 3a when the gallery should show review photos.

Step 4: Collect reviews and votes

All writes use v1. Send them from your own endpoint (Path 3 in Architecture), never straight from the browser.

4a. Submit a review

POST https://api.yotpo.com/v1/widget/reviews
Content-Type: application/json

{
  "appkey": "{store_id}",
  "sku": "{product_id}",
  "product_title": "Phone",
  "product_url": "https://www.shop.com/phone.html",
  "display_name": "John Smith",
  "email": "[email protected]",
  "review_title": "Great Phone",
  "review_content": "It's really good",
  "review_score": 5
}

These nine fields are required. A successful call returns { "code": 200, "message": "ok", "image_upload_token": "..." }. This is the asynchronous method, which Yotpo recommends for general use (Create review).

What to know before you build the form

BehaviorWhat it means for your widget
Reviews created through this endpoint are anonymous, and Yotpo emails the reviewer a verification messageYour success message should tell the shopper to check their inbox
New reviews pass through your moderation settings, and storefront reads lag by up to 3 hoursDo not add the new review to the list on screen
A missing required field returns status 400 with error_type: Exceptions::InvalidParams and a message such as Missing emailValidate before you call Yotpo, and map this error to a field message, not to a generic failure
sku accepts only letters, digits, _ and -, and must equal the product ID you use for readsReviews posted under a different ID will not show on the product
sku: "yotpo_site_reviews" Creates a site reviewUse one form component for both review types
The rate limit is 5,000 requests per minute per app keyShared by all your shoppers, which is one more reason to block bots
Submitting the same review again returns 400 with {"status": {"code": 400, "message": "duplicate_review"}}Treat it as already received: show the success message and don't retry

What your endpoint must do

  1. Validate input. Check all required fields, the email format, a whole-number score from 1 to 5, and sensible length limits.
  2. Block bots. Add a CAPTCHA or equivalent challenge, a honeypot field, and rate limits per IP and per email address.
  3. Build product fields on the server. Accept only a product ID from the browser. Look up sku, product_title, and product_url in your own catalog, and set appkey from configuration. This stops anyone from posting reviews against products or URLs of their choice.
  4. Report incentives truthfully. When the review came from a sample, coupon, or loyalty reward, set is_incentivized and incentive_type (free_product, coupon, loyalty_points, paid_promotion, employee_review, other). Disclosure rules apply to incentivized reviews.
  5. Treat the email as personal data. Do not log request bodies, link your privacy policy next to the submit button, and send the data over HTTPS only.
  6. Prevent double submits. Disable the button while the request is in flight.

Custom questions. To collect answers to your custom review form, add custom_fields keyed by question ID, for example "--94251": ["Skiing", "Hiking"]. Values must match the question type: a number for rating questions, text for text questions.

Form accessibility. Give every input a visible label. Build the star input as a radio group so it works with a keyboard. Tie each error message to its field witharia-describedby, and keep the shopper's text when validation fails.

4b. Attach photos

POST https://api.yotpo.com/images/process
Content-Type: application/json

{
  "image_upload_token": "{image_upload_token}",
  "image_urls": ["https://cdn.example.com/uploads/review-photo-1.jpg"]
}

This call needs the image_upload_token from the create-review response. You can attach up to 3 images per review, and the image URLs must be publicly accessible (Upload images with reviews).

  1. Upload the shopper's files to your own storage first, then pass the public URLs to Yotpo.
  2. Check file type and size on your server. Strip location metadata (EXIF) from photos before you store them.
  3. Run both calls inside the same server request. The token arrives once, in the create-review response.
  4. If the image call fails, keep the review. Tell the shopper the review was received, and the photos were not.

4c. Helpful votes

POST https://api.yotpo.com/reviews/{review_id}/vote/up
POST https://api.yotpo.com/reviews/{review_id}/vote/down

Each call changes the count by 1. To undo a vote, append/true, for example /vote/up/true. The response only confirms success. It does not return the new count (Upvote or downvote reviews).

  • Enforce one vote per shopper yourself. The API counts every call. Store the shopper's vote inlocalStorage, and rate-limit votes on your endpoint.
  • Update the UI optimistically. Change the count at once, and roll it back if the call fails. Keep showing your local count afterward, because the CDN will return the old number for a while.
  • Use real buttons. Mark the chosen state witharia-pressed, and give each button a label such as "Helpful, 12 votes".

SEO: make reviews readable by crawlers

Reviews loaded by JavaScript after page load may never be seen by search or AI crawlers. Put the rating, the first page of reviews, and the structured data into the HTML your server returns.

1. Render the first view on the server

Use Path 1 from Architecture: fetch the bottomline and the first page of reviews on the server, from your cache, and output real HTML. Later pages can load in the browser.

2. Add Product structured data

Output one JSON-LD block per product page, built from the same cached data you render.

<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "Product",
  "name": "Phone",
  "aggregateRating": {
    "@type": "AggregateRating",
    "ratingValue": "4.6",
    "reviewCount": "128"
  },
  "review": [
    {
      "@type": "Review",
      "author": { "@type": "Person", "name": "John S." },
      "datePublished": "2026-06-16",
      "name": "Great Phone",
      "reviewBody": "It's really good",
      "reviewRating": { "@type": "Rating", "ratingValue": "5" }
    }
  ]
}
</script>
  • Match the page. ratingValue and reviewCount must equal the numbers the shopper sees. Every review in the markup must be visible on the same page.
  • Skip it at zero. Omit aggregateRating and review when the product has no reviews.
  • Serialize safely. Build the object in code and run it through a JSON serializer. Escape < as \u003c so review text can never close the script tag.
  • One source of markup. If the Yotpo widgets or another app also output Product markup, remove one. Duplicate blocks with different numbers can cost you the rich result.
  • Product pages only. Do not mark up your own store's site reviews as an organization rating. Search engines treat ratings a business publishes about itself as self-serving. See Google's review snippet guidelines.

3. Add the LLM schema snippet

GET https://api-cdn.yotpo.com/v1/stores/{storeId}/products/{productId}/llm-schema

This endpoint returns ready-made HTML with a product's review content, built for AI crawlers that read the page source (Retrieve LLM schema HTML for a product). Use the /v1/stores/... path shown here. The /v3/storefront/... form returns 404..

  1. Fetch on the server with a 3-second timeout, and return an empty string on any failure.
  2. Cache the response and refresh it at least once a day.
  3. Inject it as returned. Render only the HTML from this endpoint, and never combine it with user input before output.
  4. Verify. View the page source and confirm the snippet the endpoint returned is there. Depending on your account, it contains a <div id="yotpo-reviews-section-data"> block, a <script type="application/ld+json"> block with Product and Review data, or both. If you only inject the snippet when it contains yotpo-reviews-section-data, as the Liquid example on the reference page does, check that your responses actually include it.

Standard Shopify stores on the Yotpo Reviews widgets v3 get this automatically. Headless Shopify, Salesforce Commerce Cloud, Adobe Commerce, BigCommerce, and custom stacks need the steps above. The reference page has code for Hydrogen, Next.js, Liquid, and SFCC.

4. Other options in v1

v1 also offers a static HTML reviews payload and hosted review pages (About SEO). Use them when server-side rendering is not possible on your platform.

Performance, rate limits, and error handling

A reviews widget should never slow down or break the product page. Keep requests few and small, watch the rate limit headers, and fail open on every read.

Performance

  • Do not fetch twice. The product reviews response already includes the bottomline. If you load reviews on the server, use that block for the stars at the top of the page and skip the separate ratings call.
  • Defer what is below the fold. If you do not render on the server, start the reviews request when the section approaches the viewport (IntersectionObserver), not on page load.
  • Reserve the space. Give the reviews container a minimum height and give images fixed dimensions, so content does not jump when data arrives.
  • Keep pages small. Request 5 to 10 reviews at a time and use thumb_url for list images.
  • Cache by product, page, sort, and filter set. Going back to page 1 should not trigger a new request.
  • Set timeouts. Use about 3 seconds on the server and show a quiet fallback in the browser. Never block page rendering on a Yotpo call.

Published rate limits and data delay

EndpointLimitData delay
V1 retrieve reviews for a product30,000 requests/minute per IP address3 hours
V1 search reviews30,000 requests/minute per IP address3 hours
V3 bottomline scores30,000 requests/minute per IP address3 hours
V1 bottomline for a specific product5,000 requests/minute per app key1 hour
v1 create review5,000 requests/minute per app key
Other V3 storefront endpointsNot published. Read the rate limit headers3 hours

Responses are sent with Cache-Control: no-store, so do your own caching. They carry these headers: ratelimit-limit, ratelimit-remaining, ratelimit-reset (seconds until the window resets), x-ratelimit-limit-minute, and x-ratelimit-remaining-minute.

On your server, read ratelimit-remaining on every response. When it runs low, serve from cache only until ratelimit-reset has passed. Per-app-key limits are shared by every shopper and every system that uses your key, so a bot hitting your review form can block real submissions.

Error handling

ResponseLikely causeWhat to do
200 with an empty reviews arrayProduct has no reviews, or filters matched nothingShow the empty state
400Invalid parameterFix the request. Do not retry.
401Access token expiredGenerate a new token and retry once
404 with Couldn't find Account, or plain-text Account not found from api-cdn.yotpo.comWrong store IDAlert your team. This is a configuration error.
404 with ActiveRecord::RecordNotFoundYotpo does not know this product IDTreat as zero reviews. Check your product ID mapping if it happens often.
400 with Exceptions::InvalidParams on create reviewMissing or invalid fieldShow a field-level message to the shopper
Other 5xx, or a timeoutTemporary failureReads: retry up to 2 times with exponential backoff and jitter, then fail open. Writes: do not retry automatically, because a retry can create a duplicate. Let the shopper resubmit.

v1 error bodies share one shape: status.code, status.message, status.error_type, and status.uuid. Log the uuid. It identifies the failed request when you open a ticket with Yotpo support.

Monitoring

Track three numbers per endpoint: error rate, latency, and ratelimit-remaining. Alert on a rising 404 rate (usually a product ID mismatch after a catalog change) and on any store-level 404.

Accessibility, UX, and review integrity

The Yotpo widgets handle accessibility and display rules for you. When you build your own widget, you own both.

Accessibility checklist

Aim for WCAG 2.2 level AA.

  • Star ratings have a text alternative ("Rated 4.6 out of 5 stars"). Decorative star icons are hidden from screen readers.
  • Star colors meet contrast requirements against the background, and the rating is never shown by color alone.
  • Every control is a real <button>, <a>, <select>, or <input>, reachable and usable with the keyboard, with a visible focus style.
  • The reviews section has a heading, and each review is an <article> with its own heading.
  • Loading more reviews, changing sort, or applying a filter announces the result through an aria-live region ("Showing 10 of 128 reviews").
  • Lightboxes and modals trap focus, close on Esc, and return focus to the trigger.
  • Carousels do not move on their own, or they have a pause control.
  • The review form has visible labels, error messages linked to fields, and no time limit.
  • Review photos have alt text. Dates use <time>.
  • The widget works at 200% zoom and at 320 px width, and it respects prefers-reduced-motion.

UX guidelines

  • Put the rating summary and the star histogram above the list. Make each histogram bar a filter.
  • Offer sorting by newest and by most helpful, and show which sort is active.
  • Cut long reviews after a few lines with a "Read more" control. Keep the full text in the page.
  • Show a skeleton or a spinner while loading, a clear empty state, and a quiet error state with a retry action.
  • Localize dates, numbers, and interface text. In V3, pass lang to localize review content as well.

Review integrity rules

Shoppers and regulators expect reviews to be shown fairly. Consumer protection rules on reviews exist in the US, the UK, and the EU, so ask your legal team which ones apply to you. These rules keep a custom widget on safe ground:

  1. Show every published review. Do not hide low scores in your code, for example, with a hidden minimum-score filter on the default view.
  2. Moderate in Yotpo, not in the widget. Publishing decisions belong in your Yotpo moderation settings, where they are recorded and consistent across channels.
  3. Never edit review text. You may shorten it visually. You may not reword it.
  4. Keep the labels. Show the verified-buyer badge and the incentive disclosure whenever the API returns them.
  5. Keep the default sort neutral. Do not default to "highest rated first". Let shoppers sort by lowest rating too.
  6. Keep reviews with their product. Show a review on another product's page only when the products are grouped in Yotpo.
  7. Keep numbers consistent. The rating on the product page, on the category page, and in the structured data must come from the same fields (see Step 1b).
  8. Explain your process. Some markets require you to tell shoppers whether and how you verify that reviews come from real buyers. A short line next to the reviews heading with a link to your policy covers this.

Testing and go-live checklist

Test against real data in a staging storefront, then run this list before launch.

Test products to prepare

Pick one product for each case, since each one breaks widgets differently: zero reviews, exactly one review, several hundred reviews, reviews with photos, reviews with custom questions, a review with a merchant reply, an incentivized review, and a product ID that Yotpo does not know.

Functional

  • Rating and count on the product page match the Yotpo admin for 5 sample products.
  • Category page ratings match product page ratings for the same products.
  • Paging reaches the last page and stops. Sort and filters reset to page 1.
  • Search with no matches shows the "no results" state and keeps the filters visible.
  • A test review submits, the verification email arrives, and the review appears in Yotpo moderation.
  • A test review with 3 photos attaches all 3.
  • Voting up, undoing, and voting down each change the count by 1. A second vote from the same browser is blocked.
  • Review text containing <script>alert(1)</script> renders as plain text.

Security

  • Search your built front-end bundle for the API secret and any access token. Neither appears.
  • The secret sits in a secrets manager or environment variable, not in the repository.
  • The write endpoint rejects requests without a valid bot challenge and enforces rate limits.
  • The write endpoint ignores product title and URL sent by the browser.

Resilience

  • With Yotpo calls blocked, the product page still loads and the reviews section shows its fallback.
  • With a 10-second simulated delay, the server-side timeout triggers and the page renders.
  • Server-side calls are served from cache. A load test does not increase calls to Yotpo in step with traffic.

SEO and accessibility

  • The rating and first reviews appear in "view source", not only in the rendered DOM.
  • The page passes Google's Rich Results Test with one Product block, and its numbers match the page.
  • The LLM schema snippet appears in the page source (if you use it).
  • A keyboard-only pass and a screen reader pass cover the list, filters, lightbox, and form.

Launch

  • Remove or disable the Yotpo widget you are replacing, so ratings and structured data do not appear twice.
  • Dashboards and alerts from Monitoring are live.
  • Subscribe to the API Changelog.

Endpoint quick reference

Storefront reads use https://api-cdn.yotpo.com. Writes and merchant calls use https://api.yotpo.com.

V3 storefront (read)

PurposeMethod and pathUsed in
Product rating bottomlineGET /v3/storefront/store/{storeId}/product/{productId}/ratingsStep 1a
Bottomline scores, all productsGET /v3/storefront/stores/{store_id}/ratingsStep 1b
Product reviewsGET /v3/storefront/store/{storeId}/product/{productId}/reviewsStep 2a
Search product reviewsPOST /v3/storefront/store/{storeId}/product/{productId}/reviewsStep 2f
Product filtersGET /v3/storefront/store/{storeId}/product/{productId}/productFiltersStep 2f
Reviews mediaGET /v3/storefront/stores/{storeId}/products/{productId}/reviewsMediaStep 3a
Top product reviewsGET /v3/storefront/store/{storeId}/product/{productId}/top_reviewsStep 3b
Top store reviewsGET /v3/storefront/store/{storeId}/top_reviews?reviewType=bothStep 3b
Store reviewsGET /v3/storefront/store/{storeId}/reviewsStep 3c
Search store reviewsPOST /v3/storefront/store/{storeId}/reviewsStep 3c
Product dataGET /v3/storefront/stores/{store_id}/products/{product_id}/productDataStep 3d
LLM schema HTMLGET /v1/stores/{storeId}/products/{productId}/llm-schemaSEO

V1

PurposeMethod and pathUsed in
Generate access tokenPOST /oauth/tokenPrerequisites
Reviews for a productGET /v1/widget/{store_id}/products/{product_id}/reviews.jsonStep 2a
Search reviewsPOST /v1/reviews/{store_id}/filter.jsonStep 2f
Bottomline for a productGET /products/{store_id}/{product_id}/bottomlineStep 1a
Widget site reviewsGET /v1/widget/{store_id}/products/yotpo_site_reviews/reviews.jsonStep 3c
Create reviewPOST /v1/widget/reviewsStep 4a
Upload images with reviewsPOST /images/processStep 4b
Vote on reviewsPOST /reviews/{review_id}/vote/{up or down}Step 4c
Q&ASee referenceStep 3e
Visual UGCSee referenceStep 3f

Did this page help you?