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

# Encrypted Integration

> Request an AES-256-GCM encrypted search response from the browser and decrypt it on your server.

## Overview

In the encrypted integration, the browser calls **SEARCH v4** with `encrypt=1`, using either a `GET` or a `POST` request. Research Defender wraps the normal JSON result in an AES-256-GCM encrypted blob before returning it, so the response that reaches the browser cannot be read without your **private key** and **encryptor key**.

Your page passes the encrypted blob to your server, and your server decrypts it. You can write the server side in Python, Node.js, C#, or any language with AES-GCM and PBKDF2 support.

<Note>
  This page builds on the [advanced integration](/research-defender/guides/search/03-advanced-integration). See also the [secured integration](/research-defender/guides/search/04-s2s-integration), where your server calls SEARCH directly.
</Note>

## How it works

```mermaid theme={null}
sequenceDiagram
    participant B as Respondent browser
    participant RD as Research Defender
    participant S as Your server

    B->>RD: Load library, init(), wait
    B->>B: pass_params()
    B->>RD: GET or POST search v4 (encrypt=1, pass_params() as parameter)
    RD-->>B: data (encrypted, base64)
    B->>S: data
    S->>S: Decrypt with private key
    S-->>B: Your decision / result
```

<Steps>
  <Step title="Collect library data in the browser">
    Load `rd_sys_util.min.js`, call `init()`, wait for the datasets, then call `pass_params()`.
  </Step>

  <Step title="Request an encrypted result">
    Call SEARCH v4 with your publishable key and `encrypt=1`. The response contains a single `data` field.
  </Step>

  <Step title="Send the blob to your server">
    Post the `data` value to an endpoint on your server.
  </Step>

  <Step title="Decrypt on your server">
    Derive the key from your private key and encryptor key, decrypt the blob, and parse the JSON.
  </Step>
</Steps>

## Prerequisites

* A Research Defender **publishable key**, used in the browser
* A Research Defender **private key** (a string), used only on your server to decrypt
* Your **encryptor key** (a string), used together with the private key to derive the decryption key
* Your **survey number** (`sy_nr`) and the respondent's **session ID** (`sn_ud`)
* A page served over **HTTP(S)**. Opening the file directly from disk (`file://`) will not work.

<Warning>
  Never include your private key or encryptor key in client-side code. Decrypt only on your server, and load both keys from environment variables or a secrets manager rather than source control.
</Warning>

## Step 1: Request an encrypted result

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

### Request parameters

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

<ParamField body="sy_nr" type="string" required>
  The survey number.
</ParamField>

<ParamField body="sn_ud" type="string" required>
  The respondent's session ID.
</ParamField>

<ParamField body="encrypt" type="string" required>
  Set to `"1"` to receive the result as an encrypted blob.
</ParamField>

<ParamField body="dupe_details" type="string">
  Set to `"1"` to enable.
</ParamField>

<ParamField body="df_details" type="string">
  Set to `"1"` to enable.
</ParamField>

<ParamField body="mfr" type="string">
  Set to `"1"` to enable.
</ParamField>

<ParamField body="{pass_params() value}" type="string" required>
  The string returned by `pass_params()`. Send it as the **name** of a parameter with an empty value: a query parameter for `GET`, or a JSON field for `POST`.
</ParamField>

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

### Encrypted response

```json theme={null}
{
  "data": "<base64-encoded-blob>"
}
```

Once decrypted, the blob contains the same JSON result as an unencrypted search.

