Skip to main content

Sending Transaction Data to DQC (Decipher / Forsta)

This guide walks you through sending survey transaction data from your Decipher/Forsta survey to the DQC Transaction API. After the DQC Quality Tool has scored each participant during fielding (see General Setup), this integration posts each participant's outcome — disposition, timestamps, identifiers, and the quality‑tool results — to DQC at the appropriate points in the survey lifecycle.


📌 Overview

The integration makes a server‑side call (v2SendRequest) from inside the survey XML at three moments, so no participant is lost:

EventWhen it firesStatus sent
In‑progressAfter the participant answers the first questionPartial
Survey CompletedWhen the participant finishes the surveyQualified
Termination / Over‑quotaWhen the participant is terminated or hits a full quotaTerminated / Overquota

Because the request is made server‑side, your DQC API key is never exposed to the participant's browser. The DQC endpoint upserts on the participant's request_id, so the in‑progress send is later overwritten by the final disposition — and if a participant abandons the survey after the first question, DQC still has their record.


⚠️ Step 0 — Request domain authorization from Forsta (required first)

Do this before anything else

Decipher only allows server‑side API calls (v2SendRequest) to pre‑approved domains. Until the DQC domain is authorized for your account, every call is rejected with an error like "API call to api.dqco-op.com is not allowed. Ask support to add this hostname to the file api.txt in your client directory."

The DQC hostname must be added to the api.txt file and allowed for v2SendRequest (the request_allowed hook), in your client directory. This is a one‑time, account‑wide configuration performed by Forsta Support — submit a request to SurveySupport@forsta.com from an email on your account's domain.

Sample request email:

Subject: Authorizing domains for API integrations — add DQC hostname to api.txt

Hello,

This request falls under your Scope of Support item: "Authorizing domains for survey access, email domains or API integrations."

We are integrating our Decipher surveys with the Data Quality Co‑op (DQC) Transaction API and need outbound API calls authorized. Please add the following hostname to api.txt and allow it for v2SendRequest (the request_allowed hook) in our client directory:

  • api.dqco-op.com

Account: <your Decipher instance, e.g. se1.decipherinc.com> · Client directory: <your client dir>

Thank you.

Ask Forsta to apply this at the company‑directory level so all your projects inherit it. You can verify it's active by running the survey and confirming the call is no longer blocked (see Viewing what was sent).


Prerequisites

Before adding the transaction code, make sure:

  1. ✅ The DQC Quality Tool is installed — the toolbox script, the save_dqc_data() function, and the dqc_data holder question, all from General Setup.
  2. ✅ Your survey is a secure survey (secure="1" on the <survey> tag).
  3. ✅ Forsta has authorized the DQC domain (Step 0 above).

Step 1 — Add the sending function

Add the send_results_dqc() function to the same <exec when="init"> block from General Setup, directly underneath your existing save_dqc_data() function — both live inside that one <exec when="init"> block. The block below is that complete block, with the concise save_dqc_data() and send_results_dqc() together, so you can copy it whole and paste it over the <exec when="init"> from General Setup — no manual merging.

<exec when="init">
DQC_API_KEY = "DQC_API_KEY" # set your DQC API key here (must match the key in the toolbox import URL)
defaultAnswer = 'Submission too quick, data not processed'
defaultSurveyMetricsAnswer = 'No Data'
def save_dqc_data():
defaultDeviceFailuresAnswer = 'None' if getattr(p, 'client_dqc_participant_id', '') else defaultAnswer
dqc_data.rid.val = getattr(p, 'client_dqc_request_id', '') or defaultAnswer
dqc_data.pid.val = getattr(p, 'client_dqc_participant_id', '') or defaultAnswer
dqc_data.per.val = getattr(p, 'client_dqc_persona', '') or 'NONE'
dqc_data.dts.val = getattr(p, 'client_dqc_data_trust_score', '') or '0'
dqc_data.dcs.val = getattr(p, 'client_dqc_device_score', '') or '0'
dqc_data.cty.val = getattr(p, 'client_dqc_country_code', '') or defaultAnswer
dqc_data.sub.val = getattr(p, 'client_dqc_subdivision_name', '') or defaultAnswer
dqc_data.dup.val = getattr(p, 'client_dqc_is_duplicate', False)
dqc_data.sid.val = getattr(p, 'client_dqc_survey_id', '') or defaultAnswer
dqc_data.dfc.val = getattr(p, 'client_dqc_device_failures', '') or defaultDeviceFailuresAnswer

