Storefront API
MarkdownThe endpoints called by the store page and the store's server.
Version v1. The Sayproof API address to put in front of the endpoint paths is shown on the Integration screen in the panel.
Authentication
Section titled “Authentication”| Caller | Method |
|---|---|
| The browser (the store page) | The site key: the ?key= parameter for read requests, the X-Sayproof-Site-Key header for write requests. The request must come from a verified domain: the domain is read from the Origin header, or from the Referer header when Origin is missing. |
| The store’s server | The Authorization: Bearer <server key> header and the ?channel=<channel code> parameter |
No endpoint uses the browser session (cookies); requests are sent without cookies.
Common rules
Section titled “Common rules”- Request and response bodies are JSON.
- The error response format is the same on all endpoints:
{"error": "<code>", "detail": "<code>"}.detailis also a code, not a sentence; convert it into the text shown to users on your side. For the list of codes, see Error codes. - The language is determined, in order, from the
languagefield of the body (write requests), the?lang=tr|enparameter or the channel’s language. TheAccept-Languageheader is not taken into account. If an unsupported language is requested, the channel’s language is used. - Every response carries the
X-Request-Idheader; include this value in support requests. - For problems with the key, channel or domain, a
403 refusedresponse is returned anddetailgives the reason (for exampleorigin_unverified: www.yourbrand.com,unknown_site_key,channel_inactive). Theorigin_unverifieddetail carries the domain the request came from. When the rate limit is exceeded,429 rate_limitedis returned.
Review list
Section titled “Review list”GET /v1/reviews/list — returns the published reviews, the summary and the required elements.
| Parameter | Default | Rule |
|---|---|---|
key |
The site key | |
product |
Required; up to 128 characters | |
scope |
variant |
variant or group |
sort |
The market’s default | A value outside the permitted list receives a 400 sort_not_allowed response |
page, page_size |
1, 10 |
page 1–1000, page_size 1–50; out-of-range values are moved to the nearest limit |
rating |
1–5; an out-of-range value receives a 400 invalid / rating response |
|
verified |
1: only reviews with proof of purchase |
|
media |
1: only reviews containing a photo or video |
|
q |
Search in the title and text; up to 100 characters. All words must appear. Letter case and the Turkish ı/i distinction are ignored. | |
topic |
A single word (up to 40 letters); reviews containing a word that starts with this word (comfort → comfortable) |
|
lang |
The channel’s language | tr or en |
via |
The script sends script. If you read reviews with your own code, do not send it; the panel’s Store appearance screen counts those pages as your site’s own design. |
Response:
{ "reviews": [ { "id": "…", "rating": 5, "title": "…", "body": "…", "author": "Jane D.", "anonymous": false, "language": "en", "published_at": "…", "verified": true, "verification_method": "tokenized_invite", "helpful_count": 3, "unhelpful_count": 1, "aspects": [{ "key": "size", "label": "Size", "scale": "fit", "value": 2, "position": 0.5, "step_label": "Adequate" }], "aspect_scores": { "size": 2 }, "aspect_template": "footwear", "media": [{ "id": "…", "kind": "image", "url": "…", "srcset": "… 320w, … 640w, … 1280w", "width": 1280, "height": 960, "alt_text": "", "poster_url": "" }], "seller_response": { "body": "…", "published_at": "…" }, "resolution": { "body": "…", "created_at": "…" }, "origin": "own", "origin_market": "", "experience": "product", "translations": {} } ], "total": 200, "page": 1, "pages": 20, "page_size": 10, "sort": "date", "shared": 0, "ai_summary": null, "sorts": [{ "code": "date", "label": "Newest" }, { "code": "rating", "label": "By rating" }, { "code": "seller", "label": "With a seller response" }], "aggregate": { "count": 200, "average": 4.1, "stars_filled": 4, "distribution": { "1": 5, "5": 120 }, "verified_count": 200 }, "aspects": [{ "key": "size", "label": "Size", "scale": "fit", "steps": ["Low", "Adequate", "High"], "average": 2.1, "position": 0.55, "step_label": "Adequate", "count": 48 }], "topics": [{ "word": "comfortable", "count": 31 }], "gallery": [{ "id": "…", "kind": "image", "url": "…", "srcset": "…", "width": 1280, "height": 960, "alt_text": "", "poster_url": "", "review_id": "…", "rating": 5, "author": "Jane D.", "anonymous": false }], "media_reviews": 14, "style": "cards", "theme": "warm", "applied": { "q": "", "topic": "", "rating": null, "media": false, "verified": false }, "core": { "method_statement": { "url": "…", "label": "How we collect reviews", "required": true }, "sort": { "allowed": ["date", "rating", "seller"], "default": "date", "locked": true }, "verification_badge": { "label": "Verified buyer", "required": true }, "source_label_required": true, "streams_together": true, "resolution_inline": true, "aggregate": { "…": "…" }, "policy_version": 1, "market": "TR" }, "language": "en", "words": { "verified": "Verified buyer", "…": "…" }, "report_reasons": [{ "code": "suspected_fake", "label": "…" }], "questions_enabled": false, "privacy_url": "https://yourbrand.com/privacy-notice", "limits": { "media_count": 6, "title_length": 200, "body_length": 5000, "name_length": 120, "alt_text_length": 300, "image_bytes": 12582912, "video_bytes": 209715200, "video_seconds": 60, "reviews_per_call": 20, "order_reference_length": 128, "email_length": 254, "report_note_length": 1000 }}aggregate.stars_filled: the number of filled stars; halves are rounded up (4.5 → 5).aspects(top level): the averages of rating topics with a stepped scale.positionis between 0 and 1, andstep_labelis the label of the nearest step. Filters do not change these values.topics: up to 5 words that appear most often in reviews, and the number of reviews each appears in. Words must have at least 4 letters and appear in at least 2 reviews; the values can be used directly in thetopicparameter.gallery: up to 12 fully processed photos or videos attached to the product’s published reviews, starting from the newest review. Each item carries themediafields and thereview_id,rating,authorandanonymousvalues of the review it belongs to. Filters do not change this list.media_reviews: the number of published reviews containing a photo or video; the number of reviews themedia=1filter returns on its own. Filters do not change this value.style: the layout chosen for the channel in the panel,cardsorsidebar.theme: the design chosen for the channel in the panel:warm,trust,editorial,minimal,soft,bold,naturalornight.applied: the filters applied.experience:product(a product review) orprocess(the experience, up to that point, of a customer whose order was cancelled or who returned the item).processreviews must be shown with thewords.experience_processlabel; the default component shows this label itself.limits: the limits the review form applies: the number of files (media_count), text lengths (title_length,body_length,name_length,alt_text_length), file sizes in bytes (image_bytes,video_bytes), the video length in seconds (video_seconds), the number of products per call (reviews_per_call), the order number and email lengths (order_reference_length,email_length) and the report note length (report_note_length).core.method_statement.url: the address of the method statement that applies to the request’s channel and language (your own page linked for the channel, or Sayproof’s page).- The response carries the
Cache-Control: private, max-age=30header. questions_enabled: whether the brand’s questions and answers are on; see Questions and answers.privacy_url: the privacy notice link set in the panel for the request’s channel; empty if not set. The script shows this address below the review, question and answer forms as a Privacy notice on the processing of personal data link (words.privacy_notice).origin,origin_market:ownis the brand’s own review in this market. If cross-market sharing is on for your brand (Settings › Publishing › Reviews from other markets), the list also carries proof-of-purchase reviews of the same product from your other markets: on these rowsoriginissyndicatedandorigin_marketis the market they came from. They do not count in the average, the rating distribution or the structured data;sharedsays how many there are in the list.origin_label,origin_url: the ready label text on every review whose source is notown(the server writes it in the visitor’s language with the right suffixes; the script only shows it). On a review imported from a marketplace (origin: "imported") the label reads like “Bought on Trendyol · Imported automatically from Trendyol”,origin_urlis the address of the review at the source (empty if unknown),verifiedis alwaysfalseandverification_methodis empty. These reviews are left out of theverified_onlyfilter, count in the average, the rating breakdown and the number of reviews, and are left out of the structured data and of the Google product reviews file. For details see Marketplace reviews.aggregate.imported_count,aggregate.imported_label: the number of imported reviews and the ready text of the summary line (“3 of 4 reviews imported from Trendyol”);0and an empty text when nothing is imported.aggregate.verified_countdoes not count imported reviews.translations(arrives with v2; currently an empty object{}on every review): if translation is on in the AI package, the review’s translations into other languages, keyed by language code:{"en": {"title", "body"}}. When a review is not in the visitor’s language, the script shows the translation and adds a link to show the original.ai_summary(arrives with v2; currently alwaysnull): if review summaries are on in the AI package and the product has enough reviews, the summary:text, the number of reviews it is based on (review_count), when it was made (created_at) and the main topics (topics[]:topic,sentiment,count); otherwisenull. The default component shows the summary with its label; the summary section cannot be hidden.
Server-side rendering (SSR)
Section titled “Server-side rendering (SSR)”GET /v1/ssr — returns the ready-made HTML, the state field with the same content as the list response, and the structured data. Parameters: product, channel, scope, name, lang, sort, style (cards or sidebar; any other value receives a 400 invalid / style response), theme (warm, trust, editorial, minimal, soft, bold, natural or night; any other value receives a 400 invalid / theme response). For usage and the response fields, see Server-side rendering (SSR).
- The response carries the
ETagandCache-Controlheaders:private, max-age=60for a call made with a server key,public, max-age=60for a call made with a site key. For a request sent withIf-None-Match,304is returned if the content has not changed. - If the channel has no site key in use,
409 refused / no_site_keyis returned.
Signed-in customers
Section titled “Signed-in customers”GET /v1/viewer — can only be called with a server key. The customer’s email address is sent in the X-Sayproof-Customer header. Parameters: product, scope, lang, channel. The state value in the response is can_write, written or none; the response also contains reviews, limits, the token and products fields if the customer can write a review, and the asker field (token, name) if the brand has turned on questions and answers. The name shown with a question is sent in the optional X-Sayproof-Customer-Name header, percent-encoded as UTF-8. The response carries the Cache-Control: no-store header. For details, see Signed-in customers.
Reviews on the order page
Section titled “Reviews on the order page”GET /v1/viewer/order — returns the items of the order and the review state of each one, for the signed-in customer’s order detail page. It can only be called with a server key; a call from the browser with a site key receives a 403 refused / server_only response. The customer’s email address is sent in the X-Sayproof-Customer header. Parameters: order (the order number, up to 128 characters), channel, lang.
{ "order": "100245", "token": "…", "lines": [ { "line_reference": "1", "variant_key": "PRODUCT-38", "group_key": "PRODUCT", "title": "Running shoe", "aspects": [{ "key": "size", "label": "Size", "kind": "product", "required": false, "scale": "fit", "steps": ["Low", "Adequate", "High"] }], "state": "can_write" }, { "line_reference": "2", "variant_key": "SOCK-2", "group_key": "SOCK", "title": "Running socks", "aspects": [], "state": "pending", "rating": 4 } ], "limits": { "media_count": 6, "body_length": 5000, "…": "…" }, "language": "en", "theme": "warm", "privacy_url": "https://yourbrand.com/privacy-notice", "words": { "order_title": "Rate your items", "…": "…" }}lines[].state:can_write,pending,published,rejected,removed,waitingorclosed. For their meanings, see Signed-in customers › Order detail page.privacy_url: the channel’s privacy notice link; empty if not set. The script shows the link below the forms.token: a single writing token for the lines that can be reviewed; empty if no line can be reviewed.aspectsis filled only oncan_writelines;ratingis present only on lines that have been reviewed.- If the order is not found or belongs to another customer, the response is still
200andlinesis empty; the response does not reveal whether the order exists. - If
orderis not sent,400 invalid / orderis returned; if theX-Sayproof-Customerheader is missing or is not a valid address,400 invalid / customer. - The response carries the
Cache-Control: no-storeheader. The rate limit is shared withGET /v1/viewer.
“Helpful” vote
Section titled ““Helpful” vote”POST /v1/reviews/<id>/helpful — body: {"vote": "up" | "down" | "none", "visitor_key": "…"}.
{ "vote": "down", "counted": true, "helpful_count": 3, "unhelpful_count": 1 }- Each visitor can cast one vote per review. If a vote in the same direction is cast again,
counted: falseis returned; if the direction changes, the vote moves to the new direction;nonewithdraws the vote (vote: null). visitor_keyis only used to remove duplicates; the rate limit is applied per IP address.
Reporting a review
Section titled “Reporting a review”POST /v1/reviews/<id>/report — body: {"reason": "…", "note": "…", "visitor_key": "…"}. The reason is chosen from the report_reasons field of the list response; a value outside the list receives a 400 invalid_reason response. The note can be up to 1000 characters. A successful request returns 201 {"id"}. Only published reviews can be reported. If the same visitor (visitor_key, or the IP address if it is not sent) already has an open report on the same review, no new record is created; the response returns the id of the existing report.
Product views
Section titled “Product views”POST /v1/views?key=… — the body is JSON sent with the text/plain content type: {"product": "…", "scope": "variant" | "group"}. The script also reports how the reviews were shown: shown (default for the default component, coded when the look is chosen in code, headless), style and theme when chosen in code, and the review area’s width in pixels as width. Sayproof only keeps the width as one of four ranges; when these fields are missing or invalid, only the view is counted. A successful request returns 204 without a body. The script sends this request itself; in headless use too, you do not need to send it separately as long as the script is loaded on the page. Views from automated clients (bots, link preview tools) and prefetch requests are not counted. In-app browsers (for example Pinterest, Telegram, Slack, Discord) and phone brands whose name contains “bot”, such as CUBOT, are counted. On a prerendered page, the script reports the view when the page is actually shown.
Method statement
Section titled “Method statement”GET /v1/method-statement?key=…&lang=tr|en — a public page; it does not require domain verification (it opens in a new tab). With the format=json parameter, it is returned in a machine-readable format. The page shows the statement of the site key’s channel; the text is built from that channel’s rules and publishing method and is the same text the panel shows for that channel.
Review writing flow endpoints
Section titled “Review writing flow endpoints”The script uses the endpoints below. Because the review writing flow stays in the script in headless use as well, you do not need to call these endpoints directly.
| Endpoint | Purpose |
|---|---|
POST /v1/eligibility |
Checks eligibility with the order number and email address. If the customer is eligible, a single-use writing token is returned; otherwise a single, general refusal is returned. |
POST /v1/eligibility/token |
Verifies the token in an invitation, shopping experience invitation or correction link. The mode value in the response is write, feedback or revise. The products in a feedback answer contain no rating topics. A revise answer contains the review to be corrected, the name the writer entered (review.author_name), whether the name is hidden on the site (review.hide_name) and the files attached to it (review.media: id, kind). If the link is opened on another market’s channel, 403 token_refused / token_foreign_market is returned. |
POST /v1/media |
Uploads a photo or video (jpeg, png, webp: up to 12 MB; mp4, mov: up to 200 MB and 60 seconds). The file goes through a security scan; location and device information is removed. A video longer than the limit is refused at upload: 422 refused / video_too_long: 60. A video larger than 3840×2160 (2160×3840 portrait), with more than 60 frames per second or not encoded with H.264, HEVC, MPEG-4, VP9 or AV1 is also refused. The answer carries no address for the file; the file is at a public address only while the review it is added to is published. |
POST /v1/reviews |
Submits reviews (up to 20 products per call). Accepts product invitation and shopping experience invitation tokens; a review written with a shopping experience invitation token is saved as experience: "process". Each row can carry the optional author_name (at most 120 characters) and hide_name (true or false; default false) fields. With hide_name: true the name shows on the site, in structured data and in the Google file as the first letter of every word followed by *** (A*** K*** instead of Ayşe Kaya); the full name is visible only in the brand’s panel. A hide_name that is not a boolean gets 400 invalid / hide_name. |
POST /v1/reviews/revise |
Submits a rejected review again in its corrected form. kept_media in the body is the list of IDs of the existing files that stay on the review; if the field is not sent, all existing files stay. Kept and new files together can be at most 6. author_name and hide_name, when sent, replace the values on the review (an empty author_name removes the name); when omitted, the current values stay. |
For the rate limit values, see Limits.
Questions and answers
Section titled “Questions and answers”Available once the brand turns on Settings › Publishing › Questions and answers in the panel; while it is off, all three endpoints return 403 refused / questions_disabled and questions_enabled in the list response is false. The script loads the section only when this value is true.
| Endpoint | Purpose |
|---|---|
GET /v1/questions |
Returns the product’s published questions and each question’s published answers. Parameters: key, product (required), scope (variant or group), page (1–1000), page_size (default 5, at most 20), lang. Can also be read with the server key and the channel parameter. |
POST /v1/questions |
Saves a visitor’s question. Body: product, scope, name (optional, at most 120 characters), email (optional; without it no email goes out when an answer arrives), body (required, at most 1000 characters), hide_name (optional boolean; with true the name shows on the site as the first letter of every word followed by ***), language. For a signed-in customer, the asker.token value from the GET /v1/viewer response is sent in the asker field instead of name and email; the name and email address are read from the token. No purchase is needed. A successful call returns 201 {"id", "state"}. |
POST /v1/questions/<id>/answers |
Saves the answer of a customer who bought the item. The writing token is sent in the X-Sayproof-Token header; a token from an invitation or from order verification is accepted, and having used it to write a review does not stop it from being used to answer. Body: body (required, at most 2000 characters), name (optional), hide_name (optional boolean; with true the name shows on the site as the first letter of every word followed by ***), language. A successful call returns 201 {"id", "state"}. |
GET /v1/questions response:
{ "questions": [ { "id": "…", "author": "Sam", "body": "Does it fit wide feet?", "published_at": "…", "answers": [ { "id": "…", "kind": "brand", "author": "Sample Brand", "verified": false, "body": "…", "published_at": "…" }, { "id": "…", "kind": "buyer", "author": "Alex", "verified": true, "body": "…", "published_at": "…" } ] } ], "total": 4, "page": 1, "pages": 1, "page_size": 5, "limits": { "question_length": 1000, "answer_length": 2000, "name_length": 120, "email_length": 254 }, "language": "en"}- Questions are pooled by market: questions asked on the channels of the same market are shown together. The most recently published question comes first; answers are listed in the order they went live.
kind:brand(the brand’s answer;authoris the channel’s brand name) orbuyer(the answer of a customer who bought the item;verified: true, shown with the “Verified buyer” label). The response never carries the email address of a question or an answer.- A question and a customer’s answer are approved before they go live. Text with personal information, swearing or a health claim is not refused; it waits for approval. If the publishing method is Automatic or Hybrid and the text passes these checks, it goes live by itself.
- Missing details return
400 invalidwith the reason indetail:body_required,body_too_long,email_invalid,email_too_long,name_too_long,asker_too_long,product_required,product_too_long. - If the
askertoken has expired (after 1 hour),403 token_refused / asker_expiredis returned; if it is not valid or belongs to another brand,403 token_refused / asker_invalid. - If the token’s order does not contain the question’s product (the same variant or the same product group),
403 token_refused / not_a_buyeris returned. Without a token401 token_requiredis returned; if the question is not live or belongs to another market,404 not_foundis returned. - When an answer goes live and the question has an email address, the person who asked gets an email in the brand’s name. If the address is on the brand’s do-not-email list, no email is sent.
- The
GET /v1/questionsresponse carries theCache-Control: private, max-age=30header.
Example requests
Section titled “Example requests”Trying the review list from the command line (the Origin header is added as if the request came from a verified domain):
curl -s "https://api.sayproof.example/v1/reviews/list?key=YOUR-SITE-KEY&product=PRODUCT-38&sort=date&page=1&lang=en" \ -H "Origin: https://www.yourbrand.com"Reporting a review from the browser:
export async function reportReview(reviewId, reason, note) { const answer = await fetch(`https://api.sayproof.example/v1/reviews/${encodeURIComponent(reviewId)}/report`, { method: "POST", credentials: "omit", headers: { "Content-Type": "application/json", "X-Sayproof-Site-Key": "YOUR-SITE-KEY" }, body: JSON.stringify({ reason, note }), }); if (answer.status === 201) return answer.json(); const problem = await answer.json().catch(() => ({})); throw new Error(problem.detail || problem.error || `sayproof_report_${answer.status}`);}