Skip to content

Search API v1 ​

  • Requires: Pro plan or above
  • Written for: front-end engineers building the storefront

Marutto Search normally renders the search results page itself. With the Search API it renders nothing at all and hands your theme the result JSON instead.

  • Your theme's design and layout are left untouched
  • You get the Japanese morphological analysis, spelling-variant handling, synonyms and faceting, and nothing else
  • The rendering is your code

1. Making a search request ​

Marutto Search returns JSON and nothing else. Calling, state, and rendering are all yours. We never load JavaScript into your storefront.

GET /apps/marutto-search/search?type=search&contract=v1&q=dress&limit=24&offset=0
  • The URL is on the store's own domain (a Shopify App Proxy). No API key and no CORS configuration — Shopify's signature identifies the shop for us
  • From a browser it is readable only from that store's own pages. We send no CORS headers, so a page on another domain cannot read the response
  • A server-side call (curl and friends) reaches it from anywhere, though. Anyone who knows the URL can call it, so this is not an endpoint designed to return secrets — what it returns is public catalogue data, which Shopify itself publishes on the same store as /products.json
  • contract=v1 is what asks for this specification. Only requests carrying it are guaranteed the shape documented here. Without it you get the app's internal response, which changes without notice — it is the shape our own widget reads, not a contract
  • contract=v1 requires the Pro plan or above. A shop below it gets 403 with {"code": "plan_required"}, so your code can tell a lapsed subscription apart from a broken search
  • v1 covers the confirmed-search response (type=search) and a product's review list (type=reviews, chapter 10). Adding contract=v1 to suggest returns 400 with {"code": "unsupported_contract_type"}. We do not return a shape as a contract when we have not written that shape down

Query parameters ​

ParameterDefaultMeaning
contract—v1. Required — this is what asks for the documented shape
type—search. Required. The review list is reviews (chapter 10)
q—Search term. Required (* browses everything)
session_id—Session identifier. Required (see §7)
limit8Results per page. Maximum 50
offset0Starting position. Maximum 2,000 (larger values are clamped)
sortrelevancerelevance / price_asc / price_desc / newest / rating
localestore'sja, en, … — picks the analyzer language on a multilingual store
filter_vendor—Vendor name
filter_type—Product type
filter_tags—Tags (comma-separated; combined with AND)
filter_price_min / filter_price_max—Price range
filter_available—true / false (in stock)
filter_on_sale—true / false (on sale)
filter_content_type—product / article / page
filter_collection—Collection handle
filter_option_name / filter_option_value—Variant option (e.g. Color / Beige)
filter_metafield—Metafield (namespace.key:value). Repeatable
filter_rating—Rating 1–5: products rated N stars and up only (see below)

sort=rating puts the highest-rated first. A product with few reviews is weighed against how many it has, so a single 5★ does not jump to the top. Products without a rating come last.

filter_rating=4 means "4★ and up", judged on the product's rating (rating.average) rounded to whole stars. Products without a rating, articles and pages are left out. Anything but a whole number from 1 to 5 is ignored.

While degraded (chapter 4), neither the rating sort, the newest sort nor the rating filter applies (results come in relevance or price order). sort still echoes the request, so stale-response checks keep working.

Metafield filtering repeats filter_metafield. The value is namespace.key:value, split on the first colon only — a key never contains one, so a value that does survives intact. Separate keys combine with AND.

&filter_metafield=custom.material:linen&filter_metafield=custom.origin:japan

Only metafields the merchant has enabled for filtering in the admin are honoured (text and list types, up to ten).

A filter that does nothing. If you pass a filter_metafield whose key the merchant has not enabled for filtering, the pair is ignored rather than rejected — you get the unfiltered result set. This is deliberate: a shared or bookmarked URL keeps naming a filter the merchant has since switched off, and refusing it would leave shoppers stuck with a filter they cannot clear. While developing, a typo in a key name therefore looks like "everything matched" — check it against a name in the facets first.

What browsing (q=*) cannot do. Browse is a separate path that reads only the products table. Two things behave differently there:

RequestBehaviour when q=*
filter_content_type=article / pageAlways zero results — browse holds products only
facets.tags / content_types / variant_optionsAlways [] (the keys are still present)

Every other filter, and filter_metafield, works exactly as it does with a search term.

To list articles or pages, use type=search with an actual search term.

Errors ​

