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
- Read v1 vs V3 and Prerequisites once. They decide which endpoints you call and where your code runs.
- Follow Steps 1 to 4 in order. Each step is a working widget on its own, so you can ship after any of them.
- Use the SEO, performance, and integrity sections as your quality bar before launch.
- 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
| v1 | V3 | |
|---|---|---|
| Scope | Full UGC API: reviews (read and write), votes, Q&A, Visual UGC, metadata, SEO | Storefront reviews, read-only |
| Base URL | https://api-cdn.yotpo.com for storefront reads, https://api.yotpo.com for writes and merchant calls | https://api-cdn.yotpo.com/v3/storefront |
| Store identifier | store_id (your app key) in the path, or appkey in the body | storeId in the path |
| Naming style | snake_case (per_page, average_score) | camelCase (perPage, averageScore) |
| Response shape | Wrapped: { status, response: { ... } } | Product reviews are flat: { bottomline, pagination, reviews } |
| Product review sort options | date, votes_up, votes_down, time, rating, reviewer_type | smart, date, badge, images, rating |
| Localized review content | Not available on the product reviews endpoint | lang query parameter |
Pick by widget
| You want to build | Use |
|---|---|
| Star rating on a product page | V3 product rating bottomline |
| Star ratings across a category or collection page | V3 bottomline scores |
| Reviews list with pagination and sorting | V3 product reviews |
| Search box, star filter, photo filter, topic filter | V3 search product reviews, plus V3 product filters |
| AI review summary and smart topics | V3 product data |
| Customer photo and video gallery from reviews | V3 reviews media |
| Featured reviews carousel | V3 top product reviews or top store reviews |
| Site (store) reviews page | V3 store reviews |
| Write-a-review form with photo upload | v1 create review, v1 upload images |
| Helpful / not helpful votes | v1 vote on reviews |
| Questions and answers | v1 Q&A endpoints |
| Instagram and album galleries | v1 Visual UGC endpoints |
| Review content readable by search and AI crawlers | V3 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
- Store ID (app key). Identifies your store in every request. See Finding your Yotpo app key and API secret.
- 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_idsupports only letters, digits,_and-. - 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
| Credential | What it does | Where it may live |
|---|---|---|
| Store ID (app key) | Identifies the store in storefront URLs and in the create-review body | Browser, server, mobile app |
| API secret | Generates access tokens | Server only, in a secrets manager or environment variable |
| Access token | Authorizes merchant (non-public) API calls | Server 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 type | Examples | Token |
|---|---|---|
| Storefront reads | V3 storefront endpoints, v1 widget endpoints on api-cdn.yotpo.com | No. The store ID in the path identifies the store. Every storefront endpoint answers without a token. |
| Merchant calls | v1 retrieve all reviews, metadata updates, Visual UGC imports | Yes |
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
- Round only for display. Show one decimal (
4.6). Keep the raw value for structured data. - Fill stars by percentage.
averageScore / 5gives the fill width (4.6 becomes 92%). Clip a filled star row over an empty one. This avoids misleading half-star rounding. - 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.
- 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. - 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
- Run a scheduled job on your server that pages through this endpoint.
pageandperPageare both required, andperPagecan be at most 10 (higher values return400). Stop when you have fetchedpagination.totalproducts or a page comes back empty. - Store each product's rating in your own cache or database, keyed by
productId. - Have your category template read ratings from that store. The shopper's page load makes no call to Yotpo.
- 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| Parameter | Values | Notes |
|---|---|---|
page | Integer, starting at 1 | |
perPage | Integer | Default 5, maximum 150. Request only what you render. |
sort | smart, date, badge, images, rating | smart is AI-powered Smart Sorting (Pro and Premium plans) |
direction | asc, desc | |
lang | Language code | Localizes review content |
prefilterFields, presortingFields | String | Not 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=descv1 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
- 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 toinnerHTMLordangerouslySetInnerHTML. - Show the trust signals the API gives you. Display a verified-buyer badge when
verified_buyeris true. Display an incentive disclosure whenis_incentivizedis true. - Show merchant replies. When
commentis present, render it under the review, labeled as a response from the store. - Use semantic dates. Wrap the date in
<time datetime="...">and format it for the shopper's locale. - Load images responsibly. Use
thumb_urlin the list andoriginal_urlin the lightbox. Addloading="lazy", fixed dimensions to prevent layout shift, and alt text such as "Photo from John D.'s review". - Render custom questions by type.
field_typetells you the shape:Ratingholds a number,SingleChoiceandMultipleChoicehold labels, and the free-text types hold sentences. - Design the empty state. With zero reviews, invite the shopper to write the first one.
2d. Paginate
- Build paging from the
paginationblock. Do not raiseperPageto 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-liveregion. pagination.totalis 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.
- Build the filter UI from data. Call
GET .../store/{storeId}/product/{productId}/productFiltersand show only the filters that exist for that product. A filter that always returns zero results is worse than no filter. - 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. - Debounce search input by about 300 ms, and cancel the previous request with
AbortControllerwhen a new one starts. - Show the state. Display the result count, list the active filters, and offer a single "Clear all" action. When
foundFilteredReviewsis nottrue, 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=12This 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
storesandproductsform.
3b. Featured reviews carousel
GET https://api-cdn.yotpo.com/v3/storefront/store/{storeId}/product/{productId}/top_reviews?reviewType=productreviewType 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=dateSite 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,reviewsSummaryinclude 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
| Behavior | What it means for your widget |
|---|---|
| Reviews created through this endpoint are anonymous, and Yotpo emails the reviewer a verification message | Your 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 hours | Do 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 email | Validate 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 reads | Reviews posted under a different ID will not show on the product |
sku: "yotpo_site_reviews" Creates a site review | Use one form component for both review types |
| The rate limit is 5,000 requests per minute per app key | Shared 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
- Validate input. Check all required fields, the email format, a whole-number score from 1 to 5, and sensible length limits.
- Block bots. Add a CAPTCHA or equivalent challenge, a honeypot field, and rate limits per IP and per email address.
- Build product fields on the server. Accept only a product ID from the browser. Look up
sku,product_title, andproduct_urlin your own catalog, and setappkeyfrom configuration. This stops anyone from posting reviews against products or URLs of their choice. - Report incentives truthfully. When the review came from a sample, coupon, or loyalty reward, set
is_incentivizedandincentive_type(free_product,coupon,loyalty_points,paid_promotion,employee_review,other). Disclosure rules apply to incentivized reviews. - 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.
- 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).
- Upload the shopper's files to your own storage first, then pass the public URLs to Yotpo.
- Check file type and size on your server. Strip location metadata (EXIF) from photos before you store them.
- Run both calls inside the same server request. The token arrives once, in the create-review response.
- 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/downEach 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 in
localStorage, 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 with
aria-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.
ratingValueandreviewCountmust equal the numbers the shopper sees. Every review in the markup must be visible on the same page. - Skip it at zero. Omit
aggregateRatingandreviewwhen the product has no reviews. - Serialize safely. Build the object in code and run it through a JSON serializer. Escape
<as\u003cso 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-schemaThis 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..
- Fetch on the server with a 3-second timeout, and return an empty string on any failure.
- Cache the response and refresh it at least once a day.
- Inject it as returned. Render only the HTML from this endpoint, and never combine it with user input before output.
- 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 containsyotpo-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_urlfor 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
| Endpoint | Limit | Data delay |
|---|---|---|
| V1 retrieve reviews for a product | 30,000 requests/minute per IP address | 3 hours |
| V1 search reviews | 30,000 requests/minute per IP address | 3 hours |
| V3 bottomline scores | 30,000 requests/minute per IP address | 3 hours |
| V1 bottomline for a specific product | 5,000 requests/minute per app key | 1 hour |
| v1 create review | 5,000 requests/minute per app key | |
| Other V3 storefront endpoints | Not published. Read the rate limit headers | 3 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
| Response | Likely cause | What to do |
|---|---|---|
200 with an empty reviews array | Product has no reviews, or filters matched nothing | Show the empty state |
400 | Invalid parameter | Fix the request. Do not retry. |
401 | Access token expired | Generate a new token and retry once |
404 with Couldn't find Account, or plain-text Account not found from api-cdn.yotpo.com | Wrong store ID | Alert your team. This is a configuration error. |
404 with ActiveRecord::RecordNotFound | Yotpo does not know this product ID | Treat as zero reviews. Check your product ID mapping if it happens often. |
400 with Exceptions::InvalidParams on create review | Missing or invalid field | Show a field-level message to the shopper |
Other 5xx, or a timeout | Temporary failure | Reads: 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-liveregion ("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
langto 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:
- Show every published review. Do not hide low scores in your code, for example, with a hidden minimum-score filter on the default view.
- Moderate in Yotpo, not in the widget. Publishing decisions belong in your Yotpo moderation settings, where they are recorded and consistent across channels.
- Never edit review text. You may shorten it visually. You may not reword it.
- Keep the labels. Show the verified-buyer badge and the incentive disclosure whenever the API returns them.
- Keep the default sort neutral. Do not default to "highest rated first". Let shoppers sort by lowest rating too.
- Keep reviews with their product. Show a review on another product's page only when the products are grouped in Yotpo.
- 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).
- 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)
| Purpose | Method and path | Used in |
|---|---|---|
| Product rating bottomline | GET /v3/storefront/store/{storeId}/product/{productId}/ratings | Step 1a |
| Bottomline scores, all products | GET /v3/storefront/stores/{store_id}/ratings | Step 1b |
| Product reviews | GET /v3/storefront/store/{storeId}/product/{productId}/reviews | Step 2a |
| Search product reviews | POST /v3/storefront/store/{storeId}/product/{productId}/reviews | Step 2f |
| Product filters | GET /v3/storefront/store/{storeId}/product/{productId}/productFilters | Step 2f |
| Reviews media | GET /v3/storefront/stores/{storeId}/products/{productId}/reviewsMedia | Step 3a |
| Top product reviews | GET /v3/storefront/store/{storeId}/product/{productId}/top_reviews | Step 3b |
| Top store reviews | GET /v3/storefront/store/{storeId}/top_reviews?reviewType=both | Step 3b |
| Store reviews | GET /v3/storefront/store/{storeId}/reviews | Step 3c |
| Search store reviews | POST /v3/storefront/store/{storeId}/reviews | Step 3c |
| Product data | GET /v3/storefront/stores/{store_id}/products/{product_id}/productData | Step 3d |
| LLM schema HTML | GET /v1/stores/{storeId}/products/{productId}/llm-schema | SEO |
V1
| Purpose | Method and path | Used in |
|---|---|---|
| Generate access token | POST /oauth/token | Prerequisites |
| Reviews for a product | GET /v1/widget/{store_id}/products/{product_id}/reviews.json | Step 2a |
| Search reviews | POST /v1/reviews/{store_id}/filter.json | Step 2f |
| Bottomline for a product | GET /products/{store_id}/{product_id}/bottomline | Step 1a |
| Widget site reviews | GET /v1/widget/{store_id}/products/yotpo_site_reviews/reviews.json | Step 3c |
| Create review | POST /v1/widget/reviews | Step 4a |
| Upload images with reviews | POST /images/process | Step 4b |
| Vote on reviews | POST /reviews/{review_id}/vote/{up or down} | Step 4c |
| Q&A | See reference | Step 3e |
| Visual UGC | See reference | Step 3f |
Updated about 17 hours ago