Skip to content

02 Get your data in /Integrations

Survicate

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

  1. S SurvicateTickets, 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 Survicate survey responses: each open-text question and the respondent’s answer, plus the comments respondents add to a choice, one conversation per response, with ratings and NPS scores alongside. You run it on your side: a small script pulls from the Survicate Data Export API and posts each message to the universal API.

  • A Survicate plan that includes API access. Survicate says the Data Export API is available on some plans only, and requests from other accounts return an error. Check Survicate’s pricing or ask Survicate.
  • A Survicate organization owner or workspace admin, to copy the API key.
  • The ID of each survey.
  • A Dimension Labs API key for a new Universal API source. See Your API key.

Survicate’s steps are in Setup.

  1. In the Survicate panel, go to Settings > Organization > Access Keys. Only organization owners and workspace admins can open that page.
  2. Copy the API key.
  3. Send it as Authorization: Basic YOUR_API_KEY. This is the raw key after Basic, not a base64 user name and password.
  4. The base URL is https://data-api.survicate.com/v2. Use v2: Survicate’s migration guide gives September 15, 2026 as the date the v1 Data Export API is turned off (migrating from v1 to v2).
  5. GET /surveys lists your surveys with their id, if you don’t have the IDs to hand.
Terminal window
curl -H "Authorization: Basic $SURVICATE_API_KEY" \
"https://data-api.survicate.com/v2/surveys/SURVEY_ID/responses?start=2026-10-26T00:00:00.000000Z&end=2026-10-01T00:00:00.000000Z&items_per_page=100"
  • GET /surveys/{survey_id}/responses returns responses newest first.
  • Incremental, and reversed: start is the newer bound and end the older one, so start must be later than end. Both are inclusive, in ISO 8601 with microseconds, for example 2024-08-31T23:59:59.000000Z. Put your cursor in end.
  • Pagination: items_per_page takes 1 to 100. Each page has pagination_data.has_more and next_url, a path relative to https://data-api.survicate.com/v2. Prepend the base URL, request it as it is, and stop when has_more is false.
  • Each response has a uuid, collected_at, language, respondent.uuid and an answers array. Skipped questions aren’t in it. A text question’s answer is a string. Choice answers can carry a comment, ratings a rating, and NPS answers a content with a tag (detractor, passive or promoter).
  • The question text comes from GET /surveys/{survey_id}/questions: each entry has an id, a type (for example text, nps, rating or form) and the question as shown to the respondent.
  • Attributes you passed to Survicate, such as your own customer ID, come back only when you name them: add attributes[]=customer_id once per name, up to 100.
  • Rate limits (Survicate’s): 5 requests at the same time, and 1,000 requests per minute per workspace. Over either, Survicate returns 429.

Each open-text answer, and each comment on a choice, becomes an outgoing message (the question) followed by an incoming message (the text), under the response UUID. Skip blank answers.

Dimension LabsSurvicateNotes
textquestion from the questions list (outgoing); answer for text questions, or answer.comment and answers[].comment (incoming)Contact form (form) and date answers aren’t sent.
userIdrespondent.uuidSurvicate’s own respondent ID. Fall back to the sessionId.
incomingfalse for the question, true for the answer.
sessionIduuidPrefix it, for example survicate-15263384-6a46-....
conversationIduuidSame value as sessionId.
dimensionlabs_timestampcollected_atISO 8601 UTC. Convert with toMillis. Add 1,000 ms per message.
platformJsonsurvey ID, language, device_type, answer.rating, NPS content and tag, the attributes you requestAttributes such as an account or order ID are the place to join on.

Personal data. Contact form answers (fields) hold names, emails and phone numbers: the code skips them. Only request attributes that aren’t personal data: never email or a name. Open-text answers can hold personal data too: redact them before you send. See PII redaction.