SituationStatuscode
q missing400query_required
q longer than 200 characters400query_too_long
type other than search / suggest / preview400invalid_type
contract other than v1400unsupported_contract
contract=v1 on a type other than search / reviews400unsupported_contract_type
Plan below Pro403plan_required
Search infrastructure failure503Normal response body (degraded: true)

A bad request (4xx) returns { error, code } (unsupported_contract also returns the supported versions in supported). Returning it as an empty result set would look like total: 0 and hide the cause. Branch on code.

A problem on our side (503) returns the normal response shape, so an outage does not also throw an exception inside your data.results loop.

Reference implementation ​

A working minimum lives at search-api-sample.js. The rendering is meant to be replaced with your own, but three things should stay:

  1. Always send contract=v1 — this is what asks for the documented shape
  2. Check degraded — every new store goes through that path on day one
  3. Both Shopify.analytics.publish calls — without them, search-attributed revenue is never recorded

What your code is responsible for ​

Because we ship no JavaScript, these are yours:

  • Debouncing autocomplete (250–300ms recommended). One type=suggest per keystroke reaches the rate limit quickly
  • Ordering of responses. Fast typing makes responses arrive out of order. The response echoes back query / offset / limit / sort from the request, so you can use them to discard a stale response
  • Suppressing repeat requests for identical parameters

2. Paging ​

The response is self-contained: it tells you what to ask for next.

FieldPurpose
totalTotal number of hits
offsetWhere this response starts
limitHow many results this response holds
js
// "Load more" / infinite scroll
const MAX_OFFSET = 2000; // as in §1, larger values are clamped to 2000
const next = data.offset + data.results.length;
if (next < data.total && next < MAX_OFFSET) {
  // fetch with offset=next and append
}

Do not test next < data.total alone. Past the offset ceiling the server keeps returning the same page, so if total is larger your "load more" button never disappears and the same products are appended over and over. Always compare against the ceiling too.

Numbered pages, "load more" and infinite scroll are all expressible with offset and limit alone.

Note: because paging is offset-based, products added, removed or restocked while a shopper is paging can make a product appear twice or be skipped. Infinite scroll shows this most. Talk to us if you need stricter consistency.

3. Rate limits ​

Requests are capped per 10-second window. Exceeding a cap returns HTTP 429.

TypePer shopPer visitorAll stores combined
type=search50 / 10s20 / 10s125 / 10s
type=suggest150 / 10s40 / 10s150 / 10s
type=reviews100 / 10s30 / 10s150 / 10s

The type=reviews limits are shared with the popular searches, recommendations and the like that Marutto Search's widget reads: the same visitor's calls to those count together.

Ordinary shopper behaviour does not reach these, but autocomplete without debouncing reaches the per-visitor cap easily. On a 429, retry after a pause or leave the previous results on screen.

About "all stores combined". Marutto Search runs as a single instance serving every store, so a request inside your shop's own cap can still be refused when concurrent load from other stores pushes the combined counter over. In other words, a 429 you cannot predict from your own request rate is possible. We have never reached this ceiling in production, but it is part of the specification. Tell us in advance if you plan a load test or any short burst of heavy traffic.

4. What the response guarantees ​

Top-level keys are always present ​

Even with zero results, and even when something failed internally, results, facets, total and the rest are never omitted: results becomes [], total becomes 0, and facets keeps every key inside it. An expression like data.facets.tags.forEach(...) will not throw under some conditions and not others.

Search degrades temporarily ​

While a store's first sync is running, or while translations are syncing on a multilingual store, the index is not usable yet. During that window degraded is set and the results are either empty or Shopify's own search results.

json
{
  "schemaVersion": "v1",
  "degraded": true,
  "degraded_reason": "paid_sync_pending",
  "total": 0,
  "results": [],
  "facets": {
    "vendors": [],
    "product_types": [],
    "tags": [],
    "content_types": [],
    "variant_options": [],
    "metafields": [],
    "ratings": [],
    "price_range": null,
    "on_sale_count": 0
  }
}
js
if (data.degraded) {
  // e.g. show "search is being prepared", or fall back to Shopify's own search
}

Every store goes through this on day one. A new install stays degraded for a while after installation, so an integration that ignores degraded looks like "it doesn't work right after installing". Please handle it.

degraded_reason identifies the cause, and the set of values will grow. Branch on degraded (the boolean); keep degraded_reason for display and logs.

5. Example response ​

A search for "dress" on an apparel store. All values are illustrative.

