Skip to content

02 Get your data in /Integrations

Qualtrics

Send survey responses from Qualtrics to Dimension Labs with the universal API.

  1. Q QualtricsTickets, chats, calls, reviews
  2. ⟳ Your jobPull new · map fields · every 15 min
  3. { } Universal APIPOST /track · /trackhistorical
  4. Dimension LabsRead in full · enriched daily

This brings in your Qualtrics survey responses: each open-text question and the respondent’s answer, with scores and respondent metadata alongside. You run it on your side with the universal API, using Qualtrics’ response export API.

  • A Qualtrics user with the Access API permission.
  • A Qualtrics API token, your datacenter ID, and the ID of each survey (it starts with SV_).
  • A Dimension Labs API key for a new Universal API source. See Your API key.

Qualtrics’ steps are in API Key Authentication and Base URL and Datacenter IDs.

  1. Log in to Qualtrics and open Account Settings from the user menu.
  2. Go to Qualtrics IDs. In the API box, click Generate Token. If a token already exists, generating a new one replaces it and the old one stops working at once.
  3. In the User box on the same page, note your datacenter ID, for example iad1. Your base URL is https://YOUR_DATACENTER.qualtrics.com/API/v3. Qualtrics says to always call your own datacenter.
  4. Send the token in the X-API-TOKEN header. Qualtrics also supports OAuth 2.0 with the read:survey_responses scope.

A response export runs in three calls: start it, poll it, download the file.

Terminal window
# 1. Start an export of the responses recorded since a date.
curl -X POST -H "X-API-TOKEN: $QUALTRICS_TOKEN" -H "Content-Type: application/json" \
-d '{"format":"json","compress":false,"startDate":"2026-10-13T00:00:00Z"}' \
"https://YOUR_DATACENTER.qualtrics.com/API/v3/surveys/SURVEY_ID/export-responses"
# 2. Poll until result.status is "complete". The result then has a fileId.
curl -H "X-API-TOKEN: $QUALTRICS_TOKEN" \
"https://YOUR_DATACENTER.qualtrics.com/API/v3/surveys/SURVEY_ID/export-responses/PROGRESS_ID"
# 3. Download the file.
curl -H "X-API-TOKEN: $QUALTRICS_TOKEN" \
"https://YOUR_DATACENTER.qualtrics.com/API/v3/surveys/SURVEY_ID/export-responses/FILE_ID/file"
  • Incremental: startDate (and optionally endDate) limits the export to responses recorded in that range, in ISO 8601 UTC. Qualtrics also offers continuation tokens (allowContinuation, then continuationToken), which expire after a week; the date filter is simpler to store and never expires.
  • Export files expire one week after the export completes.
  • format can be csv, tsv, json, ndjson, spss or xml. The file comes zipped unless you send "compress": false, which Qualtrics recommends only for small exports. Exports over 1.8 GB fail, so load a long history in date ranges.
  • The question text comes from GET /API/v3/surveys/SURVEY_ID: each entry in result.questions has questionText and questionType.type, where TE is a text entry question.
  • Rate limits (Qualtrics’): 3,000 requests per minute per brand. Lower per-endpoint limits apply: 100 per minute to start an export, 1,000 per minute to check progress, 100 per minute to download the file. Wait between progress checks.

Each open-text question becomes an outgoing message (the question) followed by an incoming message (the answer), under the response ID. Skip blank answers.

Dimension LabsQualtricsNotes
textquestionText (outgoing), the text entry answer in values (incoming)Question text can hold HTML. Strip it.
userIdresponseIdNo respondent ID is guaranteed, so use the response ID. Use externalDataReference instead if you set it to your own internal customer id (not an email).
incomingfalse for the question, true for the answer.
sessionIdresponseIdFor example R_1QasAWMu5RPGgTs.
conversationIdresponseId
dimensionlabs_timestampvalues.recordedDateConvert with toMillis. Add 1,000 ms per message to keep the order and the one-second spacing.
platformJsonnumeric values such as NPS or CSAT questions, distributionChannel, userLanguage, finished, the survey IDEmbedded data fields are a good place for account or store IDs to join on.

Personal data. A response’s values also hold fields such as recipientEmail, recipientFirstName, ipAddress and location: the code sends only the fields it names. Open-text answers can hold names and emails too: redact them before you send. See PII redaction.

