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

v1v2
GET /surveysGET /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}/visitorsGET /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 it respondent_uuid (or respondent.uuid inside 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_data and data:

Exporting responses: /visitors/responses

The most common v1 call:

Its v2 equivalent:

Date range: since/untilstart/end

v1v2
Older boundsinceend
Newer bounduntil (only honored together with since)start
Inclusivityboth inclusiveboth inclusive
FormatISO 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: pagenext_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:

  1. Make the first request, optionally with items_per_page (1–100; omitted = default of 20).
  2. Read pagination_data in the response. While has_more is true, request next_url — it is always a path relative to the base URL https://data-api.survicate.com/v2, so prepend the base URL and call it as-is.
  3. Stop when has_more is false.

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 fieldv2 equivalent
response_uuiduuid
visitor_uuidrespondent.uuid
page_urlurl
languagelanguage (unchanged)
answersanswers (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_dateNot 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).


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}/attributes returns what v1 kept in the profile fields and custom_attributes — as { "id": ..., "name": ..., "value": ... } entries. tags and comment are not returned in v2.
  • GET /respondents/{respondent_uuid}/responses returns what v1 kept in reports. Each data entry pairs a survey object with a response object. 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_namesurvey.name (plus survey.id and survey.type, which v1 did not return)
platform (desktop or mobile, detected from the user agent)response.device_type
dateresponse.collected_at
response_uuidresponse.uuid
answersresponse.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:

v1v2
survey_point.idquestion_id
survey_point.answer_typequestion_type — renamed, but the values are the same strings as in v1
survey_point.type, survey_point.pretty_typeRemoved
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
contentanswer / answers / fields, structured per question type (table below)

Answer payload by question type

question_typev2 payload
single, dropdown_listanswer: { id, content, comment }
multipleanswers: array of { id, content, comment }
rating, smiley_scale, numerical_scale, csatanswer: { id, content, rating }rating is the numeric value
text, dateanswer: the entered text as a string
npsanswer: { content, tag } (see below)
matrixanswers: array of { id, content, score } — one entry per matrix row, score is the selected column label
rankinganswers: array of { id, content, rank }rank is the position given by the respondent
formfields: array of { type, content } — one entry per contact-form field
empty, button_close, button_link, button_next, redirect_timeout, social_ctaaction_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:

tagScore
detractor0–6
passive7–8
promoter9–10

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

Limitv2
Concurrent requests5
Requests per minute per workspace1,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

  1. Switch the base URL to https://data-api.survicate.com/v2 — the API key and Authorization header stay the same.
  2. Replace /surveys/{survey_id}/visitors with /surveys/{survey_id}/responses.
  3. Swap the date parameters: your old until value becomes start, your old since value becomes end.
  4. Replace page-based pagination with the has_more / next_url loop, prepending the base URL to next_url.
  5. Read list results from the data field instead of the top-level array.
  6. Rename fields per the tables above (response_uuiduuid, visitor_uuidrespondent.uuid, page_urlurl, answer_typequestion_type).
  7. Read respondent profile fields and custom attributes from respondent.attributes instead of top-level fields — and request them explicitly via the attributes[] query parameter, or they won't be returned at all.
  8. Parse answers per question type instead of the v1 content conventions.
  9. Expect skipped questions to be absent from answers — v1 emitted placeholder entries for them, v2 does not.
  10. If you rely on ascending order, sort by collected_at on your side — v2 always returns latest first.