json
{
  "schemaVersion": "v1",
  "degraded": false,
  "degraded_reason": null,
  "query": "dress",
  "query_id": "q_8f3c1ad24b7e",
  "measurement_proof": "mp_5c19e0b7a3d84f26",
  "total": 137,
  "limit": 24,
  "offset": 0,
  "sort": "relevance",
  "redirect_url": null,

  "results": [
    {
      "content_type": "product",
      "product_id": "gid://shopify/Product/8123456789",
      "title": "Linen Blend Long Dress",
      "handle": "linen-blend-long-dress",
      "url": "/products/linen-blend-long-dress",
      "image_url": "https://cdn.shopify.com/s/files/1/0001/0002/products/linen-dress.jpg",
      "price_min": 12800,
      "price_max": 12800,
      "compare_at_price": 16000,
      "available": true,
      "tags": ["spring", "machine-washable", "new"],
      "metafields": [
        { "name": "custom.material", "label": "Material", "values": ["Linen"] }
      ],
      "rating": { "average": 4.6, "count": 23, "source": "marutto" },

      "variants": [
        {
          "id": "44001",
          "title": "Beige / M",
          "image_url": "https://cdn.shopify.com/s/files/1/0001/0002/products/linen-dress-beige.jpg",
          "price": 12800,
          "compare_at_price": 16000,
          "available": true,
          "options": [
            { "name": "Color", "value": "Beige" },
            { "name": "Size", "value": "M" }
          ]
        },
        {
          "id": "44002",
          "title": "Beige / L",
          "price": 12800,
          "available": false,
          "options": [
            { "name": "Color", "value": "Beige" },
            { "name": "Size", "value": "L" }
          ]
        }
      ]
    },
    {
      "content_type": "article",
      "product_id": "gid://shopify/Article/5500112233",
      "title": "12 Ways to Style a Dress",
      "handle": "dress-styling",
      "url": "/blogs/journal/dress-styling",
      "image_url": "https://cdn.shopify.com/s/files/1/0001/0002/articles/styling.jpg",
      "tags": ["styling"],
      "excerpt": "How to wear one dress through every season.",
      "published_at": "2026-04-18T09:00:00Z",
      "author": "Tanaka",
      "blog_handle": "journal"
    }
  ],

  "facets": {
    "vendors": [
      { "value": "ATELIER NOA", "count": 42 },
      { "value": "LUMIÈRE", "count": 31 }
    ],
    "product_types": [{ "value": "Dresses", "count": 118 }],
    "tags": [
      { "value": "spring", "count": 64 },
      { "value": "machine-washable", "count": 38 }
    ],
    "content_types": [
      { "value": "product", "count": 131 },
      { "value": "article", "count": 5 }
    ],
    "variant_options": [
      {
        "name": "Color",
        "values": [
          { "value": "Beige", "count": 57 },
          { "value": "Black", "count": 49 }
        ]
      }
    ],
    "metafields": [
      {
        "name": "custom.material",
        "label": "Material",
        "values": [{ "value": "Linen", "count": 42 }]
      }
    ],
    "ratings": [
      { "value": "4", "count": 96 },
      { "value": "3", "count": 104 }
    ],
    "price_range": [3900, 42000],
    "on_sale_count": 18
  },

  "suggestions": [],
  "related_keywords": []
}

6. Field reference ​

Top level ​

FieldTypeMeaning
schemaVersionstringVersion of the data structure. "v1" for this one
degradedbooleanWhether the index is temporarily unusable (see §4). Always present
degraded_reasonstring | nullWhy. null when degraded is false — the key is always present
querystringThe search term, exactly as sent
query_idstringIdentifier for this search; clicks are tied to it (see §7)
measurement_proofstringSignature for revenue attribution. Required for search-attributed revenue (§7)
totalnumberTotal hits before paging
limitnumberResults per page in this response
offsetnumberWhere this response starts
sortstringrelevance / price_asc / price_desc / newest / rating
redirect_urlstring | nullDestination of a keyword redirect configured in the admin. When set, navigate instead of rendering results
resultsarrayProducts, articles and pages mixed; tell them apart with content_type
facetsobjectAggregates for filtering (below)
suggestionsstring[]Candidate terms for a zero-result page. Empty array rather than absent
related_keywordsstring[]What other shoppers searched next, on a zero-result page only. Empty array otherwise, never absent