def send_results_dqc():
import datetime

# Decode HTML-encoded slashes (Decipher returns dates/URLs with &#47;).
# chr(38) is the ampersand; building it avoids a literal ampersand which breaks the XML parser.
def dqcDecode(s):
try: return str(s).replace(chr(38) + "#47;", "/")
except: return s

# Convert Decipher's MM/DD/YYYY HH:MM to ISO 8601.
def dqcToIso(v):
s = dqcDecode(v).strip()
try: return datetime.datetime.strptime(s, "%m/%d/%Y %H:%M").isoformat()
except: return s

# Coerce a value to a JSON-safe form without raising.
def dqcSafe(v):
try:
if isinstance(v, (bool, int, float)): return v
try: return ('%s' % (v,))
except: return v.encode('utf-8', 'ignore') if hasattr(v, 'encode') else ''
except: return ''

# Add one field to the payload. The lambda defers the read so a bad/missing
# field is caught here and skipped, never breaking the rest of the payload.
def addDQCField(key, getter):
try: dqcPayload[key] = dqcSafe(getter())
except: pass

# --- Disposition from Decipher markers ---
mk = []
try: mk = [x for x in p.markers] # NOT list(p.markers): 'list' is a Decipher variable that shadows the builtin
except: mk = []
if "overquota" in mk: dqcStatus = "Overquota"
elif "qualified" in mk: dqcStatus = "Qualified"
elif "terminated" in mk: dqcStatus = "Terminated"
else: dqcStatus = "Partial"

# --- Termination reason (why a DQC termination ended the survey; "" when none) ---
DQC_TERMINATION_REASONS = {
"dqc_device_score_termination": "deviceScore",
"dqc_data_trust_score_termination": "dataTrustScore",
"dqc_persona_termination": "persona",
"dqc_duplicate_termination": "duplicate",
}
dqcTerminationReason = ""
for m in mk:
key = str(m).strip()
if key in DQC_TERMINATION_REASONS:
dqcTerminationReason = DQC_TERMINATION_REASONS[key]
break

# --- Timestamps in UTC ---
# Decipher returns start_date in the SERVER's local timezone. The endpoint expects UTC,
# so we measure the server offset at runtime (now minus utcnow) and shift, then derive end_date.
dqcStart = ""
dqcEnd = ""
try:
sdtLocal = datetime.datetime.strptime(dqcToIso(start_date.val), "%Y-%m-%dT%H:%M:%S")
dqcOffset = datetime.datetime.now() - datetime.datetime.utcnow()
sdtUtc = (sdtLocal - dqcOffset).replace(microsecond=0) # whole seconds to ...SSZ
dqcStart = sdtUtc.isoformat() + "Z"
try:
dqcDur = getattr(qtime, "val", qtime) # qtime = total interview time (seconds)
dqcEnd = (sdtUtc + datetime.timedelta(seconds=float(dqcDur))).replace(microsecond=0).isoformat() + "Z"
except: pass
except: pass

# --- Build the payload ---
dqcPayload = {}
addDQCField("status", lambda: dqcStatus)
addDQCField("uuid", lambda: uuid)
addDQCField("start_date", lambda: dqcStart)
addDQCField("end_date", lambda: dqcEnd)
addDQCField("terminationReason", lambda: dqcTerminationReason)
addDQCField("source", lambda: gv.request.variables.get("list", ""))
addDQCField("surveyId", lambda: dqcDecode(dqc_data.sid.val))
addDQCField("request_id", lambda: dqc_data.rid.val)
addDQCField("participant_id", lambda: dqc_data.pid.val)
addDQCField("device_score", lambda: dqc_data.dcs.val)
addDQCField("data_trust_score", lambda: dqc_data.dts.val)
addDQCField("persona", lambda: dqc_data.per.val)
addDQCField("is_duplicate", lambda: dqc_data.dup.val)
addDQCField("country", lambda: dqc_data.cty.val)
addDQCField("subdivision", lambda: dqc_data.sub.val)
addDQCField("device_failures", lambda: dqc_data.dfc.val)