### Browser example

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

    // Replace with the values for the respondent you are checking.
    const SURVEY_NUMBER = "YOUR_SURVEY_NUMBER"; // sy_nr
    const SESSION_ID = "YOUR_SESSION_ID";       // sn_ud

    // The endpoint on YOUR server that decrypts the response (see the server samples).
    const BACKEND_ENDPOINT = "/api/rd-decrypt";

    // Time to let the library collect its datasets before reading them.
    const DATASET_READY_DELAY_MS = 1000;

    const BASE_URL = `https://${ENVIRONMENT}.rtymgt.com`;
    const LIBRARY_URL = `${BASE_URL}/static/js/rd_sys_util.min.js?v=${LIBRARY_VERSION}`;
    const SEARCH_ENDPOINT = `${BASE_URL}/api/v4/respondents/search/${PUBLISHABLE_KEY}`;

    // ---------------------------------------------------------------
    // Helpers
    // ---------------------------------------------------------------
    const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

    /**
     * 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));
      }
    }

    /** Loads the library, starts data collection, and returns its pass-through key. */
    async function collectLibraryData() {
      const { init, pass_params } = await import(LIBRARY_URL);

      if (typeof init !== "function" || typeof pass_params !== "function") {
        throw new Error("Research Defender library did not load correctly.");
      }

      await init({ key: PUBLISHABLE_KEY, env: ENVIRONMENT });

      // Give the library time to finish collecting its datasets.
      await sleep(DATASET_READY_DELAY_MS);

      const pass = pass_params();
      console.log("[rd] pass_params computed:", pass ? `${pass.length} chars` : "empty");
      return pass;
    }

    /** Calls SEARCH v4 (POST) with encrypt=1 and returns the encrypted blob (base64). */
    async function searchEncrypted({ surveyNumber, sessionId, pass }) {
      const requestBody = {
        sy_nr: surveyNumber,
        sn_ud: sessionId,
        dupe_details: "1",
        df_details: "1",
        mfr: "1",
        encrypt: "1",
      };

      // The value returned by pass_params() is sent as a key with an empty
      // string value. JSON.stringify takes care of quoting and escaping.
      if (pass) {
        requestBody[pass] = "";
      }

      const response = await fetch(SEARCH_ENDPOINT, {
        method: "POST",
        credentials: "include", // send Research Defender cookies
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(requestBody),
      });

      if (!response.ok) {
        throw new Error(`Search request failed with status ${response.status}`);
      }

      const body = parseResponse(await response.text());

      if (typeof body.data !== "string") {
        throw new Error("Search response did not include encrypted data.");
      }

      return body.data;
    }

    /** Sends the encrypted blob to your server, which decrypts it with your private key. */
    async function decryptWithBackend(encryptedBlob) {
      const response = await fetch(BACKEND_ENDPOINT, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ data: encryptedBlob }),
      });

      if (!response.ok) {
        throw new Error(`Backend request failed with status ${response.status}`);
      }

      return response.json();
    }

    // ---------------------------------------------------------------
    // Usage
    // ---------------------------------------------------------------
    const pass = await collectLibraryData();

    const encryptedBlob = await searchEncrypted({
      surveyNumber: SURVEY_NUMBER,
      sessionId: SESSION_ID,
      pass,
    });

    const { result } = await decryptWithBackend(encryptedBlob);

    console.log("Threat Potential:", result.Respondent?.threat_potential);
    console.log("Duplicate Flag:", result.Surveys?.[0]?.flag);
    console.log("Survey Number:", result.Surveys?.[0]?.survey_number);
    ```
  </Tab>

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

    // Replace with the values for the respondent you are checking.
    const SURVEY_NUMBER = "YOUR_SURVEY_NUMBER"; // sy_nr
    const SESSION_ID = "YOUR_SESSION_ID";       // sn_ud

    // The endpoint on YOUR server that decrypts the response (see the server samples).
    const BACKEND_ENDPOINT = "/api/rd-decrypt";

    // Time to let the library collect its datasets before reading them.
    const DATASET_READY_DELAY_MS = 1000;

    const BASE_URL = `https://${ENVIRONMENT}.rtymgt.com`;
    const LIBRARY_URL = `${BASE_URL}/static/js/rd_sys_util.min.js?v=${LIBRARY_VERSION}`;
    const SEARCH_ENDPOINT = `${BASE_URL}/api/v4/respondents/search/${PUBLISHABLE_KEY}`;

    // ---------------------------------------------------------------
    // Helpers
    // ---------------------------------------------------------------
    const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

    /**
     * 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));
      }
    }

    /** Loads the library, starts data collection, and returns its pass-through key. */
    async function collectLibraryData() {
      const { init, pass_params } = await import(LIBRARY_URL);

      if (typeof init !== "function" || typeof pass_params !== "function") {
        throw new Error("Research Defender library did not load correctly.");
      }

      await init({ key: PUBLISHABLE_KEY, env: ENVIRONMENT });

      // Give the library time to finish collecting its datasets.
      await sleep(DATASET_READY_DELAY_MS);

      const pass = pass_params();
      console.log("[rd] pass_params computed:", pass ? `${pass.length} chars` : "empty");
      return pass;
    }

    /** Calls SEARCH v4 (GET) with encrypt=1 and returns the encrypted blob (base64). */
    async function searchEncrypted({ surveyNumber, sessionId, pass }) {
      const params = new URLSearchParams({
        sy_nr: surveyNumber,
        sn_ud: sessionId,
        dupe_details: "1",
        df_details: "1",
        mfr: "1",
        encrypt: "1",
      });

      // The value returned by pass_params() is sent as a parameter name with
      // an empty value.
      if (pass) {
        params.set(pass, "");
      }

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

      if (!response.ok) {
        throw new Error(`Search request failed with status ${response.status}`);
      }

      const body = parseResponse(await response.text());

      if (typeof body.data !== "string") {
        throw new Error("Search response did not include encrypted data.");
      }

      return body.data;
    }

    /** Sends the encrypted blob to your server, which decrypts it with your private key. */
    async function decryptWithBackend(encryptedBlob) {
      const response = await fetch(BACKEND_ENDPOINT, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ data: encryptedBlob }),
      });

      if (!response.ok) {
        throw new Error(`Backend request failed with status ${response.status}`);
      }

      return response.json();
    }

    // ---------------------------------------------------------------
    // Usage
    // ---------------------------------------------------------------
    const pass = await collectLibraryData();

    const encryptedBlob = await searchEncrypted({
      surveyNumber: SURVEY_NUMBER,
      sessionId: SESSION_ID,
      pass,
    });

    const { result } = await decryptWithBackend(encryptedBlob);

    console.log("Threat Potential:", result.Respondent?.threat_potential);
    console.log("Duplicate Flag:", result.Surveys?.[0]?.flag);
    console.log("Survey Number:", result.Surveys?.[0]?.survey_number);
    ```
  </Tab>
</Tabs>

## Step 2: Decrypt on your server

### Blob format

After base64 decoding, the blob has this layout:

| Bytes | Length | Contents |
| - | - | - |
| 0–15 | 16 bytes | PBKDF2 salt |
| 16–27 | 12 bytes | AES-GCM nonce |
| 28–end | Variable | Ciphertext followed by the 16-byte authentication tag |

### Keys

| Key | Type | Description |
| - | - | - |
| Private key | String | Your Research Defender private key. Used as the password when deriving the decryption key. |
| Encryptor key | String | Appended to the salt from the blob when deriving the decryption key. |

### Key derivation

| Setting | Value |
| - | - |
| Algorithm | PBKDF2-HMAC-SHA256 |
| Password | Your private key, as UTF-8 bytes |
| Salt | The 16-byte salt from the blob, followed by your encryptor key as UTF-8 bytes |
| Iterations | `100000` |
| Key length | 32 bytes (AES-256) |

### Decryption

Decrypt with **AES-256-GCM**, using the nonce from the blob and no additional authenticated data. Decryption fails if either key is wrong or the data was modified.

<Tabs>
  <Tab title="Python">
    ```python decrypt.py theme={null}
    # pip install cryptography
    import base64
    import json

    from cryptography.hazmat.primitives import hashes
    from cryptography.hazmat.primitives.ciphers.aead import AESGCM
    from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC

    # ---------------------------------------------------------------
    # Configuration
    # ---------------------------------------------------------------
    # Your PRIVATE key. Keep it on the server only: never send it to a browser,
    # and never commit it to source control.
    PRIVATE_KEY = "YOUR_PRIVATE_KEY"

    # Your encryptor key (a string). Keep it on the server only, like the private key.
    ENCRYPTOR_KEY = "YOUR_ENCRYPTOR_KEY"

    PBKDF2_ITERATIONS = 100_000

    SALT_LENGTH = 16
    NONCE_LENGTH = 12
    TAG_LENGTH = 16


    # ---------------------------------------------------------------
    # Decryption
    # ---------------------------------------------------------------
    def decrypt_response(b64_blob: str, private_key: str, encryptor_key: str) -> dict:
        """Decrypt the "data" value of an encrypted search response.

        Blob layout (after base64 decoding):
            bytes 0-15   PBKDF2 salt
            bytes 16-27  AES-GCM nonce
            bytes 28-end ciphertext followed by the 16-byte authentication tag
        """
        blob = base64.b64decode(b64_blob)

        if len(blob) < SALT_LENGTH + NONCE_LENGTH + TAG_LENGTH:
            raise ValueError("Encrypted response is too short")

        pbkdf2_salt = blob[:SALT_LENGTH]
        nonce = blob[SALT_LENGTH:SALT_LENGTH + NONCE_LENGTH]
        ciphertext_tag = blob[SALT_LENGTH + NONCE_LENGTH:]

        # Derive the AES-256 key from your private key and encryptor key.
        kdf = PBKDF2HMAC(
            algorithm=hashes.SHA256(),
            length=32,
            salt=pbkdf2_salt + encryptor_key.encode("utf-8"),
            iterations=PBKDF2_ITERATIONS,
        )
        key = kdf.derive(private_key.encode("utf-8"))

        # Decrypt and verify the authentication tag (no additional data).
        plaintext = AESGCM(key).decrypt(nonce, ciphertext_tag, None)

        return json.loads(plaintext.decode("utf-8"))


    # Usage: encrypted = {"data": "<base64-encoded-blob>"}  (the search response)
    # result = decrypt_response(encrypted["data"], PRIVATE_KEY, ENCRYPTOR_KEY)
    ```
  </Tab>

  <Tab title="Node.js">
    ```javascript decrypt.js theme={null}
    // Node.js built-in crypto module, no packages required
    const crypto = require("crypto");

    // ---------------------------------------------------------------
    // Configuration
    // ---------------------------------------------------------------
    // Your PRIVATE key. Keep it on the server only: never send it to a browser,
    // and never commit it to source control.
    const PRIVATE_KEY = "YOUR_PRIVATE_KEY";

    // Your encryptor key (a string). Keep it on the server only, like the private key.
    const ENCRYPTOR_KEY = "YOUR_ENCRYPTOR_KEY";

    const PBKDF2_ITERATIONS = 100000;

    const SALT_LENGTH = 16;
    const NONCE_LENGTH = 12;
    const TAG_LENGTH = 16;

    // ---------------------------------------------------------------
    // Decryption
    // ---------------------------------------------------------------

    /**
     * Decrypts the "data" value of an encrypted search response.
     *
     * Blob layout (after base64 decoding):
     *   bytes 0-15    PBKDF2 salt
     *   bytes 16-27   AES-GCM nonce
     *   bytes 28-end  ciphertext followed by the 16-byte authentication tag
     */
    function decryptResponse(b64Blob, privateKey, encryptorKey) {
      const blob = Buffer.from(b64Blob, "base64");

      if (blob.length < SALT_LENGTH + NONCE_LENGTH + TAG_LENGTH) {
        throw new Error("Encrypted response is too short");
      }

      const pbkdf2Salt = blob.subarray(0, SALT_LENGTH);
      const nonce = blob.subarray(SALT_LENGTH, SALT_LENGTH + NONCE_LENGTH);
      const ciphertextWithTag = blob.subarray(SALT_LENGTH + NONCE_LENGTH);

      // Derive the AES-256 key from your private key and encryptor key.
      const key = crypto.pbkdf2Sync(
        Buffer.from(privateKey, "utf-8"),
        Buffer.concat([pbkdf2Salt, Buffer.from(encryptorKey, "utf-8")]),
        PBKDF2_ITERATIONS,
        32,
        "sha256"
      );

      // AES-GCM appends the 16-byte authentication tag to the ciphertext.
      const authTag = ciphertextWithTag.subarray(-TAG_LENGTH);
      const ciphertext = ciphertextWithTag.subarray(0, -TAG_LENGTH);

      // Decrypt and verify the authentication tag (final() throws if it does not match).
      const decipher = crypto.createDecipheriv("aes-256-gcm", key, nonce);
      decipher.setAuthTag(authTag);
      const decrypted = Buffer.concat([decipher.update(ciphertext), decipher.final()]);

      return JSON.parse(decrypted.toString("utf-8"));
    }

    // Usage: const encrypted = { data: "<base64-encoded-blob>" };  // the search response
    // const result = decryptResponse(encrypted.data, PRIVATE_KEY, ENCRYPTOR_KEY);
    ```
  </Tab>

  <Tab title="C#">
    ```csharp ResearchDefenderDecryptor.cs theme={null}
    using System.Security.Cryptography;
    using System.Text;
    using System.Text.Json;

    public static class ResearchDefenderDecryptor
    {
        // Your PRIVATE key. Keep it on the server only: load it from configuration
        // or a secrets manager, and never send it to a browser.
        private static readonly string PrivateKey =
            Environment.GetEnvironmentVariable("RD_PRIVATE_KEY") ?? "YOUR_PRIVATE_KEY";

        // Your encryptor key (a string). Keep it on the server only, like the private key.
        private static readonly string EncryptorKey =
            Environment.GetEnvironmentVariable("RD_ENCRYPTOR_KEY") ?? "YOUR_ENCRYPTOR_KEY";

        private const int Pbkdf2Iterations = 100_000;
        private const int SaltLength = 16;
        private const int NonceLength = 12;
        private const int TagLength = 16;

        /// <summary>
        /// Decrypts the "data" value of an encrypted search response.
        ///
        /// Blob layout (after base64 decoding):
        ///   bytes 0-15    PBKDF2 salt
        ///   bytes 16-27   AES-GCM nonce
        ///   bytes 28-end  ciphertext followed by the 16-byte authentication tag
        /// </summary>
        public static JsonElement Decrypt(string base64Blob)
        {
            var blob = Convert.FromBase64String(base64Blob);

            if (blob.Length < SaltLength + NonceLength + TagLength)
            {
                throw new CryptographicException("Encrypted response is too short");
            }

            var pbkdf2Salt = blob.AsSpan(0, SaltLength);
            var nonce = blob.AsSpan(SaltLength, NonceLength);
            var ciphertextWithTag = blob.AsSpan(SaltLength + NonceLength);
            var ciphertext = ciphertextWithTag[..^TagLength];
            var tag = ciphertextWithTag[^TagLength..];

            // Derive the AES-256 key from your private key and encryptor key.
            var fullSalt = pbkdf2Salt.ToArray().Concat(Encoding.UTF8.GetBytes(EncryptorKey)).ToArray();
            var key = Rfc2898DeriveBytes.Pbkdf2(
                Encoding.UTF8.GetBytes(PrivateKey),
                fullSalt,
                Pbkdf2Iterations,
                HashAlgorithmName.SHA256,
                32);

            // Decrypt and verify the authentication tag (no additional data).
            var plaintext = new byte[ciphertext.Length];
            using var aes = new AesGcm(key, TagLength);
            aes.Decrypt(nonce, ciphertext, tag, plaintext);

            return JsonDocument.Parse(plaintext).RootElement.Clone();
        }
    }

    // Usage: var result = ResearchDefenderDecryptor.Decrypt(encryptedBlobFromSearchResponse);
    ```
  </Tab>
</Tabs>

## Full samples

The samples below work together. The browser page posts the encrypted blob to `/api/rd-decrypt` on your server, and your server decrypts it.

<AccordionGroup>
  <Accordion title="Browser page, POST (HTML)">
    ```html encrypted-client-post.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 – Encrypted Integration (POST)</title>
    </head>
    <body>
      <h1>Research Defender – Encrypted Integration (POST)</h1>
      <pre id="output">Loading…</pre>

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

        // Replace with the values for the respondent you are checking.
        const SURVEY_NUMBER = "YOUR_SURVEY_NUMBER"; // sy_nr
        const SESSION_ID = "YOUR_SESSION_ID";       // sn_ud

        // The endpoint on YOUR server that decrypts the response (see the server samples).
        const BACKEND_ENDPOINT = "/api/rd-decrypt";

        // Time to let the library collect its datasets before reading them.
        const DATASET_READY_DELAY_MS = 1000;

        const BASE_URL = `https://${ENVIRONMENT}.rtymgt.com`;
        const LIBRARY_URL = `${BASE_URL}/static/js/rd_sys_util.min.js?v=${LIBRARY_VERSION}`;
        const SEARCH_ENDPOINT = `${BASE_URL}/api/v4/respondents/search/${PUBLISHABLE_KEY}`;

        // ---------------------------------------------------------------
        // Helpers
        // ---------------------------------------------------------------
        const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

        /**
         * 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));
          }
        }

        /** Loads the library, starts data collection, and returns its pass-through key. */
        async function collectLibraryData() {
          const { init, pass_params } = await import(LIBRARY_URL);

          if (typeof init !== "function" || typeof pass_params !== "function") {
            throw new Error("Research Defender library did not load correctly.");
          }

          await init({ key: PUBLISHABLE_KEY, env: ENVIRONMENT });

          // Give the library time to finish collecting its datasets.
          await sleep(DATASET_READY_DELAY_MS);

          const pass = pass_params();
          console.log("[rd] pass_params computed:", pass ? `${pass.length} chars` : "empty");
          return pass;
        }

        /** Calls SEARCH v4 (POST) with encrypt=1 and returns the encrypted blob (base64). */
        async function searchEncrypted({ surveyNumber, sessionId, pass }) {
          const requestBody = {
            sy_nr: surveyNumber,
            sn_ud: sessionId,
            dupe_details: "1",
            df_details: "1",
            mfr: "1",
            encrypt: "1",
          };

          // The value returned by pass_params() is sent as a key with an empty
          // string value. JSON.stringify takes care of quoting and escaping.
          if (pass) {
            requestBody[pass] = "";
          }

          const response = await fetch(SEARCH_ENDPOINT, {
            method: "POST",
            credentials: "include", // send Research Defender cookies
            headers: { "Content-Type": "application/json" },
            body: JSON.stringify(requestBody),
          });

          if (!response.ok) {
            throw new Error(`Search request failed with status ${response.status}`);
          }

          const body = parseResponse(await response.text());

          if (typeof body.data !== "string") {
            throw new Error("Search response did not include encrypted data.");
          }

          return body.data;
        }

        /** Sends the encrypted blob to your server, which decrypts it with your private key. */
        async function decryptWithBackend(encryptedBlob) {
          const response = await fetch(BACKEND_ENDPOINT, {
            method: "POST",
            headers: { "Content-Type": "application/json" },
            body: JSON.stringify({ data: encryptedBlob }),
          });

          if (!response.ok) {
            throw new Error(`Backend request failed with status ${response.status}`);
          }

          return response.json();
        }

        // ---------------------------------------------------------------
        // Run
        // ---------------------------------------------------------------
        const output = document.getElementById("output");

        try {
          const pass = await collectLibraryData();

          const encryptedBlob = await searchEncrypted({
            surveyNumber: SURVEY_NUMBER,
            sessionId: SESSION_ID,
            pass,
          });

          const { result } = await decryptWithBackend(encryptedBlob);

          console.log("Full results:", result);

          const survey = result.Surveys?.[0];
          output.textContent = [
            `Threat Potential: ${result.Respondent?.threat_potential}`,
            `Duplicate Flag:   ${survey?.flag}`,
            `Survey Number:    ${survey?.survey_number}`,
            "",
            "Full response:",
            JSON.stringify(result, null, 2),
          ].join("\n");
        } catch (error) {
          console.error(error);
          output.textContent = `Error: ${error.message}`;
        }
      </script>
    </body>
    </html>
    ```
  </Accordion>

  <Accordion title="Browser page, GET (HTML)">
    ```html encrypted-client-get.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 – Encrypted Integration (GET)</title>
    </head>
    <body>
      <h1>Research Defender – Encrypted Integration (GET)</h1>
      <pre id="output">Loading…</pre>

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

        // Replace with the values for the respondent you are checking.
        const SURVEY_NUMBER = "YOUR_SURVEY_NUMBER"; // sy_nr
        const SESSION_ID = "YOUR_SESSION_ID";       // sn_ud

        // The endpoint on YOUR server that decrypts the response (see the server samples).
        const BACKEND_ENDPOINT = "/api/rd-decrypt";

        // Time to let the library collect its datasets before reading them.
        const DATASET_READY_DELAY_MS = 1000;

        const BASE_URL = `https://${ENVIRONMENT}.rtymgt.com`;
        const LIBRARY_URL = `${BASE_URL}/static/js/rd_sys_util.min.js?v=${LIBRARY_VERSION}`;
        const SEARCH_ENDPOINT = `${BASE_URL}/api/v4/respondents/search/${PUBLISHABLE_KEY}`;

        // ---------------------------------------------------------------
        // Helpers
        // ---------------------------------------------------------------
        const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

        /**
         * 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));
          }
        }

        /** Loads the library, starts data collection, and returns its pass-through key. */
        async function collectLibraryData() {
          const { init, pass_params } = await import(LIBRARY_URL);

          if (typeof init !== "function" || typeof pass_params !== "function") {
            throw new Error("Research Defender library did not load correctly.");
          }

          await init({ key: PUBLISHABLE_KEY, env: ENVIRONMENT });

          // Give the library time to finish collecting its datasets.
          await sleep(DATASET_READY_DELAY_MS);

          const pass = pass_params();
          console.log("[rd] pass_params computed:", pass ? `${pass.length} chars` : "empty");
          return pass;
        }

        /** Calls SEARCH v4 (GET) with encrypt=1 and returns the encrypted blob (base64). */
        async function searchEncrypted({ surveyNumber, sessionId, pass }) {
          const params = new URLSearchParams({
            sy_nr: surveyNumber,
            sn_ud: sessionId,
            dupe_details: "1",
            df_details: "1",
            mfr: "1",
            encrypt: "1",
          });

          // The value returned by pass_params() is sent as a parameter name with
          // an empty value.
          if (pass) {
            params.set(pass, "");
          }

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

          if (!response.ok) {
            throw new Error(`Search request failed with status ${response.status}`);
          }

          const body = parseResponse(await response.text());

          if (typeof body.data !== "string") {
            throw new Error("Search response did not include encrypted data.");
          }

          return body.data;
        }

        /** Sends the encrypted blob to your server, which decrypts it with your private key. */
        async function decryptWithBackend(encryptedBlob) {
          const response = await fetch(BACKEND_ENDPOINT, {
            method: "POST",
            headers: { "Content-Type": "application/json" },
            body: JSON.stringify({ data: encryptedBlob }),
          });

          if (!response.ok) {
            throw new Error(`Backend request failed with status ${response.status}`);
          }

          return response.json();
        }

        // ---------------------------------------------------------------
        // Run
        // ---------------------------------------------------------------
        const output = document.getElementById("output");

        try {
          const pass = await collectLibraryData();

          const encryptedBlob = await searchEncrypted({
            surveyNumber: SURVEY_NUMBER,
            sessionId: SESSION_ID,
            pass,
          });

          const { result } = await decryptWithBackend(encryptedBlob);

          console.log("Full results:", result);

          const survey = result.Surveys?.[0];
          output.textContent = [
            `Threat Potential: ${result.Respondent?.threat_potential}`,
            `Duplicate Flag:   ${survey?.flag}`,
            `Survey Number:    ${survey?.survey_number}`,
            "",
            "Full response:",
            JSON.stringify(result, null, 2),
          ].join("\n");
        } catch (error) {
          console.error(error);
          output.textContent = `Error: ${error.message}`;
        }
      </script>
    </body>
    </html>
    ```
  </Accordion>

  <Accordion title="Python server (Flask)">
    ```python encrypted_server.py theme={null}
    """Research Defender - encrypted response sample (Python / Flask).

    Receives the encrypted blob from your page and decrypts it with your
    PRIVATE key.

        pip install flask cryptography
        export RD_PRIVATE_KEY="your-private-key"
        export RD_ENCRYPTOR_KEY="your-encryptor-key"
        python encrypted_server.py
    """

    import base64
    import json
    import os

    from cryptography.exceptions import InvalidTag
    from cryptography.hazmat.primitives import hashes
    from cryptography.hazmat.primitives.ciphers.aead import AESGCM
    from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
    from flask import Flask, jsonify, request, send_from_directory

    # ---------------------------------------------------------------
    # Configuration
    # ---------------------------------------------------------------
    # Your PRIVATE key. Keep it on the server only: never send it to a browser,
    # and never commit it to source control.
    PRIVATE_KEY = os.getenv("RD_PRIVATE_KEY", "YOUR_PRIVATE_KEY")

    # Your encryptor key (a string). Keep it on the server only, like the private key.
    ENCRYPTOR_KEY = os.getenv("RD_ENCRYPTOR_KEY", "YOUR_ENCRYPTOR_KEY")

    PBKDF2_ITERATIONS = 100_000

    SALT_LENGTH = 16
    NONCE_LENGTH = 12
    TAG_LENGTH = 16


    # ---------------------------------------------------------------
    # Decryption
    # ---------------------------------------------------------------
    def decrypt_response(b64_blob: str, private_key: str, encryptor_key: str) -> dict:
        """Decrypt the "data" value of an encrypted search response.

        Blob layout (after base64 decoding):
            bytes 0-15   PBKDF2 salt
            bytes 16-27  AES-GCM nonce
            bytes 28-end ciphertext followed by the 16-byte authentication tag
        """
        blob = base64.b64decode(b64_blob)

        if len(blob) < SALT_LENGTH + NONCE_LENGTH + TAG_LENGTH:
            raise ValueError("Encrypted response is too short")

        pbkdf2_salt = blob[:SALT_LENGTH]
        nonce = blob[SALT_LENGTH:SALT_LENGTH + NONCE_LENGTH]
        ciphertext_tag = blob[SALT_LENGTH + NONCE_LENGTH:]

        # Derive the AES-256 key from your private key and encryptor key.
        kdf = PBKDF2HMAC(
            algorithm=hashes.SHA256(),
            length=32,
            salt=pbkdf2_salt + encryptor_key.encode("utf-8"),
            iterations=PBKDF2_ITERATIONS,
        )
        key = kdf.derive(private_key.encode("utf-8"))

        # Decrypt and verify the authentication tag (no additional data).
        plaintext = AESGCM(key).decrypt(nonce, ciphertext_tag, None)

        return json.loads(plaintext.decode("utf-8"))


    # ---------------------------------------------------------------
    # Web app (serves the sample page and exposes /api/rd-decrypt)
    # ---------------------------------------------------------------
    app = Flask(__name__, static_folder="static", static_url_path="/static")


    @app.get("/")
    def index():
        return send_from_directory("static", "index.html")


    @app.post("/api/rd-decrypt")
    def rd_decrypt():
        data = request.get_json(silent=True) or {}
        blob = data.get("data")

        if not isinstance(blob, str) or not blob:
            return jsonify(error="data is required"), 400

        try:
            result = decrypt_response(blob, PRIVATE_KEY, ENCRYPTOR_KEY)
        except (InvalidTag, ValueError) as error:
            # Wrong key, tampered data, or malformed blob.
            app.logger.error("Decryption failed: %s", type(error).__name__)
            return jsonify(error="Could not decrypt the response"), 422

        # For production, return only what your page needs (for example a
        # pass/fail decision) instead of the full result.
        return jsonify(result=result)


    if __name__ == "__main__":
        app.run(port=8000, debug=True)
    ```
  </Accordion>

  <Accordion title="Node.js server (Express)">
    ```javascript encrypted_server.js theme={null}
    /**
     * Research Defender - encrypted response sample (Node.js / Express).
     *
     * Receives the encrypted blob from your page and decrypts it with your
     * PRIVATE key.
     *
     *   npm install express
     *   export RD_PRIVATE_KEY="your-private-key"
     *   export RD_ENCRYPTOR_KEY="your-encryptor-key"
     *   node encrypted_server.js
     */

    const crypto = require("crypto");
    const path = require("path");
    const express = require("express");

    // ---------------------------------------------------------------
    // Configuration
    // ---------------------------------------------------------------
    // Your PRIVATE key. Keep it on the server only: never send it to a browser,
    // and never commit it to source control.
    const PRIVATE_KEY = process.env.RD_PRIVATE_KEY || "YOUR_PRIVATE_KEY";

    // Your encryptor key (a string). Keep it on the server only, like the private key.
    const ENCRYPTOR_KEY = process.env.RD_ENCRYPTOR_KEY || "YOUR_ENCRYPTOR_KEY";

    const PBKDF2_ITERATIONS = 100000;

    const SALT_LENGTH = 16;
    const NONCE_LENGTH = 12;
    const TAG_LENGTH = 16;

    // ---------------------------------------------------------------
    // Decryption
    // ---------------------------------------------------------------

    /**
     * Decrypts the "data" value of an encrypted search response.
     *
     * Blob layout (after base64 decoding):
     *   bytes 0-15    PBKDF2 salt
     *   bytes 16-27   AES-GCM nonce
     *   bytes 28-end  ciphertext followed by the 16-byte authentication tag
     */
    function decryptResponse(b64Blob, privateKey, encryptorKey) {
      const blob = Buffer.from(b64Blob, "base64");

      if (blob.length < SALT_LENGTH + NONCE_LENGTH + TAG_LENGTH) {
        throw new Error("Encrypted response is too short");
      }

      const pbkdf2Salt = blob.subarray(0, SALT_LENGTH);
      const nonce = blob.subarray(SALT_LENGTH, SALT_LENGTH + NONCE_LENGTH);
      const ciphertextWithTag = blob.subarray(SALT_LENGTH + NONCE_LENGTH);

      // Derive the AES-256 key from your private key and encryptor key.
      const key = crypto.pbkdf2Sync(
        Buffer.from(privateKey, "utf-8"),
        Buffer.concat([pbkdf2Salt, Buffer.from(encryptorKey, "utf-8")]),
        PBKDF2_ITERATIONS,
        32,
        "sha256"
      );

      // AES-GCM appends the 16-byte authentication tag to the ciphertext.
      const authTag = ciphertextWithTag.subarray(-TAG_LENGTH);
      const ciphertext = ciphertextWithTag.subarray(0, -TAG_LENGTH);

      // Decrypt and verify the authentication tag (final() throws if it does not match).
      const decipher = crypto.createDecipheriv("aes-256-gcm", key, nonce);
      decipher.setAuthTag(authTag);
      const decrypted = Buffer.concat([decipher.update(ciphertext), decipher.final()]);

      return JSON.parse(decrypted.toString("utf-8"));
    }

    // ---------------------------------------------------------------
    // Web app (serves the sample page and exposes /api/rd-decrypt)
    // ---------------------------------------------------------------
    const app = express();
    app.use(express.json());

    app.get("/", (req, res) => {
      res.sendFile(path.join(__dirname, "static", "index.html"));
    });

    app.post("/api/rd-decrypt", (req, res) => {
      const { data } = req.body ?? {};

      if (typeof data !== "string" || !data) {
        return res.status(400).json({ error: "data is required" });
      }

      try {
        const result = decryptResponse(data, PRIVATE_KEY, ENCRYPTOR_KEY);

        // For production, return only what your page needs (for example a
        // pass/fail decision) instead of the full result.
        return res.json({ result });
      } catch (error) {
        // Wrong key, tampered data, or malformed blob.
        console.error(`Decryption failed: ${error.name}`);
        return res.status(422).json({ error: "Could not decrypt the response" });
      }
    });

    app.listen(8000, () => console.log("Listening on http://localhost:8000"));
    ```
  </Accordion>

  <Accordion title="C# server (ASP.NET Core)">
    ```csharp EncryptedServer.cs theme={null}
    // Research Defender - encrypted response sample (C# / ASP.NET Core minimal API, .NET 8).
    //
    // Receives the encrypted blob from your page and decrypts it with your
    // PRIVATE key.
    //
    //   dotnet new web -n EncryptedServer
    //   (replace Program.cs with this file, put the client page in wwwroot/ as index.html)
    //   export RD_PRIVATE_KEY="your-private-key"
    //   export RD_ENCRYPTOR_KEY="your-encryptor-key"
    //   dotnet run

    using System.Security.Cryptography;
    using System.Text;
    using System.Text.Json;

    var builder = WebApplication.CreateBuilder(args);
    var app = builder.Build();

    app.UseDefaultFiles();
    app.UseStaticFiles();

    app.MapPost("/api/rd-decrypt", (DecryptRequest request, ILogger<Program> logger) =>
    {
        if (string.IsNullOrWhiteSpace(request.Data))
        {
            return Results.BadRequest(new { error = "data is required" });
        }

        try
        {
            var result = ResearchDefenderDecryptor.Decrypt(request.Data);

            // For production, return only what your page needs (for example a
            // pass/fail decision) instead of the full result.
            return Results.Json(new { result });
        }
        catch (Exception ex) when (ex is CryptographicException or FormatException or JsonException)
        {
            // Wrong key, tampered data, or malformed blob.
            logger.LogError("Decryption failed: {ErrorType}", ex.GetType().Name);
            return Results.Json(new { error = "Could not decrypt the response" }, statusCode: StatusCodes.Status422UnprocessableEntity);
        }
    });

    app.Run();

    // ---------------------------------------------------------------
    // Request model (JSON property names are matched case-insensitively)
    // ---------------------------------------------------------------
    public record DecryptRequest(string Data);

    // ---------------------------------------------------------------
    // Decryption
    // ---------------------------------------------------------------
    public static class ResearchDefenderDecryptor
    {
        // Your PRIVATE key. Keep it on the server only: load it from configuration
        // or a secrets manager, and never send it to a browser.
        private static readonly string PrivateKey =
            Environment.GetEnvironmentVariable("RD_PRIVATE_KEY") ?? "YOUR_PRIVATE_KEY";

        // Your encryptor key (a string). Keep it on the server only, like the private key.
        private static readonly string EncryptorKey =
            Environment.GetEnvironmentVariable("RD_ENCRYPTOR_KEY") ?? "YOUR_ENCRYPTOR_KEY";

        private const int Pbkdf2Iterations = 100_000;
        private const int SaltLength = 16;
        private const int NonceLength = 12;
        private const int TagLength = 16;

        /// <summary>
        /// Decrypts the "data" value of an encrypted search response.
        ///
        /// Blob layout (after base64 decoding):
        ///   bytes 0-15    PBKDF2 salt
        ///   bytes 16-27   AES-GCM nonce
        ///   bytes 28-end  ciphertext followed by the 16-byte authentication tag
        /// </summary>
        public static JsonElement Decrypt(string base64Blob)
        {
            var blob = Convert.FromBase64String(base64Blob);

            if (blob.Length < SaltLength + NonceLength + TagLength)
            {
                throw new CryptographicException("Encrypted response is too short");
            }

            var pbkdf2Salt = blob.AsSpan(0, SaltLength);
            var nonce = blob.AsSpan(SaltLength, NonceLength);
            var ciphertextWithTag = blob.AsSpan(SaltLength + NonceLength);
            var ciphertext = ciphertextWithTag[..^TagLength];
            var tag = ciphertextWithTag[^TagLength..];

            // Derive the AES-256 key from your private key and encryptor key.
            var fullSalt = pbkdf2Salt.ToArray().Concat(Encoding.UTF8.GetBytes(EncryptorKey)).ToArray();
            var key = Rfc2898DeriveBytes.Pbkdf2(
                Encoding.UTF8.GetBytes(PrivateKey),
                fullSalt,
                Pbkdf2Iterations,
                HashAlgorithmName.SHA256,
                32);

            // Decrypt and verify the authentication tag (no additional data).
            var plaintext = new byte[ciphertext.Length];
            using var aes = new AesGcm(key, TagLength);
            aes.Decrypt(nonce, ciphertext, tag, plaintext);

            return JsonDocument.Parse(plaintext).RootElement.Clone();
        }
    }
    ```
  </Accordion>
</AccordionGroup>

To run the Python or Node.js sample, copy either browser page into a `static` folder next to the server file as `index.html`, set the `RD_PRIVATE_KEY` and `RD_ENCRYPTOR_KEY` environment variables, start the server (`python encrypted_server.py` or `node encrypted_server.js`), and open `http://localhost:8000`. The Python sample needs `pip install flask cryptography`, and the Node.js sample needs `npm install express`.

## Security recommendations

* **Keep the private key and encryptor key on your server.** Load them from environment variables or a secrets manager, and never send them to the browser.
* **Treat a decryption failure as an error.** AES-GCM verifies the data as it decrypts, so a failure means a wrong key or modified data. Do not fall back to trusting the blob.
* **Return only what the page needs.** The samples return the full decrypted result for demonstration. In production, return a decision such as pass or fail.
* **Keep decrypted results out of logs** unless you need them, and never log your private key or encryptor key.

## Troubleshooting

| Symptom | Likely cause |
| - | - |
| Authentication or "invalid tag" error | The private key or encryptor key is wrong, or the blob was modified. |
| "Encrypted response is too short" | The value passed to your server is not the full `data` string. |
| Base64 or format error | The blob was truncated or altered in transit. Post the `data` value unchanged. |
| Browser shows "did not include encrypted data" | The search request did not include `encrypt=1`, or the API returned an error. |


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