폼 전송 API

폼 전송 및 전달 상태 추적을 위한 API 레퍼런스

폼 전송 API

폼 전송 API로 이메일이나 SMS를 통해 폼을 전송하고, 전송 상태와 응답 여부를 추적할 수 있습니다.

이 API는 특정 수신자에게 폼을 직접 보내고 싶을 때 사용합니다. 수신자별로 고유한 customerKey를 지정해 전송하면, 이후 전송 상태(전송됨/열람/응답 완료)를 추적할 수 있습니다. 수신자 등록과 전송은 한 번에 처리할 수도 있고, 등록과 전송을 분리해 필요한 시점에 전송할 수도 있습니다. 수신자의 응답 데이터는 폼 응답 API로 조회하세요.

주의사항

폼 전송 API는 Walla와 사전에 협의된 경우에만 사용할 수 있습니다. 이 기능을 활성화하려면 Walla 지원팀에 문의하세요.

전송 방식 선택

수신자 등록과 전송을 어떻게 조합할지에 따라 사용하는 엔드포인트가 달라집니다.

상황사용 엔드포인트
수신자를 등록하면서 바로 이메일이나 문자로 보냅니다POST /delivery
연락처 없이 링크만 발급받아 자체 채널(앱 푸시, 카카오톡 등)로 배포합니다POST /delivery/recipients
미리 등록해 두고 나중에 Walla를 통해 전송하거나 재전송합니다POST /delivery/recipients → POST /delivery/send

참고: 세 방식 모두 동일한 상태 모델로 추적됩니다. 전송 없이 링크만 배포한 수신자도 폼을 열람하면 OPENED, 제출하면 RESPONDED 로 전이합니다.

엔드포인트

수신자에게 폼 전송

하나 이상의 수신자에게 게시된 폼을 전송합니다. 수신자를 등록하는 동시에 즉시 전송합니다. 등록만 하려면 POST /delivery/recipients를 사용하세요.

POST /open-api/v1/forms/{formId}/delivery

참고: 게시된 폼만 전송할 수 있습니다. 게시되지 않은 폼은 404 오류가 반환됩니다.

매개변수

경로 매개변수

매개변수타입필수 여부설명
formIdstring예고유한 폼 식별자

요청 본문

필드타입필수 여부설명
recipientsarray예수신자 객체 배열 (최소 1개)
recipients[].customerKeystring예고유한 고객 식별자
recipients[].namestring | null아니오수신자 이름
recipients[].emailstring아니오수신자 이메일 주소
recipients[].phoneNumberstring아니오수신자 전화번호
recipients[].hiddenFieldValuesobject아니오수신자별 히든 필드 초기값. 키는 폼에 선언된 히든 필드 라벨입니다 (히든 필드 조회)
subjectstring아니오이메일 제목 (기본값: "설문이 도착했습니다.")
messagestring아니오전송 메시지 (기본값: "설문 링크를 확인하세요.")
deliveryChannelstring아니오전송 채널 (기본값: emailFirst)

참고: 각 수신자에는 이메일 또는 전화번호 중 최소 하나가 필요합니다.

전송 채널

값동작
emailFirst (기본값)이메일이 있으면 이메일로만 전송하고, 없으면 전화번호로 전송합니다. 기존 동작과 동일합니다
phoneFirst전화번호를 쓸 수 있으면 문자로만 전송하고, 아니면 이메일로 전송합니다
both사용 가능한 모든 채널로 전송합니다. 한 채널만 성공해도 해당 수신자는 성공으로 집계됩니다

참고: 채널 선택은 요청에 포함된 모든 수신자에게 일괄 적용되며, 폴백 기준은 채널 가용성입니다. 전송이 실패해도 다른 채널로 재시도하지 않습니다.

참고: 문자 채널은 문자 전송 기능이 활성화된 환경에서만 사용할 수 있습니다. 비활성 환경에서 전화번호만 가진 수신자는 failedDeliveries 로 반환됩니다.

요청

요청 예시

curl -X POST "https://api.walla.my/open-api/v1/forms/form_abc/delivery" \
  -H "X-WALLA-API-KEY: your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "recipients": [
      {
        "customerKey": "customer_001",
        "name": "홍길동",
        "email": "john@example.com",
        "phoneNumber": "+821012345678"
      },
      {
        "customerKey": "customer_002",
        "email": "jane@example.com"
      }
    ],
    "subject": "We value your feedback!",
    "message": "Please take a moment to complete our survey.",
    "deliveryChannel": "emailFirst"
  }'

응답

성공 응답 (200 OK)