# --- Company Mapping (REQUIRED): who traded in this transaction ---
# The buyer is the company hosting the survey; the seller is the supplier the
# respondent came from. A seller is mandatory. Your Company Mapping lives here:
# add or edit the addDQCField lines below (buyerName / sellerName / supplierIndex).
# See the Company Mapping guide.

# --- Failures Mapping (OPTIONAL): your own in-survey quality checks ---
# honeyPot, trapQuestion, openEndQuestion, other. Your Failures Mapping lives here:
# add your addDQCField lines right below, or leave this section out if you send none.
# See the Failures Mapping guide.

# --- Send (wrapped so a failure never breaks the survey) ---
# Guard: an unreplaced placeholder or empty key would 401 every send, so we
# record it in dqc_debug and raise. This throws an error until a valid
# DQC_API_KEY is set in the init block, making the misconfiguration obvious
# instead of letting every send fail silently.
if not DQC_API_KEY or DQC_API_KEY == "DQC_API_KEY":
try: dqc_debug.dump.val = "ERROR: DQC_API_KEY placeholder not replaced - set your key in the init block"
except: pass
raise ValueError("DQC_API_KEY placeholder not replaced - set your key in the init block")
try:
v2SendRequest(url="https://api.dqco-op.com/data/decipher",
method="post", type="json",
headers={"Authorization": "apikey " + DQC_API_KEY,
"Content-Type": "application/json"},
args=dqcPayload)
try: dqc_debug.dump.val = "SENT(%s): %s" % (dqcStatus, repr(dqcPayload))
except: pass
except Exception as e:
try: dqc_debug.dump.val = "ERROR: " + str(e)
except: pass
</exec>
Customized your save_dqc_data()?

If you added fields to save_dqc_data() (for example in verbose mode), keep your version and just paste the send_results_dqc() function underneath it, inside the same <exec when="init"> block. Also copy the DQC_API_KEY = "…" line to the top of that block — send_results_dqc() reads it.

note

Replace DQC_API_KEY (the variable at the top of the block) with your DQC API key — the same key you used in the toolbox import URL.


Adding extra fields to the payload

The payload is assembled one field at a time inside send_results_dqc() with addDQCField(name, getter):

addDQCField("device_score", lambda: dqc_data.dcs.val)
# ^ key sent to DQC ^ how to read the value
  • The first argument is the field name exactly as it will be sent to DQC.
  • The second argument is a zero‑argument lambda that returns the value. Each read is wrapped in its own try/except, so a missing or malformed value is simply skipped — it never breaks the rest of the payload.
Capture the value before you send it

addDQCField can only send a value that already exists in the dqc_data holder. So for a DQC Quality Tool field you must first store it in save_dqc_data() — adding the addDQCField(...) line alone will send an empty value.

Adding a DQC Quality Tool field (three steps)

Sending a new Quality Tool field is a three‑step chain — capture it, then send it:

  1. Add the row to the dqc_data holder so there's somewhere to store the value.
  2. Populate it in save_dqc_data() (the General Setup function) from the toolbox's client_dqc_* value.
  3. Send it in send_results_dqc() with addDQCField().

Example — also send the average device score (ads):

<!-- Step 1 - dqc_data holder: add a row -->
<row label="ads">dqc-average-device-score</row>
# Step 2 - save_dqc_data(): store the value the toolbox captured
dqc_data.ads.val = getattr(p, 'client_dqc_avg_device_score', '') or '0'

# Step 3 - send_results_dqc(): add it to the payload
addDQCField("avg_device_score", lambda: dqc_data.ads.val)
tip

In verbose mode, the Decipher XML Generator does all three steps for you — it adds every field below to the dqc_data holder, to save_dqc_data(), and to the payload in send_results_dqc(). Use the manual steps above only to add a field individually (for example while staying in concise mode) or to add a Decipher variable.

Adding a Decipher variable

Values that don't come from the Quality Tool — Decipher's own survey/meta variables available inside <exec> (e.g. uuid, start_date.val, p.markers, or sample/source variables via gv.request.variables.get("<name>", "")) — don't need steps 1–2. Just add the addDQCField(...) line, since the value is already available at send time:

addDQCField("language", lambda: gv.request.variables.get("decLang", ""))

Available DQC Quality Tool fields

