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

# Server-to-Server Integration

> Get a respondent token in the browser and call the Research Defender search API from your own server.

## Overview

The secured integration builds on the [advanced integration](/research-defender/guides/search/03-advanced-integration) and moves the search call to your server. Instead of the browser calling the search API directly, the browser collects two values and hands them to your backend:

1. **`pass_params()`**, the value produced by the Research Defender JavaScript library.
2. **A token** from the `get_token` API that identifies the respondent.

Your server then calls the **SEARCH v3** API with your **private key**, sending `pass_params()` as a request parameter and the token in a header named `token`. You can write the server side in any language. This page includes Python, Node.js, and C# samples.

## 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 get_token
    RD-->>B: token
    B->>S: pass_params() + token + survey number + session ID
    S->>RD: GET search v3 (private key, pass_params() as parameter, token in header)
    RD-->>S: Search result
    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="Get a token">
    Call the `get_token` API from the browser. It returns a token that contains information about the respondent.
  </Step>

  <Step title="Send both values to your server">
    Post `pass_params()` and the token to an endpoint on your server, along with your survey number and the session ID.
  </Step>

  <Step title="Call the search API from your server">
    Call SEARCH v3 with your private key, send `pass_params()` as part of the request parameters, and send the token in the `token` header.
  </Step>
</Steps>

## Prerequisites

* A Research Defender **publishable key**, used in the browser for the library and `get_token`
* A Research Defender **private key**, used only on your server for SEARCH v3
* Your **survey number** (`sy_nr`) and the respondent's **session ID** (`sn_ud`)
* A server endpoint your page can call, ideally on the **same origin** as the page
* A page served over **HTTP(S)**. Opening the file directly from disk (`file://`) will not work.

<Warning>
  Use your **publishable** key in the browser and your **private** key only on your server. Never include the private key in client-side code, and load it from an environment variable or secrets manager rather than source control.
</Warning>

## Step 1: Get a token

Call `get_token` from the browser, after the library has been initialized.

<ParamField path="Endpoint" type="string">
  `GET https://{environment}.rtymgt.com/api/v3/respondents/get_token/{publishable_key}`
</ParamField>

### Query parameters

<ParamField query="rt_sr_pd" type="string">
  Optional.
</ParamField>

<ParamField query="rt_hd_el" type="string">
  Optional.
</ParamField>

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

### Example request

```text theme={null}
https://staging.rtymgt.com/api/v3/respondents/get_token/YOUR_PUBLISHABLE_KEY?rt_sr_pd=AAA&rt_hd_el=BBBB
```

### Example response

```json theme={null}
{
  "results": [
    {
      "token": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
      "timestamp": "2026-10-18 15:47:31.984570 [UTC]",
      "message": "Token successfully generated"
    }
  ]
}
```

<ResponseField name="results[0].token" type="string">
  The token to send to your server.
</ResponseField>

<ResponseField name="results[0].timestamp" type="string">
  When the token was generated, in UTC.
</ResponseField>

<ResponseField name="results[0].message" type="string">
  A status message, for example `Token successfully generated`.
</ResponseField>

## Step 2: Browser integration

