# Signed-in customers

> Opening 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.

Source: /en/integrate/signed-in-customers/

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](#questions-and-answers). On the order detail page, the items of the order can also be reviewed inside the page: [Order detail 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

1. The `data-review-viewer="pending"` attribute is added to the container. Until the answer arrives, the script does not show the review writing area.
2. The browser asks an endpoint on **your** site (for example `/api/sayproof/viewer`) for the customer's status for the product.
3. Your server reads the signed-in email address and asks Sayproof using the server key: `GET /v1/viewer`.
4. 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

| `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

The call to Sayproof:

```http
GET https://api.sayproof.example/v1/viewer?product=PRODUCT-CODE&scope=variant&lang=en&channel=web
Authorization: Bearer YOUR-SERVER-KEY
X-Sayproof-Customer: jane@example.com
```

- The email address is only sent in the `X-Sayproof-Customer` header; 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_only` response.
- 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-store` header; 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:

```ts
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

```html
<div id="reviews"
     data-review-mount
     data-review-product="PRODUCT-CODE"
     data-review-viewer="pending"></div>
```

```js
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

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](/en/reference/javascript-api.md).

## 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.

```json
{
  "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 optional `X-Sayproof-Customer-Name` header; percent-encode the value as UTF-8 for non-ASCII characters (`encodeURIComponent` in JavaScript). Spaces are tidied and at most 120 characters are kept. Without the header, `name` is 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_expired` or `asker_invalid` response. The script then turns the form into the form of a visitor who is not signed in, keeping the question already written.

```http
GET https://api.sayproof.example/v1/viewer?product=PRODUCT-CODE&scope=variant&lang=en&channel=web
Authorization: Bearer YOUR-SERVER-KEY
X-Sayproof-Customer: jane@example.com
X-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

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.

```http
GET https://api.sayproof.example/v1/viewer/order?order=100245&channel=web&lang=en
Authorization: Bearer YOUR-SERVER-KEY
X-Sayproof-Customer: jane@example.com
```

Each 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:

```ts
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:

```html
<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](/en/reference/javascript-api.md). 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](/en/reference/dom-attributes.md#reviews-on-the-order-page).
