Skip to content

02 Get your data in /Integrations

SurveyMonkey

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

  1. S SurveyMonkeyTickets, 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 SurveyMonkey survey responses: each open-ended question and the respondent’s answer, with choice answers and response metadata alongside. You run it on your side with the universal API, using SurveyMonkey’s API v3.

  • A paid SurveyMonkey plan. SurveyMonkey says a Basic plan account gets error 1005 after 25 responses.
  • A SurveyMonkey app (a Private app for your own account) and its access token, with the scope to view response details.
  • A Dimension Labs API key for a new Universal API source. See Your API key.

SurveyMonkey’s steps are in the API v3 documentation.

  1. Register an app in the SurveyMonkey developer portal. Registering creates a draft app with an access token that works against your own account for 90 days.
  2. Before the 90 days end, deploy it as a Private app. All users of a Private app must be on the same SurveyMonkey team with a paid plan that includes API access.
  3. Open the app’s Settings in the My Apps tab and copy the access token. If you will read other people’s accounts, use SurveyMonkey’s OAuth 2.0 flow instead.
  4. Send the token as Authorization: Bearer YOUR_ACCESS_TOKEN. The bulk responses endpoint needs the View Response Details scope (responses_read_detail).
Terminal window
curl -H "Authorization: Bearer $SURVEYMONKEY_TOKEN" \
"https://api.surveymonkey.com/v3/surveys/SURVEY_ID/responses/bulk?per_page=100&sort_by=date_modified&sort_order=ASC&start_modified_at=2026-10-01T00:00:00"
  • GET /v3/surveys/{id}/responses/bulk returns full responses with every answer. per_page goes up to 100 (default 50).
  • Incremental: start_modified_at returns responses modified after that date. Sort with sort_by=date_modified and sort_order=ASC. Add status=completed to skip partial responses.
  • Pagination: request links.next until it is missing.
  • Answers carry question and choice IDs. Get the question text from GET /v3/surveys/{id}/details: each page’s questions has id, family (for example open_ended) and headings[].heading. SurveyMonkey suggests caching it.
  • Rate limits (SurveyMonkey’s): draft and private apps start at 120 requests per minute and 500 requests per day, reset at midnight GMT. Your current limits are in the X-Ratelimit-App-Global-* response headers.

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

Dimension LabsSurveyMonkeyNotes
textheadings[0].heading from survey details (outgoing), answers[].text (incoming)text holds open-ended answers and “other” text.
userIdrecipient_idSurveyMonkey’s internal id, only set for email collectors. Otherwise use the response id.
incomingfalse for the question, true for the answer.
sessionIdid (the response ID)
conversationIdid
dimensionlabs_timestampdate_createdSurveyMonkey dates are UTC, sometimes without an offset; toMillis reads those as UTC. Add 1,000 ms per message.
platformJsonchoice_id and row_id of choice questions (for example an NPS question), collector_id, response_status, custom_variables, languageLook up choice IDs in survey details to get the label. custom_variables is a good place for account IDs to join on.

Personal data. Don’t put emails or names in custom_variables, and leave out the response’s ip_address and any contact fields. Open-ended answers can hold them too: redact them before you send. See PII redaction.

// surveymonkey.mjs. Environment: SURVEYMONKEY_TOKEN, SURVEYMONKEY_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 API = 'https://api.surveymonkey.com/v3';
// Retries SurveyMonkey's rate limits (429) and server errors (5xx), honoring Retry-After when it is sent.
async function get(url) {
for (let attempt = 0; ; attempt++) {
const res = await fetch(url, { headers: { Authorization: `Bearer ${process.env.SURVEYMONKEY_TOKEN}`, Accept: 'application/json' } });
if (res.ok) return res.json();
if ((res.status !== 429 && res.status < 500) || attempt === 3) throw new Error(`SurveyMonkey returned ${res.status}: ${await res.text()}`);
await sleep((Number(res.headers.get('retry-after')) || 2 ** attempt) * 1000);
}
}
// Sends every completed response modified at or after `since` (Unix ms), for each survey. Returns the new `since`.
export async function syncSurveyMonkey({ since, historical = false }) {
let newest = since;
for (const surveyId of process.env.SURVEYMONKEY_SURVEY_IDS.split(',').map((x) => x.trim()).filter(Boolean)) {
const details = await get(`${API}/surveys/${surveyId}/details`);
const questions = Object.fromEntries(details.pages.flatMap((p) => p.questions).map((q) => [q.id, q]));
let next = `${API}/surveys/${surveyId}/responses/bulk?per_page=100&status=completed&sort_by=date_modified&sort_order=ASC&start_modified_at=${new Date(since).toISOString().slice(0, 19)}`;
while (next) {
const page = await get(next);
for (const r of page.data) {
const answered = r.pages.flatMap((p) => p.questions);
const choices = Object.fromEntries(answered.map((q) => [q.id, q.answers.filter((a) => a.choice_id).map((a) => a.choice_id)]).filter(([, c]) => c.length));
const base = { userId: String(r.recipient_id || r.id), sessionId: String(r.id), conversationId: String(r.id),
platformJson: { surveyId, collectorId: r.collector_id, status: r.response_status, language: r.language, customVariables: r.custom_variables, choices } };
const created = toMillis(r.date_created);
const old = historical || created < Date.now() - HOUR;
let t = created;
for (const q of answered) {
const texts = q.answers.map((a) => a.text?.trim()).filter(Boolean);
if (!texts.length) continue;
const heading = questions[q.id]?.headings?.[0]?.heading ?? `Question ${q.id}`;
await sendToDimensionLabs({ ...base, text: heading, incoming: false, dimensionlabs_timestamp: t }, { historical: old });
await sendToDimensionLabs({ ...base, text: texts.join('\n'), incoming: true, dimensionlabs_timestamp: t + 1000 }, { historical: old });
t += 2000;
}
newest = Math.max(newest, toMillis(r.date_modified));
}
next = page.links?.next ?? null;
}
}
return newest;
}

A response that is edited later comes back with a new date_modified and is sent again under the same sessionId. The parts that didn’t change are duplicates, and are removed automatically. One Dimension Labs source can hold all your surveys: the survey ID goes in platformJson.surveyId.

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

  • Check the daily limit first. Each run uses at least two requests per survey, and draft and private apps start at 500 requests a day. A 15-minute schedule is 96 runs a day, so with more than two surveys, 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 SurveyMonkey as a file instead, upload it and check it first with the file checker, then run a backfill.

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