02 Get your data in /Integrations
QuestionPro
Send survey responses from QuestionPro to Dimension Labs with the universal API.
- Q QuestionProTickets, 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 QuestionPro survey responses: each open-text question and the respondent’s answer, with scale answers and response metadata alongside. You run it on your side with the universal API, using QuestionPro’s API v2.
What you need
Section titled “What you need”- A QuestionPro license that includes API calls. QuestionPro lists its free API call allowance per license on its Limits page.
- A QuestionPro API key and the ID of each survey.
- A Dimension Labs API key for a new Universal API source. See Your API key.
1. Get API access in QuestionPro
Section titled “1. Get API access in QuestionPro”QuestionPro’s steps are in How to generate API key.
- Log in and go to Surveys » Integration » API.
- Click Get API Key and copy the key. The key is specific to your account. The same page has Regenerate and an Inactive toggle.
- Find your datacenter: open the My Account menu in the top-right corner; the datacenter is at the bottom. It sets your API domain, for example
api.questionpro.com(US),api.questionpro.eu(EU) orapi.questionpro.ca(CA). See Environments. - Send the key in the
api-keyheader.
2. Pull new responses
Section titled “2. Pull new responses”curl -X POST -H "api-key: $QUESTIONPRO_API_KEY" -H "Content-Type: application/json" \ -d '{"resultMode":2,"startDate":"2026-10-01T00:00:00","endDate":"2026-10-13T00:00:00","timeZone":"GMT+00:00"}' \ "https://api.questionpro.com/a/api/v2/surveys/SURVEY_ID/responses/filter?page=1&perPage=100"POST /a/api/v2/surveys/{survey-id}/responses/filterfilters responses bystartDateandendDate(formatYYYY-MM-DDTHH24:MI:SS).resultMode2returns completed responses only.- Time zone: without
timeZone(GMT format, for exampleGMT-07:00), QuestionPro applies PST/PDT to the dates. - Pagination:
pageandperPage. The default is 100 per page and QuestionPro supports up to 1,000. Request the next page until a page comes back with fewer thanperPageresponses. - Each response holds its questions and answers in
responseSet, so there is no second call per response. - Rate limits (QuestionPro’s): 300 calls per 60 seconds per API key and 2,400 per 60 seconds per organization. Separately, each license has a free API call allowance for the subscription (for example 5,000 calls on Advanced and 20,000 on Research Edition). Once it is used up, calls return
429until you buy more.
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 | QuestionPro | Notes |
|---|---|---|
text | responseSet[].questionText (outgoing), answerValues[].value.text (incoming) | |
userId | externalReference if it is your own internal customer id, otherwise responseID | Never an email address. |
incoming | false for the question, true for the answer. | |
sessionId | responseID | A number: send it as a string. |
conversationId | responseID | |
dimensionlabs_timestamp | timestamp | Looks like 05 Oct, 2020 02:51:51 PM PDT. Node’s Date parses it; pass the Date to toMillis. Add 1,000 ms per message. |
platformJson | value.scale and answerText of scale questions (for example NPS), surveyID, responseStatus, language, customVariables, location.countryCode | Custom variables are a good place for account IDs to join on. |
Personal data. Contact information questions put names, emails and phone numbers in value.text, like any open-text answer. The code skips questions whose text asks for a name, email, phone or address. To choose exactly which questions to send, list their questionCode values in QUESTIONPRO_QUESTION_CODES. Redact open-text answers too. See PII redaction.
4. Send it
Section titled “4. Send it”// questionpro.mjs. Environment: QUESTIONPRO_API_KEY, QUESTIONPRO_SURVEY_IDS (comma-separated),// QUESTIONPRO_HOST (default api.questionpro.com), QUESTIONPRO_QUESTION_CODES (optional), DL_API_KEY.import { sendToDimensionLabs, toMillis } from './dimensionlabs.mjs';
const HOUR = 60 * 60 * 1000;const sleep = (ms) => new Promise((r) => setTimeout(r, ms));const API = `https://${process.env.QUESTIONPRO_HOST ?? 'api.questionpro.com'}/a/api/v2`;const stamp = (ms) => new Date(ms).toISOString().slice(0, 19); // UTC, YYYY-MM-DDTHH:MM:SSconst ONLY = new Set((process.env.QUESTIONPRO_QUESTION_CODES ?? '').split(',').map((x) => x.trim()).filter(Boolean));const CONTACT = /\b(name|e-?mail|phone|mobile|address)\b/i; // contact information questions
// Retries QuestionPro's rate limits (429) and server errors (5xx), honoring Retry-After when it is sent.async function post(url, body) { for (let attempt = 0; ; attempt++) { const res = await fetch(url, { method: 'POST', body, headers: { 'api-key': process.env.QUESTIONPRO_API_KEY, 'Content-Type': 'application/json' } }); if (res.ok) return res.json(); if ((res.status !== 429 && res.status < 500) || attempt === 3) throw new Error(`QuestionPro returned ${res.status}: ${await res.text()}`); await sleep((Number(res.headers.get('retry-after')) || 2 ** attempt) * 1000); }}
// Sends every completed response from `since` (Unix ms) up to now, for each survey. Returns the new `since`.export async function syncQuestionPro({ since, historical = false }) { const upTo = Date.now(); const body = JSON.stringify({ resultMode: 2, startDate: stamp(since), endDate: stamp(upTo), timeZone: 'GMT+00:00' }); for (const surveyId of process.env.QUESTIONPRO_SURVEY_IDS.split(',').map((x) => x.trim()).filter(Boolean)) { for (let page = 1; ; page++) { const responses = (await post(`${API}/surveys/${surveyId}/responses/filter?page=${page}&perPage=100`, body)).response ?? []; for (const r of responses) { const id = String(r.responseID); const scores = {}; for (const q of r.responseSet ?? []) { const scale = q.answerValues.find((a) => a.value?.scale)?.value.scale; if (scale) scores[q.questionCode] = Number(scale); } const base = { userId: r.externalReference || id, sessionId: id, conversationId: id, platformJson: { surveyId: r.surveyID, status: r.responseStatus, language: r.language, customVariables: r.customVariables, scores } }; const submitted = toMillis(new Date(r.timestamp)); const old = historical || submitted < Date.now() - HOUR; let t = submitted; for (const q of r.responseSet ?? []) { if (ONLY.size ? !ONLY.has(q.questionCode) : CONTACT.test(q.questionText)) continue; const texts = q.answerValues.map((a) => a.value?.text?.trim()).filter(Boolean); if (!texts.length) continue; await sendToDimensionLabs({ ...base, text: q.questionText, incoming: false, dimensionlabs_timestamp: t }, { historical: old }); await sendToDimensionLabs({ ...base, text: texts.join('\n'), incoming: true, dimensionlabs_timestamp: t + 1000 }, { historical: old }); t += 2000; } } if (responses.length < 100) break; } } return upTo;}One Dimension Labs source can hold all your surveys: the survey ID goes in platformJson.surveyId.
5. Run it
Section titled “5. Run it”Save the code as questionpro.mjs in a project set up as in Set up your project, and run it with the scheduled sync runner: import { syncQuestionPro as sync } from './questionpro.mjs';.
- Check your call allowance first. A 15-minute schedule makes at least 96 calls a day per survey. If that is too many for your license, run it less often, for example hourly.
- History first:
node run.mjs 2025-01-01sends every completed response since that date to/trackhistorical. Then start a backfill from the Backfill tab. - Then on the schedule: the 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.
If you export responses from QuestionPro as a file instead, upload it and check it first with the file checker, then run a backfill.
QuestionPro reference
Section titled “QuestionPro reference”- How to generate API key
- Authentication
- Environments
- Pagination
- Limits
- Get Responses
- Create Responses Filter
Checked against QuestionPro’s documentation on 2026-10-13.