{
  "success": true,
  "data": {
    "totalRecipients": 2,
    "successfulDeliveries": [
      {
        "customerKey": "customer_001",
        "name": "홍길동",
        "email": "john@example.com",
        "phoneNumber": "+821012345678",
        "aliasId": "8c1a4e2b-7d3f-4a9b-9c2e-1a2b3c4d5e6f",
        "formUrl": "https://walla.my/a/8c1a4e2b-7d3f-4a9b-9c2e-1a2b3c4d5e6f"
      },
      {
        "customerKey": "customer_002",
        "name": null,
        "email": "jane@example.com",
        "phoneNumber": null,
        "aliasId": "2f5d6e1c-9a4b-4c3d-8e7f-0a1b2c3d4e5f",
        "formUrl": "https://walla.my/a/2f5d6e1c-9a4b-4c3d-8e7f-0a1b2c3d4e5f"
      }
    ],
    "failedDeliveries": []
  }
}

alias id 보안 주의

aliasId 는 인증 없이 폼에 접근할 수 있는 토큰이므로, 외부 시스템 등록·로그·분석 도구 등 비공개 채널 외부로 노출되지 않도록 주의합니다. formUrl 도 동일한 토큰을 포함하므로 수신자 본인에게만 전달합니다.

부분 성공 응답 (200 OK)

일부 수신자에게 전송이 실패한 경우:

{
  "success": true,
  "data": {
    "totalRecipients": 3,
    "successfulDeliveries": [
      {
        "customerKey": "customer_001",
        "name": "홍길동",
        "email": "john@example.com",
        "phoneNumber": "+821012345678",
        "aliasId": "8c1a4e2b-7d3f-4a9b-9c2e-1a2b3c4d5e6f",
        "formUrl": "https://walla.my/a/8c1a4e2b-7d3f-4a9b-9c2e-1a2b3c4d5e6f"
      },
      {
        "customerKey": "customer_002",
        "name": null,
        "email": "jane@example.com",
        "phoneNumber": null,
        "aliasId": "2f5d6e1c-9a4b-4c3d-8e7f-0a1b2c3d4e5f",
        "formUrl": "https://walla.my/a/2f5d6e1c-9a4b-4c3d-8e7f-0a1b2c3d4e5f"
      }
    ],
    "failedDeliveries": [
      {
        "customerKey": "customer_003",
        "name": null,
        "email": "invalid-email",
        "phoneNumber": null,
        "error": "Invalid email format"
      }
    ]
  }
}

응답 필드

필드타입설명
successboolean요청 성공 여부
data.totalRecipientsnumber전체 수신자 수
data.successfulDeliveriesarray성공적으로 전송된 수신자 객체 배열
data.successfulDeliveries[].customerKeystring요청에 사용한 고객 식별자
data.successfulDeliveries[].namestring | null요청에 전달한 수신자 이름 (전달하지 않은 경우 null)
data.successfulDeliveries[].emailstring | null수신자 이메일 (요청에 포함되지 않은 경우 null)
data.successfulDeliveries[].phoneNumberstring | null수신자 전화번호 (요청에 포함되지 않은 경우 null)
data.successfulDeliveries[].aliasIdstring (UUID)발급된 alias 식별자. 폼 접근 토큰 역할을 합니다
data.successfulDeliveries[].formUrlstringalias 단축 URL (https://walla.my/a/<aliasId> 형태)
data.failedDeliveriesarray오류 세부 정보가 포함된 실패한 전송 목록
data.failedDeliveries[].customerKeystring요청에 사용한 고객 식별자
data.failedDeliveries[].namestring | null저장된 수신자 이름. 저장된 값 기준이므로 재전송 시 요청에 전달한 값과 다를 수 있습니다
data.failedDeliveries[].emailstring | null수신자 이메일
data.failedDeliveries[].phoneNumberstring | null수신자 전화번호
data.failedDeliveries[].errorstring전송 실패 사유

참고: 동일한 customerKey 로 다시 전송해도 저장된 이름은 갱신되지 않습니다. 저장된 이름을 변경하려면 PATCH /delivery/recipients 를 사용하세요.

오류 응답

  • 400 Bad Request: 잘못된 요청 데이터

    {
      "error": "Invalid request: recipients array is required"
    }

    400 응답 본문에는 오류 메시지 error 와 함께 오류 종류를 나타내는 detailsType, 항목별 세부 정보 배열 details 가 포함되며, 추가 설명이 있으면 description 이 함께 반환됩니다.

  • 401 Unauthorized: API 키 누락 또는 유효하지 않음

  • 403 Forbidden: 해당 리소스에 대한 권한 없음

    {
      "error": "Authentication failed or insufficient permissions"
    }
  • 404 Not Found: 폼을 찾을 수 없거나 게시되지 않음

    {
      "error": "Form not found or not published"
    }
  • 500 Internal Server Error: 서버 오류


수신자 등록

수신자를 등록하고 수신자별 응답 링크를 발급합니다. 폼을 전송하지는 않습니다.

POST /open-api/v1/forms/{formId}/delivery/recipients

참고: 게시된 폼에만 수신자를 등록할 수 있습니다. 게시되지 않은 폼은 404 오류가 반환됩니다.

매개변수

경로 매개변수

매개변수타입필수 여부설명
formIdstring예고유한 폼 식별자

요청 본문

필드타입필수 여부설명
recipientsarray예수신자 객체 배열 (최소 1개)
recipients[].customerKeystring예고유한 고객 식별자
recipients[].namestring아니오수신자 이름
recipients[].emailstring아니오수신자 이메일 주소
recipients[].phoneNumberstring아니오수신자 전화번호
recipients[].hiddenFieldValuesobject아니오수신자별 히든 필드 초기값. 키는 폼에 선언된 히든 필드 라벨입니다 (히든 필드 조회)

참고: 연락처 없이 customerKey 만으로도 등록할 수 있습니다. 이 경우 발급된 formUrl 을 자체 채널로 배포합니다.

참고: 요청 본문은 정의되지 않은 필드를 허용하지 않습니다. 알 수 없는 필드가 있으면 400 오류가 반환됩니다.

요청

요청 예시

curl -X POST "https://api.walla.my/open-api/v1/forms/form_abc/delivery/recipients" \
  -H "X-WALLA-API-KEY: your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "recipients": [
      {
        "customerKey": "customer_003",
        "name": "김하늘",
        "email": "sky@example.com",
        "hiddenFieldValues": {
          "utm_source": "newsletter"
        }
      },
      {
        "customerKey": "customer_004"
      }
    ]
  }'

