SurveyCTO setup guide
Run an authorized voice module in a SurveyCTO text field. Resume state uses plug-in metadata; the answer field receives only the completed recorded result.
These instructions load the survey from voice over the internet. Saving a ZIP on a tablet does not make this integration offline. If the connection drops, collection pauses; reconnect and resume the same interview.
A host is the outer survey that opens voice and receives its recorded results. For this setup you need permission to edit that host survey, a published voice survey, and its integration code.
How answers reach the host
One voice module saves one JSON text value in one host question, even when the module contains several questions. JSON is structured text: its reports list holds the individual recorded answers. The integration does not automatically create a separate host column for each voice question.
- Create the SurveyCTO text field strudl_result and attach the plug-in to that field. This field is the destination for all answers from this voice module; hostFields in config.js maps inputs going in the other direction.
- During collection, voice accepts each recorded answer online and the bridge passes it to the plug-in. The plug-in saves progress in field metadata. It leaves strudl_result empty until the whole module is complete.
- At completion, the plug-in calls setAnswer() with the full result as JSON text. SurveyCTO then holds it as this question’s answer. Save and submit the host form using its normal workflow, then export strudl_result. Read JSON.parse(value).reports.
Once installed and configured, the supplied adapter performs these writes automatically. The enumerator does not copy answers between surveys. Native storage and export still need the device/account acceptance test described below.
The recorded result contains garbled reports for protected questions and ordinary recorded values for other questions, plus identifiers and completion status. Intended protected answers and garbling randomness are never returned. Host-provided enumerator/respondent IDs are not echoed in the result: keep those native host fields beside the module answer in the export.
See the saved result and extraction example1. Prepare the voice survey
Do this on the voice website. Start with the downloadable example, then substitute your own questionnaire once the integration works.
Download example questionnaireIn the editor, open Upload or download Markdown → Upload questionnaire Markdown and select the .md file. After publishing, keep the management page open: the Embed panel provides the dedicated iframe URL and integration code.
- Create one question or a complete questionnaire. For a first test, use the example below and synthetic IDs only.
- Publish a survey with one or more questions and a closing date that covers your fieldwork.
- On its management page, open Embed in another survey. Choose a label and consent mode, then generate an integration code. Save the code when it is shown; you can revoke it in the same panel.
Save the example as example-questionnaire.md and import it using the questionnaire tools in the editor. The same file is included in every download.
<!-- strudl:questionnaire v1 -->
# Online host example
```variables
respondent_id = host("respondent_id", "")
```
## first. Participant $respondent_id: have you ever missed an appointment?
[choose one]
- Yes*
- No
## last. Would you like more information?
[choose one]
- Yes
- No
Choose one consent mode
- For the first test, keep respondent consent in voice and leave scriptVersion empty. voice displays its own consent screen; leave any host consent field empty or 0.
- If the enumerator collects consent, choose external consent in Embed, enter the actual study script and version, then set scriptVersion to that exact version. The host must separately confirm consent for each interview. Setting the version alone does not confirm consent.
// Respondent consent:
scriptVersion: ""
// External consent (also requires this interview's host consent field = "1"):
scriptVersion: "study-v1"
Codes allow participation in this survey, not management. Keep codes and interview credentials out of URLs. Use HTTPS, except for local tests. Third-party cookies are not required.
2. Configure the download
Extract strudl-online.fieldplugin.zip. Edit config.js: replace url with the Embed iframe URL and code with the integration code. Set scriptVersion only for external consent. Keep the files at the ZIP root when repackaging; preserve the filename.
Edit config.js on your computer. You may replace its contents with this block. Replace the URL and code placeholders; leave scriptVersion empty for the first respondent-consent test.
window.STRUDL_SURVEYCTO = {
url: "https://strudl.io/embed/s/REPLACE-SURVEY-UUID",
code: "REPLACE-INTEGRATION-CODE",
scriptVersion: "",
hostFields: {
enumerator_id: "enumerator_id",
respondent_id: "respondent_id"
}
};
Files to keep in the installed ZIP
strudl-online.fieldplugin.zip
├── manifest.json
├── template.html
├── style.css
├── script.js
├── config.js
├── embed-bridge.js
└── embed-host.js
These seven files must be directly inside the ZIP, without an enclosing folder. Only config.js needs editing. The download also includes README.md, example-questionnaire.md and simulator files; those are for reference and local tests.
After saving config.js, run these commands from the extracted folder to create and inspect the configured ZIP. Keep this exact ZIP filename because the host appearance setting refers to it.
You can also create the ZIP with your file manager: select its contents, not the enclosing folder. The commands use Python 3; on Windows replace python3 with py -3.
cd "/path/to/extracted/package"
python3 -m zipfile -c ../strudl-online.fieldplugin.zip manifest.json template.html style.css script.js config.js embed-bridge.js embed-host.js
python3 -m zipfile -l ../strudl-online.fieldplugin.zip
3. Install and map host fields
In SurveyCTO Form Designer, open Form settings → Attachments and attach the ZIP. Create the fields below before the module. Add a required text field named strudl_result, select the attached plug-in in its appearance options, and pass the parameters below. In a spreadsheet form definition, use the complete appearance expression.
| Field | Example value |
|---|---|
interview_id | synthetic-001 |
enumerator_id | E007 |
respondent_id | R0042 |
consent | 1 (only after consent) |
Paste into Form Designer’s Plug-in parameters box
interview_id=${interview_id}, enumerator_id=${enumerator_id}, respondent_id=${respondent_id}, consent=${consent}
Or paste into a spreadsheet form’s appearance cell
custom-strudl-online(interview_id=${interview_id}, enumerator_id=${enumerator_id}, respondent_id=${respondent_id}, consent=${consent})
Use text fields for IDs to preserve leading zeroes. Parameters are read with getPluginParameter; add custom input mappings in hostFields. The module answer stays empty until server completion and successful saving. Mark it required, and do not bypass validation when advancing.
4. Save, reopen and export
Save and deploy the host form, then test on every intended Collect or web-form version. Reopening reads getMetaData(); each checkpoint calls setMetaData() and checks it before acknowledgement. The completed JSON goes to setAnswer(). Read-only views do not start collection; clearing the field leaves a tombstone and cannot restart it.
Export strudl_result and parse its JSON. Analyze reports, deduplicating by event_id. Metadata contains the resume credential; it is separate from ordinary answer exports but can appear in raw submission XML. Protect that data and verify native save durability on real devices.
What the recorded result looks like
The example below is the complete type of JSON stored in strudl_result after completion. Resume credentials are stored separately and are not in this answer field.
{
"version": 1,
"survey": "example-survey-id",
"run_id": "example-run-id",
"completed": true,
"reports": [
{"question": "first", "reported": 1, "event_id": "example-run-id:first"},
{"question": "last", "reported": 0, "event_id": "example-run-id:last"}
]
}
These are illustrative recorded values for the downloadable two-question example, not predictions of what a respondent will report. Both questions list Yes first and No second: reported: 0 means recorded Yes; reported: 1 means recorded No. For the protected first question, this does not reveal the intended answer.
| Report field | How to read it |
|---|---|
reported | One recorded choice index, starting at 0. Match it to the answer order in the exact questionnaire used. |
text / number | The ordinary recorded text or number. |
multiple | A list of recorded choice indices, each starting at 0. |
skipped: true | An explicit skip. A question absent from reports has no recorded answer; do not convert it to No or zero. |
event_id | A stable answer identifier. Use it to remove duplicates if you combine repeated callbacks or exports. |
Turn the host answer into analysis columns
Run this JavaScript after exporting the host data, in your export-processing code. exportedRow means one parsed CSV row. Replace the column names if your host export uses different names. This is not code to paste into the voice questionnaire or the host question editor.
const result = JSON.parse(exportedRow.strudl_result);
if (result?.completed !== true) throw new Error("Incomplete module");
const byQuestion = Object.fromEntries(
result.reports.map(report => [report.question, report])
);
const analysisRow = {
interview_id: exportedRow.interview_id,
enumerator_id: exportedRow.enumerator_id,
respondent_id: exportedRow.respondent_id,
first_recorded: byQuestion.first?.reported ?? null,
last_recorded: byQuestion.last?.reported ?? null
};
For the example result above, first_recorded is 1 and last_recorded is 0. The IDs come from the host’s existing columns, not from the result. Keep first and last for the supplied example; replace them with your questionnaire’s question IDs for your study. The code preserves zero and leaves missing/skipped choices as null.
This creates an analysisRow object; it does not modify the host survey or write a CSV file by itself. Save those objects using your own export workflow. To use a recorded answer in a later host question during the interview, add platform-specific parsing/routing after module completion; the supplied adapters do not create those extra host fields.
5. Test before fieldwork
Extract the download, run python3 -m http.server 8081 in that directory, and open http://localhost:8081/simulator.html. Enter the dedicated voice URL, integration code, synthetic interview ID and consent details. The simulator shows the recorded answer and enables Next only after completion. It does not prove native platform compatibility.
Run these commands in a terminal on your computer, from the extracted package folder. Replace the example folder path with your own. On Windows, use py -3 instead of python3 if needed. Keep the terminal open while testing.
cd "/path/to/extracted/package"
python3 -m http.server 8081 --bind 127.0.0.1
This local server is only for testing on the same computer. A localhost or 127.0.0.1 address on a tablet points to the tablet itself. Fieldwork uses the installed host package or the published host survey, not this local test address.
The simulator takes its connection details from the form you fill in. Passing this test does not prove that the settings you edited in the production file are correct. Test the installed package or pasted question code too.
- Verify the entire handoff: finish a synthetic module, save/submit the host interview, export the receiving field, parse its JSON and locate both first and last by question ID. Check recorded values against the module result, not against the protected choices clicked by the respondent.
- Repeat the test in the actual host platform: complete two different interviews, return to the module, close and reopen the app or browser, and confirm distinct run IDs with unchanged answers.
- Interrupt connectivity before sending and after server acceptance. Reconnect and verify that reports are recovered without new random draws or duplicate answer rows.
- Confirm that onward routing waits for completion, failed host saves block progress, and missing or stale external consent is rejected.
- Test blocked third-party cookies, tablet layout, a revoked code, an expired survey and the private withdrawal link. Export a completed host interview and compare every recorded answer with voice results; check that long JSON values are not truncated.
Keep the resume journal confidential and separate from analyst results. Server withdrawal does not delete host answers, browser journals or earlier exports. Set a retention and deletion procedure before fieldwork. Provide a host exit route for participants who decline or leave the module.