related_keywords is filled only for a search that returned no results, had no collection filter (filter_collection) and triggered no keyword redirect. A search with any results always gets an empty array.

Nothing outside this table is returned. Internal scores and review-matching details change with every improvement, so they are not published. results arrives sorted.

facets ​

FieldTypeMeaning
vendorsarrayCounts by vendor. Always present ([] when nothing matched)
product_typesarrayCounts by product type. Always present
tagsarrayCounts by tag. Always present
content_typesarrayCounts by product / article / page. Always present
variant_optionsarrayCounts by variant axis (colour, size…). Always present
metafieldsarrayCounts by metafield. Always present
ratingsarrayCounts by "N★ and up" (see below). Always present
price_rangenumber[] | nullLowest and highest price. null when nothing matched — never [0, 0]
on_sale_countnumberHow many are on sale. 0 when none

Each entry in facets.metafields[] is { name, label?, values[] }. name is namespace.key; label is the heading the merchant set in the admin and is omitted when unset. Render label ?? name.

Each entry in facets.ratings[] is { value, count }, where value is "4" (4★ and up) or "3" (3★ and up). Counts are cumulative: a 4★ product is counted in 3★ and up too. A row with no products, or with as many as the row above it, is left out — choosing it would change nothing. Pass value to filter_rating as it is. A store with no ratings at all gets [].

Ordering is by count descending, then by value ascending. The same search over the same catalogue always comes back in the same order. Each group holds at most 20 entries.

Counts are taken before filtering, over everything the search terms matched. For a search with terms (q other than *), vendors, product_types, tags, variant_options, metafields, price_range and on_sale_count do not change when filter_* parameters are added. Values that are not selected keep their counts. For the count after filtering, use total.

With a single filter, an unselected value's count is how many results switching to that value would give. With two or more filters combined, the counts leave the other filters out as well, so they do not match what switching would give.

  • content_types alone reflects every filter except filter_content_type, so the tabs for products, articles and pages do not drop to 0 when one of them is selected
  • With filter_collection, the keyword display rules limited to that collection (pin to top, hide) apply, and the counts change with them
  • When browsing everything (q=*), every group counts what is left after filtering

results[] (all content types) ​

FieldTypeMeaning
content_typestringproduct / article / page
product_idstringShopify GID. The key is named this way for every type (historical)
titlestringTitle
handlestringHandle
urlstringRelative URL within the store
image_urlstring?Image URL, when there is one
tagsstring[]Shopify tags. Empty array when there are none — the key is always present

results[] (content_type: "product" only) ​

FieldTypeMeaning
price_min / price_maxnumberPrice range (see below)
compare_at_pricenumber | nullCompare-at price, for sale badges
availablebooleanWhether purchasable stock exists
variants[]array?Variants. id is the numeric id (see below)
metafields[]arrayMetafield values (see below). [] when the product has none — the key is always present
ratingobject | nullRating (see below). null when the product has none — the key is always present

