폼 응답 API

폼 응답 및 제출 데이터를 조회하기 위한 API 레퍼런스

폼 응답 API

폼 응답 API로 폼 제출 데이터를 조회하고 분석할 수 있습니다. 페이지네이션을 지원하며, 전체 응답 목록 조회와 개별 응답 상세 조회가 가능합니다.

엔드포인트

폼 응답 조회 (페이지네이션)

페이지네이션을 지원하는 응답 목록을 반환합니다.

GET /open-api/v1/forms/{formId}/responses

파라미터

경로 파라미터

파라미터타입필수설명
formIdstring고유한 폼 식별자

쿼리 파라미터

파라미터타입필수기본값설명
pageinteger아니오1페이지 번호 (1부터 시작)
limitinteger아니오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
    }
  }
}

응답 필드

필드타입설명
successboolean요청 성공 여부
dataobject응답 데이터 컨테이너
data.responsesarray응답 객체의 배열
data.responses[].responseIdstring고유한 응답 식별자
data.responses[].customerKeystring | null고객 식별자 (전달 시 제공된 경우)
data.responses[].submittedAtstring (date-time)제출 타임스탬프
data.responses[].*any동적 폼 필드 값
data.paginationobject페이지네이션 메타데이터
data.pagination.pageinteger현재 페이지 번호
data.pagination.limitinteger페이지당 응답 수
data.pagination.totalCountinteger전체 응답 수
data.pagination.totalPagesinteger전체 페이지 수

참고: 응답 객체에는 폼 구조에 따른 동적 필드가 포함됩니다. 각 폼 필드의 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}

파라미터

경로 파라미터

파라미터타입필수설명
formIdstring고유한 폼 식별자
responseIdstring고유한 응답 식별자

요청

요청 예시

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"
  }
}

응답 필드

필드타입설명
successboolean요청 성공 여부
dataobject응답 세부 정보
data.responseIdstring고유한 응답 식별자
data.formIdstring폼 식별자
data.teamIdstring팀 식별자
data.workspaceIdstring워크스페이스 식별자
data.responseobject폼 필드 값 (동적 구조)
data.submittedAtstring (date-time) | null제출 타임스탬프 (제출되지 않은 경우 null)
data.customerKeystring | null고객 식별자
data.startedAtstring (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건의 속도 제한이 적용됩니다

데이터 처리

  • submittedAtnull인 경우 (미완료 응답)를 처리하세요
  • 동적 필드 이름을 고려한 처리 로직을 작성하세요
  • 필드 타입이 다양하므로 처리 전에 데이터 타입을 확인하세요

다음 단계

목차