// survicate.mjs. Environment: SURVICATE_API_KEY, SURVICATE_SURVEY_IDS (comma-separated),
// SURVICATE_ATTRIBUTES (optional, comma-separated attribute names, for example customer_id), 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://data-api.survicate.com/v2';
const list = (v) => (v ?? '').split(',').map((x) => x.trim()).filter(Boolean);
const micro = (ms) => new Date(ms).toISOString().replace(/\.(\d{3})Z$/, '.$1000Z'); // ISO 8601 with microseconds
const strip = (s) => String(s ?? '').replace(/<[^>]+>/g, ' ').replace(/&nbsp;/g, ' ').replace(/\s+/g, ' ').trim();
// Retries Survicate's rate limits (429) and server errors (5xx), honoring Retry-After when it is sent.
async function get(path) {
for (let attempt = 0; ; attempt++) {
const res = await fetch(`${API}${path}`, { headers: { Authorization: `Basic ${process.env.SURVICATE_API_KEY}` } });
if (res.ok) return res.json();
if ((res.status !== 429 && res.status < 500) || attempt === 5) throw new Error(`Survicate returned ${res.status}: ${await res.text()}`);
await sleep((Number(res.headers.get('retry-after')) || 2 ** attempt) * 1000);
}
}
// Follows next_url one page at a time, as Survicate asks.
async function* pages(path) {
for (let next = path; next; ) {
const page = await get(next);
yield page.data ?? [];
next = page.pagination_data?.has_more ? page.pagination_data.next_url : null;
}
}
async function syncSurvey(surveyId, since, historical) {
const questions = {};
for await (const items of pages(`/surveys/${surveyId}/questions?items_per_page=100`)) {
for (const q of items) questions[q.id] = q;
}
const q = new URLSearchParams({ start: micro(Date.now()), end: micro(since), items_per_page: '100' });
for (const name of list(process.env.SURVICATE_ATTRIBUTES)) q.append('attributes[]', name);
let newest = since;
for await (const responses of pages(`/surveys/${surveyId}/responses?${q}`)) {
for (const r of responses) {
const collected = toMillis(r.collected_at);
newest = Math.max(newest, collected);
const attributes = Object.fromEntries([...(r.respondent?.attributes ?? []), ...(r.attributes ?? [])].map((a) => [a.name, a.value]));
const scores = {};
const texts = [];
for (const a of r.answers ?? []) {
const question = questions[a.question_id];
if (typeof a.answer?.rating === 'number') scores[a.question_id] = a.answer.rating;
if (a.answer?.tag) scores[a.question_id] = { score: Number(a.answer.content), tag: a.answer.tag };
const comments = [a.answer?.comment, ...(a.answers ?? []).map((x) => x.comment)];
const text = typeof a.answer === 'string' && question?.type === 'text' ? a.answer : comments.filter((c) => c?.trim()).join('\n');
if (text?.trim()) texts.push({ question: strip(question?.question) || `Question ${a.question_id}`, text: text.trim() });
}
const sessionId = `survicate-${r.uuid}`;
const base = { userId: r.respondent?.uuid ? `survicate-${r.respondent.uuid}` : sessionId, sessionId, conversationId: sessionId,
platformJson: { surveyId, language: r.language, device_type: r.device_type, scores, attributes } };
const old = historical || collected < Date.now() - HOUR;
let t = collected;
for (const { question, text } of texts) {
await sendToDimensionLabs({ ...base, text: question, incoming: false, dimensionlabs_timestamp: t }, { historical: old });
await sendToDimensionLabs({ ...base, text, incoming: true, dimensionlabs_timestamp: t + 1000 }, { historical: old });
t += 2000;
}
}
}
return newest;
}
// Sends every response collected at or after `since` (Unix ms), for each survey. Returns the new `since`.
export async function syncSurvicate({ since, historical = false }) {
let newest = since;
for (const surveyId of list(process.env.SURVICATE_SURVEY_IDS)) {
newest = Math.max(newest, await syncSurvey(surveyId, since, historical));
}
return newest;
}

The code fetches one page at a time, so it never comes near Survicate’s limit of 5 requests at once. One Dimension Labs source can hold all your surveys: the survey ID goes in platformJson.surveyId.

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

  • History first: node run.mjs 2025-01-01 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. Because end is inclusive, the last response of one run comes back in the next; it is removed as a duplicate.

If your plan has no API access, export instead: open the survey’s Analyze tab, click Export, choose CSV, and pick all responses or only the filtered ones. Survicate emails the link, and past exports are under Settings > Exports. Export dates are in UTC. Upload the file, check it first with the file checker, then run a backfill. File export is also available on some plans only.

Checked against Survicate’s documentation on 2026-10-26.