Reference of the extra fields the toolbox can capture. For each, add the holder row and the save_dqc_data() assignment (steps 1–2), then send it with addDQCField() (step 3).

dqc_data accessorsave_dqc_data() sourceDescription
dqc_data.ads.valclient_dqc_avg_device_scoreAverage device score
dqc_data.lds.valclient_dqc_lowest_device_scoreLowest device score
dqc_data.tsv.valclient_dqc_total_surveysTotal surveys
dqc_data.cmr.valclient_dqc_completion_rateCompletion rate
dqc_data.dpr.valclient_dqc_duplication_rateDuplication rate
dqc_data.flr.valclient_dqc_failure_rateFailure rate
dqc_data.qlr.valclient_dqc_qualification_rateQualification rate
dqc_data.lst.valclient_dqc_last_survey_takenLast survey taken
dqc_data.mir.valclient_dqc_manual_isq_rateManual ISQ rate
dqc_data.air.valclient_dqc_automated_isq_rateAutomated ISQ rate
dqc_data.osr.valclient_dqc_osq_rateOSQ rate
dqc_data.bfa.valclient_dqc_brand_familiarityBrand familiarity
dqc_data.oen.valclient_dqc_open_endOpen end
dqc_data.spd.valclient_dqc_speedingSpeeding
dqc_data.hpt.valclient_dqc_honey_potHoney pot
dqc_data.stl.valclient_dqc_straightliningStraightlining
dqc_data.scn.valclient_dqc_distinct_supplier_countDistinct supplier count
dqc_data.sup.valclient_dqc_suppliersSuppliers
Fields sent by default

status, terminationReason, uuid, start_date, end_date, source, surveyId, request_id, participant_id, device_score, data_trust_score, persona, is_duplicate, country, subdivision, and device_failures.


Step 2 — Call the function at the lifecycle points

save_dqc_data() is already called in <exec when="submit"> per General Setup. Add two calls to send_results_dqc():

CallPlacementPurpose
send_results_dqc()a plain <exec> after the first question (before the second)In‑progress send. Runs once the first answer is in, so drop‑offs are still captured (status="Partial").
send_results_dqc()<exec when="finished">Final send. The finished hook fires on completion and on termination/over‑quota, sending the final disposition.
<!-- in-progress send: after the first question, before the second -->
<exec>
send_results_dqc()
</exec>

<!-- ...remaining questions / termination logic... -->

<!-- final send: completion, termination, or over-quota -->
<exec when="finished">
send_results_dqc()
</exec>
note

The finished hook is a lifecycle hook, not positional — it fires when the response is finalized regardless of where it sits in the XML, including when a <term> ends the survey early.


Step 3 — Identify the companies (required)

Every transaction DQC receives is a buyer → seller: the company hosting the survey and the supplier the respondent came through. DQC requires a seller on every response — a response with no seller is rejected — so this step is not optional.

You attach them as payload fields using the same addDQCField pattern from Step 1. Because the seller can be identified in three different ways, the full setup — the buyer (buyerName) and the seller (sellerName / supplierIndex) — lives in its own guide:

➡️ Company Mapping — add the buyer and seller (the seller is required).


Full XML Survey Example

Here is a complete example of the final XML, with the transaction‑data egress integrated on top of the General Setup code: the send_results_dqc() function inside the init exec, the dqc_debug holder, the in‑progress send after the first question, and the final <exec when="finished">.

Replace DQC_API_KEY in two places

The placeholder DQC_API_KEY appears twice in this example and both must be set to your key:

  1. The toolbox import URL near the top — .../tools/toolbox/DQC_API_KEY (client‑side).
  2. The DQC_API_KEY = "…" variable at the top of <exec when="init"> (server‑side), which the send_results_dqc() Authorization header reads.

If you replace only the first, the transaction send would return 401 on every response. As a safeguard, send_results_dqc() writes ERROR: DQC_API_KEY placeholder not replaced to the dqc_debug holder and throws an error while the key is still the placeholder — so the misconfiguration surfaces immediately instead of failing silently, and keeps throwing until a valid key is set. The key shown here is not valid—generate your own.

