# JavaScript API

> The script's window.Sayproof interface, its options, events and usage examples.

Source: /en/reference/javascript-api/

When the script loads, it creates the `window.Sayproof` object, finds the containers on the page and starts watching the page for changes. On most sites this interface is not needed: adding a container, removing it or changing its attributes is sufficient. When you need to manage the component from code, use the methods below.

## Waiting for the script to be ready

The script is loaded as a module, and your code may run before it. If `window.Sayproof` is not yet defined, wait for the `sayproof:ready` event:

```js
function whenSayproofReady(callback) {
  if (window.Sayproof) callback(window.Sayproof);
  else window.addEventListener("sayproof:ready", () => callback(window.Sayproof), { once: true });
}
```

The examples on this page use this helper function.

## Methods

| Method | Purpose |
|---|---|
| `mount(container, options?)` | Binds the component to the container. `options` values take precedence over the container's attributes. If the container is already bound, the component is updated with the new options. |
| `update(container, options)` | Merges the new options with the previous ones and updates the component. Also used to pass the signed-in customer's status. |
| `unmount(container)` | Removes the component from the container. |
| `scan()` | Scans the page again: binds new containers and `data-review-order` elements, and cleans up those removed from the page. |
| `write(container)` | Opens the review writing flow. Returns `true` if the flow was opened, `false` if not. |
| `mountOrder(element, answer?)` | Builds the reviews section of the order page in the element. `answer` is the answer your server received from the `GET /v1/viewer/order` call; if it is not given, the `data-review-order-state` tag inside the element is read. Calling it again on the same element rebuilds the section with the new answer. |
| `version` | The contract version (`"3"`) |

:::caution[Order of precedence]
An option passed with `mount` or `update` takes precedence over the attribute value for the same field and is kept in later calls. Manage each field (for example the product code) either only with attributes or only with these methods; if you use both, attribute changes have no effect.
:::

### Options

| Option | Corresponding attribute |
|---|---|
| `product` | `data-review-product` |
| `scope` | `data-review-scope` (`"variant"` or `"group"`) |
| `productName` | `data-review-product-name` |
| `locale` | `data-review-locale`. If it is not given anywhere, `<html lang>` is used, and failing that the channel's language. |
| `layout` | `data-review-layout` (`"list"` or `"grid"`) |
| `style` | `data-review-style` (`"cards"` or `"sidebar"`). If it is not given anywhere, the layout chosen for the channel in the panel is used. |
| `theme` | `data-review-theme` (`"warm"`, `"trust"`, `"editorial"`, `"minimal"`, `"soft"`, `"bold"`, `"natural"` or `"night"`). If it is not given anywhere or is not recognised, the design chosen for the channel in the panel is used. |
| `mode` | `data-review-mode` (`"default"` or `"headless"`) |
| `gate` | `data-review-gate` |
| `siteKey` | `data-review-site-key` |
| `base` | `data-review-base` |
| `nonce` | The nonce value to add to the style tags |
| `viewer` | The signed-in customer's status: `{ state, token, products, reviews, limits, asker }`, `"pending"` or `null` |

### When `write` returns `false`

- If the container is not ready (no `data-review-ready` attribute).
- If a dialog is open or opening on the page.
- If the signed-in customer has already written a review for this product (`written`).
- If the signed-in customer's status is still awaited (`data-review-viewer="pending"`).

In all other cases, the review form opens if the customer can write a review; otherwise the order verification opens first and, if verification succeeds, the flow moves to the form in the same dialog. The form never opens before the order has been verified on the server.

## Events

| Event | Dispatched on | When |
|---|---|---|
| `sayproof:ready` | `window` | The script has loaded; `window.Sayproof` can be used |
| `sayproof:submitted` | Container; on the order page the `data-review-order` element (bubbling) | A review was submitted successfully. On a product page `event.detail.product` contains the product code; on the order page `event.detail.order` contains the order number and `event.detail.line` the ID of the order line. Because each item is sent separately on the order page, the event is dispatched for each item. |

A submitted review is published after approval; you therefore do not need to refresh the list immediately on the `sayproof:submitted` event.

## Invitation links

The invitation link carries the token in the part of the address after the `#` sign (`#review_token=…`); the script also reads the `?review_token=…` parameter of older invitations. The script reads the value as soon as it loads and removes it from the address bar. The part after `#` is not sent to servers and does not reach the page address recorded by analytics tools (such as `page_location`). The form opens in the container that displays the invited product or, if there is no such container, in the first container on the page. If the link is invalid or has expired, an explanatory message is shown in the review writing area.

## Watching for page changes

The script detects changes on the page by itself:

- When an element carrying the `data-review-mount` attribute is added to the page, the component is bound; when it is removed, the component is cleaned up.
- When one of the container's configuration attributes changes (for example a variant selection), the component is updated.
- When new server HTML arrives inside the container (for example when React re-creates the container), the script takes over this content.
- When a `data-review-order` element is added to the page, the reviews section of the order page is built.

While the product is changing, the old content is made non-interactive (`inert`) until the new content has loaded. If the new load fails, the `data-review-failed` attribute is added to the container.

## Examples

### Binding the component from code

To pass the options from code instead of attributes:

```html
<div id="reviews" data-review-mount></div>

<script type="module">
  whenSayproofReady((sayproof) => {
    sayproof.mount(document.querySelector("#reviews"), {
      product: "PRODUCT-38",
      scope: "variant",
      locale: "en",
      layout: "list",
      style: "sidebar",
      theme: "trust",
    });
  });
</script>
```

The site key and the API address are taken from the `data-review-site-key` and `data-review-base` values on the script tag. The `data-review-mount` attribute on the container is required so that the script can watch the container and invitation links can open in it. The examples assume that the `whenSayproofReady` function is defined on the page.

### Changing the product from code

```js
const container = document.querySelector("#reviews");

document.querySelector("#size").addEventListener("change", (event) => {
  whenSayproofReady((sayproof) => sayproof.update(container, { product: event.target.value }));
});
```

### Binding and removing in single-page applications (SPA)

The script detects by itself when a container is added to or removed from the page. If you want to manage the component's lifecycle from code, in React:

```tsx
import { useEffect, useRef } from "react";

export function Reviews({ product, locale }: { product: string; locale: string }) {
  const container = useRef<HTMLDivElement>(null);

  useEffect(() => {
    const element = container.current;
    if (!element) return;
    whenSayproofReady((sayproof) => sayproof.mount(element, { product, locale }));
    return () => window.Sayproof?.unmount(element);
  }, [product, locale]);

  return <div ref={container} data-review-mount />;
}
```

In Vue:

```vue
<script setup>
import { onBeforeUnmount, onMounted, ref, watch } from "vue";

const props = defineProps({ product: String, locale: String });
const container = ref(null);

onMounted(() => {
  whenSayproofReady((sayproof) => sayproof.mount(container.value, { product: props.product, locale: props.locale }));
});

watch(
  () => [props.product, props.locale],
  ([product, locale]) => window.Sayproof?.update(container.value, { product, locale })
);

onBeforeUnmount(() => window.Sayproof?.unmount(container.value));
</script>

<template>
  <div ref="container" data-review-mount />
</template>
```

### Scanning the page immediately

Because the script detects changes by itself, `scan()` is usually not needed. You can use it if you want binding to be completed immediately after replacing a large part of the page:

```js
document.querySelector("#product-detail").innerHTML = html;
window.Sayproof?.scan();
```

### Your own "Write a review" button

```js
const container = document.querySelector("#reviews");

document.querySelector("#write-review").addEventListener("click", () => {
  const opened = window.Sayproof?.write(container) ?? false;
  if (!opened) container.scrollIntoView({ behavior: "smooth", block: "start" });
});
```

### Forwarding submissions to your analytics tool

```js
document.addEventListener("sayproof:submitted", (event) => {
  window.dataLayer?.push({ event: "review_submitted", product: event.detail.product });
});
```

### Passing the signed-in customer's status

```js
const container = document.querySelector("#reviews");

fetch(`/api/sayproof/viewer?product=${encodeURIComponent(container.dataset.reviewProduct)}`, { credentials: "same-origin" })
  .then((answer) => (answer.ok ? answer.json() : { state: "anonymous" }))
  .catch(() => ({ state: "anonymous" }))
  .then((viewer) => whenSayproofReady((sayproof) => sayproof.update(container, { viewer })));
```

For the server side and the response values, see [Signed-in customers](/en/integrate/signed-in-customers.md).

### Passing the order answer from code

If the order detail page is built in the browser, fetch the answer from your own endpoint and pass it with `mountOrder`. Do not build the section when the answer has no items:

```js
const element = document.querySelector("[data-review-order]");

fetch(`/api/sayproof/order?order=${encodeURIComponent(element.dataset.reviewOrder)}`, { credentials: "same-origin" })
  .then((answer) => (answer.ok ? answer.json() : null))
  .catch(() => null)
  .then((order) => {
    if (order?.lines?.length) whenSayproofReady((sayproof) => sayproof.mountOrder(element, order));
  });
```

`/api/sayproof/order` is your own server endpoint that asks Sayproof with the signed-in email address: [Signed-in customers › Order detail page](/en/integrate/signed-in-customers.md#order-detail-page).