응답

성공 응답 (200 OK)

{
  "success": true,
  "data": {
    "totalRecipients": 2,
    "recipients": [
      {
        "customerKey": "customer_003",
        "created": true,
        "aliasId": "3a7b9c0d-1e2f-4a5b-8c9d-0e1f2a3b4c5d",
        "formUrl": "https://walla.my/a/3a7b9c0d-1e2f-4a5b-8c9d-0e1f2a3b4c5d"
      },
      {
        "customerKey": "customer_004",
        "created": false,
        "aliasId": "6b8c0d1e-2f3a-4b5c-9d0e-1f2a3b4c5d6e",
        "formUrl": "https://walla.my/a/6b8c0d1e-2f3a-4b5c-9d0e-1f2a3b4c5d6e"
      }
    ]
  }
}

alias id 보안 주의

aliasId 는 인증 없이 폼에 접근할 수 있는 토큰이므로, 외부 시스템 등록·로그·분석 도구 등 비공개 채널 외부로 노출되지 않도록 주의합니다. 자체 채널로 링크를 배포할 때도 formUrl 은 수신자 본인에게만 전달합니다.

응답 필드

필드타입설명
successboolean요청 성공 여부
data.totalRecipientsnumber요청에 포함된 수신자 수
data.recipientsarray등록 결과 객체 배열. 요청 순서를 유지합니다
data.recipients[].customerKeystring요청에 사용한 고객 식별자
data.recipients[].createdboolean이번 요청으로 새로 등록되면 true, 이미 등록되어 있으면 false
data.recipients[].aliasIdstring (UUID) | null발급된 alias 식별자. 폼 접근 토큰 역할을 합니다
data.recipients[].formUrlstring | nullalias 단축 URL (https://walla.my/a/<aliasId> 형태)

참고: 이미 등록된 customerKey 는 값이 변경되지 않고 기존 링크가 그대로 반환됩니다. 저장된 값을 바꾸려면 PATCH /delivery/recipients 를 사용하세요. 저장된 연락처는 응답에 포함되지 않습니다.

오류 응답

  • 400 Bad Request: 요청 형식이 올바르지 않거나, 한 요청 안에 같은 customerKey 가 여러 번 있거나, 폼에 선언되지 않은 히든 필드 라벨을 전달한 경우. 응답 본문의 detailsType 으로 원인을 구분합니다
  • 401 Unauthorized: API 키 누락 또는 유효하지 않음
  • 403 Forbidden: 해당 리소스에 대한 권한 없음
  • 404 Not Found: 폼을 찾을 수 없거나 게시되지 않음
  • 500 Internal Server Error: 서버 오류

400 오류 종류

값원인
schema요청 본문이 스키마를 만족하지 않음
duplicateCustomerKey한 요청 안에 같은 customerKey 가 여러 번 포함됨
unknownHiddenFieldLabel폼에 선언되지 않은 히든 필드 라벨을 전달함

400 응답 본문에는 오류 메시지 error, 오류 종류 detailsType, 항목별 세부 정보 배열 details 가 포함되며, 추가 설명이 있으면 description 이 함께 반환됩니다.


수신자 정보 수정

이미 등록된 수신자의 정보를 수정합니다. 새 수신자를 만들지는 않습니다.

PATCH /open-api/v1/forms/{formId}/delivery/recipients

매개변수

경로 매개변수

매개변수타입필수 여부설명
formIdstring예고유한 폼 식별자

요청 본문

필드타입필수 여부설명
updatesarray예수정 객체 배열 (최소 1개)
updates[].customerKeystring예수정할 수신자의 고객 식별자
updates[].namestring아니오수신자 이름
updates[].emailstring아니오수신자 이메일 주소
updates[].phoneNumberstring아니오수신자 전화번호
updates[].hiddenFieldValuesobject아니오수신자별 히든 필드 초기값. 키는 폼에 선언된 히든 필드 라벨입니다

참고: name, email, phoneNumber, hiddenFieldValues 중 최소 하나를 전달해야 합니다. 보낸 필드만 갱신되고 생략한 필드는 그대로 유지됩니다. null 로 값을 지울 수는 없습니다.

참고: hiddenFieldValues 를 전달하면 해당 수신자의 히든 필드 초기값이 전체 교체됩니다.

참고: 등록되지 않은 customerKey 는 notFoundCustomerKeys 로 반환되며 새로 등록되지 않습니다. 신규 등록은 POST /delivery/recipients 를 사용하세요.

요청

요청 예시

curl -X PATCH "https://api.walla.my/open-api/v1/forms/form_abc/delivery/recipients" \
  -H "X-WALLA-API-KEY: your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "updates": [
      {
        "customerKey": "customer_003",
        "email": "sky.kim@example.com",
        "hiddenFieldValues": {
          "utm_source": "campaign_q3"
        }
      },
      {
        "customerKey": "customer_005",
        "phoneNumber": "+821098765432"
      }
    ]
  }'