<CodeGroup>
  ```javascript secured-client.js theme={null}
  // ---------------------------------------------------------------
  // Configuration
  // ---------------------------------------------------------------
  const ENVIRONMENT = "prod"; // "staging" or "prod"
  const PUBLISHABLE_KEY = "YOUR_PUBLISHABLE_KEY";
  const LIBRARY_VERSION = "2.0.1";

  // Optional get_token parameters. Leave empty to omit them.
  const RT_SR_PD = "";
  const RT_HD_EL = "";

  // 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 performs the search (see the server samples).
  const BACKEND_ENDPOINT = "/api/rd-check";

  // 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 GET_TOKEN_ENDPOINT = `${BASE_URL}/api/v3/respondents/get_token/${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;
  }

  /** Requests a token that identifies this respondent. */
  async function getToken() {
    const params = new URLSearchParams();
    if (RT_SR_PD) params.set("rt_sr_pd", RT_SR_PD);
    if (RT_HD_EL) params.set("rt_hd_el", RT_HD_EL);

    const query = params.toString();
    const url = query ? `${GET_TOKEN_ENDPOINT}?${query}` : GET_TOKEN_ENDPOINT;

    const response = await fetch(url, {
      method: "GET",
      credentials: "include", // send Research Defender cookies
    });

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

    const data = parseResponse(await response.text());
    const token = data.results?.[0]?.token;

    if (!token) {
      throw new Error("get_token response did not include a token.");
    }

    return token;
  }

  /** Sends pass_params() and the token to your server, which calls the search API. */
  async function checkWithBackend({ surveyNumber, sessionId, pass, token }) {
    const response = await fetch(BACKEND_ENDPOINT, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ surveyNumber, sessionId, pass, token }),
    });

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

    return response.json();
  }

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

  const { result } = await checkWithBackend({
    surveyNumber: SURVEY_NUMBER,
    sessionId: SESSION_ID,
    pass,
    token,
  });

  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);
  ```
</CodeGroup>

`collectLibraryData()` is the same function used in the advanced integration. `checkWithBackend()` posts `surveyNumber`, `sessionId`, `pass` and `token` to your server.

## Step 3: Server integration

Your server receives the values from the browser and calls the search API.

<ParamField path="Endpoint" type="string">
  `GET https://{environment}.rtymgt.com/api/v3/respondents/search/{private_key}`
</ParamField>

<Warning>
  The private key is part of this URL. Call this endpoint from your server only, and avoid writing full request URLs to logs.
</Warning>

<ParamField header="token" type="string" required>
  The token returned by `get_token`.
</ParamField>

### Query parameters

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

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

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

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

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

<ParamField query="{pass_params() value}" type="string" required>
  The string returned by `pass_params()`. Send it as the **name** of a query parameter with an empty value.
</ParamField>

### Example response (excerpt)

```json theme={null}
{
  "Surveys": [
    {
      "sn_ud": "YOUR_SESSION_ID",
      "duplicate_potential": "high",
      "failure_reason": "02",
      "duplicate_score": 100,
      "flag": 1,
      "country_mismatch": 0,
      "survey_number": "YOUR_SURVEY_NUMBER",
      "duplicate_initial_ud": "YOUR_INITIAL_SESSION_ID",
      "duplicate_fingerprint_weight_score": 100,
      "destination": {},
      "duplicate_details": ["03", "04"],
      "description": "Duplicate device fingerprint detected compared to ..."
    }
  ],
  "Respondent": { }
}
```

<Note>
  The response also includes a `Respondent` object, which contains `threat_potential`.
</Note>