Each entry in metafields[] is { name, label?, values[] }. name is namespace.key; label is the heading set in the admin and is omitted when none is set (as in facets.metafields[]). values is an array of value strings without counts (for example ["愛媛県"]), not the { value, count } entries of facets.metafields[].

  • Only metafields enabled as "Use as a filter" in the admin are included, and only those the product has a value for. Metafields used for search only are not included
  • Entries follow the order they are arranged in the admin; values are sorted by value. A list metafield (list.single_line_text_field) carries several values under one name
  • Values are not translated. On a multilingual store they are the values stored in Shopify, as they are
  • While degraded (degraded: true, when results are empty or come from Shopify's native search), every product has []

rating is { average, count, source }. average is the mean, to two decimal places; count is how many reviews it stands on. The star count ("N★ and up") is average rounded to a whole number.

sourceWhere the rating comes from
maruttoReviews collected by Marutto Search. type=reviews lists them
externalThe rating another review app writes to Shopify's standard rating (reviews.rating). Its reviews are not here, so none are listed
  • A product with even one Marutto Search review is marutto. The two are never combined
  • While degraded, every product has null

Prices are in the shop's base currency. Whether they include tax follows the store's Shopify settings.

On a store using Shopify Markets for multiple currencies, this will not match the currency the shopper is actually seeing. Let us know if multi-currency support matters to you.

Detecting this on a multi-currency store ​

Do not multiply by an exchange rate. Shopify Markets prices are not a conversion: they can be set per country with their own rounding. A computed figure is a wrong price that looks right, which is worse than showing none.

The theme knows both currencies, so just check whether they agree.

js
// Embed the shop's base currency with Liquid
const shopCurrency = "{{ shop.currency }}"; // e.g. JPY — what this API returns
const displayCurrency = Shopify.currency.active; // e.g. USD — what the shopper sees

if (shopCurrency === displayCurrency) {
  render(item.price_min); // safe to render as-is
} else {
  // don't render the price, or fetch it from the Storefront API
}

On a single-currency store — most of them — the two always agree and the else never runs.

variants[].id is the numeric id. While a product's product_id is a gid://shopify/Product/…, a variant id comes back as a numeric string such as "44001", because that is the form Shopify itself requires.

  • Selecting a variant on the product page: /products/xxx?variant=44001
  • Adding to cart (Cart AJAX API): { "id": 44001, "quantity": 1 }

A GID in either place is ignored rather than rejected — the product page simply opens with nothing selected. Build a GID with "gid://shopify/ProductVariant/" + variant.id when you need one.

results[] (content_type: "article" / "page" only) ​

FieldTypeMeaning
excerptstring?Excerpt of the body
published_atstring?Publication time (ISO 8601)
authorstring?Author (articles only)
blog_handlestring?Handle of the parent blog (articles only)

7. Analytics ​

Because Marutto Search does not render the product cards, without this the merchant's search analytics stop working. Click-through rate, the ranking of clicked products, and search-attributed revenue all depend on it.

Measurement travels through Shopify's Web Pixel, so all you do is publish two events. The HTTP requests are made by our own pixel.

Send session_id on the search request (required) ​

Anyone who knows the URL can reach this endpoint, so a request without session_id is treated as not-a-shopper and excluded from search analytics (the results come back normally). Forget it and search works while the merchant's analytics stay empty.

session_id is any string you generate. The format does not matter as long as it is stable for the duration of a session.

js
Shopify.analytics.publish("marutto_search", {
  query: data.query,
  query_id: data.query_id,
  session_id: sessionId, // the same value you sent on the request
  measurement_proof: data.measurement_proof,
  result_count: data.total,
});

2) On a result click ​

js
Shopify.analytics.publish("marutto_result_clicked", {
  query_id: data.query_id,
  session_id: sessionId,
  measurement_proof: data.measurement_proof,
  product_id: item.product_id,
  position: index, // zero-based
});

About measurement_proof ​

Revenue attribution runs on this value. Our Web Pixel stores it when it receives marutto_search and re-attaches it to the subsequent add-to-cart and checkout events, which is how a purchase is traced back to the search that led to it.

Without publishing it, that store records no search-attributed revenue. Search keeps working, so this is easy to miss — please verify it during integration.

8. Versioning ​

  • v1 is maintained for at least 12 months. Removal is announced 6 months ahead
  • Field names, types and meanings published as v1 do not change. The same holds for event names
  • Fields may be added within v1 — existing integrations are unaffected
  • If something must change, it ships as v2 and v1 is kept for the period above

When the work is contracted out, there is often nobody left to touch the code after delivery. The 12-month figure assumes exactly that. Shopify's own API versions run on the same 12-month cycle, so you can align with the updates you are already doing.

9. Where you can use it ​

The Search API is not limited to the search results page. It is same-origin traffic through the App Proxy, so it works from any storefront page.

  • Search results page
  • Filtering on collection pages
  • Related products on a product page
  • "Products matching this keyword" embedded in a campaign or landing page
  • Reviews on a product page (type=reviews)

Checkout is out of scope. Shopify runs checkout extensions in a sandbox with restricted network access, so the App Proxy is not reachable there as same-origin. The thank-you and order-status pages are the same.

Use from outside the store — Hydrogen on your own domain, a native app — is also not covered by the App Proxy and is not offered today. Tell us if you need it.

10. Review list (type=reviews) ​

Returns a product's published reviews, newest first.

GET /apps/marutto-search/search?type=reviews&contract=v1&product_id=gid://shopify/Product/8123456789&limit=10
  • Only reviews collected by Marutto Search are listed. A product whose rating comes from another review app (rating.source is external) has none
  • Like contract=v1 itself, it requires the Pro plan or above; below it you get 403 with {"code": "plan_required"}
  • Newest first only (by the date a review was written). There is no rating order and no star filter, so that the list cannot be used to show only favourable reviews
  • Reviews are not translated. On a multilingual store they come back in the words they were written in (locale has no effect)
  • summary counts the published reviews. A search result's rating is counted from the search index, so right after a review is published or hidden the two can differ until the index catches up
  • No measurement events are needed (chapter 7 is for searches)
  • It answers with Cache-Control: private, no-store. Fetch it each time rather than storing it, so a review the store hid, or erased on the customer's request, does not linger

