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:
| Event | When it fires | Status sent |
|---|---|---|
| In‑progress | After the participant answers the first question | Partial |
| Survey Completed | When the participant finishes the survey | Qualified |
| Termination / Over‑quota | When the participant is terminated or hits a full quota | Terminated / 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)
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.txtand allow it forv2SendRequest(therequest_allowedhook) in our client directory:
api.dqco-op.comAccount: <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:
- ✅ The DQC Quality Tool is installed — the toolbox script, the
save_dqc_data()function, and thedqc_dataholder question, all from General Setup. - ✅ Your survey is a secure survey (
secure="1"on the<survey>tag). - ✅ 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 /).
# 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>
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.
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
lambdathat returns the value. Each read is wrapped in its owntry/except, so a missing or malformed value is simply skipped — it never breaks the rest of the payload.
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:
- Add the row to the
dqc_dataholder so there's somewhere to store the value. - Populate it in
save_dqc_data()(the General Setup function) from the toolbox'sclient_dqc_*value. - Send it in
send_results_dqc()withaddDQCField().
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)
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 accessor | save_dqc_data() source | Description |
|---|---|---|
dqc_data.ads.val | client_dqc_avg_device_score | Average device score |
dqc_data.lds.val | client_dqc_lowest_device_score | Lowest device score |
dqc_data.tsv.val | client_dqc_total_surveys | Total surveys |
dqc_data.cmr.val | client_dqc_completion_rate | Completion rate |
dqc_data.dpr.val | client_dqc_duplication_rate | Duplication rate |
dqc_data.flr.val | client_dqc_failure_rate | Failure rate |
dqc_data.qlr.val | client_dqc_qualification_rate | Qualification rate |
dqc_data.lst.val | client_dqc_last_survey_taken | Last survey taken |
dqc_data.mir.val | client_dqc_manual_isq_rate | Manual ISQ rate |
dqc_data.air.val | client_dqc_automated_isq_rate | Automated ISQ rate |
dqc_data.osr.val | client_dqc_osq_rate | OSQ rate |
dqc_data.bfa.val | client_dqc_brand_familiarity | Brand familiarity |
dqc_data.oen.val | client_dqc_open_end | Open end |
dqc_data.spd.val | client_dqc_speeding | Speeding |
dqc_data.hpt.val | client_dqc_honey_pot | Honey pot |
dqc_data.stl.val | client_dqc_straightlining | Straightlining |
dqc_data.scn.val | client_dqc_distinct_supplier_count | Distinct supplier count |
dqc_data.sup.val | client_dqc_suppliers | Suppliers |
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():
| Call | Placement | Purpose |
|---|---|---|
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>
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">.
DQC_API_KEY in two placesThe placeholder DQC_API_KEY appears twice in this example and both must be set to your key:
- The toolbox import URL near the top —
.../tools/toolbox/DQC_API_KEY(client‑side). - The
DQC_API_KEY = "…"variable at the top of<exec when="init">(server‑side), which thesend_results_dqc()Authorizationheader 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 /).
# 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).

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_datein the server's local timezone; the code converts to UTC so it matches the endpoint. Do not send the raw local value. listis reserved. Decipher exposes a built‑inlistvariable (the sample source), which shadows Python'slist(). Read markers with[x for x in p.markers], neverlist(p.markers).- No literal
&,<, or>in<exec>code (including comments) — Decipher parses the block as XML and will reject them. Build an ampersand fromchr(38), or wrap the exec in<![CDATA[ … ]]>. - HTML‑encoded values.
start_date.valanddqc_data.sid.valcome back with/instead of/;dqcDecodefixes this. qtimeis read withgetattr(qtime, "val", qtime)so it works whetherqtimeis 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
- First, have Forsta authorize the DQC domain (
api.txt+request_allowed) — Step 0. - Add
send_results_dqc()belowsave_dqc_data()in yourinitexec. - Call
send_results_dqc()after the first question (in‑progress) and again in<exec when="finished">(final). - Add the
dqc_debugholder and confirm via its column that records send with the correctstatusand UTC timestamps.
For automatic early termination of low‑quality respondents, see Termination Scripts.