Overview
The secured integration builds on the 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:pass_params(), the value produced by the Research Defender JavaScript library.- A token from the
get_tokenAPI that identifies the respondent.
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
1
Collect library data in the browser
Load
rd_sys_util.min.js, call init(), wait for the datasets, then call pass_params().2
Get a token
Call the
get_token API from the browser. It returns a token that contains information about the respondent.3
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.4
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.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.
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.
Step 1: Get a token
Callget_token from the browser, after the library has been initialized.
string
GET https://{environment}.rtymgt.com/api/v3/respondents/get_token/{publishable_key}Query parameters
string
Optional.
string
Optional.
Send the request with credentials (
credentials: "include") so Research Defender cookies are included.Example request
https://staging.rtymgt.com/api/v3/respondents/get_token/YOUR_PUBLISHABLE_KEY?rt_sr_pd=AAA&rt_hd_el=BBBB
Example response
{
"results": [
{
"token": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
"timestamp": "2026-10-18 15:47:31.984570 [UTC]",
"message": "Token successfully generated"
}
]
}
string
The token to send to your server.
string
When the token was generated, in UTC.
string
A status message, for example
Token successfully generated.Step 2: Browser integration
// ---------------------------------------------------------------
// 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);
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.string
GET https://{environment}.rtymgt.com/api/v3/respondents/search/{private_key}The private key is part of this URL. Call this endpoint from your server only, and avoid writing full request URLs to logs.
string
required
The token returned by
get_token.Query parameters
string
required
The survey number.
string
required
The respondent’s session ID.
string
Set to
"1" to enable.string
Set to
"1" to enable.string
Set to
"1" to enable.string
required
The string returned by
pass_params(). Send it as the name of a query parameter with an empty value.Example response (excerpt)
{
"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": { }
}
The response also includes a
Respondent object, which contains threat_potential.- Python
- Node.js
- C#
- cURL
search.py
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)
search.js
// 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());
}
ResearchDefenderClient.cs
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();
}
}
}
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="
Full samples
The samples below work together. The browser page posts to/api/rd-check on your server, and your server calls SEARCH v3.
Browser page (HTML)
Browser page (HTML)
secured-client.html
<!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>
Python server (Flask)
Python server (Flask)
secured_server.py
"""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)
Node.js server (Express)
Node.js server (Express)
secured_server.js
/**
* 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"));
C# server (ASP.NET Core)
C# server (ASP.NET Core)
SecuredServer.cs
// 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();
}
}
}
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. |
Decide in advance what your survey should do when any step fails, so a temporary error never blocks legitimate respondents.