Query parameters ​

ParameterDefaultMeaning
contract—v1. Required
type—reviews. Required
product_id—The product's GID (gid://shopify/Product/…) or numeric id. Required
limit10How many. 1–20 (anything else becomes 10)
cursor—The next page: the previous response's next_cursor, as it is
sortnewestnewest only. Any other value is 400

Response ​

json
{
  "schemaVersion": "v1",
  "product_id": "gid://shopify/Product/8123456789",
  "summary": {
    "average": 4.6,
    "count": 23,
    "distribution": { "5": 16, "4": 5, "3": 2, "2": 0, "1": 0 }
  },
  "reviews": [
    {
      "id": "cm2r8x1k0000abcd",
      "rating": 5,
      "title": "Keeps its shape",
      "body": "Washed many times and still fits. I wear it spring to autumn.",
      "author_name": "Midori",
      "verification": "shopify_order",
      "incentivized": false,
      "helpful_count": 4,
      "reply": {
        "body": "Thank you for wearing it so often.",
        "replied_at": "2026-05-02T01:00:00.000Z"
      },
      "images": [
        {
          "url": "https://cdn.shopify.com/s/files/1/0001/0002/files/review.jpg"
        }
      ],
      "published_at": "2026-04-30T03:12:00.000Z",
      "created_at": "2026-04-29T22:40:00.000Z"
    }
  ],
  "next_cursor": "eyJzb3J0IjoibmV3ZXN0Ii…"
}
FieldTypeMeaning
product_idstringThe product's GID
summary.averagenumber | nullMean of the published reviews, to two decimals. null with none
summary.countnumberHow many published reviews
summary.distributionobjectHow many per star ("5"–"1")
reviews[].idstringThe review's id
reviews[].ratingnumberStars, 1–5
reviews[].titlestring | nullTitle. null when there is none
reviews[].bodystringThe text (plain text; may contain line breaks)
reviews[].author_namestringThe name the reviewer entered (see below)
reviews[].verificationstringHow the purchase was confirmed (see below)
reviews[].incentivizedbooleanWhether it was written in return for a reward (a coupon, say)
reviews[].helpful_countnumberHow many found it helpful
reviews[].replyobject | nullThe store's reply, { body, replied_at }. null when there is none
reviews[].images[]arrayPhotos, { url } (up to 5). [] when there are none
reviews[].published_atstring | nullWhen it was published (ISO 8601)
reviews[].created_atstringWhen it was written (ISO 8601). The list is in this order
next_cursorstring | nullPass it as cursor for the next page. null on the last page

verification is one of three values. The two kinds of verified purchase mean different things, so show them apart, as Marutto Search's review block does.

ValueMeaningThe review block shows
shopify_orderThe purchase was confirmed against this store's ordersVerified purchase
merchant_importImported, with the purchase confirmed by a previous review serviceVerified purchase (imported)
noneThe purchase was not confirmed(nothing)

When you display reviews ​

  • Titles, bodies, names and replies are text written by shoppers and the store. Escape them rather than inserting them as HTML (use textContent, for example). Line breaks in the body arrive as \n
  • Always mark a review whose incentivized is true as written for a reward. This follows stealth-marketing rules; Marutto Search's own review block cannot hide that mark either
  • Do not choose reviews by their stars. Show them newest first. Showing only favourable reviews can fall foul of Japan's stealth-marketing rules (the Act against Unjustifiable Premiums and Misleading Representations) and the US FTC's rules. Marutto Search's review feature is built the same way (Reviews)
  • author_name is the name the reviewer entered; an imported review may carry a real name. Marutto Search's review block shows it publicly too. Whether to show it is the store's call

Errors ​

Request errors return { error, code }.

SituationStatuscode
product_id missing or malformed400invalid_product_id
sort other than newest400invalid_sort
filter_rating given400unsupported_filter
cursor broken, or from another list400invalid_cursor
Plan below Pro403plan_required
Temporarily unreadable503reviews_unavailable / settings_unavailable

Marutto Search