// qualtrics.mjs. Environment: QUALTRICS_DC, QUALTRICS_TOKEN, QUALTRICS_SURVEY_IDS (comma-separated), DL_API_KEY.
import { sendToDimensionLabs, toMillis } from './dimensionlabs.mjs';
const HOUR = 60 * 60 * 1000;
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const headers = { 'X-API-TOKEN': process.env.QUALTRICS_TOKEN, 'Content-Type': 'application/json' };
const strip = (s) => String(s).replace(/<[^>]+>/g, ' ').replace(/&nbsp;/g, ' ').replace(/\s+/g, ' ').trim();
// Retries Qualtrics' rate limits (429) and server errors (5xx), honoring Retry-After when it is sent.
async function call(url, init) {
for (let attempt = 0; ; attempt++) {
const res = await fetch(url, { headers, ...init });
if (res.ok) return res.json();
if ((res.status !== 429 && res.status < 500) || attempt === 5) throw new Error(`Qualtrics returned ${res.status}: ${await res.text()}`);
await sleep((Number(res.headers.get('retry-after')) || 2 ** attempt) * 1000);
}
}
async function exportSince(api, since) {
const body = { format: 'json', compress: false, startDate: new Date(since).toISOString().replace(/\.\d{3}Z$/, 'Z') };
const { result: job } = await call(`${api}/export-responses`, { method: 'POST', body: JSON.stringify(body) });
for (let wait = 2000; ; wait = Math.min(wait * 2, 30000)) {
const { result: progress } = await call(`${api}/export-responses/${job.progressId}`);
if (progress.status === 'complete') return call(`${api}/export-responses/${progress.fileId}/file`);
if (progress.status === 'failed') throw new Error('Qualtrics export failed');
await sleep(wait);
}
}
// Sends every response recorded at or after `since` (Unix ms), for each survey. Returns the new `since`.
export async function syncQualtrics({ since, historical = false }) {
let newest = since;
for (const surveyId of process.env.QUALTRICS_SURVEY_IDS.split(',').map((x) => x.trim()).filter(Boolean)) {
const api = `https://${process.env.QUALTRICS_DC}.qualtrics.com/API/v3/surveys/${surveyId}`;
const { result: survey } = await call(api);
const textQuestions = Object.entries(survey.questions).filter(([, q]) => q.questionType.type === 'TE');
const file = await exportSince(api, since);
for (const r of file.responses ?? []) { // confirm this layout against one export file of your own
const v = r.values;
const scores = Object.fromEntries(Object.entries(v).filter(([k, x]) => k.startsWith('QID') && typeof x === 'number'));
const base = { userId: r.responseId, sessionId: r.responseId, conversationId: r.responseId,
platformJson: { surveyId, distributionChannel: v.distributionChannel, userLanguage: v.userLanguage, finished: v.finished, scores } };
const recorded = toMillis(v.recordedDate);
const historicalNow = historical || recorded < Date.now() - HOUR;
let t = recorded;
for (const [qid, q] of textQuestions) {
const answer = Object.entries(v).find(([k, x]) => (k === qid || k.startsWith(`${qid}_`)) && typeof x === 'string' && x.trim())?.[1];
if (!answer) continue;
await sendToDimensionLabs({ ...base, text: strip(q.questionText), incoming: false, dimensionlabs_timestamp: t }, { historical: historicalNow });
await sendToDimensionLabs({ ...base, text: answer.trim(), incoming: true, dimensionlabs_timestamp: t + 1000 }, { historical: historicalNow });
t += 2000;
}
newest = Math.max(newest, recorded);
}
}
return newest;
}

Qualtrics’ docs describe the fields of one response (responseId, values, labels) but not the layout of a JSON export file, so open one file before you go live and adjust the file.responses line if it differs. Check the keys your text entry answers use in values the same way.

One Dimension Labs source can hold all your surveys: the survey ID goes in platformJson.surveyId, so you can filter by survey.

Save the code as qualtrics.mjs in a project set up as in Set up your project, and run it with the scheduled sync runner: import { syncQualtrics as sync } from './qualtrics.mjs';.

  • History first: node run.mjs 2025-01-01 exports and sends every response since that date to /trackhistorical. Then start a backfill from the Backfill tab.
  • Then every 15 minutes: the scheduled job sends new responses to /track, and they are enriched automatically every day. A response that is more than an hour old when the job sees it goes to /trackhistorical, and needs a backfill.
  • Re-runs are safe: duplicates are removed automatically.

You can also export responses from the survey’s Data & Analysis tab, upload the file (see Wide survey exports), and check it first with the file checker, then run a backfill.

Checked against Qualtrics’ documentation on 2026-10-13.