응답

성공 응답 (200 OK)

{
  "success": true,
  "data": {
    "totalRequested": 2,
    "updatedCustomerKeys": ["customer_003"],
    "notFoundCustomerKeys": ["customer_005"]
  }
}

응답 필드

필드타입설명
successboolean요청 성공 여부
data.totalRequestednumber요청에 포함된 수정 대상 수
data.updatedCustomerKeysarray수정된 수신자의 고객 키 목록
data.notFoundCustomerKeysarray등록되지 않아 수정하지 못한 고객 키 목록

등록되지 않은 고객 키가 섞여 있어도 요청 자체는 200을 반환합니다. 수정 결과는 updatedCustomerKeys 와 notFoundCustomerKeys 로 확인하세요.

오류 응답

  • 400 Bad Request: 요청 형식이 올바르지 않거나, 한 요청 안에 같은 customerKey 가 여러 번 있는 경우. 응답 본문의 detailsType 으로 원인을 구분합니다
  • 401 Unauthorized: API 키 누락 또는 유효하지 않음
  • 403 Forbidden: 해당 리소스에 대한 권한 없음
  • 404 Not Found: 폼을 찾을 수 없거나 게시되지 않음
  • 500 Internal Server Error: 서버 오류

400 오류 종류

값원인
schema요청 본문이 스키마를 만족하지 않음
duplicateCustomerKey한 요청 안에 같은 customerKey 가 여러 번 포함됨

등록된 수신자에게 전송

이미 등록된 수신자에게 저장된 연락처로 폼을 전송합니다. 요청에 개인정보를 담지 않습니다.

POST /open-api/v1/forms/{formId}/delivery/send

매개변수

경로 매개변수

매개변수타입필수 여부설명
formIdstring예고유한 폼 식별자

요청 본문

필드타입필수 여부설명
customerKeysarray예전송할 고객 키 배열 (최소 1개)
subjectstring아니오이메일 제목 (기본값: "설문이 도착했습니다.")
messagestring아니오전송 메시지 (기본값: "설문 링크를 확인하세요.")
deliveryChannelstring아니오전송 채널 (기본값: emailFirst). 값과 동작은 수신자에게 폼 전송의 전송 채널 표를 참조하세요

참고: 수신자는 미리 등록되어 있어야 합니다. POST /delivery, POST /delivery/recipients, 대시보드 업로드 등으로 등록된 수신자에게 전송합니다.

참고: 요청에 같은 customerKey 가 여러 번 있으면 한 번만 전송합니다. 같은 키로 다시 호출하면 재전송됩니다.

요청

요청 예시

curl -X POST "https://api.walla.my/open-api/v1/forms/form_abc/delivery/send" \
  -H "X-WALLA-API-KEY: your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "customerKeys": ["customer_001", "customer_003", "customer_004", "customer_005"],
    "subject": "We value your feedback!",
    "message": "Please take a moment to complete our survey.",
    "deliveryChannel": "both"
  }'

