02 Get your data in /Integrations
Qualtrics
Send survey responses from Qualtrics to Dimension Labs with the universal API.
- Q QualtricsTickets, chats, calls, reviews
- ⟳ Your jobPull new · map fields · every 15 min
- { } Universal APIPOST /track · /trackhistorical
- 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.
What you need
Section titled “What you need”- 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.
1. Get API access in Qualtrics
Section titled “1. Get API access in Qualtrics”Qualtrics’ steps are in API Key Authentication and Base URL and Datacenter IDs.
- Log in to Qualtrics and open Account Settings from the user menu.
- 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.
- In the User box on the same page, note your datacenter ID, for example
iad1. Your base URL ishttps://YOUR_DATACENTER.qualtrics.com/API/v3. Qualtrics says to always call your own datacenter. - Send the token in the
X-API-TOKENheader. Qualtrics also supports OAuth 2.0 with theread:survey_responsesscope.
2. Pull new responses
Section titled “2. Pull new responses”A response export runs in three calls: start it, poll it, download the file.
# 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 optionallyendDate) limits the export to responses recorded in that range, in ISO 8601 UTC. Qualtrics also offers continuation tokens (allowContinuation, thencontinuationToken), which expire after a week; the date filter is simpler to store and never expires. - Export files expire one week after the export completes.
formatcan becsv,tsv,json,ndjson,spssorxml. 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 inresult.questionshasquestionTextandquestionType.type, whereTEis 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.
3. Map the fields
Section titled “3. Map the fields”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 Labs | Qualtrics | Notes |
|---|---|---|
text | questionText (outgoing), the text entry answer in values (incoming) | Question text can hold HTML. Strip it. |
userId | responseId | No 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). |
incoming | false for the question, true for the answer. | |
sessionId | responseId | For example R_1QasAWMu5RPGgTs. |
conversationId | responseId | |
dimensionlabs_timestamp | values.recordedDate | Convert with toMillis. Add 1,000 ms per message to keep the order and the one-second spacing. |
platformJson | numeric values such as NPS or CSAT questions, distributionChannel, userLanguage, finished, the survey ID | Embedded 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.
4. Send it
Section titled “4. Send it”// 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(/ /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.
5. Run it
Section titled “5. Run it”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-01exports 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.
Qualtrics reference
Section titled “Qualtrics reference”- API Key Authentication
- Base URL and Datacenter IDs
- Survey Response Export Guide
- Surveys Response Import/Export API
- Start Response Export
- Get Response Export Progress
- Get Response Export File
- Get Survey
- Retrieve a Survey Response
- Limits
- Getting Started with the Qualtrics API
Checked against Qualtrics’ documentation on 2026-10-13.