Skip to content

02 Get your data in /Integrations

QuestionPro

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

  1. Q QuestionProTickets, 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 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.

  • 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.

QuestionPro’s steps are in How to generate API key.

  1. Log in and go to Surveys » Integration » API.
  2. Click Get API Key and copy the key. The key is specific to your account. The same page has Regenerate and an Inactive toggle.
  3. 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) or api.questionpro.ca (CA). See Environments.
  4. Send the key in the api-key header.
Terminal window
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/filter filters responses by startDate and endDate (format YYYY-MM-DDTHH24:MI:SS). resultMode 2 returns completed responses only.
  • Time zone: without timeZone (GMT format, for example GMT-07:00), QuestionPro applies PST/PDT to the dates.
  • Pagination: page and perPage. The default is 100 per page and QuestionPro supports up to 1,000. Request the next page until a page comes back with fewer than perPage responses.
  • 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 429 until you buy more.

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 LabsQuestionProNotes
textresponseSet[].questionText (outgoing), answerValues[].value.text (incoming)
userIdexternalReference if it is your own internal customer id, otherwise responseIDNever an email address.
incomingfalse for the question, true for the answer.
sessionIdresponseIDA number: send it as a string.
conversationIdresponseID
dimensionlabs_timestamptimestampLooks 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.
platformJsonvalue.scale and answerText of scale questions (for example NPS), surveyID, responseStatus, language, customVariables, location.countryCodeCustom 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.

// 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:SS
const 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.

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-01 sends 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.

Checked against QuestionPro’s documentation on 2026-10-13.