응답

성공 응답 (200 OK)

{
  "success": true,
  "data": {
    "totalRequested": 4,
    "successfulDeliveries": [
      {
        "customerKey": "customer_001"
      },
      {
        "customerKey": "customer_003"
      }
    ],
    "failedDeliveries": [
      {
        "customerKey": "customer_004",
        "error": "No contact information registered for this recipient"
      }
    ],
    "notFoundCustomerKeys": ["customer_005"]
  }
}

개인정보 보호

요청과 응답 어디에도 이메일과 전화번호가 포함되지 않습니다. 전송은 등록 시 저장된 연락처로 수행되며, 응답은 customerKey 만 돌려줍니다.

응답 필드

필드타입설명
successboolean요청 성공 여부
data.totalRequestednumber중복을 제거한 요청 수신자 수
data.successfulDeliveriesarray전송에 성공한 수신자 객체 배열
data.successfulDeliveries[].customerKeystring고객 식별자
data.failedDeliveriesarray전송에 실패한 수신자 객체 배열
data.failedDeliveries[].customerKeystring고객 식별자
data.failedDeliveries[].errorstring전송 실패 사유
data.notFoundCustomerKeysarray등록되지 않아 전송하지 못한 고객 키 목록

개별 전송이 실패하거나 등록되지 않은 고객 키가 섞여 있어도 요청 자체는 200을 반환합니다. 등록 시 저장된 연락처가 없는 수신자는 failedDeliveries 로 반환됩니다.

오류 응답

  • 400 Bad Request: 잘못된 요청 (예: customerKeys 배열이 비어있음)
  • 401 Unauthorized: API 키 누락 또는 유효하지 않음
  • 403 Forbidden: 해당 리소스에 대한 권한 없음
  • 404 Not Found: 폼을 찾을 수 없거나 게시되지 않음
  • 500 Internal Server Error: 서버 오류

모든 전송 수신자 조회

특정 폼의 전체 수신자 목록을 반환합니다.

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

매개변수

경로 매개변수

매개변수타입필수 여부설명
formIdstring예고유한 폼 식별자

요청

요청 예시

curl -X GET "https://api.walla.my/open-api/v1/forms/form_abc/delivery" \
  -H "X-WALLA-API-KEY: your_api_key_here" \
  -H "Content-Type: application/json"

응답

성공 응답 (200 OK)

{
  "success": true,
  "data": [
    {
      "id": "delivery_123",
      "formId": "form_abc",
      "customerKey": "customer_001",
      "name": "홍길동",
      "email": "john@example.com",
      "phone": "+821012345678",
      "aliasId": "8c1a4e2b-7d3f-4a9b-9c2e-1a2b3c4d5e6f",
      "formUrl": "https://walla.my/a/8c1a4e2b-7d3f-4a9b-9c2e-1a2b3c4d5e6f",
      "hiddenFieldValues": {
        "utm_source": "newsletter"
      },
      "createdAt": "2024-01-15T10:30:00Z",
      "updatedAt": "2024-01-15T11:45:00Z"
    },
    {
      "id": "delivery_456",
      "formId": "form_abc",
      "customerKey": "customer_002",
      "name": null,
      "email": "jane@example.com",
      "phone": null,
      "aliasId": null,
      "formUrl": null,
      "hiddenFieldValues": {},
      "createdAt": "2024-01-15T10:30:00Z",
      "updatedAt": "2024-01-15T10:30:00Z"
    }
  ]
}

응답 필드

필드타입설명
successboolean요청 성공 여부
dataarray전송 객체 배열
data[].idstring전송 식별자
data[].formIdstring폼 식별자
data[].customerKeystring고객 식별자
data[].namestring | null등록 시 저장된 수신자 이름
data[].emailstring | null수신자 이메일
data[].phonestring | null수신자 전화번호
data[].aliasIdstring (UUID) | null발급된 alias 식별자. alias 가 없는 legacy 행은 null
data[].formUrlstring | nullalias 단축 URL (https://walla.my/a/<aliasId> 형태). aliasId 가 null 이면 formUrl 도 null
data[].hiddenFieldValuesobject수신자별 히든 필드 초기값. 히든 필드 라벨을 키로 반환합니다
data[].createdAtstring (date-time)최초 전송 생성 타임스탬프
data[].updatedAtstring (date-time)마지막 업데이트 타임스탬프

참고: aliasId 와 formUrl 은 alias 단축 URL 도입 이전에 생성된 legacy 전송 행에서는 null 로 반환됩니다.

오류 응답

  • 401 Unauthorized: API 키 누락 또는 유효하지 않음
  • 403 Forbidden: 해당 리소스에 대한 권한 없음
  • 404 Not Found: 폼을 찾을 수 없거나 게시되지 않음
  • 500 Internal Server Error: 서버 오류

특정 수신자의 전송 상태 조회

특정 수신자의 전송 상태와 응답 여부를 확인합니다.

POST /open-api/v1/forms/{formId}/delivery/status

매개변수

경로 매개변수

매개변수타입필수 여부설명
formIdstring예고유한 폼 식별자

요청 본문

필드타입필수 여부설명
customerKeysarray예확인할 고객 키 배열 (최소 1개)

요청

요청 예시

curl -X POST "https://api.walla.my/open-api/v1/forms/form_abc/delivery/status" \
  -H "X-WALLA-API-KEY: your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "customerKeys": ["customer_001", "customer_002", "customer_003"]
  }'