<Tabs>
  <Tab title="Python">
    ```python search.py theme={null}
    import json

    import requests

    # ---------------------------------------------------------------
    # Configuration
    # ---------------------------------------------------------------
    ENVIRONMENT = "prod"  # "staging" or "prod"

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

    # The name of the header that carries the token.
    TOKEN_HEADER_NAME = "token"

    SEARCH_ENDPOINT = (
        f"https://{ENVIRONMENT}.rtymgt.com/api/v3/respondents/search/{PRIVATE_KEY}"
    )
    REQUEST_TIMEOUT_SECONDS = 10


    # ---------------------------------------------------------------
    # Research Defender search
    # ---------------------------------------------------------------
    def parse_response(text: str) -> dict:
        """Parse the API response body.

        If the body has a stray trailing character that breaks json.loads,
        it is trimmed and parsing is retried.
        """
        try:
            return json.loads(text)
        except json.JSONDecodeError:
            return json.loads(text[:-1])


    def search_respondent(survey_number: str, session_id: str, pass_value: str, token: str) -> dict:
        """Call the SEARCH v3 API and return the parsed JSON result."""
        params = {
            "sy_nr": survey_number,
            "sn_ud": session_id,
            "dupe_details": "1",
            "df_details": "1",
            "mfr": "1",
        }

        # The value returned by pass_params() is sent as a parameter name with an
        # empty value.
        if pass_value:
            params[pass_value] = ""

        response = requests.get(
            SEARCH_ENDPOINT,
            params=params,
            headers={TOKEN_HEADER_NAME: token},  # the token is sent in a header
            timeout=REQUEST_TIMEOUT_SECONDS,
        )
        response.raise_for_status()

        return parse_response(response.text)
    ```
  </Tab>

  <Tab title="Node.js">
    ```javascript search.js theme={null}
    // Requires Node.js 18 or later (built-in fetch)

    // ---------------------------------------------------------------
    // Configuration
    // ---------------------------------------------------------------
    const ENVIRONMENT = "prod"; // "staging" or "prod"

    // 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";

    // The name of the header that carries the token.
    const TOKEN_HEADER_NAME = "token";

    const SEARCH_ENDPOINT = `https://${ENVIRONMENT}.rtymgt.com/api/v3/respondents/search/${PRIVATE_KEY}`;
    const REQUEST_TIMEOUT_MS = 10000;

    // ---------------------------------------------------------------
    // Research Defender search
    // ---------------------------------------------------------------

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

    /** Calls the SEARCH v3 API and returns the parsed JSON result. */
    async function searchRespondent({ surveyNumber, sessionId, pass, token }) {
      const params = new URLSearchParams({
        sy_nr: surveyNumber,
        sn_ud: sessionId,
        dupe_details: "1",
        df_details: "1",
        mfr: "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",
        headers: { [TOKEN_HEADER_NAME]: token }, // the token is sent in a header
        signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
      });

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

      return parseResponse(await response.text());
    }
    ```
  </Tab>

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

    public class ResearchDefenderClient
    {
        // Configuration
        private const string Environment = "prod"; // "staging" or "prod"

        // 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 =
            System.Environment.GetEnvironmentVariable("RD_PRIVATE_KEY") ?? "YOUR_PRIVATE_KEY";

        // The name of the header that carries the token.
        private const string TokenHeaderName = "token";

        private static readonly string SearchEndpoint =
            $"https://{Environment}.rtymgt.com/api/v3/respondents/search/{PrivateKey}";

        private readonly HttpClient _http;

        public ResearchDefenderClient(HttpClient http) => _http = http;

        /// <summary>Calls the SEARCH v3 API and returns the parsed JSON result.</summary>
        public async Task<JsonElement> SearchAsync(string surveyNumber, string sessionId, string? pass, string token)
        {
            var parameters = new List<KeyValuePair<string, string>>
            {
                new("sy_nr", surveyNumber),
                new("sn_ud", sessionId),
                new("dupe_details", "1"),
                new("df_details", "1"),
                new("mfr", "1"),
            };

            // The value returned by pass_params() is sent as a parameter name with
            // an empty value.
            if (!string.IsNullOrEmpty(pass))
            {
                parameters.Add(new(pass, ""));
            }

            var query = string.Join("&", parameters.Select(p =>
                $"{Uri.EscapeDataString(p.Key)}={Uri.EscapeDataString(p.Value)}"));

            using var request = new HttpRequestMessage(HttpMethod.Get, $"{SearchEndpoint}?{query}");
            request.Headers.Add(TokenHeaderName, token); // the token is sent in a header

            using var response = await _http.SendAsync(request);
            response.EnsureSuccessStatusCode();

            return ParseResponse(await response.Content.ReadAsStringAsync());
        }

        /// <summary>
        /// Parses the API response body. If the body has a stray trailing character
        /// that breaks parsing, it is trimmed and parsing is retried.
        /// </summary>
        private static JsonElement ParseResponse(string text)
        {
            try
            {
                return JsonDocument.Parse(text).RootElement.Clone();
            }
            catch (JsonException)
            {
                return JsonDocument.Parse(text[..^1]).RootElement.Clone();
            }
        }
    }
    ```
  </Tab>

  <Tab title="cURL">
    ```bash theme={null}
    curl -G "https://prod.rtymgt.com/api/v3/respondents/search/YOUR_PRIVATE_KEY" \
      -H "token: TOKEN_FROM_GET_TOKEN" \
      --data-urlencode "sy_nr=YOUR_SURVEY_NUMBER" \
      --data-urlencode "sn_ud=YOUR_SESSION_ID" \
      --data-urlencode "dupe_details=1" \
      --data-urlencode "df_details=1" \
      --data-urlencode "mfr=1" \
      --data-urlencode "PASS_PARAMS_VALUE="
    ```
  </Tab>
</Tabs>

## Full samples

The samples below work together. The browser page posts to `/api/rd-check` on your server, and your server calls SEARCH v3.

