Migrating from v1 to v2
Data Export API v1 will be turned off on September 15, 2026. This guide maps every v1 endpoint, parameter and response field to its v2 equivalent, so you can update your integration without guessing.
Authentication is unchanged: pass your API key in the Authorization header as Basic {{apiKey}}. Only the base URL and the request/response shapes change.
Endpoint map
| v1 | v2 |
|---|---|
GET /surveys | GET /surveys — now paginated, results wrapped in data |
GET /surveys/{survey_id} | Split in two: GET /surveys/{survey_id} for survey metadata, GET /surveys/{survey_id}/questions for the question list |
GET /surveys/{survey_id}/visitors | GET /surveys/{survey_id}/responses |
GET /visitors/{visitor_uuid} | Split in two: GET /respondents/{respondent_uuid}/responses for their responses, GET /respondents/{respondent_uuid}/attributes for their attributes |
| — | New in v2: GET /surveys/{survey_id}/responses/{response_uuid} fetches a single response |
Two naming changes run through the whole API:
- "Visitor" became "respondent". Both versions identify a person by the same UUID — v1 called it
visitor_uuid, v2 calls itrespondent_uuid(orrespondent.uuidinside a response). Numeric visitor IDs don't exist in v2; v1 itself stopped returning them in January 2025. - Every list endpoint wraps results. v1 returned a bare JSON array. v2 returns an object with
pagination_dataanddata:
Exporting responses: /visitors → /responses
The most common v1 call:
Its v2 equivalent:
Date range: since/until → start/end
The two timestamps swap places compared to v1. v2 orders responses from latest to oldest, so start is the newer bound (where the page starts) and end is the older bound. Passing your old since value as start returns an empty result.
| v1 | v2 | |
|---|---|---|
| Older bound | since | end |
| Newer bound | until (only honored together with since) | start |
| Inclusivity | both inclusive | both inclusive |
| Format | ISO 8601 (2024-08-26T00:00:00Z) | ISO 8601; seconds precision is accepted, timestamps in responses always carry microseconds (2024-08-26T00:00:00.000000Z) |
Ordering
v1 returned responses oldest-to-latest by default, with an optional order=desc parameter. v2 always returns latest-to-oldest; there is no order parameter.
Pagination: page → next_url
v1 pages were fixed at 1,000 responses each. In v2 there are no page numbers — instead of requesting page=2, follow the link the API builds for you:
- Make the first request, optionally with
items_per_page(1–100; omitted = default of 20). - Read
pagination_datain the response. Whilehas_moreistrue, requestnext_url— it is always a path relative to the base URLhttps://data-api.survicate.com/v2, so prepend the base URL and call it as-is. - Stop when
has_moreisfalse.
Under the hood next_url shifts the start timestamp back to where the previous page ended, keeping your end and filters intact — you never compute page boundaries yourself. A full walkthrough with example requests is in Setup → Pagination.
Filtering
v1 had no filters beyond the date range. v2 accepts an optional filters array (operating system, device type, platform, survey language, response URL, custom attributes, question answers) and an attributes[] parameter naming the attributes to include in each response — see the note in Response object changes below. See Response for the schemas.
Response object changes
v1 returned one flat object per visitor, mixing identifiers, profile fields and answers. v2 returns a response object with a fixed set of native fields, a nested respondent object, and attribute arrays.
| v1 field | v2 equivalent |
|---|---|
response_uuid | uuid |
visitor_uuid | respondent.uuid |
page_url | url |
language | language (unchanged) |
answers | answers (same name, new per-type structure, see below) |
custom_attributes (object of key/value) | respondent.attributes — an array of { "name": ..., "value": ... } pairs (values are always strings), returned only for the names you list in the attributes[] query parameter |
Profile fields at top level (first_name, last_name, email, organization, phone, country, and the rest) | Removed as dedicated fields. If you pass them to Survicate, they come back in respondent.attributes like any other attribute — v2 makes no distinction between profile and custom attributes |
identity, first_seen_date, first_response_date | Not returned in v2 |
| — | New in v2: collected_at (response timestamp), device_type, operating_system, platform (a device platform identifier such as MacIntel or Win32), and a top-level attributes array holding attributes recorded for that specific response |
Note there are two attribute arrays in a v2 response: respondent.attributes (attributes of the person, the v1 custom_attributes) and the top-level attributes (attributes recorded with that one response).
v1 returned all of a visitor's custom_attributes with every response by default. On the v2 responses list, attributes are returned only when you ask for them: list the attribute names in the attributes[] query parameter (?attributes[]=order_id&attributes[]=store). Without it, both attribute arrays come back empty — if your import suddenly sees no attributes after migrating, this is why. (The single-response endpoint differs: it always returns the response-level attributes and applies the parameter only to respondent.attributes.)
Fetching one person's data: /visitors/{uuid} → /respondents/{uuid}
The v1 GET /visitors/{visitor_uuid} endpoint returned one object combining the visitor's profile fields, custom_attributes, tags, comment, and a reports array with their responses across surveys. In v2 that splits into two paginated endpoints:
GET /respondents/{respondent_uuid}/attributesreturns what v1 kept in the profile fields andcustom_attributes— as{ "id": ..., "name": ..., "value": ... }entries.tagsandcommentare not returned in v2.GET /respondents/{respondent_uuid}/responsesreturns what v1 kept inreports. Eachdataentry pairs asurveyobject with aresponseobject. Response objects on this endpoint always carry empty attribute arrays — the respondent's attributes come only from the attributes endpoint above:
v1 (reports[] entry) | v2 (data[] entry) |
|---|---|
survey_name | survey.name (plus survey.id and survey.type, which v1 did not return) |
platform (desktop or mobile, detected from the user agent) | response.device_type |
date | response.collected_at |
response_uuid | response.uuid |
answers | response.answers (new per-type structure, see Answer format changes) |
Answer format changes
Each entry in the v1 answers array had the same wrapper regardless of question type:
The content handling varied by type, though: multiple joined the selections into one string with " | ", matrix combined a joined string with a nested answers array, ranking had no content at all (only a nested answers array), and a contact form produced one entry per form field. v2 replaces all of that with one explicit payload per question type:
What moved where:
| v1 | v2 |
|---|---|
survey_point.id | question_id |
survey_point.answer_type | question_type — renamed, but the values are the same strings as in v1 |
survey_point.type, survey_point.pretty_type | Removed |
question (question text repeated in every answer) | Not repeated per answer. Fetch the survey's questions once via GET /surveys/{survey_id}/questions and join by question_id |
content | answer / answers / fields, structured per question type (table below) |
Answer payload by question type
question_type | v2 payload |
|---|---|
single, dropdown_list | answer: { id, content, comment } |
multiple | answers: array of { id, content, comment } |
rating, smiley_scale, numerical_scale, csat | answer: { id, content, rating } — rating is the numeric value |
text, date | answer: the entered text as a string |
nps | answer: { content, tag } (see below) |
matrix | answers: array of { id, content, score } — one entry per matrix row, score is the selected column label |
ranking | answers: array of { id, content, rank } — rank is the position given by the respondent |
form | fields: array of { type, content } — one entry per contact-form field |
empty, button_close, button_link, button_next, redirect_timeout, social_cta | action_performed: true — welcome/thank-you screen interactions. The entry only appears when the action happened |
Payloads also carry auxiliary fields serialized as null when they don't apply — comment, translated_comment and disclaimer_accepted on most types, plus insight on choice answers and translated_answer / ai_followups / translated_ai_followups on text answers. Parse the fields you need and ignore unknown keys rather than validating against a closed set.
NPS
question_type is nps. answer.content is the 0–10 score as a string, and answer.tag is the NPS group derived from it:
tag | Score |
|---|---|
detractor | 0–6 |
passive | 7–8 |
promoter | 9–10 |
dropdown_list and ranking
Neither is a new question type — both existed in v1 under the same answer_type strings. What changed is the payload shape: dropdown_list answers arrive like single-choice answers (one answer object), and ranking answers arrive as an answers array ordered by the respondent, each with its rank.
Skipped questions
This behavior changed between versions. v1 emitted an entry for every question of the survey in every response — a skipped question still appeared, just with content missing or empty. In v2 a skipped question does not appear in the answers array at all: no placeholder entries, no null answers. If your import expects every question to be present, join the response against the survey's question list and treat missing question_ids as skipped.
Rate limits
| Limit | v2 |
|---|---|
| Concurrent requests | 5 |
| Requests per minute per workspace | 1,000 |
Exceeding either returns 429 Too Many Requests. Because pagination follows next_url, pages must be fetched sequentially — you learn the next URL only from the previous response.
Migration checklist
- Switch the base URL to
https://data-api.survicate.com/v2— the API key andAuthorizationheader stay the same. - Replace
/surveys/{survey_id}/visitorswith/surveys/{survey_id}/responses. - Swap the date parameters: your old
untilvalue becomesstart, your oldsincevalue becomesend. - Replace
page-based pagination with thehas_more/next_urlloop, prepending the base URL tonext_url. - Read list results from the
datafield instead of the top-level array. - Rename fields per the tables above (
response_uuid→uuid,visitor_uuid→respondent.uuid,page_url→url,answer_type→question_type). - Read respondent profile fields and custom attributes from
respondent.attributesinstead of top-level fields — and request them explicitly via theattributes[]query parameter, or they won't be returned at all. - Parse answers per question type instead of the v1
contentconventions. - Expect skipped questions to be absent from
answers— v1 emitted placeholder entries for them, v2 does not. - If you rely on ascending order, sort by
collected_aton your side — v2 always returns latest first.