View Complete XML Example
<?xml version="1.0" encoding="UTF-8"?>
<survey
alt="Quality Tools Integration - Testing"
autosave="0"
builder:wizardCompleted="1"
builderCompatible="1"
compat="155"
delphi="1"
extraVariables="source,record,decLang,list,userAgent"
fir="on"
html:showNumber="0"
mobile="compat"
mobileDevices="smartphone,tablet,desktop"
name="Survey"
secure="1"
setup="term,decLang,quota,time"
ss:disableBackButton="1"
ss:enableNavigation="1"
ss:hideProgressBar="0"
state="testing">

<samplesources default="0">
<samplesource list="0">
<title>Open Survey</title>
<invalid>You are missing information in the URL. Please verify the URL with the original invite.</invalid>
<completed>It seems you have already completed this survey.</completed>
<exit cond="terminated">Thank you for taking our survey.</exit>
<exit cond="qualified">Thank you for taking our survey. Your efforts are greatly appreciated!</exit>
<exit cond="overquota">Thank you for taking our survey.</exit>
</samplesource>
</samplesources>

<style name="respview.client.meta"><![CDATA[
<link rel="preconnect" href="https://api.dqco-op.com" crossorigin="anonymous">
<link rel="preconnect" href="https://fpmetrics.dqco-op.com" crossorigin="anonymous">
]]></style>

<style name="global.page.head" wrap="ready"><![CDATA[
(async () => {
try {
const { DQCToolBox } = await import('https://api.dqco-op.com/tools/toolbox/DQC_API_KEY');
await DQCToolBox.getIdentity();
} catch (error) {
console.error('Error in client code:', error);
}
})();
]]></style>
<suspend/>

<exec when="init">
DQC_API_KEY = "DQC_API_KEY" # set your DQC API key here (must match the key in the toolbox import URL)
defaultAnswer = 'Submission too quick, data not processed'
defaultSurveyMetricsAnswer = 'No Data'
def save_dqc_data():
defaultDeviceFailuresAnswer = 'None' if getattr(p, 'client_dqc_participant_id', '') else defaultAnswer
dqc_data.rid.val = getattr(p, 'client_dqc_request_id', '') or defaultAnswer
dqc_data.pid.val = getattr(p, 'client_dqc_participant_id', '') or defaultAnswer
dqc_data.per.val = getattr(p, 'client_dqc_persona', '') or 'NONE'
dqc_data.dts.val = getattr(p, 'client_dqc_data_trust_score', '') or '0'
dqc_data.dcs.val = getattr(p, 'client_dqc_device_score', '') or '0'
dqc_data.cty.val = getattr(p, 'client_dqc_country_code', '') or defaultAnswer
dqc_data.sub.val = getattr(p, 'client_dqc_subdivision_name', '') or defaultAnswer
dqc_data.dup.val = getattr(p, 'client_dqc_is_duplicate', False)
dqc_data.sid.val = getattr(p, 'client_dqc_survey_id', '') or defaultAnswer
dqc_data.dfc.val = getattr(p, 'client_dqc_device_failures', '') or defaultDeviceFailuresAnswer

def send_results_dqc():
import datetime

# Decode HTML-encoded slashes (Decipher returns dates/URLs with &#47;).
# chr(38) is the ampersand; building it avoids a literal ampersand which breaks the XML parser.
def dqcDecode(s):
try: return str(s).replace(chr(38) + "#47;", "/")
except: return s

# Convert Decipher's MM/DD/YYYY HH:MM to ISO 8601.
def dqcToIso(v):
s = dqcDecode(v).strip()
try: return datetime.datetime.strptime(s, "%m/%d/%Y %H:%M").isoformat()
except: return s

# Coerce a value to a JSON-safe form without raising.
def dqcSafe(v):
try:
if isinstance(v, (bool, int, float)): return v
try: return ('%s' % (v,))
except: return v.encode('utf-8', 'ignore') if hasattr(v, 'encode') else ''
except: return ''

# Add one field to the payload. The lambda defers the read so a bad/missing
# field is caught here and skipped, never breaking the rest of the payload.
def addDQCField(key, getter):
try: dqcPayload[key] = dqcSafe(getter())
except: pass

# --- Disposition from Decipher markers ---
mk = []
try: mk = [x for x in p.markers] # NOT list(p.markers): 'list' is a Decipher variable that shadows the builtin
except: mk = []
if "overquota" in mk: dqcStatus = "Overquota"
elif "qualified" in mk: dqcStatus = "Qualified"
elif "terminated" in mk: dqcStatus = "Terminated"
else: dqcStatus = "Partial"

