Signed-in customers
MarkdownOpening the review form for a customer signed in to your site without asking for an order number, asking questions without typing a name or email address, and reviewing the items on the order detail page.
If the customer is signed in to your site, they can be allowed to write a review without being asked for an order number and email address. To do this, your server asks Sayproof, using the signed-in email address, whether the customer can review this product, and passes the answer to the script.
While questions and answers are on, a signed-in customer does not type a name or email address when asking a question either: Questions and answers. On the order detail page, the items of the order can also be reviewed inside the page: Order detail page.
The core rule does not change: the right to write a review is determined by the order record in Sayproof, not by the browser. The session only establishes who the customer is.
How it works
Section titled “How it works”- The
data-review-viewer="pending"attribute is added to the container. Until the answer arrives, the script does not show the review writing area. - The browser asks an endpoint on your site (for example
/api/sayproof/viewer) for the customer’s status for the product. - Your server reads the signed-in email address and asks Sayproof using the server key:
GET /v1/viewer. - The answer returns to the browser, and the page passes it to the script with
Sayproof.update(container, { viewer }).
The customer’s status is not added to the page’s HTML; it is personal and is queried each time without caching. This allows the product page itself to remain cached.
Answers
Section titled “Answers”state |
Meaning | What the customer sees |
|---|---|---|
can_write |
They have a delivered purchase that has not yet been reviewed | A “Write a review” button; the form opens without an order check |
written |
They have already reviewed this product | The status of their own review: published, under review, rejected or removed |
none |
They have no purchase they can review | The same verification area as a visitor who is not signed in |
anonymous |
No session, or no answer could be obtained | The same verification area as a visitor who is not signed in |
Only orders in the channel’s market are taken into account. The email address is converted to lower case and matched exactly.
Server side
Section titled “Server side”The call to Sayproof:
GET https://api.sayproof.example/v1/viewer?product=PRODUCT-CODE&scope=variant&lang=en&channel=webAuthorization: Bearer YOUR-SERVER-KEYX-Sayproof-Customer: jane@example.com- The email address is only sent in the
X-Sayproof-Customerheader; it is never added to the URL. - This endpoint only works with a server key. A call made from the browser with a site key receives a
403 refused / server_onlyresponse. - A server key can make 6,000 calls in 5 minutes. Because all customer requests come through your server, customers do not affect each other’s limits.
- The response carries the
Cache-Control: no-storeheader; do not cache it on your side either.
A Next.js Route Handler example. Take the session from your own authentication library; auth() here is only an example:
import "server-only";import { auth } from "@/auth";
export const dynamic = "force-dynamic";
const base = process.env.SAYPROOF_BASE || process.env.NEXT_PUBLIC_SAYPROOF_BASE;const noStore = { "Cache-Control": "no-store" };const anonymous = () => Response.json({ state: "anonymous" }, { headers: noStore });
export async function GET(request: Request) { const asked = new URL(request.url).searchParams; const product = asked.get("product"); const email = (await auth())?.user?.email; if (!product || !email) return anonymous(); const query = new URLSearchParams({ product, scope: asked.get("scope") === "group" ? "group" : "variant", lang: asked.get("locale") ?? "en", channel: process.env.SAYPROOF_CHANNEL ?? "web", }); try { const answer = await fetch(`${base}/v1/viewer?${query}`, { headers: { Authorization: `Bearer ${process.env.SAYPROOF_SSR_KEY}`, "X-Sayproof-Customer": email, }, cache: "no-store", signal: AbortSignal.timeout(3000), }); return answer.ok ? Response.json(await answer.json(), { headers: noStore }) : anonymous(); } catch { return anonymous(); }}The same three rules apply in any other server language: read the email address from the session, not from the browser; return anonymous on failure; do not cache the answer.
Page side
Section titled “Page side”<div id="reviews" data-review-mount data-review-product="PRODUCT-CODE" data-review-viewer="pending"></div>function whenSayproofReady(callback) { if (window.Sayproof) callback(window.Sayproof); else window.addEventListener("sayproof:ready", () => callback(window.Sayproof), { once: true });}
async function askViewer(container) { const query = new URLSearchParams({ product: container.dataset.reviewProduct, scope: container.dataset.reviewScope ?? "variant", locale: document.documentElement.lang || "en", }); try { const answer = await fetch(`/api/sayproof/viewer?${query}`, { credentials: "same-origin", cache: "no-store", signal: AbortSignal.timeout(8000), }); return answer.ok ? await answer.json() : { state: "anonymous" }; } catch { return { state: "anonymous" }; }}
const container = document.querySelector("#reviews");askViewer(container).then((viewer) => whenSayproofReady((sayproof) => sayproof.update(container, { viewer })));Even if no answer ever arrives, pass a value to the script: { state: "anonymous" } or viewer: null. Otherwise the review writing area remains in its waiting state.
After a review is submitted
Section titled “After a review is submitted”When a submission succeeds, the script updates the customer’s status to written by itself; the form does not open again on the same page. If you want to refresh your own list, listen for the sayproof:submitted event emitted by the container: JavaScript API.
Questions and answers
Section titled “Questions and answers”If the brand has turned on questions and answers, the GET /v1/viewer answer also carries an asker field, whatever the state value is. When the script sees this field, it does not show the name and email fields in the question form and shows the line “You are asking as Ayşe K. We will email you when it is answered.” instead. The question is saved with the signed-in email address, and that address is told when an answer goes live.
{ "state": "none", "reviews": [], "limits": { "…": "…" }, "asker": { "token": "…", "name": "Ayşe K." }}asker.token: a signed token bound to your brand and valid for 1 hour. The script sends the question with this token; Sayproof reads the email address and the name from the token, and the browser does not send the email address. Do not read or change the token’s content in your own code.asker.name: the name shown next to the question. It is sent with the optionalX-Sayproof-Customer-Nameheader; percent-encode the value as UTF-8 for non-ASCII characters (encodeURIComponentin JavaScript). Spaces are tidied and at most 120 characters are kept. Without the header,nameis empty, the form says “You are asking with your account. We will email you when it is answered.” and the question shows as “Anonymous”. The name is visible to everyone on the page; send a short form such as “Ayşe K.” rather than the full name.- If the token has expired or is not valid, the question receives a
403 token_refused / asker_expiredorasker_invalidresponse. The script then turns the form into the form of a visitor who is not signed in, keeping the question already written.
GET https://api.sayproof.example/v1/viewer?product=PRODUCT-CODE&scope=variant&lang=en&channel=webAuthorization: Bearer YOUR-SERVER-KEYX-Sayproof-Customer: jane@example.comX-Sayproof-Customer-Name: Jane%20D.In the Next.js example above, adding "X-Sayproof-Customer-Name": encodeURIComponent(name) to the headers is enough; read the name from the session like the email address.
Order detail page
Section titled “Order detail page”On the order detail page in your account area, a rating and review form can be shown inside the page for each item of the order. No dialog opens and no order number is asked for; each item has its own Send button. Your server asks Sayproof with the signed-in email address and the order number and adds the answer to the page’s HTML; the script builds the section from this answer.
GET https://api.sayproof.example/v1/viewer/order?order=100245&channel=web&lang=enAuthorization: Bearer YOUR-SERVER-KEYX-Sayproof-Customer: jane@example.comEach line in the answer is an item of the order and carries a state value:
state |
Meaning | What the customer sees |
|---|---|---|
can_write |
The item has been delivered and not yet reviewed | Stars, rating topics if defined for the product, review text, the photo and video area and the row’s Send button |
pending |
A review has been sent and is under review | “Your review is in; it goes live after moderation.” |
published |
The review is live | “Your review of this item is live. Thank you!” |
rejected |
The review was not published | “Your review was not published. You can correct it and send it again with the link in the email we sent you.” |
removed |
The review is not live at the moment | “Your review of this item is not live at the moment.” |
waiting |
The delivery has not been confirmed yet | “You can rate this item once it has reached you.” |
closed |
A review can no longer be written for this item (for example the time to write has run out, or the item was cancelled or returned) | “A review cannot be written for this item.” |
If the order is not found or belongs to another customer, the response is 200 and lines is empty; the script then draws nothing and the container stays empty. The rules of the product page call apply here too: read the email address from the session, call only with the server key (a call from the browser receives 403 refused / server_only), and do not cache the answer. The rate limit is shared with GET /v1/viewer.
A server-side function that writes the answer safely into a <script type="application/json"> tag:
import "server-only";
const base = process.env.SAYPROOF_BASE || process.env.NEXT_PUBLIC_SAYPROOF_BASE;const unsafe: Record<string, string> = { "<": "\\u003c", ">": "\\u003e", "&": "\\u0026", "
": "\\u2028", "
": "\\u2029" };
export async function sayproofOrderState(order: string, email: string | null | undefined, locale: string) { if (!order || !email) return null; const query = new URLSearchParams({ order, channel: process.env.SAYPROOF_CHANNEL ?? "web", lang: locale }); try { const answer = await fetch(`${base}/v1/viewer/order?${query}`, { headers: { Authorization: `Bearer ${process.env.SAYPROOF_SSR_KEY}`, "X-Sayproof-Customer": email }, cache: "no-store", signal: AbortSignal.timeout(3000), }); if (!answer.ok) return null; const state = await answer.json(); if (!state.lines.length) return null; const json = JSON.stringify(state).replace(/[<>&
]/g, (character) => unsafe[character]); return `<script type="application/json" data-review-order-state>${json}</script>`; } catch { return null; }}On the page, the tag is written inside a data-review-order element whose value is the order number; the Sayproof script must also be on the page. The script finds the element by itself:
<div data-review-order="100245"> <script type="application/json" data-review-order-state>{"order":"100245","token":"…","lines":[…]}</script></div>If the page is built in the browser, the same answer is passed with window.Sayproof.mountOrder(element, answer): JavaScript API. Each item is sent separately; when a submission succeeds, the row changes to the pending state and the element dispatches the sayproof:submitted event with detail: { order, line }. If the customer tries to send without giving stars, the message “Please add a star rating before sending.” is shown. For the elements and states, see DOM attributes › Reviews on the order page.