> ## Documentation Index
> Fetch the complete documentation index at: https://docs.repdata.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Review Integration

> Send a respondent's open-ended answer, with the timing and behavioral data collected by the review library, to the Research Defender Review API.

## Overview

The Review API evaluates an open-ended text answer together with data about how it was entered. The Research Defender review library runs in the respondent's browser and collects that data, including pasted text, timestamps, and behavioral data. When the respondent submits, your page sends the answer and the collected data to the Review API.

You can send the request with `POST` or `GET`. Both use the same fields.

<Tip>
  Use `POST`. Answers and behavioral data can be long and may contain line breaks, quotes, and personal content. With `GET`, all of it goes into the URL, which has length limits and is often written to server and proxy logs.
</Tip>

## How it works

```mermaid theme={null}
sequenceDiagram
    participant B as Respondent browser
    participant L as Review library
    participant RD as Research Defender

    B->>L: Load library, create scReviewLib
    L->>B: Fill hidden fields (timestamps, pasted text, behavioral data)
    B->>L: Respondent submits answer
    L->>B: submitEventListener called
    B->>RD: POST or GET review request
    RD-->>B: Review result
```

<Steps>
  <Step title="Load the library">
    Add the review library script to the `<head>` of your page.
  </Step>

  <Step title="Add the page elements">
    Add the answer field, a submit button, and the fields the library writes its data into.
  </Step>

  <Step title="Create the library">
    Create `scReviewLib` when the page has loaded, passing your environment URL, publishable key, the element IDs, and your listeners.
  </Step>

  <Step title="Send the review on submit">
    In `submitEventListener`, read the answer and the collected data from the page and call the Review API.
  </Step>
</Steps>

## Prerequisites

* A Research Defender **publishable key**
* A page served over **HTTP(S)**. Opening the file directly from disk (`file://`) will not work.

## Step 1: Load the library

Add the library to the `<head>` of your page.

```html theme={null}
<script src="https://prod.rtymgt.com/static/js/review/sc_review_lib_secured_v1-3.min.js?v=1.0.2.1"></script>
```

<Warning>
  Use the same environment in the library URL and in your code. For example, if the script loads from `prod.rtymgt.com`, set `ENVIRONMENT` to `"prod"`. A mix of environments is a common cause of failures.
</Warning>

## Step 2: Add the page elements

The library reads from and writes to elements on your page. Each one needs an ID, which you pass to the library.

| Element | Purpose | Sample ID |
| - | - | - |
| Answer field | The respondent's answer | `text_element` |
| Submit button | The button the respondent clicks | `submit_entry` |
| Pasted text field | The library writes pasted text here | `encoded_pasted_text_element` |
| Page load timestamp field | The library writes the page load time here | `page_load_timestamp_text_element` |
| Text entry start field | The library writes when the respondent started typing | `answer_start_timestamp_text_element` |
| Submit timestamp field | The library writes the submit time here | `submit_timestamp_text_element` |
| Behavioral data field | The library writes behavioral data here | `behavioral_data_text_element` |

<Note>
  The five fields the library writes to are meant to be hidden. The sample shows them so you can see the collected data. In production, use `type="hidden"` for them.
</Note>

## Step 3: Create the library and send the review

Create `scReviewLib` after the page has loaded. The constructor takes these arguments, in order.

| Argument | Description |
| - | - |
| `domain` | Your environment URL, for example `https://prod.rtymgt.com` |
| `publishableKey` | Your Research Defender publishable key |
| `elementIds` | An object with the element IDs from the table above |
| `pageLoadListener` | Called when the page load is recorded |
| `copyPasteListener` | Called when text is pasted |
| `textEventListener` | Called when the respondent starts entering text |
| `submitEventListener` | Called when the respondent submits. Send the review request here. |

### Review API request

<ParamField path="Endpoint" type="string">
  `GET` or `POST` `https://{environment}.rtymgt.com/api/v4/respondents/review/{publishable_key}`