# --- Termination reason (why a DQC termination ended the survey; "" when none) ---
DQC_TERMINATION_REASONS = {
"dqc_device_score_termination": "deviceScore",
"dqc_data_trust_score_termination": "dataTrustScore",
"dqc_persona_termination": "persona",
"dqc_duplicate_termination": "duplicate",
}
dqcTerminationReason = ""
for m in mk:
key = str(m).strip()
if key in DQC_TERMINATION_REASONS:
dqcTerminationReason = DQC_TERMINATION_REASONS[key]
break

# --- Timestamps in UTC ---
# Decipher returns start_date in the SERVER's local timezone. The endpoint expects UTC,
# so we measure the server offset at runtime (now minus utcnow) and shift, then derive end_date.
dqcStart = ""
dqcEnd = ""
try:
sdtLocal = datetime.datetime.strptime(dqcToIso(start_date.val), "%Y-%m-%dT%H:%M:%S")
dqcOffset = datetime.datetime.now() - datetime.datetime.utcnow()
sdtUtc = (sdtLocal - dqcOffset).replace(microsecond=0) # whole seconds to ...SSZ
dqcStart = sdtUtc.isoformat() + "Z"
try:
dqcDur = getattr(qtime, "val", qtime) # qtime = total interview time (seconds)
dqcEnd = (sdtUtc + datetime.timedelta(seconds=float(dqcDur))).replace(microsecond=0).isoformat() + "Z"
except: pass
except: pass

# --- Build the payload ---
dqcPayload = {}
addDQCField("status", lambda: dqcStatus)
addDQCField("uuid", lambda: uuid)
addDQCField("start_date", lambda: dqcStart)
addDQCField("end_date", lambda: dqcEnd)
addDQCField("terminationReason", lambda: dqcTerminationReason)
addDQCField("source", lambda: gv.request.variables.get("list", ""))
addDQCField("surveyId", lambda: dqcDecode(dqc_data.sid.val))
addDQCField("request_id", lambda: dqc_data.rid.val)
addDQCField("participant_id", lambda: dqc_data.pid.val)
addDQCField("device_score", lambda: dqc_data.dcs.val)
addDQCField("data_trust_score", lambda: dqc_data.dts.val)
addDQCField("persona", lambda: dqc_data.per.val)
addDQCField("is_duplicate", lambda: dqc_data.dup.val)
addDQCField("country", lambda: dqc_data.cty.val)
addDQCField("subdivision", lambda: dqc_data.sub.val)
addDQCField("device_failures", lambda: dqc_data.dfc.val)

# --- Company Mapping (REQUIRED): who traded in this transaction ---
# The buyer is the company hosting the survey; the seller is the supplier the
# respondent came from. A seller is mandatory. Your Company Mapping lives here:
# add or edit the addDQCField lines below (buyerName / sellerName / supplierIndex).
# See the Company Mapping guide.

# --- Failures Mapping (OPTIONAL): your own in-survey quality checks ---
# honeyPot, trapQuestion, openEndQuestion, other. Your Failures Mapping lives here:
# add your addDQCField lines right below, or leave this section out if you send none.
# See the Failures Mapping guide.

# --- Send (wrapped so a failure never breaks the survey) ---
# Guard: an unreplaced placeholder or empty key would 401 every send, so we
# record it in dqc_debug and raise. This throws an error until a valid
# DQC_API_KEY is set in the init block, making the misconfiguration obvious
# instead of letting every send fail silently.
if not DQC_API_KEY or DQC_API_KEY == "DQC_API_KEY":
try: dqc_debug.dump.val = "ERROR: DQC_API_KEY placeholder not replaced - set your key in the init block"
except: pass
raise ValueError("DQC_API_KEY placeholder not replaced - set your key in the init block")
try:
v2SendRequest(url="https://api.dqco-op.com/data/decipher",
method="post", type="json",
headers={"Authorization": "apikey " + DQC_API_KEY,
"Content-Type": "application/json"},
args=dqcPayload)
try: dqc_debug.dump.val = "SENT(%s): %s" % (dqcStatus, repr(dqcPayload))
except: pass
except Exception as e:
try: dqc_debug.dump.val = "ERROR: " + str(e)
except: pass
</exec>

