02 Get your data in /Integrations
Survicate
Send survey responses from Survicate to Dimension Labs with the universal API.
- S SurvicateTickets, 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 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.
What you need
Section titled “What you need”- 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.
1. Get API access in Survicate
Section titled “1. Get API access in Survicate”Survicate’s steps are in Setup.
- In the Survicate panel, go to Settings > Organization > Access Keys. Only organization owners and workspace admins can open that page.
- Copy the API key.
- Send it as
Authorization: Basic YOUR_API_KEY. This is the raw key afterBasic, not a base64 user name and password. - 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). GET /surveyslists your surveys with theirid, if you don’t have the IDs to hand.
2. Pull new responses
Section titled “2. Pull new responses”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}/responsesreturns responses newest first.- Incremental, and reversed:
startis the newer bound andendthe older one, sostartmust be later thanend. Both are inclusive, in ISO 8601 with microseconds, for example2024-08-31T23:59:59.000000Z. Put your cursor inend. - Pagination:
items_per_pagetakes 1 to 100. Each page haspagination_data.has_moreandnext_url, a path relative tohttps://data-api.survicate.com/v2. Prepend the base URL, request it as it is, and stop whenhas_moreisfalse. - Each response has a
uuid,collected_at,language,respondent.uuidand ananswersarray. Skipped questions aren’t in it. A text question’sansweris a string. Choice answers can carry acomment, ratings arating, and NPS answers acontentwith atag(detractor,passiveorpromoter). - The question text comes from
GET /surveys/{survey_id}/questions: each entry has anid, atype(for exampletext,nps,ratingorform) and thequestionas 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_idonce 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.
3. Map the fields
Section titled “3. Map the fields”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 Labs | Survicate | Notes |
|---|---|---|
text | question 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. |
userId | respondent.uuid | Survicate’s own respondent ID. Fall back to the sessionId. |
incoming | false for the question, true for the answer. | |
sessionId | uuid | Prefix it, for example survicate-15263384-6a46-.... |
conversationId | uuid | Same value as sessionId. |
dimensionlabs_timestamp | collected_at | ISO 8601 UTC. Convert with toMillis. Add 1,000 ms per message. |
platformJson | survey ID, language, device_type, answer.rating, NPS content and tag, the attributes you request | Attributes 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.
4. Send it
Section titled “4. Send it”// 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 microsecondsconst strip = (s) => String(s ?? '').replace(/<[^>]+>/g, ' ').replace(/ /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.
5. Run it
Section titled “5. Run it”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-01sends 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
endis 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.
Survicate reference
Section titled “Survicate reference”- Data Export API setup (authentication, pagination, attributes, rate limits)
- Survey (surveys and questions)
- Response
- Respondent
- Migrating from v1 to v2
- Data Export API (help center)
- Data export: CSV and XLSX reports
Checked against Survicate’s documentation on 2026-10-26.