폼 응답 API
폼 응답 및 제출 데이터를 조회하기 위한 API 레퍼런스
폼 응답 API
폼 응답 API로 폼 제출 데이터를 조회하고 분석할 수 있습니다. 페이지네이션을 지원하며, 전체 응답 목록 조회와 개별 응답 상세 조회가 가능합니다.
엔드포인트
폼 응답 조회 (페이지네이션)
페이지네이션을 지원하는 응답 목록을 반환합니다.
GET /open-api/v1/forms/{formId}/responses파라미터
경로 파라미터
| 파라미터 | 타입 | 필수 | 설명 |
|---|---|---|---|
formId | string | 예 | 고유한 폼 식별자 |
쿼리 파라미터
| 파라미터 | 타입 | 필수 | 기본값 | 설명 |
|---|---|---|---|---|
page | integer | 아니오 | 1 | 페이지 번호 (1부터 시작) |
limit | integer | 아니오 | 20 | 페이지당 응답 수 (최대 100) |
요청
요청 예시
# 첫 페이지 조회 (기본값)
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: 서버 오류
특정 응답 조회
특정 응답의 상세 정보를 반환합니다.
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인 경우 (미완료 응답)를 처리하세요- 동적 필드 이름을 고려한 처리 로직을 작성하세요
- 필드 타입이 다양하므로 처리 전에 데이터 타입을 확인하세요