<exec when="submit">
save_dqc_data()
</exec>

<text
cond="0"
label="dqc_data"
optional="0"
size="10"
translateable="0"
where="execute,survey,report">
<title>DQC Data Holder</title>
<row label="rid">dqc-request-id</row>
<row label="pid">dqc-participant-id</row>
<row label="dts">dqc-data-trust-score</row>
<row label="per">dqc-persona</row>
<row label="dcs">dqc-device-score</row>
<row label="dup">dqc-is-duplicate</row>
<row label="cty">dqc-country</row>
<row label="sub">dqc-subdivision</row>
<row label="sid">dqc-survey-id</row>
<row label="dfc">dqc-device-failures</row>
</text>

<text label="dqc_debug" cond="0" size="1000" translateable="0" where="execute,survey,report">
<title>DQC debug holder</title>
<row label="dump">debug</row>
</text>

<suspend/>

<radio
label="Q1">
<title>Q1: Are you human?</title>
<comment>Select one</comment>
<row label="r1">Yes</row>
<row label="r2">No</row>
</radio>

<suspend/>

<exec>
send_results_dqc()
</exec>

<radio
label="Q2">
<title>Q2: Are you a duplicate?</title>
<comment>Select one</comment>
<row label="r1">Yes</row>
<row label="r2">No</row>
</radio>

<suspend/>

<radio
label="Q3">
<title>Q3: You made it to the end of the example survey</title>
<comment>Select one</comment>
<row label="r1">Yay</row>
<row label="r2">Bummer</row>
</radio>

<suspend/>



<exec when="finished">
send_results_dqc()
</exec>
</survey>

Viewing what was sent

Add one hidden question so you can inspect what each send dispatched, directly in Responses → View/Edit Responses. (The dqc_data holder is already part of General Setup and is not repeated here.)

<text label="dqc_debug" cond="0" size="1000" translateable="0" where="execute,survey,report">
<title>DQC debug holder</title>
<row label="dump">debug</row>
</text>

Each send records its outcome in the dqc_debug holder. In Responses → View/Edit Responses, click Choose Columns and add the dqc_debug: DQC debug holder survey variable, then Apply:

  • SENT(Qualified): {…} — the call was dispatched, with the exact payload.
  • ERROR: … — a synchronous error (for example, the domain not yet authorized — see Step 0).

Choose Columns dialog with the dqc_debug survey variable selected, and the dqc_debug column showing SENT(...) payloads per response

Figure: adding the dqc_debug column in Responses → View/Edit Responses. Each row shows the dispatched payload, e.g. SENT(Qualified) / SENT(Terminated).

v2SendRequest is asynchronous; final delivery is logged to survey.log in the survey directory.


📌 Notes & gotchas

  • Timestamps are UTC. Decipher records start_date in the server's local timezone; the code converts to UTC so it matches the endpoint. Do not send the raw local value.
  • list is reserved. Decipher exposes a built‑in list variable (the sample source), which shadows Python's list(). Read markers with [x for x in p.markers], never list(p.markers).
  • No literal &, <, or > in <exec> code (including comments) — Decipher parses the block as XML and will reject them. Build an ampersand from chr(38), or wrap the exec in <![CDATA[ … ]]>.
  • HTML‑encoded values. start_date.val and dqc_data.sid.val come back with &#47; instead of /; dqcDecode fixes this.
  • qtime is read with getattr(qtime, "val", qtime) so it works whether qtime is an object (.val) or a bare value.
  • Don't break the survey. Field reads are isolated per‑field and the whole send is wrapped in try/except, so a DQC outage or a malformed value never shows the participant an error.

Next steps

  • Failures — send your custom in-survey quality checks (honeypots, traps, open-ends). Optional.
  • Termination Scripts — automatically end low-quality respondents early. Optional.

✅ Summary

  1. First, have Forsta authorize the DQC domain (api.txt + request_allowed) — Step 0.
  2. Add send_results_dqc() below save_dqc_data() in your init exec.
  3. Call send_results_dqc() after the first question (in‑progress) and again in <exec when="finished"> (final).
  4. Add the dqc_debug holder and confirm via its column that records send with the correct status and UTC timestamps.

For automatic early termination of low‑quality respondents, see Termination Scripts.