</ParamField>

For a `POST` request, send the fields as a JSON body. For a `GET` request, send them as query parameters.

<ParamField body="q_id" type="string" required>
  The question ID.
</ParamField>

<ParamField body="question" type="string" required>
  The question text shown to the respondent.
</ParamField>

<ParamField body="text" type="string" required>
  The respondent's answer.
</ParamField>

<ParamField body="c_text" type="string">
  The pasted text collected by the library.
</ParamField>

<ParamField body="s_text_length" type="number">
  A number. The sample uses `5`.
</ParamField>

<ParamField body="t_page_load" type="string">
  The page load timestamp collected by the library.
</ParamField>

<ParamField body="t_text_typed" type="string">
  The timestamp of when the respondent started entering the answer, collected by the library.
</ParamField>

<ParamField body="t_submit" type="string">
  The submit timestamp collected by the library.
</ParamField>

<ParamField body="p_fields" type="string">
  The behavioral data collected by the library.
</ParamField>

<Note>
  Send the request with credentials (`credentials: "include"`) so Research Defender cookies are included.
</Note>

### Examples

<Tabs>
  <Tab title="POST">
    ```javascript review-post.js theme={null}
    // ---------------------------------------------------------------
    // Configuration
    // ---------------------------------------------------------------
    const ENVIRONMENT = "prod"; // "staging" or "prod"
    const PUBLISHABLE_KEY = "YOUR_PUBLISHABLE_KEY";

    const QUESTION_ID = "1"; // q_id
    const S_TEXT_LENGTH = 5; // s_text_length

    const BASE_URL = `https://${ENVIRONMENT}.rtymgt.com`;
    const REVIEW_ENDPOINT = `${BASE_URL}/api/v4/respondents/review/${PUBLISHABLE_KEY}`;

    // The IDs of the page elements the library reads from and writes to.
    const ELEMENT_IDS = {
      textElementId: "text_element",
      pastedTextElementId: "encoded_pasted_text_element",
      pageLoadTextTimeElementId: "page_load_timestamp_text_element",
      entryStartTextTimeElementId: "answer_start_timestamp_text_element",
      submitTextTimeElementId: "submit_timestamp_text_element",
      submitButtonElementId: "submit_entry",
      behavioralDataElementId: "behavioral_data_text_element",
    };

    // ---------------------------------------------------------------
    // Helpers
    // ---------------------------------------------------------------
    const byId = (id) => document.getElementById(id);

    /**
     * Parses a response body. If the body has a stray trailing character
     * that breaks JSON.parse, it is trimmed and parsing is retried.
     */
    function parseResponse(text) {
      try {
        return JSON.parse(text);
      } catch {
        return JSON.parse(text.slice(0, -1));
      }
    }

    /** Collects the review data from the page. The library fills in the hidden fields. */
    function buildPayload() {
      return {
        q_id: QUESTION_ID,
        question: byId("question_text").textContent.trim(),
        text: byId(ELEMENT_IDS.textElementId).value,
        c_text: byId(ELEMENT_IDS.pastedTextElementId).value,
        s_text_length: S_TEXT_LENGTH,
        t_page_load: byId(ELEMENT_IDS.pageLoadTextTimeElementId).value,
        t_text_typed: byId(ELEMENT_IDS.entryStartTextTimeElementId).value,
        t_submit: byId(ELEMENT_IDS.submitTextTimeElementId).value,
        p_fields: byId(ELEMENT_IDS.behavioralDataElementId).value,
      };
    }

    /** Reads a response, raising a helpful error for non-200 results. */
    async function readResponse(response) {
      const text = await response.text();

      if (!response.ok) {
        throw new Error(`Review request failed with status ${response.status}: ${text.slice(0, 300)}`);
      }

      return parseResponse(text);
    }

    /** Sends the review as a POST request with a JSON body. */
    async function sendReview(payload) {
      const response = await fetch(REVIEW_ENDPOINT, {
        method: "POST",
        credentials: "include", // send Research Defender cookies
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(payload), // handles quotes, line breaks and special characters
      });

      return readResponse(response);
    }

    // ---------------------------------------------------------------
    // Library listeners
    // ---------------------------------------------------------------

    // Optional hooks. The library also fills in the hidden fields on its own.
    const onPageLoad = (time, error) => {};
    const onCopyPaste = (pastedText, error) => {};
    const onTextEvent = (answerTime, error) => {};

    // Called by the library when the respondent submits. By now the library
    // has filled in the submit timestamp and behavioral data fields.
    async function onSubmit(submitTime, error) {
      if (error) {
        console.error("Review library error:", error);
      }

      const answer = byId(ELEMENT_IDS.textElementId).value;
      const button = byId(ELEMENT_IDS.submitButtonElementId);
      const output = byId("output");

      if (!answer.trim()) {
        alert("Please provide your answer.");
        return;
      }

      button.disabled = true;
      button.textContent = "Please wait...";

      try {
        const result = await sendReview(buildPayload());
        console.log("Review result:", result);
        output.textContent = JSON.stringify(result, null, 2);
      } catch (err) {
        console.error(err);
        output.textContent = `Error: ${err.message}`;
      } finally {
        // Always re-enable the button, whether the request worked or not.
        button.disabled = false;
        button.textContent = "Submit";
      }
    }

    // ---------------------------------------------------------------
    // Start the library
    // ---------------------------------------------------------------
    window.addEventListener("load", () => {
      try {
        if (typeof scReviewLib === "undefined") {
          throw new Error("The review library did not load. Check the script tag in the <head>.");
        }

        new scReviewLib(
          BASE_URL,
          PUBLISHABLE_KEY,
          ELEMENT_IDS,
          onPageLoad,
          onCopyPaste,
          onTextEvent,
          onSubmit
        );
      } catch (error) {
        console.error(error);
        byId("output").textContent = `Error: ${error.message}`;
      }
    });
    ```
  </Tab>

  <Tab title="GET">
    ```javascript review-get.js theme={null}
    // ---------------------------------------------------------------
    // Configuration
    // ---------------------------------------------------------------
    const ENVIRONMENT = "prod"; // "staging" or "prod"
    const PUBLISHABLE_KEY = "YOUR_PUBLISHABLE_KEY";

    const QUESTION_ID = "1"; // q_id
    const S_TEXT_LENGTH = 5; // s_text_length

    const BASE_URL = `https://${ENVIRONMENT}.rtymgt.com`;
    const REVIEW_ENDPOINT = `${BASE_URL}/api/v4/respondents/review/${PUBLISHABLE_KEY}`;

    // The IDs of the page elements the library reads from and writes to.
    const ELEMENT_IDS = {
      textElementId: "text_element",
      pastedTextElementId: "encoded_pasted_text_element",
      pageLoadTextTimeElementId: "page_load_timestamp_text_element",
      entryStartTextTimeElementId: "answer_start_timestamp_text_element",
      submitTextTimeElementId: "submit_timestamp_text_element",
      submitButtonElementId: "submit_entry",
      behavioralDataElementId: "behavioral_data_text_element",
    };

    // ---------------------------------------------------------------
    // Helpers
    // ---------------------------------------------------------------
    const byId = (id) => document.getElementById(id);

    /**
     * Parses a response body. If the body has a stray trailing character
     * that breaks JSON.parse, it is trimmed and parsing is retried.
     */
    function parseResponse(text) {
      try {
        return JSON.parse(text);
      } catch {
        return JSON.parse(text.slice(0, -1));
      }
    }

    /** Collects the review data from the page. The library fills in the hidden fields. */
    function buildPayload() {
      return {
        q_id: QUESTION_ID,
        question: byId("question_text").textContent.trim(),
        text: byId(ELEMENT_IDS.textElementId).value,
        c_text: byId(ELEMENT_IDS.pastedTextElementId).value,
        s_text_length: S_TEXT_LENGTH,
        t_page_load: byId(ELEMENT_IDS.pageLoadTextTimeElementId).value,
        t_text_typed: byId(ELEMENT_IDS.entryStartTextTimeElementId).value,
        t_submit: byId(ELEMENT_IDS.submitTextTimeElementId).value,
        p_fields: byId(ELEMENT_IDS.behavioralDataElementId).value,
      };
    }

    /** Reads a response, raising a helpful error for non-200 results. */
    async function readResponse(response) {
      const text = await response.text();

      if (!response.ok) {
        throw new Error(`Review request failed with status ${response.status}: ${text.slice(0, 300)}`);
      }

      return parseResponse(text);
    }

    /** Sends the review as a GET request with URL parameters. */
    async function sendReview(payload) {
      const params = new URLSearchParams();
      for (const [name, value] of Object.entries(payload)) {
        params.set(name, String(value));
      }

      const response = await fetch(`${REVIEW_ENDPOINT}?${params}`, {
        method: "GET",
        credentials: "include", // send Research Defender cookies
      });

      return readResponse(response);
    }

    // ---------------------------------------------------------------
    // Library listeners
    // ---------------------------------------------------------------

    // Optional hooks. The library also fills in the hidden fields on its own.
    const onPageLoad = (time, error) => {};
    const onCopyPaste = (pastedText, error) => {};
    const onTextEvent = (answerTime, error) => {};

    // Called by the library when the respondent submits. By now the library
    // has filled in the submit timestamp and behavioral data fields.
    async function onSubmit(submitTime, error) {
      if (error) {
        console.error("Review library error:", error);
      }

      const answer = byId(ELEMENT_IDS.textElementId).value;
      const button = byId(ELEMENT_IDS.submitButtonElementId);
      const output = byId("output");

      if (!answer.trim()) {
        alert("Please provide your answer.");
        return;
      }

      button.disabled = true;
      button.textContent = "Please wait...";

      try {
        const result = await sendReview(buildPayload());
        console.log("Review result:", result);
        output.textContent = JSON.stringify(result, null, 2);
      } catch (err) {
        console.error(err);
        output.textContent = `Error: ${err.message}`;
      } finally {
        // Always re-enable the button, whether the request worked or not.
        button.disabled = false;
        button.textContent = "Submit";
      }
    }

    // ---------------------------------------------------------------
    // Start the library
    // ---------------------------------------------------------------
    window.addEventListener("load", () => {
      try {
        if (typeof scReviewLib === "undefined") {
          throw new Error("The review library did not load. Check the script tag in the <head>.");
        }

        new scReviewLib(
          BASE_URL,
          PUBLISHABLE_KEY,
          ELEMENT_IDS,
          onPageLoad,
          onCopyPaste,
          onTextEvent,
          onSubmit
        );
      } catch (error) {
        console.error(error);
        byId("output").textContent = `Error: ${error.message}`;
      }
    });
    ```
  </Tab>
</Tabs>

The two examples differ only in `sendReview()`. The `POST` version sends a JSON body built with `JSON.stringify`, which correctly handles quotes, line breaks, and special characters in the answer. The `GET` version encodes the same fields into the URL with `URLSearchParams`.

## Full HTML pages

If you want a complete page you can open in a browser, use one of the samples below. Replace the placeholder values at the top of the script, then serve the file over HTTP(S).

<AccordionGroup>
  <Accordion title="Review integration with POST (HTML)">
    ```html review-post-integration.html theme={null}
    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8" />
      <meta name="viewport" content="width=device-width, initial-scale=1" />
      <title>Research Defender – Review Integration (POST)</title>

      <!--
        Review library. Use the same environment host here and in ENVIRONMENT below
        ("staging" or "prod"). Load it in the <head>, as shown.
      -->
      <script src="https://prod.rtymgt.com/static/js/review/sc_review_lib_secured_v1-3.min.js?v=1.0.2.1"></script>

      <style>
        body { font-family: system-ui, sans-serif; background: #ebebeb; margin: 0; padding: 2rem; }
        .card { max-width: 720px; margin: 0 auto; background: #fff; padding: 2rem; border-radius: 0.5rem; }
        label { display: block; font-weight: 600; margin-bottom: 0.5rem; }
        textarea { width: 100%; box-sizing: border-box; }
        button { background: #25274d; color: #fff; font-weight: 600; border: 0; padding: 0.6rem 1.5rem; border-radius: 0.25rem; cursor: pointer; }
        button:disabled { opacity: 0.6; cursor: default; }
        pre { background: #f6f6f6; padding: 1rem; overflow-x: auto; white-space: pre-wrap; }
        details { margin-top: 1.5rem; }
        details input { width: 100%; box-sizing: border-box; margin-bottom: 0.75rem; }
      </style>
    </head>
    <body>
      <div class="card">
        <h1>Research Defender – Review Integration (POST)</h1>

        <label id="question_text" for="text_element">What is your favorite holiday?</label>
        <textarea id="text_element" rows="5"></textarea>

        <p><button type="button" id="submit_entry">Submit</button></p>

        <pre id="output"></pre>

        <!-- The library writes the data it collects into these fields. -->
        <details>
          <summary>Data collected by the library (hidden fields)</summary>
          <p>These are visible here for demonstration. In production, use <code>type="hidden"</code> for them.</p>

          <label for="encoded_pasted_text_element">Pasted text</label>
          <input type="text" id="encoded_pasted_text_element" readonly />

          <label for="page_load_timestamp_text_element">Page load timestamp</label>
          <input type="text" id="page_load_timestamp_text_element" readonly />

          <label for="answer_start_timestamp_text_element">Start of text entry timestamp</label>
          <input type="text" id="answer_start_timestamp_text_element" readonly />

          <label for="submit_timestamp_text_element">Submit timestamp</label>
          <input type="text" id="submit_timestamp_text_element" readonly />

          <label for="behavioral_data_text_element">Behavioral data</label>
          <input type="text" id="behavioral_data_text_element" readonly />
        </details>
      </div>

      <script>
        // ---------------------------------------------------------------
        // Configuration
        // ---------------------------------------------------------------
        const ENVIRONMENT = "prod"; // "staging" or "prod"
        const PUBLISHABLE_KEY = "YOUR_PUBLISHABLE_KEY";

        const QUESTION_ID = "1"; // q_id
        const S_TEXT_LENGTH = 5; // s_text_length

        const BASE_URL = `https://${ENVIRONMENT}.rtymgt.com`;
        const REVIEW_ENDPOINT = `${BASE_URL}/api/v4/respondents/review/${PUBLISHABLE_KEY}`;

        // The IDs of the page elements the library reads from and writes to.
        const ELEMENT_IDS = {
          textElementId: "text_element",
          pastedTextElementId: "encoded_pasted_text_element",
          pageLoadTextTimeElementId: "page_load_timestamp_text_element",
          entryStartTextTimeElementId: "answer_start_timestamp_text_element",
          submitTextTimeElementId: "submit_timestamp_text_element",
          submitButtonElementId: "submit_entry",
          behavioralDataElementId: "behavioral_data_text_element",
        };

        // ---------------------------------------------------------------
        // Helpers
        // ---------------------------------------------------------------
        const byId = (id) => document.getElementById(id);

        /**
         * Parses a response body. If the body has a stray trailing character
         * that breaks JSON.parse, it is trimmed and parsing is retried.
         */
        function parseResponse(text) {
          try {
            return JSON.parse(text);
          } catch {
            return JSON.parse(text.slice(0, -1));
          }
        }

        /** Collects the review data from the page. The library fills in the hidden fields. */
        function buildPayload() {
          return {
            q_id: QUESTION_ID,
            question: byId("question_text").textContent.trim(),
            text: byId(ELEMENT_IDS.textElementId).value,
            c_text: byId(ELEMENT_IDS.pastedTextElementId).value,
            s_text_length: S_TEXT_LENGTH,
            t_page_load: byId(ELEMENT_IDS.pageLoadTextTimeElementId).value,
            t_text_typed: byId(ELEMENT_IDS.entryStartTextTimeElementId).value,
            t_submit: byId(ELEMENT_IDS.submitTextTimeElementId).value,
            p_fields: byId(ELEMENT_IDS.behavioralDataElementId).value,
          };
        }

        /** Reads a response, raising a helpful error for non-200 results. */
        async function readResponse(response) {
          const text = await response.text();

          if (!response.ok) {
            throw new Error(`Review request failed with status ${response.status}: ${text.slice(0, 300)}`);
          }

          return parseResponse(text);
        }

        /** Sends the review as a POST request with a JSON body. */
        async function sendReview(payload) {
          const response = await fetch(REVIEW_ENDPOINT, {
            method: "POST",
            credentials: "include", // send Research Defender cookies
            headers: { "Content-Type": "application/json" },
            body: JSON.stringify(payload), // handles quotes, line breaks and special characters
          });

          return readResponse(response);
        }

        // ---------------------------------------------------------------
        // Library listeners
        // ---------------------------------------------------------------

        // Optional hooks. The library also fills in the hidden fields on its own.
        const onPageLoad = (time, error) => {};
        const onCopyPaste = (pastedText, error) => {};
        const onTextEvent = (answerTime, error) => {};

        // Called by the library when the respondent submits. By now the library
        // has filled in the submit timestamp and behavioral data fields.
        async function onSubmit(submitTime, error) {
          if (error) {
            console.error("Review library error:", error);
          }

          const answer = byId(ELEMENT_IDS.textElementId).value;
          const button = byId(ELEMENT_IDS.submitButtonElementId);
          const output = byId("output");

          if (!answer.trim()) {
            alert("Please provide your answer.");
            return;
          }

          button.disabled = true;
          button.textContent = "Please wait...";

          try {
            const result = await sendReview(buildPayload());
            console.log("Review result:", result);
            output.textContent = JSON.stringify(result, null, 2);
          } catch (err) {
            console.error(err);
            output.textContent = `Error: ${err.message}`;
          } finally {
            // Always re-enable the button, whether the request worked or not.
            button.disabled = false;
            button.textContent = "Submit";
          }
        }

        // ---------------------------------------------------------------
        // Start the library
        // ---------------------------------------------------------------
        window.addEventListener("load", () => {
          try {
            if (typeof scReviewLib === "undefined") {
              throw new Error("The review library did not load. Check the script tag in the <head>.");
            }

            new scReviewLib(
              BASE_URL,
              PUBLISHABLE_KEY,
              ELEMENT_IDS,
              onPageLoad,
              onCopyPaste,
              onTextEvent,
              onSubmit
            );
          } catch (error) {
            console.error(error);
            byId("output").textContent = `Error: ${error.message}`;
          }
        });
      </script>
    </body>
    </html>
    ```
  </Accordion>

  <Accordion title="Review integration with GET (HTML)">
    ```html review-get-integration.html theme={null}
    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8" />
      <meta name="viewport" content="width=device-width, initial-scale=1" />
      <title>Research Defender – Review Integration (GET)</title>

      <!--
        Review library. Use the same environment host here and in ENVIRONMENT below
        ("staging" or "prod"). Load it in the <head>, as shown.
      -->
      <script src="https://prod.rtymgt.com/static/js/review/sc_review_lib_secured_v1-3.min.js?v=1.0.2.1"></script>

      <style>
        body { font-family: system-ui, sans-serif; background: #ebebeb; margin: 0; padding: 2rem; }
        .card { max-width: 720px; margin: 0 auto; background: #fff; padding: 2rem; border-radius: 0.5rem; }
        label { display: block; font-weight: 600; margin-bottom: 0.5rem; }
        textarea { width: 100%; box-sizing: border-box; }
        button { background: #25274d; color: #fff; font-weight: 600; border: 0; padding: 0.6rem 1.5rem; border-radius: 0.25rem; cursor: pointer; }
        button:disabled { opacity: 0.6; cursor: default; }
        pre { background: #f6f6f6; padding: 1rem; overflow-x: auto; white-space: pre-wrap; }
        details { margin-top: 1.5rem; }
        details input { width: 100%; box-sizing: border-box; margin-bottom: 0.75rem; }
      </style>
    </head>
    <body>
      <div class="card">
        <h1>Research Defender – Review Integration (GET)</h1>

        <label id="question_text" for="text_element">What is your favorite holiday?</label>
        <textarea id="text_element" rows="5"></textarea>

        <p><button type="button" id="submit_entry">Submit</button></p>

        <pre id="output"></pre>

        <!-- The library writes the data it collects into these fields. -->
        <details>
          <summary>Data collected by the library (hidden fields)</summary>
          <p>These are visible here for demonstration. In production, use <code>type="hidden"</code> for them.</p>

          <label for="encoded_pasted_text_element">Pasted text</label>
          <input type="text" id="encoded_pasted_text_element" readonly />

          <label for="page_load_timestamp_text_element">Page load timestamp</label>
          <input type="text" id="page_load_timestamp_text_element" readonly />

          <label for="answer_start_timestamp_text_element">Start of text entry timestamp</label>
          <input type="text" id="answer_start_timestamp_text_element" readonly />

          <label for="submit_timestamp_text_element">Submit timestamp</label>
          <input type="text" id="submit_timestamp_text_element" readonly />

          <label for="behavioral_data_text_element">Behavioral data</label>
          <input type="text" id="behavioral_data_text_element" readonly />
        </details>
      </div>

      <script>
        // ---------------------------------------------------------------
        // Configuration
        // ---------------------------------------------------------------
        const ENVIRONMENT = "prod"; // "staging" or "prod"
        const PUBLISHABLE_KEY = "YOUR_PUBLISHABLE_KEY";

        const QUESTION_ID = "1"; // q_id
        const S_TEXT_LENGTH = 5; // s_text_length

        const BASE_URL = `https://${ENVIRONMENT}.rtymgt.com`;
        const REVIEW_ENDPOINT = `${BASE_URL}/api/v4/respondents/review/${PUBLISHABLE_KEY}`;

        // The IDs of the page elements the library reads from and writes to.
        const ELEMENT_IDS = {
          textElementId: "text_element",
          pastedTextElementId: "encoded_pasted_text_element",
          pageLoadTextTimeElementId: "page_load_timestamp_text_element",
          entryStartTextTimeElementId: "answer_start_timestamp_text_element",
          submitTextTimeElementId: "submit_timestamp_text_element",
          submitButtonElementId: "submit_entry",
          behavioralDataElementId: "behavioral_data_text_element",
        };

        // ---------------------------------------------------------------
        // Helpers
        // ---------------------------------------------------------------
        const byId = (id) => document.getElementById(id);

        /**
         * Parses a response body. If the body has a stray trailing character
         * that breaks JSON.parse, it is trimmed and parsing is retried.
         */
        function parseResponse(text) {
          try {
            return JSON.parse(text);
          } catch {
            return JSON.parse(text.slice(0, -1));
          }
        }

        /** Collects the review data from the page. The library fills in the hidden fields. */
        function buildPayload() {
          return {
            q_id: QUESTION_ID,
            question: byId("question_text").textContent.trim(),
            text: byId(ELEMENT_IDS.textElementId).value,
            c_text: byId(ELEMENT_IDS.pastedTextElementId).value,
            s_text_length: S_TEXT_LENGTH,
            t_page_load: byId(ELEMENT_IDS.pageLoadTextTimeElementId).value,
            t_text_typed: byId(ELEMENT_IDS.entryStartTextTimeElementId).value,
            t_submit: byId(ELEMENT_IDS.submitTextTimeElementId).value,
            p_fields: byId(ELEMENT_IDS.behavioralDataElementId).value,
          };
        }

        /** Reads a response, raising a helpful error for non-200 results. */
        async function readResponse(response) {
          const text = await response.text();

          if (!response.ok) {
            throw new Error(`Review request failed with status ${response.status}: ${text.slice(0, 300)}`);
          }

          return parseResponse(text);
        }

        /** Sends the review as a GET request with URL parameters. */
        async function sendReview(payload) {
          const params = new URLSearchParams();
          for (const [name, value] of Object.entries(payload)) {
            params.set(name, String(value));
          }

          const response = await fetch(`${REVIEW_ENDPOINT}?${params}`, {
            method: "GET",
            credentials: "include", // send Research Defender cookies
          });

          return readResponse(response);
        }

        // ---------------------------------------------------------------
        // Library listeners
        // ---------------------------------------------------------------

        // Optional hooks. The library also fills in the hidden fields on its own.
        const onPageLoad = (time, error) => {};
        const onCopyPaste = (pastedText, error) => {};
        const onTextEvent = (answerTime, error) => {};

        // Called by the library when the respondent submits. By now the library
        // has filled in the submit timestamp and behavioral data fields.
        async function onSubmit(submitTime, error) {
          if (error) {
            console.error("Review library error:", error);
          }

          const answer = byId(ELEMENT_IDS.textElementId).value;
          const button = byId(ELEMENT_IDS.submitButtonElementId);
          const output = byId("output");

          if (!answer.trim()) {
            alert("Please provide your answer.");
            return;
          }

          button.disabled = true;
          button.textContent = "Please wait...";

          try {
            const result = await sendReview(buildPayload());
            console.log("Review result:", result);
            output.textContent = JSON.stringify(result, null, 2);
          } catch (err) {
            console.error(err);
            output.textContent = `Error: ${err.message}`;
          } finally {
            // Always re-enable the button, whether the request worked or not.
            button.disabled = false;
            button.textContent = "Submit";
          }
        }

        // ---------------------------------------------------------------
        // Start the library
        // ---------------------------------------------------------------
        window.addEventListener("load", () => {
          try {
            if (typeof scReviewLib === "undefined") {
              throw new Error("The review library did not load. Check the script tag in the <head>.");
            }

            new scReviewLib(
              BASE_URL,
              PUBLISHABLE_KEY,
              ELEMENT_IDS,
              onPageLoad,
              onCopyPaste,
              onTextEvent,
              onSubmit
            );
          } catch (error) {
            console.error(error);
            byId("output").textContent = `Error: ${error.message}`;
          }
        });
      </script>
    </body>
    </html>
    ```
  </Accordion>
</AccordionGroup>

## Handling errors

The samples show the API's error message on the page, write it to the browser console, and always re-enable the submit button so the respondent is never left waiting. Decide in advance what your survey should do when a review fails.

## Troubleshooting

| Symptom | Likely cause |
| - | - |
| The submit button stays on "Please wait" | The request failed and the page did not handle the error. Always re-enable the button in a `finally` block, as the samples do. |
| `400` response | The request is missing a required field or is not valid. Check the response message, and build a `POST` body with `JSON.stringify` rather than by joining strings. |
| The question text is empty or `undefined` | A `<label>` has no `value` property. Read the question with `textContent`. |
| Hidden fields stay empty | The library did not start. Check the script tag, that `scReviewLib` is created after the page loads, and that every element ID matches the page. |
| The library fails to load or the request is rejected | The library URL and your `ENVIRONMENT` setting use different environments, or the publishable key belongs to another environment. |
| Requests fail only with long answers | You are using `GET` and the URL is too long. Use `POST`. |


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.