Surveys API
Read access to Creative Survey / Ask One answer data held in Databricks. See API conventions for the shared rules.
Base path: /api/v1/surveys
| Method | Path | Summary |
|---|---|---|
| GET | / |
List surveys |
| GET | /{surveyId} |
Get survey detail |
| GET | /{surveyId}/questions |
List survey questions |
| GET | /{surveyId}/preview |
Preview survey data |
| GET | /{surveyId}/panels |
List respondents |
| GET | /{surveyId}/panels/{panelId} |
Get respondent detail |
| GET | /{surveyId}/respondents |
List respondents (legacy) |
Every endpoint requires Authorization: Bearer <access_token>.
No role restriction
This is the one router that does not apply requireRole(). Now that it is read-only the exposure is limited, but a viewer can still read the answers of every survey.
Pagination is offset / limit
List endpoints take offset / limit rather than page, and return { items, total, limit, offset } inside data. The shared pagination field is not present.
How the Data Flows
graph LR
CS[(Creative Survey)]
S3[(S3<br/>answers.csv.gz / panels.csv.gz)]
Pipe[cs_data_pipeline_s3_delivery<br/>daily 05:00:45 JST]
DM[(cs.cs_dm.dm_answers_<survey_id><br/>answers joined with panels)]
DWH[(cs.cs_dwh)]
Meta[databricks-meta-sync]
Cache[(databricks_surveys)]
API[Surveys API]
CS --> S3 --> Pipe --> DM
Pipe --> DWH
DWH --> Meta --> Cache
DM --> API
Cache --> API
A pipeline on the Databricks side ingests the S3 files and materialises one table per survey, dm_answers_<survey_id>, with answers already joined onto panels. The API reads that directly.
| Source | Used by |
|---|---|
databricks_surveys (Postgres) |
List and detail. The aggregation takes 15 seconds, so it is cached daily |
cs.cs_dm.dm_answers_<survey_id> |
Questions, preview, respondent endpoints |
Roughly one second of fixed overhead per query
A round trip through the Databricks SQL Statement Execution API costs about a second. The per-survey tables are therefore not touched to render a list; metadata comes from the Postgres cache instead.
Unknown survey IDs return 404
Per-survey endpoints first check that a row exists in databricks_surveys, so an unknown ID does not surface as a Databricks "table does not exist" error (502).
List Surveys
Lists from databricks_surveys (the metadata cache).
URI
Query Parameters
| Field | Type | Required | Default | Rules | Description |
|---|---|---|---|---|---|
search |
string | - | - | - | Partial match on survey name |
sort |
string | - | -latestResponseAt |
- | Prefix with - for descending |
limit |
integer | - | 20 |
1–100 | Items per page |
offset |
integer | - | 0 |
≥ 0 | Starting position |
Response (200 OK)
{
"success": true,
"status": "success",
"statusCode": 200,
"data": {
"surveys": [
{
"surveyId": "234567",
"name": "Spring 2026 User Survey",
"lastModified": "2026-01-31T05:00:00Z",
"fileCount": 5,
"respondentCount": 1234
}
],
"total": 12,
"limit": 20,
"offset": 0
},
"path": "/api/v1/surveys",
"method": "GET"
}
Errors
| Description | Status code | Status name |
|---|---|---|
| Malformed query | 400 | Bad Request |
| Token missing or invalid | 401 | Unauthorized |
Databricks not configured (DATABRICKS_NOT_CONFIGURED) |
503 | Service Unavailable |
Get Survey Detail
URI
Path Parameters
| Field | Rules |
|---|---|
surveyId |
Required, at least 1 character |
Response (200 OK)
{
"success": true,
"status": "success",
"statusCode": 200,
"data": {
"surveyId": "234567",
"surveyName": "Spring 2026 User Survey",
"collectorName": "Main collector",
"statistics": {
"totalRespondents": 1234,
"completedRespondents": 1100,
"questionCount": 25,
"earliestResponseAt": "2026-01-15T10:00:00+09:00",
"latestResponseAt": "2026-01-30T18:30:00+09:00"
},
"syncedAt": "2026-01-31T05:00:00+09:00"
},
"path": "/api/v1/surveys/234567",
"method": "GET"
}
The statistics are a snapshot from sync time.
Errors
| Description | Status code | Status name |
|---|---|---|
| Token missing or invalid | 401 | Unauthorized |
| Survey does not exist | 404 | Not Found |
List Survey Questions
Used to build segment definitions and import mapping choices.
URI
Query Parameters
| Field | Type | Required | Default | Rules | Description |
|---|---|---|---|---|---|
search |
string | - | - | - | Partial match on question text |
limit |
integer | - | 50 |
1–200 | Items per page |
offset |
integer | - | 0 |
≥ 0 | Starting position |
includeOptions |
boolean | - | false |
- | Whether to include options |
Response (200 OK)
{
"success": true,
"status": "success",
"statusCode": 200,
"data": {
"surveyId": "234567",
"questions": [
{
"questionId": "q_1",
"questionText": "How satisfied are you with our service?",
"questionType": "single_select",
"options": ["Very satisfied", "Satisfied", "Neutral", "Dissatisfied"]
}
],
"total": 25,
"limit": 50,
"offset": 0
},
"path": "/api/v1/surveys/234567/questions",
"method": "GET"
}
questionType |
Meaning |
|---|---|
single_select |
Single choice |
multi_select |
Multiple choice |
text |
Free text |
number |
Numeric |
date |
Date |
other |
Other |
options is present only when includeOptions=true.
How questions are identified
The mart has no question ID — questions are identified by the question text itself. Import mappings also specify question text in sourceField. Matrix questions use the composite key question text|||answer item.
Errors
| Description | Status code | Status name |
|---|---|---|
| Malformed query | 400 | Bad Request |
| Token missing or invalid | 401 | Unauthorized |
| Survey does not exist | 404 | Not Found |
Preview Survey Data
Returns the question list and a sample of respondents together.
URI
Response (200 OK)
{
"success": true,
"status": "success",
"statusCode": 200,
"data": {
"surveyId": "234567",
"questions": [
{ "questionId": "q_1", "questionText": "How satisfied are you with our service?", "questionType": "single_select" }
],
"totalRespondents": 1234,
"sampleRespondents": [
{
"respondentId": "119466675",
"answers": { "q_1": "Very satisfied", "q_2": "Easy to use" },
"completedAt": "2026-01-28T14:30:00Z",
"metadata": {}
}
]
},
"path": "/api/v1/surveys/234567/preview",
"method": "GET"
}
Errors
| Description | Status code | Status name |
|---|---|---|
| Token missing or invalid | 401 | Unauthorized |
| Survey does not exist | 404 | Not Found |
List Respondents
Lists respondents by deduplicating the answer rows of the per-survey table.
URI
Query Parameters
| Field | Type | Required | Default | Rules | Description |
|---|---|---|---|---|---|
isCompleted |
boolean | - | - | - | Filter by completion |
search |
string | - | - | - | Search by panel ID and similar |
sort |
string | - | -completedAt |
- | Prefix with - for descending |
limit |
integer | - | 20 |
1–100 | Items per page |
offset |
integer | - | 0 |
≥ 0 | Starting position |
Response (200 OK)
{
"success": true,
"status": "success",
"statusCode": 200,
"data": {
"panels": [
{
"panelId": "119466675",
"isCompleted": true,
"customKey": "user_abc123",
"completedAt": "2026-01-28T14:30:00+09:00",
"deviceInfo": {
"platform": "Windows",
"browser": "Chrome",
"browserVersion": "120.0",
"os": "Windows 11",
"resolution": "1920x1080",
"isMobile": false,
"ipAddress": "192.168.1.xxx"
},
"createdAt": "2026-01-28T14:00:00+09:00"
}
],
"total": 1100,
"limit": 20,
"offset": 0
},
"path": "/api/v1/surveys/234567/panels",
"method": "GET"
}
Errors
| Description | Status code | Status name |
|---|---|---|
| Malformed query | 400 | Bad Request |
| Token missing or invalid | 401 | Unauthorized |
| Survey does not exist | 404 | Not Found |
Get Respondent Detail
Returns one respondent's attributes and all of their answers.
URI
Path Parameters
| Field | Rules |
|---|---|
surveyId |
Required, at least 1 character |
panelId |
Required, at least 1 character (respondent ID) |
Query Parameters
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
includeUnanswered |
boolean | - | false |
Whether to include unanswered questions |
Response (200 OK)
{
"success": true,
"status": "success",
"statusCode": 200,
"data": {
"panel": {
"panelId": "119466675",
"isCompleted": true,
"customKey": "user_abc123",
"urlParameter": "source=email&campaign=spring2026",
"completedAt": "2026-01-28T14:30:00+09:00",
"deviceInfo": { "platform": "Windows", "browser": "Chrome", "browserVersion": "120.0", "os": "Windows 11", "resolution": "1920x1080", "isMobile": false, "ipAddress": "192.168.1.xxx" },
"createdAt": "2026-01-28T14:00:00+09:00",
"updatedAt": "2026-01-28T14:30:00+09:00"
},
"answers": [
{
"questionSentence": "Please enter your name",
"answerType": "Free text",
"answerItemSentence": null,
"subItemSentence": null,
"value": "Taro Yamada",
"isAnswered": true
}
],
"answersSummary": {
"totalQuestions": 25,
"answeredQuestions": 23,
"unansweredQuestions": 2
}
},
"path": "/api/v1/surveys/234567/panels/119466675",
"method": "GET"
}
answers mirrors the vertical layout of the mart (one row per question × option). A single question spans several rows, so the UI groups them by question.
Errors
| Description | Status code | Status name |
|---|---|---|
| Token missing or invalid | 401 | Unauthorized |
| Survey or respondent does not exist | 404 | Not Found |
List Respondents (Legacy)
Deprecated
Superseded by the /panels endpoints. Do not use in new code.
URI
Query Parameters
| Field | Type | Required | Rules |
|---|---|---|---|
limit |
integer | - | 1–1000 |
offset |
integer | - | ≥ 0 |
onlyCompleted |
boolean | - | - |
Response (200 OK)
{
"success": true,
"status": "success",
"statusCode": 200,
"data": {
"surveyId": "234567",
"respondents": [
{ "respondentId": "119466675", "answers": { "q_1": "Very satisfied" }, "completedAt": "2026-01-28T14:30:00Z" }
],
"total": 1100,
"limit": 100,
"offset": 0
},
"path": "/api/v1/surveys/234567/respondents",
"method": "GET"
}
Unlike /panels/{panelId}, answers come back pivoted into an answers object.