폼 전송 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 오류가 반환됩니다.
매개변수
경로 매개변수
| 매개변수 | 타입 | 필수 여부 | 설명 |
|---|---|---|---|
formId | string | 예 | 고유한 폼 식별자 |
요청 본문
| 필드 | 타입 | 필수 여부 | 설명 |
|---|---|---|---|
recipients | array | 예 | 수신자 객체 배열 (최소 1개) |
recipients[].customerKey | string | 예 | 고유한 고객 식별자 |
recipients[].name | string | null | 아니오 | 수신자 이름 |
recipients[].email | string | 아니오 | 수신자 이메일 주소 |
recipients[].phoneNumber | string | 아니오 | 수신자 전화번호 |
recipients[].hiddenFieldValues | object | 아니오 | 수신자별 히든 필드 초기값. 키는 폼에 선언된 히든 필드 라벨입니다 (히든 필드 조회) |
subject | string | 아니오 | 이메일 제목 (기본값: "설문이 도착했습니다.") |
message | string | 아니오 | 전송 메시지 (기본값: "설문 링크를 확인하세요.") |
deliveryChannel | string | 아니오 | 전송 채널 (기본값: 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"
}
]
}
}응답 필드
| 필드 | 타입 | 설명 |
|---|---|---|
success | boolean | 요청 성공 여부 |
data.totalRecipients | number | 전체 수신자 수 |
data.successfulDeliveries | array | 성공적으로 전송된 수신자 객체 배열 |
data.successfulDeliveries[].customerKey | string | 요청에 사용한 고객 식별자 |
data.successfulDeliveries[].name | string | null | 요청에 전달한 수신자 이름 (전달하지 않은 경우 null) |
data.successfulDeliveries[].email | string | null | 수신자 이메일 (요청에 포함되지 않은 경우 null) |
data.successfulDeliveries[].phoneNumber | string | null | 수신자 전화번호 (요청에 포함되지 않은 경우 null) |
data.successfulDeliveries[].aliasId | string (UUID) | 발급된 alias 식별자. 폼 접근 토큰 역할을 합니다 |
data.successfulDeliveries[].formUrl | string | alias 단축 URL (https://walla.my/a/<aliasId> 형태) |
data.failedDeliveries | array | 오류 세부 정보가 포함된 실패한 전송 목록 |
data.failedDeliveries[].customerKey | string | 요청에 사용한 고객 식별자 |
data.failedDeliveries[].name | string | null | 저장된 수신자 이름. 저장된 값 기준이므로 재전송 시 요청에 전달한 값과 다를 수 있습니다 |
data.failedDeliveries[].email | string | null | 수신자 이메일 |
data.failedDeliveries[].phoneNumber | string | null | 수신자 전화번호 |
data.failedDeliveries[].error | string | 전송 실패 사유 |
참고: 동일한
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 오류가 반환됩니다.
매개변수
경로 매개변수
| 매개변수 | 타입 | 필수 여부 | 설명 |
|---|---|---|---|
formId | string | 예 | 고유한 폼 식별자 |
요청 본문
| 필드 | 타입 | 필수 여부 | 설명 |
|---|---|---|---|
recipients | array | 예 | 수신자 객체 배열 (최소 1개) |
recipients[].customerKey | string | 예 | 고유한 고객 식별자 |
recipients[].name | string | 아니오 | 수신자 이름 |
recipients[].email | string | 아니오 | 수신자 이메일 주소 |
recipients[].phoneNumber | string | 아니오 | 수신자 전화번호 |
recipients[].hiddenFieldValues | object | 아니오 | 수신자별 히든 필드 초기값. 키는 폼에 선언된 히든 필드 라벨입니다 (히든 필드 조회) |
참고: 연락처 없이
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 은 수신자 본인에게만 전달합니다.
응답 필드
| 필드 | 타입 | 설명 |
|---|---|---|
success | boolean | 요청 성공 여부 |
data.totalRecipients | number | 요청에 포함된 수신자 수 |
data.recipients | array | 등록 결과 객체 배열. 요청 순서를 유지합니다 |
data.recipients[].customerKey | string | 요청에 사용한 고객 식별자 |
data.recipients[].created | boolean | 이번 요청으로 새로 등록되면 true, 이미 등록되어 있으면 false |
data.recipients[].aliasId | string (UUID) | null | 발급된 alias 식별자. 폼 접근 토큰 역할을 합니다 |
data.recipients[].formUrl | string | null | alias 단축 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매개변수
경로 매개변수
| 매개변수 | 타입 | 필수 여부 | 설명 |
|---|---|---|---|
formId | string | 예 | 고유한 폼 식별자 |
요청 본문
| 필드 | 타입 | 필수 여부 | 설명 |
|---|---|---|---|
updates | array | 예 | 수정 객체 배열 (최소 1개) |
updates[].customerKey | string | 예 | 수정할 수신자의 고객 식별자 |
updates[].name | string | 아니오 | 수신자 이름 |
updates[].email | string | 아니오 | 수신자 이메일 주소 |
updates[].phoneNumber | string | 아니오 | 수신자 전화번호 |
updates[].hiddenFieldValues | object | 아니오 | 수신자별 히든 필드 초기값. 키는 폼에 선언된 히든 필드 라벨입니다 |
참고:
name,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"]
}
}응답 필드
| 필드 | 타입 | 설명 |
|---|---|---|
success | boolean | 요청 성공 여부 |
data.totalRequested | number | 요청에 포함된 수정 대상 수 |
data.updatedCustomerKeys | array | 수정된 수신자의 고객 키 목록 |
data.notFoundCustomerKeys | array | 등록되지 않아 수정하지 못한 고객 키 목록 |
등록되지 않은 고객 키가 섞여 있어도 요청 자체는 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매개변수
경로 매개변수
| 매개변수 | 타입 | 필수 여부 | 설명 |
|---|---|---|---|
formId | string | 예 | 고유한 폼 식별자 |
요청 본문
| 필드 | 타입 | 필수 여부 | 설명 |
|---|---|---|---|
customerKeys | array | 예 | 전송할 고객 키 배열 (최소 1개) |
subject | string | 아니오 | 이메일 제목 (기본값: "설문이 도착했습니다.") |
message | string | 아니오 | 전송 메시지 (기본값: "설문 링크를 확인하세요.") |
deliveryChannel | string | 아니오 | 전송 채널 (기본값: 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 만 돌려줍니다.
응답 필드
| 필드 | 타입 | 설명 |
|---|---|---|
success | boolean | 요청 성공 여부 |
data.totalRequested | number | 중복을 제거한 요청 수신자 수 |
data.successfulDeliveries | array | 전송에 성공한 수신자 객체 배열 |
data.successfulDeliveries[].customerKey | string | 고객 식별자 |
data.failedDeliveries | array | 전송에 실패한 수신자 객체 배열 |
data.failedDeliveries[].customerKey | string | 고객 식별자 |
data.failedDeliveries[].error | string | 전송 실패 사유 |
data.notFoundCustomerKeys | array | 등록되지 않아 전송하지 못한 고객 키 목록 |
개별 전송이 실패하거나 등록되지 않은 고객 키가 섞여 있어도 요청 자체는 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매개변수
경로 매개변수
| 매개변수 | 타입 | 필수 여부 | 설명 |
|---|---|---|---|
formId | string | 예 | 고유한 폼 식별자 |
요청
요청 예시
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"
}
]
}응답 필드
| 필드 | 타입 | 설명 |
|---|---|---|
success | boolean | 요청 성공 여부 |
data | array | 전송 객체 배열 |
data[].id | string | 전송 식별자 |
data[].formId | string | 폼 식별자 |
data[].customerKey | string | 고객 식별자 |
data[].name | string | null | 등록 시 저장된 수신자 이름 |
data[].email | string | null | 수신자 이메일 |
data[].phone | string | null | 수신자 전화번호 |
data[].aliasId | string (UUID) | null | 발급된 alias 식별자. alias 가 없는 legacy 행은 null |
data[].formUrl | string | null | alias 단축 URL (https://walla.my/a/<aliasId> 형태). aliasId 가 null 이면 formUrl 도 null |
data[].hiddenFieldValues | object | 수신자별 히든 필드 초기값. 히든 필드 라벨을 키로 반환합니다 |
data[].createdAt | string (date-time) | 최초 전송 생성 타임스탬프 |
data[].updatedAt | string (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매개변수
경로 매개변수
| 매개변수 | 타입 | 필수 여부 | 설명 |
|---|---|---|---|
formId | string | 예 | 고유한 폼 식별자 |
요청 본문
| 필드 | 타입 | 필수 여부 | 설명 |
|---|---|---|---|
customerKeys | array | 예 | 확인할 고객 키 배열 (최소 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"
}
]
}응답 필드
| 필드 | 타입 | 설명 |
|---|---|---|
success | boolean | 요청 성공 여부 |
data | array | 전송 상태 객체 배열 |
data[].id | string | 전송 식별자 |
data[].formId | string | 폼 식별자 |
data[].customerKey | string | 고객 식별자 |
data[].name | string | null | 등록 시 저장된 수신자 이름 |
data[].email | string | 수신자 이메일 |
data[].phone | string | 수신자 전화번호 (요청 시 phoneNumber로 전송, 응답에서는 phone으로 반환) |
data[].status | string | 전송 상태 (아래 참조) |
data[].lastSentAt | string (date-time) | 마지막 전송 타임스탬프 |
data[].createdAt | string (date-time) | 최초 전송 생성 타임스탬프 |
data[].updatedAt | string (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);