응답

성공 응답 (200 OK)

{
  "success": true,
  "data": [
    {
      "id": "delivery_123",
      "formId": "form_abc",
      "customerKey": "customer_001",
      "name": "홍길동",
      "email": "john@example.com",
      "phone": "+821012345678",
      "status": "RESPONDED",
      "lastSentAt": "2024-01-15T10:30:00Z",
      "createdAt": "2024-01-15T10:30:00Z",
      "updatedAt": "2024-01-15T14:20:00Z"
    },
    {
      "id": "delivery_456",
      "formId": "form_abc",
      "customerKey": "customer_002",
      "name": null,
      "email": "jane@example.com",
      "phone": null,
      "status": "OPENED",
      "lastSentAt": "2024-01-15T10:30:00Z",
      "createdAt": "2024-01-15T10:30:00Z",
      "updatedAt": "2024-01-15T11:15:00Z"
    },
    {
      "id": "delivery_789",
      "formId": "form_abc",
      "customerKey": "customer_003",
      "name": null,
      "email": "bob@example.com",
      "phone": null,
      "status": "SENT",
      "lastSentAt": "2024-01-15T10:30:00Z",
      "createdAt": "2024-01-15T10:30:00Z",
      "updatedAt": "2024-01-15T10:30:00Z"
    }
  ]
}

응답 필드

필드타입설명
successboolean요청 성공 여부
dataarray전송 상태 객체 배열
data[].idstring전송 식별자
data[].formIdstring폼 식별자
data[].customerKeystring고객 식별자
data[].namestring | null등록 시 저장된 수신자 이름
data[].emailstring수신자 이메일
data[].phonestring수신자 전화번호 (요청 시 phoneNumber로 전송, 응답에서는 phone으로 반환)
data[].statusstring전송 상태 (아래 참조)
data[].lastSentAtstring (date-time)마지막 전송 타임스탬프
data[].createdAtstring (date-time)최초 전송 생성 타임스탬프
data[].updatedAtstring (date-time)마지막 업데이트 타임스탬프

전송 상태 값

상태설명
NOT_SENT수신자가 등록되었지만 아직 전송되지 않음 (POST /delivery/recipients 로 등록만 한 경우 포함)
SENT폼이 성공적으로 전달됨
OPENED수신자가 폼을 열람함
RESPONDED수신자가 폼을 작성하고 제출함

참고: 전송하지 않고 formUrl 만 자체 채널로 배포한 경우에도 수신자가 링크를 열면 OPENED, 폼을 제출하면 RESPONDED 로 전이합니다.

오류 응답

  • 400 Bad Request: 잘못된 요청 (예: customerKeys 배열이 비어있음)
  • 401 Unauthorized: API 키 누락 또는 유효하지 않음
  • 403 Forbidden: 해당 리소스에 대한 권한 없음
  • 404 Not Found: 폼을 찾을 수 없거나 게시되지 않음
  • 500 Internal Server Error: 서버 오류

코드 예제

JavaScript/TypeScript

interface Recipient {
  customerKey: string;
  name?: string;
  email?: string;
  phoneNumber?: string;
  hiddenFieldValues?: Record<string, string>;
}

interface DeliveryOptions {
  subject?: string;
  message?: string;
  deliveryChannel?: 'emailFirst' | 'phoneFirst' | 'both';
}

async function sendForm(
  formId: string,
  recipients: Recipient[],
  options?: DeliveryOptions
) {
  const response = await fetch(
    `https://api.walla.my/open-api/v1/forms/${formId}/delivery`,
    {
      method: 'POST',
      headers: {
        'X-WALLA-API-KEY': process.env.WALLA_API_KEY!,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        recipients,
        subject: options?.subject || '설문이 도착했습니다.',
        message: options?.message || '설문 링크를 확인하세요.',
        deliveryChannel: options?.deliveryChannel || 'emailFirst'
      })
    }
  );

  if (!response.ok) {
    throw new Error(`HTTP error! status: ${response.status}`);
  }

  return await response.json();
}