<AccordionGroup>
  <Accordion title="Browser page (HTML)">
    ```html secured-client.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 – Secured Integration (Browser)</title>
    </head>
    <body>
      <h1>Research Defender – Secured Integration (Browser)</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";

        // Optional get_token parameters. Leave empty to omit them.
        const RT_SR_PD = "";
        const RT_HD_EL = "";

        // 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 performs the search (see the server samples).
        const BACKEND_ENDPOINT = "/api/rd-check";

        // 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 GET_TOKEN_ENDPOINT = `${BASE_URL}/api/v3/respondents/get_token/${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;
        }

        /** Requests a token that identifies this respondent. */
        async function getToken() {
          const params = new URLSearchParams();
          if (RT_SR_PD) params.set("rt_sr_pd", RT_SR_PD);
          if (RT_HD_EL) params.set("rt_hd_el", RT_HD_EL);

          const query = params.toString();
          const url = query ? `${GET_TOKEN_ENDPOINT}?${query}` : GET_TOKEN_ENDPOINT;

          const response = await fetch(url, {
            method: "GET",
            credentials: "include", // send Research Defender cookies
          });

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

          const data = parseResponse(await response.text());
          const token = data.results?.[0]?.token;

          if (!token) {
            throw new Error("get_token response did not include a token.");
          }

          return token;
        }

        /** Sends pass_params() and the token to your server, which calls the search API. */
        async function checkWithBackend({ surveyNumber, sessionId, pass, token }) {
          const response = await fetch(BACKEND_ENDPOINT, {
            method: "POST",
            headers: { "Content-Type": "application/json" },
            body: JSON.stringify({ surveyNumber, sessionId, pass, token }),
          });

          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 token = await getToken();

          const { result } = await checkWithBackend({
            surveyNumber: SURVEY_NUMBER,
            sessionId: SESSION_ID,
            pass,
            token,
          });

          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 secured_server.py theme={null}
    """Research Defender - secured server-side search sample (Python / Flask).

    Receives pass_params() and the token from your page, then calls the
    Research Defender SEARCH v3 API from your server using your PRIVATE key.

        pip install flask requests
        export RD_PRIVATE_KEY="your-private-key"
        python secured_server.py
    """

    import json
    import os

    import requests
    from flask import Flask, jsonify, request, send_from_directory

    # ---------------------------------------------------------------
    # Configuration
    # ---------------------------------------------------------------
    ENVIRONMENT = os.getenv("RD_ENVIRONMENT", "prod")  # "staging" or "prod"

    # 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")

    # The name of the header that carries the token.
    TOKEN_HEADER_NAME = "token"

    SEARCH_ENDPOINT = (
        f"https://{ENVIRONMENT}.rtymgt.com/api/v3/respondents/search/{PRIVATE_KEY}"
    )
    REQUEST_TIMEOUT_SECONDS = 10


    # ---------------------------------------------------------------
    # Research Defender search
    # ---------------------------------------------------------------
    def parse_response(text: str) -> dict:
        """Parse the API response body.

        If the body has a stray trailing character that breaks json.loads,
        it is trimmed and parsing is retried.
        """
        try:
            return json.loads(text)
        except json.JSONDecodeError:
            return json.loads(text[:-1])


    def search_respondent(survey_number: str, session_id: str, pass_value: str, token: str) -> dict:
        """Call the SEARCH v3 API and return the parsed JSON result."""
        params = {
            "sy_nr": survey_number,
            "sn_ud": session_id,
            "dupe_details": "1",
            "df_details": "1",
            "mfr": "1",
        }

        # The value returned by pass_params() is sent as a parameter name with an
        # empty value.
        if pass_value:
            params[pass_value] = ""

        response = requests.get(
            SEARCH_ENDPOINT,
            params=params,
            headers={TOKEN_HEADER_NAME: token},  # the token is sent in a header
            timeout=REQUEST_TIMEOUT_SECONDS,
        )
        response.raise_for_status()

        return parse_response(response.text)


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


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


    @app.post("/api/rd-check")
    def rd_check():
        data = request.get_json(silent=True) or {}

        survey_number = data.get("surveyNumber")
        session_id = data.get("sessionId")
        pass_value = data.get("pass", "")
        token = data.get("token")

        if not survey_number or not session_id or not token:
            return jsonify(error="surveyNumber, sessionId and token are required"), 400

        try:
            result = search_respondent(survey_number, session_id, pass_value, token)
        except (requests.RequestException, ValueError) as error:
            # Log the error type only: request errors can include the URL,
            # which contains your private key.
            app.logger.error("Research Defender search failed: %s", type(error).__name__)
            return jsonify(error="Research Defender search failed"), 502

        # 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 secured_server.js theme={null}
    /**
     * Research Defender - secured server-side search sample (Node.js / Express).
     *
     * Receives pass_params() and the token from your page, then calls the
     * Research Defender SEARCH v3 API from your server using your PRIVATE key.
     *
     * Requires Node.js 18 or later (built-in fetch).
     *
     *   npm install express
     *   export RD_PRIVATE_KEY="your-private-key"
     *   node secured_server.js
     */

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

    // ---------------------------------------------------------------
    // Configuration
    // ---------------------------------------------------------------
    const ENVIRONMENT = process.env.RD_ENVIRONMENT || "prod"; // "staging" or "prod"

    // 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";

    // The name of the header that carries the token.
    const TOKEN_HEADER_NAME = "token";

    const SEARCH_ENDPOINT = `https://${ENVIRONMENT}.rtymgt.com/api/v3/respondents/search/${PRIVATE_KEY}`;
    const REQUEST_TIMEOUT_MS = 10000;

    // ---------------------------------------------------------------
    // Research Defender search
    // ---------------------------------------------------------------

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

    /** Calls the SEARCH v3 API and returns the parsed JSON result. */
    async function searchRespondent({ surveyNumber, sessionId, pass, token }) {
      const params = new URLSearchParams({
        sy_nr: surveyNumber,
        sn_ud: sessionId,
        dupe_details: "1",
        df_details: "1",
        mfr: "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",
        headers: { [TOKEN_HEADER_NAME]: token }, // the token is sent in a header
        signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
      });

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

      return parseResponse(await response.text());
    }

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

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

    app.post("/api/rd-check", async (req, res) => {
      const { surveyNumber, sessionId, pass = "", token } = req.body ?? {};

      if (!surveyNumber || !sessionId || !token) {
        return res.status(400).json({ error: "surveyNumber, sessionId and token are required" });
      }

      try {
        const result = await searchRespondent({ surveyNumber, sessionId, pass, token });

        // 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) {
        // Log the error name and message only, never the request URL,
        // which contains your private key.
        console.error(`Research Defender search failed: ${error.name}: ${error.message}`);
        return res.status(502).json({ error: "Research Defender search failed" });
      }
    });

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

  <Accordion title="C# server (ASP.NET Core)">
    ```csharp SecuredServer.cs theme={null}
    // Research Defender - secured server-side search sample (C# / ASP.NET Core minimal API, .NET 8).
    //
    // Receives pass_params() and the token from your page, then calls the
    // Research Defender SEARCH v3 API from your server using your PRIVATE key.
    //
    //   dotnet new web -n SecuredServer
    //   (replace Program.cs with this file, put secured-client.html in wwwroot/ as index.html)
    //   dotnet run

    using System.Text.Json;

    var builder = WebApplication.CreateBuilder(args);
    builder.Services.AddHttpClient<ResearchDefenderClient>(client =>
    {
        client.Timeout = TimeSpan.FromSeconds(10);
    });

    var app = builder.Build();

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

    app.MapPost("/api/rd-check", async (CheckRequest request, ResearchDefenderClient researchDefender, ILogger<Program> logger) =>
    {
        if (string.IsNullOrWhiteSpace(request.SurveyNumber) ||
            string.IsNullOrWhiteSpace(request.SessionId) ||
            string.IsNullOrWhiteSpace(request.Token))
        {
            return Results.BadRequest(new { error = "surveyNumber, sessionId and token are required" });
        }

        try
        {
            var result = await researchDefender.SearchAsync(
                request.SurveyNumber, request.SessionId, request.Pass, request.Token);

            // 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 HttpRequestException or TaskCanceledException or JsonException)
        {
            // Log the error type only: request errors can include the URL,
            // which contains your private key.
            logger.LogError("Research Defender search failed: {ErrorType}", ex.GetType().Name);
            return Results.Json(new { error = "Research Defender search failed" }, statusCode: StatusCodes.Status502BadGateway);
        }
    });

    app.Run();

    // ---------------------------------------------------------------
    // Request model (JSON property names are matched case-insensitively)
    // ---------------------------------------------------------------
    public record CheckRequest(string SurveyNumber, string SessionId, string? Pass, string Token);

    // ---------------------------------------------------------------
    // Research Defender search client
    // ---------------------------------------------------------------
    public class ResearchDefenderClient
    {
        // Configuration
        private const string Environment = "prod"; // "staging" or "prod"

        // 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 =
            System.Environment.GetEnvironmentVariable("RD_PRIVATE_KEY") ?? "YOUR_PRIVATE_KEY";

        // The name of the header that carries the token.
        private const string TokenHeaderName = "token";

        private static readonly string SearchEndpoint =
            $"https://{Environment}.rtymgt.com/api/v3/respondents/search/{PrivateKey}";

        private readonly HttpClient _http;

        public ResearchDefenderClient(HttpClient http) => _http = http;

        /// <summary>Calls the SEARCH v3 API and returns the parsed JSON result.</summary>
        public async Task<JsonElement> SearchAsync(string surveyNumber, string sessionId, string? pass, string token)
        {
            var parameters = new List<KeyValuePair<string, string>>
            {
                new("sy_nr", surveyNumber),
                new("sn_ud", sessionId),
                new("dupe_details", "1"),
                new("df_details", "1"),
                new("mfr", "1"),
            };

            // The value returned by pass_params() is sent as a parameter name with
            // an empty value.
            if (!string.IsNullOrEmpty(pass))
            {
                parameters.Add(new(pass, ""));
            }

            var query = string.Join("&", parameters.Select(p =>
                $"{Uri.EscapeDataString(p.Key)}={Uri.EscapeDataString(p.Value)}"));

            using var request = new HttpRequestMessage(HttpMethod.Get, $"{SearchEndpoint}?{query}");
            request.Headers.Add(TokenHeaderName, token); // the token is sent in a header

            using var response = await _http.SendAsync(request);
            response.EnsureSuccessStatusCode();

            return ParseResponse(await response.Content.ReadAsStringAsync());
        }

        /// <summary>
        /// Parses the API response body. If the body has a stray trailing character
        /// that breaks parsing, it is trimmed and parsing is retried.
        /// </summary>
        private static JsonElement ParseResponse(string text)
        {
            try
            {
                return JsonDocument.Parse(text).RootElement.Clone();
            }
            catch (JsonException)
            {
                return JsonDocument.Parse(text[..^1]).RootElement.Clone();
            }
        }
    }
    ```
  </Accordion>
</AccordionGroup>

To run the Python or Node.js sample, put `secured-client.html` in a `static` folder next to the server file, set the `RD_PRIVATE_KEY` environment variable, start the server (`python secured_server.py` or `node secured_server.js`), and open `http://localhost:8000`. The Node.js sample requires Node.js 18 or later and `npm install express`.

## Security recommendations

* **Keep the private key on your server.** Load it from an environment variable or secrets manager, and never send it to the browser.
* **Keep the token and full request URLs out of logs.** Treat the token as a credential. The SEARCH URL contains your private key, so log error types or status codes instead of full URLs.
* **Validate input on your server.** Check the survey number and session ID before calling the search API.
* **Return only what the page needs.** The samples return the full result for demonstration. In production, return a decision such as pass or fail.
* **Set a timeout on the search call**, as the samples do, so a slow response never hangs your respondent flow.

## Handling errors

| Where | Failure | Suggested handling |
| - | - | - |
| Browser | Library fails to load | Log the failure and decide whether the respondent continues. |
| Browser | `get_token` returns a non-`200` status or no token | Do not call your server. Handle it the same way as a library failure. |
| Your server | SEARCH v3 returns a non-`200` status | Return an error to the page and log it. |

<Tip>
  Decide in advance what your survey should do when any step fails, so a temporary error never blocks legitimate respondents.
</Tip>


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