폼 응답 API
폼 응답 및 제출 데이터를 조회하기 위한 API 레퍼런스
폼 응답 API
폼 응답 API로 폼 제출 데이터를 조회하고 분석할 수 있습니다. 페이지네이션을 지원하며, 전체 응답 목록 조회, 필터를 적용한 응답 검색, 개별 응답 상세 조회가 가능합니다.
엔드포인트
폼 응답 조회 (페이지네이션)
페이지네이션을 지원하는 응답 목록을 반환합니다.
GET /open-api/v1/forms/{formId}/responses파라미터
경로 파라미터
| 파라미터 | 타입 | 필수 | 설명 |
|---|---|---|---|
formId | string | 예 | 고유한 폼 식별자 |
쿼리 파라미터
| 파라미터 | 타입 | 필수 | 기본값 | 설명 |
|---|---|---|---|---|
page | integer | 아니오 | 1 | 페이지 번호 (1부터 시작) |
limit | integer | 아니오 | 20 | 페이지당 응답 수 (최대 100) |
customerKey | string | 아니오 | — | 특정 customerKey를 가진 응답만 조회 |
참고: 이 엔드포인트는 단일
customerKey필터만 지원합니다. 기간·히든 필드·응답 필드 값 등 더 풍부한 필터가 필요하면 아래 응답 검색 엔드포인트를 사용하세요.
요청
요청 예시
# 첫 페이지 조회 (기본값)
curl -X GET "https://api.walla.my/open-api/v1/forms/form_abc/responses" \
-H "X-WALLA-API-KEY: your_api_key_here" \
-H "Content-Type: application/json"
# 사용자 지정 제한으로 특정 페이지 조회
curl -X GET "https://api.walla.my/open-api/v1/forms/form_abc/responses?page=2&limit=50" \
-H "X-WALLA-API-KEY: your_api_key_here" \
-H "Content-Type: application/json"응답
성공 응답 (200 OK)
{
"success": true,
"data": {
"responses": [
{
"responseId": "response_123",
"customerKey": "customer_001",
"submittedAt": "2024-01-15T14:30:00Z",
"question_1": "Very satisfied",
"question_2": 5,
"question_3": ["Feature A", "Feature B"],
"question_4": "Great product!"
},
{
"responseId": "response_456",
"customerKey": "customer_002",
"submittedAt": "2024-01-15T15:45:00Z",
"question_1": "Satisfied",
"question_2": 4,
"question_3": ["Feature A"],
"question_4": "Good experience"
}
],
"pagination": {
"page": 1,
"limit": 20,
"totalCount": 150,
"totalPages": 8
}
}
}응답 필드
| 필드 | 타입 | 설명 |
|---|---|---|
success | boolean | 요청 성공 여부 |
data | object | 응답 데이터 컨테이너 |
data.responses | array | 응답 객체의 배열 |
data.responses[].responseId | string | 고유한 응답 식별자 |
data.responses[].customerKey | string | null | 고객 식별자 (전달 시 제공된 경우) |
data.responses[].submittedAt | string (date-time) | 제출 타임스탬프 |
data.responses[].* | any | 동적 폼 필드 값 |
data.pagination | object | 페이지네이션 메타데이터 |
data.pagination.page | integer | 현재 페이지 번호 |
data.pagination.limit | integer | 페이지당 응답 수 |
data.pagination.totalCount | integer | 필터를 적용한 전체 응답의 정확한 개수 |
data.pagination.totalPages | integer | 전체 페이지 수 |
참고: 응답 객체에는 폼 구조에 따른 동적 필드가 포함됩니다. 각 폼 필드의 ID가 키로, 필드 타입에 따른 값이 동적으로 들어갑니다. 필드 타입별 출력 형식은 필드 출력 스키마 문서를 참고하세요.
오류 응답
-
400 Bad Request: 잘못된 페이지 번호 또는 제한
{ "error": "Invalid page number. Page must be >= 1" } -
403 Forbidden: 인증 실패 또는 권한 부족
-
404 Not Found: 폼을 찾을 수 없음
-
500 Internal Server Error: 서버 오류
응답 검색 (필터)
응답 목록을 풍부한 필터와 함께 조회합니다. 응답 형식은 위의 페이지네이션 목록과 동일하지만, Parquet 내보내기와 동일한 필터 계약(customerKeys, 제출일시 범위, 히든 필드, 응답 필드)을 JSON 본문으로 받습니다. 쿼리 스트링으로 표현하기 어려운 중첩 필터를 다룰 수 있어, 단일 customerKey 필터만으로 부족할 때 사용합니다.
POST /open-api/v1/forms/{formId}/responses/search파라미터
경로 파라미터
| 파라미터 | 타입 | 필수 | 설명 |
|---|---|---|---|
formId | string | 예 | 고유한 폼 식별자 |
요청 본문 (모든 필드 선택)
| 필드 | 타입 | 기본값 | 설명 |
|---|---|---|---|
page | integer | 1 | 페이지 번호 (1부터 시작) |
limit | integer | 20 | 페이지당 응답 수 (최대 100) |
customerKeys | string[] | — | 지정한 customerKey를 가진 응답만 포함. 빈 배열이거나 생략하면 전체 |
submittedFrom | string (date-time) | — | 이 시각 이후(이 시각 포함)에 제출된 응답만. ISO 8601 |
submittedTo | string (date-time) | — | 이 시각 이전(이 시각 포함)에 제출된 응답만. ISO 8601 |
hiddenFields | object | — | 히든 필드 라벨 → 값. 정확히 일치하는 응답만. 등록되지 않은 라벨은 400 |
responseFields | object | — | 응답 필드 ID → 값(문자열) 또는 값 배열 |
참고: 본문이 비어 있으면 폼의 전체 응답을 페이지 단위로 반환합니다. 서로 다른 필터는 AND로 결합됩니다.
responseFields의 값 형태별 해석(필드 타입에 따른 단일/배열 값 의미, 미지원 타입)은 Parquet 내보내기의 응답 필드 필터 의미론과 동일합니다.
요청
요청 예시
# 전체 응답 첫 페이지
curl -X POST "https://api.walla.my/open-api/v1/forms/form_abc/responses/search" \
-H "X-WALLA-API-KEY: your_api_key_here" \
-H "Content-Type: application/json" \
-d '{ "page": 1, "limit": 50 }'
# 기간 + 고객 키 + 히든 필드 + 응답 필드 필터
curl -X POST "https://api.walla.my/open-api/v1/forms/form_abc/responses/search" \
-H "X-WALLA-API-KEY: your_api_key_here" \
-H "Content-Type: application/json" \
-d '{
"page": 1,
"limit": 50,
"submittedFrom": "2024-01-01T00:00:00Z",
"submittedTo": "2024-01-31T23:59:59Z",
"customerKeys": ["customer_001", "customer_002"],
"hiddenFields": { "utm_source": "newsletter" },
"responseFields": {
"field_score": "5",
"field_features": ["Feature A", "Feature B"]
}
}'응답
성공 응답 (200 OK)
응답 형식은 폼 응답 조회와 동일합니다. pagination.totalCount는 필터를 적용한 전체 응답의 정확한 개수입니다(현재 페이지가 마지막인지와 무관).
{
"success": true,
"data": {
"responses": [
{
"responseId": "response_123",
"customerKey": "customer_001",
"submittedAt": "2024-01-15T14:30:00Z",
"field_score": "5",
"field_features": ["Feature A", "Feature B"]
}
],
"pagination": {
"page": 1,
"limit": 50,
"totalCount": 27,
"totalPages": 1
}
}
}참고: 마스킹이 설정된 필드는 키는 유지되고 값이 문자열
REDACTED로 대체되어 반환됩니다. Open API에서는 마스킹 해제를 제공하지 않으며(대시보드는 다운로드 사유 기록 후 마스킹 값 열람 가능), 마스킹 필드를responseFields필터로 사용하는 것은 지원되지 않습니다. 마스킹 필드를 통째로 제외하는 Parquet 내보내기와는 처리 방식이 다릅니다.
오류 응답
- 400 Bad Request: 요청 본문 형식 위반, 등록되지 않은 히든 필드 라벨, 또는 미지원 응답 필드.
detailsType으로 원인을 구분합니다 (schema/unknownHiddenFieldLabel/unsupportedResponseField).{ "error": "Unsupported response field", "detailsType": "unsupportedResponseField", "details": [ { "fieldId": "field_file", "reason": "unsupported_type", "fieldType": "FILE_UPLOAD" } ] } - 401 Unauthorized: API 키 누락 또는 유효하지 않음
- 403 Forbidden: 권한 없음 (다른 팀/워크스페이스의 폼)
- 404 Not Found: 폼을 찾을 수 없음
활동 로그와 PII 보호
응답 검색 요청은 활동 로그에 기록되지만, 필터로 전달한 raw 값(이메일 주소, 전화번호 등)은 로그에 남지 않습니다. 필드 라벨, fieldId, 적용된 값의 개수, 필터 사용 여부(boolean)만 기록됩니다.
특정 응답 조회
특정 응답의 상세 정보를 반환합니다.
GET /open-api/v1/forms/{formId}/responses/{responseId}파라미터
경로 파라미터
| 파라미터 | 타입 | 필수 | 설명 |
|---|---|---|---|
formId | string | 예 | 고유한 폼 식별자 |
responseId | string | 예 | 고유한 응답 식별자 |
요청
요청 예시
curl -X GET "https://api.walla.my/open-api/v1/forms/form_abc/responses/response_123" \
-H "X-WALLA-API-KEY: your_api_key_here" \
-H "Content-Type: application/json"응답
성공 응답 (200 OK)
{
"success": true,
"data": {
"responseId": "response_123",
"formId": "form_abc",
"teamId": "team_456",
"workspaceId": "workspace_789",
"response": {
"question_1": "Very satisfied",
"question_2": 5,
"question_3": ["Feature A", "Feature B"],
"question_4": "Great product!",
"email": "john@example.com",
"name": "John Doe"
},
"submittedAt": "2024-01-15T14:30:00Z",
"customerKey": "customer_001",
"startedAt": "2024-01-15T14:25:00Z"
}
}응답 필드
| 필드 | 타입 | 설명 |
|---|---|---|
success | boolean | 요청 성공 여부 |
data | object | 응답 세부 정보 |
data.responseId | string | 고유한 응답 식별자 |
data.formId | string | 폼 식별자 |
data.teamId | string | 팀 식별자 |
data.workspaceId | string | 워크스페이스 식별자 |
data.response | object | 폼 필드 값 (동적 구조) |
data.submittedAt | string (date-time) | null | 제출 타임스탬프 (제출되지 않은 경우 null) |
data.customerKey | string | null | 고객 식별자 |
data.startedAt | string (date-time) | 응답이 시작된 시간 |
오류 응답
- 400 Bad Request: 잘못된 요청
- 403 Forbidden: 인증 실패 또는 권한 부족
- 404 Not Found: 폼 또는 응답을 찾을 수 없음
- 500 Internal Server Error: 서버 오류
응답 데이터 이해하기
동적 응답 구조
폼 응답에는 폼 구조에 따라 동적 필드가 포함됩니다. 응답의 키는 폼 필드 ID이며, 값은 필드 타입에 따라 문자열, 배열, 객체 등 다양한 형태로 반환됩니다. 각 필드 타입별 출력 형식은 필드 출력 스키마 문서에서 확인할 수 있습니다.
폼 구조 예시:
{
"questions": [
{ "id": "satisfaction", "type": "rating", "label": "How satisfied are you?" },
{ "id": "feedback", "type": "text", "label": "Additional comments" },
{ "id": "features", "type": "multiple_choice", "label": "Which features do you use?" }
]
}응답 예시:
{
"responseId": "response_123",
"satisfaction": 5,
"feedback": "Excellent service!",
"features": ["Feature A", "Feature C"],
"submittedAt": "2024-01-15T14:30:00Z"
}응답 타임라인
응답에는 두 가지 타임스탬프가 기록됩니다.
- startedAt: 수신자가 폼에 처음 접근한 시각
- submittedAt: 수신자가 폼을 제출한 시각
null이면 폼을 열었지만 아직 제출하지 않은 상태입니다
완료 시간 계산:
const timeToComplete = new Date(response.submittedAt) - new Date(response.startedAt);
const minutes = timeToComplete / 1000 / 60;
console.log(`Completed in ${minutes.toFixed(2)} minutes`);코드 예시
JavaScript/TypeScript
interface PaginationParams {
page?: number;
limit?: number;
}
interface ResponseData {
responseId: string;
customerKey: string | null;
submittedAt: string;
[key: string]: any; // Dynamic form fields
}
interface PaginatedResponse {
responses: ResponseData[];
pagination: {
page: number;
limit: number;
totalCount: number;
totalPages: number;
};
}
class WallaResponsesAPI {
constructor(private apiKey: string) {}
async getResponses(
formId: string,
params: PaginationParams = {}
): Promise<PaginatedResponse> {
const { page = 1, limit = 20 } = params;
const url = new URL(
`https://api.walla.my/open-api/v1/forms/${formId}/responses`
);
url.searchParams.set('page', page.toString());
url.searchParams.set('limit', limit.toString());
const response = await fetch(url.toString(), {
method: 'GET',
headers: {
'X-WALLA-API-KEY': this.apiKey,
'Content-Type': 'application/json'
}
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
return data.data;
}
async getResponseById(formId: string, responseId: string) {
const response = await fetch(
`https://api.walla.my/open-api/v1/forms/${formId}/responses/${responseId}`,
{
method: 'GET',
headers: {
'X-WALLA-API-KEY': this.apiKey,
'Content-Type': 'application/json'
}
}
);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
return data.data;
}
async getAllResponses(formId: string): Promise<ResponseData[]> {
const allResponses: ResponseData[] = [];
let page = 1;
let hasMore = true;
while (hasMore) {
const data = await this.getResponses(formId, { page, limit: 100 });
allResponses.push(...data.responses);
hasMore = page < data.pagination.totalPages;
page++;
}
return allResponses;
}
async exportToCSV(formId: string): Promise<string> {
const responses = await this.getAllResponses(formId);
if (responses.length === 0) {
return '';
}
// Get all unique keys from responses
const keys = Array.from(
new Set(responses.flatMap(r => Object.keys(r)))
);
// Create CSV header
const csv = [
keys.join(','),
...responses.map(response =>
keys.map(key => {
const value = response[key];
if (Array.isArray(value)) {
return `"${value.join(', ')}"`;
}
return typeof value === 'string' && value.includes(',')
? `"${value}"`
: value;
}).join(',')
)
].join('\n');
return csv;
}
}
// Usage
const api = new WallaResponsesAPI(process.env.WALLA_API_KEY!);
// Get paginated responses
const page1 = await api.getResponses('form_abc', { page: 1, limit: 50 });
console.log(`Total responses: ${page1.pagination.totalCount}`);
// Get specific response
const response = await api.getResponseById('form_abc', 'response_123');
console.log(response);
// Export all responses to CSV
const csv = await api.exportToCSV('form_abc');Python
import requests
import csv
import io
from typing import List, Dict, Any, Optional
class WallaResponsesAPI:
def __init__(self, api_key: str):
self.api_key = api_key
self.base_url = "https://api.walla.my/open-api/v1"
self.headers = {
"X-WALLA-API-KEY": api_key,
"Content-Type": "application/json"
}
def get_responses(
self,
form_id: str,
page: int = 1,
limit: int = 20
) -> Dict[str, Any]:
"""Get paginated form responses"""
url = f"{self.base_url}/forms/{form_id}/responses"
params = {"page": page, "limit": limit}
response = requests.get(url, headers=self.headers, params=params)
response.raise_for_status()
return response.json()["data"]
def get_response_by_id(
self,
form_id: str,
response_id: str
) -> Dict[str, Any]:
"""Get specific response details"""
url = f"{self.base_url}/forms/{form_id}/responses/{response_id}"
response = requests.get(url, headers=self.headers)
response.raise_for_status()
return response.json()["data"]
def get_all_responses(self, form_id: str) -> List[Dict[str, Any]]:
"""Get all responses by paginating through all pages"""
all_responses = []
page = 1
while True:
data = self.get_responses(form_id, page=page, limit=100)
all_responses.extend(data["responses"])
if page >= data["pagination"]["totalPages"]:
break
page += 1
return all_responses
def export_to_csv(self, form_id: str, filename: str) -> None:
"""Export all responses to CSV file"""
responses = self.get_all_responses(form_id)
if not responses:
print("No responses to export")
return
# Get all unique keys
keys = set()
for response in responses:
keys.update(response.keys())
# Write CSV
with open(filename, 'w', newline='', encoding='utf-8') as f:
writer = csv.DictWriter(f, fieldnames=sorted(keys))
writer.writeheader()
writer.writerows(responses)
print(f"Exported {len(responses)} responses to {filename}")
def get_response_statistics(self, form_id: str) -> Dict[str, Any]:
"""Calculate basic statistics for form responses"""
responses = self.get_all_responses(form_id)
if not responses:
return {"total": 0}
# Calculate completion time statistics
completion_times = []
for resp in responses:
detail = self.get_response_by_id(form_id, resp["responseId"])
if detail["submittedAt"] and detail["startedAt"]:
from datetime import datetime
started = datetime.fromisoformat(
detail["startedAt"].replace('Z', '+00:00')
)
submitted = datetime.fromisoformat(
detail["submittedAt"].replace('Z', '+00:00')
)
completion_times.append(
(submitted - started).total_seconds() / 60
)
stats = {
"total": len(responses),
"with_customer_key": sum(
1 for r in responses if r.get("customerKey")
),
"average_completion_time_minutes": (
sum(completion_times) / len(completion_times)
if completion_times else None
)
}
return stats
# Usage
api = WallaResponsesAPI(os.getenv("WALLA_API_KEY"))
# Get responses
data = api.get_responses("form_abc", page=1, limit=50)
print(f"Total responses: {data['pagination']['totalCount']}")
# Get all responses
all_responses = api.get_all_responses("form_abc")
# Export to CSV
api.export_to_csv("form_abc", "responses.csv")
# Get statistics
stats = api.get_response_statistics("form_abc")
print(f"Statistics: {stats}")사용 사례
데이터 내보내기 및 분석
// 응답 내보내기 및 분석
const responses = await api.getAllResponses('form_abc');
// 평균 평점 계산
const ratings = responses
.map(r => r.rating)
.filter(r => typeof r === 'number');
const avgRating = ratings.reduce((a, b) => a + b, 0) / ratings.length;
// 고객 세그먼트별 그룹화
const bySegment = responses.reduce((acc, response) => {
const segment = response.segment || 'unknown';
acc[segment] = acc[segment] || [];
acc[segment].push(response);
return acc;
}, {});실시간 모니터링
// 새로운 응답 폴링
let lastCheck = new Date();
setInterval(async () => {
const data = await api.getResponses('form_abc', { page: 1, limit: 10 });
const newResponses = data.responses.filter(r =>
new Date(r.submittedAt) > lastCheck
);
if (newResponses.length > 0) {
console.log(`${newResponses.length} new responses!`);
// 새로운 응답 처리
}
lastCheck = new Date();
}, 60000); // 매분마다 확인분석 도구와의 통합
# 분석 플랫폼으로 응답 전송
import analytics
responses = api.get_all_responses("form_abc")
for response in responses:
analytics.track(
user_id=response.get("customerKey"),
event="Survey Completed",
properties={
"form_id": "form_abc",
"rating": response.get("rating"),
"submitted_at": response["submittedAt"]
}
)모범 사례
페이지네이션
- 대량 내보내기 시
limit=100으로 API 호출 횟수를 줄이세요 - 테스트할 때는 작은 limit 값으로 시작하세요
totalPages를 확인해서 페이지네이션 종료 시점을 판단하세요
성능
- 필요한 경우 응답을 캐시하세요
- 대량 데이터는 한 번에 가져오지 말고 페이지네이션을 활용하세요
- API 키당 시간당 6,000건의 속도 제한이 적용됩니다
데이터 처리
submittedAt이null인 경우 (미완료 응답)를 처리하세요- 동적 필드 이름을 고려한 처리 로직을 작성하세요
- 필드 타입이 다양하므로 처리 전에 데이터 타입을 확인하세요