async function registerRecipients(formId: string, recipients: Recipient[]) {
  const response = await fetch(
    `https://api.walla.my/open-api/v1/forms/${formId}/delivery/recipients`,
    {
      method: 'POST',
      headers: {
        'X-WALLA-API-KEY': process.env.WALLA_API_KEY!,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({ recipients })
    }
  );

  if (!response.ok) {
    throw new Error(`HTTP error! status: ${response.status}`);
  }

  return await response.json();
}

async function sendToRegistered(
  formId: string,
  customerKeys: string[],
  options?: DeliveryOptions
) {
  const response = await fetch(
    `https://api.walla.my/open-api/v1/forms/${formId}/delivery/send`,
    {
      method: 'POST',
      headers: {
        'X-WALLA-API-KEY': process.env.WALLA_API_KEY!,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        customerKeys,
        subject: options?.subject,
        message: options?.message,
        deliveryChannel: options?.deliveryChannel
      })
    }
  );

  if (!response.ok) {
    throw new Error(`HTTP error! status: ${response.status}`);
  }

  return await response.json();
}

async function getDeliveryStatus(formId: string, customerKeys: string[]) {
  const response = await fetch(
    `https://api.walla.my/open-api/v1/forms/${formId}/delivery/status`,
    {
      method: 'POST',
      headers: {
        'X-WALLA-API-KEY': process.env.WALLA_API_KEY!,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({ customerKeys })
    }
  );

  if (!response.ok) {
    throw new Error(`HTTP error! status: ${response.status}`);
  }

  const data = await response.json();
  return data.data;
}

// Usage example
const result = await sendForm('form_abc', [
  {
    customerKey: 'customer_001',
    email: 'john@example.com'
  },
  {
    customerKey: 'customer_002',
    email: 'jane@example.com',
    phoneNumber: '+821012345678'
  }
], {
  subject: 'We need your feedback!',
  message: 'Please help us improve our services.'
});

console.log(`Successfully sent to ${result.data.successfulDeliveries.length} recipients`);

// Register only, then distribute the links through your own channel
const registered = await registerRecipients('form_abc', [
  { customerKey: 'customer_003' }
]);
registered.data.recipients.forEach(r => pushToOwnChannel(r.customerKey, r.formUrl));

// Send (or resend) later with customerKey only
await sendToRegistered('form_abc', ['customer_003'], { deliveryChannel: 'both' });

// Check status
const statuses = await getDeliveryStatus('form_abc', ['customer_001', 'customer_002']);
statuses.forEach(status => {
  console.log(`${status.customerKey}: ${status.status}`);
});

Python

import requests
import os
from typing import List, Dict, Any, Optional

class WallaDeliveryAPI:
    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 send_form(
        self,
        form_id: str,
        recipients: List[Dict[str, Any]],
        subject: Optional[str] = None,
        message: Optional[str] = None,
        delivery_channel: Optional[str] = None
    ) -> Dict[str, Any]:
        """Send form to recipients"""
        url = f"{self.base_url}/forms/{form_id}/delivery"
        payload: Dict[str, Any] = {
            "recipients": recipients,
            "subject": subject or "설문이 도착했습니다.",
            "message": message or "설문 링크를 확인하세요."
        }
        if delivery_channel:
            payload["deliveryChannel"] = delivery_channel

        response = requests.post(url, headers=self.headers, json=payload)
        response.raise_for_status()
        return response.json()

    def register_recipients(
        self,
        form_id: str,
        recipients: List[Dict[str, Any]]
    ) -> Dict[str, Any]:
        """Register recipients without sending"""
        url = f"{self.base_url}/forms/{form_id}/delivery/recipients"
        payload = {"recipients": recipients}

        response = requests.post(url, headers=self.headers, json=payload)
        response.raise_for_status()
        return response.json()

    def update_recipients(
        self,
        form_id: str,
        updates: List[Dict[str, Any]]
    ) -> Dict[str, Any]:
        """Update already registered recipients"""
        url = f"{self.base_url}/forms/{form_id}/delivery/recipients"
        payload = {"updates": updates}

        response = requests.patch(url, headers=self.headers, json=payload)
        response.raise_for_status()
        return response.json()

    def send_to_registered(
        self,
        form_id: str,
        customer_keys: List[str],
        subject: Optional[str] = None,
        message: Optional[str] = None,
        delivery_channel: Optional[str] = None
    ) -> Dict[str, Any]:
        """Send form to already registered recipients"""
        url = f"{self.base_url}/forms/{form_id}/delivery/send"
        payload: Dict[str, Any] = {"customerKeys": customer_keys}
        if subject:
            payload["subject"] = subject
        if message:
            payload["message"] = message
        if delivery_channel:
            payload["deliveryChannel"] = delivery_channel

        response = requests.post(url, headers=self.headers, json=payload)
        response.raise_for_status()
        return response.json()

    def get_all_deliveries(self, form_id: str) -> List[Dict[str, Any]]:
        """Get all delivery recipients for a form"""
        url = f"{self.base_url}/forms/{form_id}/delivery"
        response = requests.get(url, headers=self.headers)
        response.raise_for_status()
        return response.json()["data"]

    def get_delivery_status(
        self,
        form_id: str,
        customer_keys: List[str]
    ) -> List[Dict[str, Any]]:
        """Get delivery status for specific recipients"""
        url = f"{self.base_url}/forms/{form_id}/delivery/status"
        payload = {"customerKeys": customer_keys}

        response = requests.post(url, headers=self.headers, json=payload)
        response.raise_for_status()
        return response.json()["data"]

    def get_response_rate(self, form_id: str) -> float:
        """Calculate response rate for a form"""
        deliveries = self.get_all_deliveries(form_id)
        if not deliveries:
            return 0.0

        customer_keys = [d["customerKey"] for d in deliveries]
        statuses = self.get_delivery_status(form_id, customer_keys)

        responded = sum(1 for s in statuses if s["status"] == "RESPONDED")
        return (responded / len(statuses)) * 100

# Usage
api = WallaDeliveryAPI(os.getenv("WALLA_API_KEY"))

# Send form
result = api.send_form(
    "form_abc",
    [
        {"customerKey": "customer_001", "email": "john@example.com"},
        {"customerKey": "customer_002", "email": "jane@example.com"}
    ],
    subject="Your feedback matters!",
    message="Please take 2 minutes to complete our survey."
)

print(f"Sent to {len(result['data']['successfulDeliveries'])} recipients")

# Register first, send later
registered = api.register_recipients(
    "form_abc",
    [{"customerKey": "customer_003", "email": "sky@example.com"}]
)
for recipient in registered["data"]["recipients"]:
    print(f"{recipient['customerKey']}: {recipient['formUrl']}")

send_result = api.send_to_registered(
    "form_abc",
    ["customer_003"],
    delivery_channel="both"
)
print(f"Sent to {len(send_result['data']['successfulDeliveries'])} recipients")

# Check response rate
response_rate = api.get_response_rate("form_abc")
print(f"Response rate: {response_rate:.2f}%")

모범 사례

고객 키 관리

  • 시스템 전체에서 일관된 고객 키를 사용하세요
  • 고객 키는 고객별로 고유해야 합니다
  • 내부 고객 ID를 고객 키로 쓰는 것도 좋은 방법입니다
  • 고객 키와 내부 시스템 간의 매핑을 기록해 두세요

전송 채널 선택

  • 기본값 유지: 이메일 도달률이 충분하면 기본값 emailFirst 를 그대로 사용하세요
  • 문자 우선: 전화번호 기반으로 관리하는 고객군에는 phoneFirst 를 사용하세요
  • 도달률 우선: 모든 채널로 보내야 하는 캠페인에는 both 를 사용하세요. 한 채널만 성공해도 성공으로 집계됩니다
  • 폴백 확인: 폴백은 채널 가용성 기준입니다. 전송 실패 시 다른 채널로 재시도하지 않으므로 failedDeliveries 를 확인해 재전송하세요

개인정보 최소 수집

  • 자체 채널로 링크를 배포한다면 연락처 없이 customerKey 만으로 등록하세요
  • 등록과 전송을 분리하면 전송 요청에 개인정보를 담지 않아도 됩니다
  • 저장된 연락처는 응답에 포함되지 않습니다. 저장된 값을 바꾸려면 PATCH /delivery/recipients 를 사용하세요
  • aliasId 와 formUrl 은 수신자 본인에게만 전달하세요

전송 최적화

  • 일괄 처리: 한 번의 요청으로 여러 수신자에게 보내세요
  • 등록과 전송 분리: 같은 수신자에게 반복 전송한다면 미리 등록해 두고 POST /delivery/send 로 customerKey 만 전달하세요
  • 오류 처리: failedDeliveries 배열을 항상 확인하고, 실패 건은 재시도하세요
  • 상태 추적: 주기적으로 전송 상태를 확인해서 참여도를 파악하세요
  • 속도 제한: API 키당 시간당 6,000건의 속도 제한이 적용됩니다

제목 및 메시지 맞춤화

// 캠페인에 따른 맞춤화
const campaigns = {
  feedback: {
    subject: 'We value your feedback!',
    message: 'Help us improve our services by completing this quick survey.'
  },
  nps: {
    subject: 'How likely are you to recommend us?',
    message: 'Your opinion matters. Please share your thoughts.'
  }
};

await sendForm(formId, recipients, campaigns.feedback);

다음 단계

목차