템플릿 검수 가이드
알림톡 템플릿은 등록 후 카카오 심사를 통과(승인)해야 발송에 사용할 수 있습니다. 이 가이드는 검수 상태가 어떻게 바뀌는지, 반려되면 사유를 어디서 확인하고 어떤 순서로 대응하는지, 그리고 반려를 미리 줄이는 방법(사전 점검)과 일일 검수 한도를 설명합니다.
https://docs.almani-center.com/open/v1 입니다.
모든 요청에는 인증에서 발급한
API 키(Authorization: Bearer ak_live_xxxx — 예시는 더미 키)와
대상 사이트를 지정하는 X-Site-Id 헤더가 필요합니다.
템플릿 API 전체 규격은 템플릿 API 레퍼런스를 참고하세요.
검수 상태 흐름
일반적인 흐름은 REG → REQ → APR 또는 REJ 입니다. 반려된 템플릿은 수정 후 재검수를 요청해 다시 REQ가 되고, 검수중(REQ) 요청을 취소하면 REG로 되돌아갑니다.
-
REG→REQ— 검수 요청. 템플릿 신청은 기본값 (autoReview: true)으로 등록 즉시 검수까지 요청하며, 초안(REG)은 재검수 요청으로 검수를 시작합니다. -
REQ→APR— 카카오 심사 승인.sendable: true가 되어 발송에 사용할 수 있습니다. -
REQ→REJ— 카카오 심사 반려.rejectReason으로 사유를 확인합니다. -
REJ→REQ— 수정 후 재검수 요청. -
REQ→REG— 검수 요청 취소. 카카오 심사가 이미 시작된 뒤에는 취소가 거부될 수 있으며, 이때6000에러로 사유를 전달합니다.
수정중(UPT) 상태의 템플릿도
재검수 요청 대상입니다. 재검수는 REG·REJ·UPT
상태에서만 요청할 수 있습니다.
상태별 의미
| 상태 | 의미 | 설명 |
|---|---|---|
| REG | 등록 | 검수 요청 전 초안 상태입니다. autoReview: false로 등록하거나
검수 요청을 취소하면 이 상태가 됩니다. |
| REQ | 검수중 | 카카오 심사가 진행 중입니다. |
| APR | 승인 | 심사를 통과했습니다. 발송에 사용할 수 있습니다(sendable: true). |
| REJ | 반려 | 심사에서 반려되었습니다. rejectReason으로 사유를 확인하고
수정 후 재검수를 요청하세요. |
| UPT | 수정중 | 템플릿을 수정하고 있는 상태입니다. 재검수 요청 대상입니다. |
상태별로 모아 보려면 템플릿 목록 조회의
status 필터를 사용하세요. 복수 상태는 콤마로 구분합니다
(예: status=REQ,REJ).
반려 사유 확인법
반려(REJ) 사유는 세 곳에서 확인할 수 있습니다.
-
템플릿 조회 —
data.rejectReason에 카카오 심사 코멘트 전문(최신)이 담깁니다.recentHistory로 최근 검수 이력 5건도 함께 확인합니다. -
템플릿 목록 조회 —
status=REJ필터로 반려된 템플릿을 모아 보고,items[].rejectReason에서 최신 반려 사유 요약을 확인합니다. -
검수 이력 — 여러 번 반려·재검수를 거친 템플릿은
review-history로 전체 이력을 시간순으로 확인합니다. 카카오 심사 코멘트는items[].reviewer가"카카오"인 항목입니다.
반려 사유 확인 — 템플릿 조회
/templates/{templateCode}
필요 스코프: template:read
템플릿 단건 상세와 검수 상태를 조회합니다. 반려된 템플릿이면
rejectReason에 카카오 심사 코멘트 전문(최신)이 담깁니다.
전체 응답 필드는 템플릿 조회 레퍼런스를
참고하세요.
Path 파라미터
템플릿 코드입니다 (예: TPL_20260725143012).
Response (반려 확인 관련 필드)
현재 상태입니다 — REG/REQ/APR/REJ/UPT.
REJ일 때 카카오 심사 코멘트 전문(최신)입니다.
APR 여부 — 발송에 사용할 수 있는지를 나타냅니다.
최근 검수 이력 5건입니다 — { status, comment, at }.
전체 이력은 검수 이력 조회를 사용합니다.
curl "https://docs.almani-center.com/open/v1/templates/TPL_20260725143012" \
-H "Authorization: Bearer ak_live_xxxx" \
-H "X-Site-Id: 1024"
// Java 17 + Spring Framework 6 RestClient
RestClient restClient = RestClient.builder()
.baseUrl("https://docs.almani-center.com/open/v1")
.defaultHeader(HttpHeaders.AUTHORIZATION, "Bearer ak_live_xxxx")
.defaultHeader("X-Site-Id", "1024")
.build();
String response = restClient.get()
.uri("/templates/{templateCode}", "TPL_20260725143012")
.retrieve()
.body(String.class);
HTTP/1.1 200 OK
{
"code": "0000",
"message": "OK",
"data": {
"templateCode": "TPL_20260725143012",
"templateName": "주문완료 안내 v3",
"approveStatus": "REJ",
"rejectReason": "버튼 링크가 메시지 내용과 무관한 페이지로 연결됩니다. 주문 상세 페이지로 수정 후 재요청해 주세요.",
"templateType": "BA",
"category": "004001",
"securityEnabled": false,
"content": "#{고객명}님, 주문이 완료되었습니다.\n\n▶ 주문번호: #{주문번호}\n▶ 결제금액: #{결제금액}원\n\n배송이 시작되면 다시 알려드릴게요.",
"buttons": [
{
"name": "주문 상세 보기",
"type": "WL",
"urlMobile": "https://event.example.com/summer"
}
],
"mappingData": {
"고객명": "고객명",
"주문번호": "주문번호",
"결제금액": "결제금액"
},
"sendable": false,
"recentHistory": [
{
"status": "REJ",
"comment": "버튼 링크가 메시지 내용과 무관한 페이지로 연결됩니다. 주문 상세 페이지로 수정 후 재요청해 주세요.",
"at": "2026-07-25T15:01:10+09:00"
},
{
"status": "REQ",
"comment": "주문 완료 시 발송되는 정보성 메시지입니다.",
"at": "2026-07-25T14:30:12+09:00"
}
]
},
"meta": {
"requestId": "req_4c07d9b2",
"quota": {
"rateLimit": {
"limit": 120,
"remaining": 116,
"resetAt": "2026-07-25T14:31:00+09:00"
}
}
}
}
HTTP/1.1 404 Not Found
{
"code": "4040",
"message": "템플릿을 찾을 수 없습니다: TPL_20260725149999",
"data": null,
"meta": {
"requestId": "req_b3e17a6d",
"quota": {
"rateLimit": {
"limit": 120,
"remaining": 115,
"resetAt": "2026-07-25T14:31:00+09:00"
}
}
}
}
검수 이력 조회
/templates/{templateCode}/review-history
필요 스코프: template:read
템플릿의 전체 검수 이력을 시간순으로 반환합니다. 반려가 반복된 템플릿에서 이전 반려 사유와 수정 내역을 되짚어볼 때 사용합니다. 커서 페이지네이션(공통 규약 참조)을 지원합니다.
Path 파라미터
템플릿 코드입니다.
Query 파라미터
페이지 크기입니다. 기본 20, 최대 100.
이전 응답의 data.nextCursor 값입니다.
Response
REQ | APR | REJ.
검수 요청 의견 또는 카카오 심사 코멘트입니다.
코멘트 작성 주체입니다. 카카오 심사 코멘트는 "카카오"입니다.
이력 발생 시각입니다.
null이면 마지막 페이지입니다.
curl "https://docs.almani-center.com/open/v1/templates/TPL_20260725143012/review-history" \
-H "Authorization: Bearer ak_live_xxxx" \
-H "X-Site-Id: 1024"
// Java 17 + Spring Framework 6 RestClient
RestClient restClient = RestClient.builder()
.baseUrl("https://docs.almani-center.com/open/v1")
.defaultHeader(HttpHeaders.AUTHORIZATION, "Bearer ak_live_xxxx")
.defaultHeader("X-Site-Id", "1024")
.build();
String response = restClient.get()
.uri("/templates/{templateCode}/review-history", "TPL_20260725143012")
.retrieve()
.body(String.class);
HTTP/1.1 200 OK
{
"code": "0000",
"message": "OK",
"data": {
"items": [
{
"status": "REJ",
"comment": "버튼 링크가 메시지 내용과 무관한 페이지로 연결됩니다. 주문 상세 페이지로 수정 후 재요청해 주세요.",
"reviewer": "카카오",
"at": "2026-07-25T15:01:10+09:00"
},
{
"status": "REQ",
"comment": "주문 완료 시 발송되는 정보성 메시지입니다.",
"reviewer": null,
"at": "2026-07-25T14:30:12+09:00"
}
],
"nextCursor": null
},
"meta": {
"requestId": "req_38ab90fe",
"quota": {
"rateLimit": {
"limit": 120,
"remaining": 119,
"resetAt": "2026-07-25T14:31:00+09:00"
}
}
}
}
HTTP/1.1 404 Not Found
{
"code": "4040",
"message": "템플릿을 찾을 수 없습니다: TPL_20260725149999",
"data": null,
"meta": {
"requestId": "req_f61b04d8",
"quota": {
"rateLimit": {
"limit": 120,
"remaining": 118,
"resetAt": "2026-07-25T14:31:00+09:00"
}
}
}
}
반려 대응 절차
-
사유 확인 —
템플릿 조회의
rejectReason(전문)과recentHistory를 확인합니다. 반려가 반복된 템플릿은 검수 이력 조회로 전체 흐름을 되짚습니다. -
템플릿 수정 — 사유에 해당하는 요소를 수정합니다.
예를 들어
'특가','할인'등 광고성 표현은 정보성 알림톡에서 반려 사유이고, 버튼 URL이 메시지 내용과 무관한 페이지로 연결되면 반려됩니다. 변수만으로 구성된 본문도 반려됩니다. - 사전 점검 (권장) — 수정본을 사전 정책 검증으로 먼저 진단합니다. 검수 쿼터를 소모하지 않습니다.
-
재검수 요청 —
재검수 요청 바디에
수정할 필드를 함께 보내면 수정과 재검수를 한 번에 처리합니다.
comments에 반려 사유에 대한 소명을 담는 것을 권장합니다. - 결과 확인 — 템플릿 조회에서 알마니에 저장된 현재 상태와 반려 사유를 확인합니다. 이 조회는 비즈톡 상태를 실시간으로 재조회하지 않습니다.
REG·REJ·UPT 상태에서만 가능하며,
그 외 상태에서 호출하면 4090 에러가 반환됩니다.
재검수 요청도 일일 검수 한도를
소모하므로, 신청 전 사전 점검으로 반려 가능성을 먼저 줄이세요.
수정 후 재검수 요청
/templates/{templateCode}/review
필요 스코프: template:write
반려(REJ) · 초안(REG) · 수정중(UPT) 상태 템플릿의 검수를 (재)요청합니다. 요청 바디에 수정할 필드를 함께 보내면 수정과 재검수를 한 번에 처리합니다. 성공 시 상태는 REQ가 됩니다.
Path 파라미터
템플릿 코드입니다.
Request Body
템플릿 신청과 동일 규격입니다. 보낸 필드만 수정한 뒤 검수를 요청합니다.
카카오 심사 담당자에게 전달되는 의견입니다. 반려 사유에 대한 소명을 권장합니다.
Response
템플릿 코드입니다.
성공 시 REQ가 됩니다.
검수 요청 시각입니다.
주요 에러
| 코드 | HTTP | 설명 |
|---|---|---|
4090 | 409 | 상태 전이 불가 — REG·REJ·UPT 이외 상태에서 요청
(message에 현재 상태 포함) |
4291 | 429 | 일일 템플릿 검수 요청 한도 초과 |
curl -X POST "https://docs.almani-center.com/open/v1/templates/TPL_20260725143012/review" \
-H "Authorization: Bearer ak_live_xxxx" \
-H "X-Site-Id: 1024" \
-H "Idempotency-Key: 8c25d1b7-3e4f-4a90-b6c2-1d9e8f7a3c50" \
-H "Content-Type: application/json" \
-d '{
"buttons": [
{
"name": "주문 상세 보기",
"type": "WL",
"urlMobile": "https://shop.example.com/orders/#{주문번호}"
}
],
"comments": "버튼 링크를 주문 상세 페이지로 수정했습니다."
}'
// Java 17 + Spring Framework 6 RestClient
RestClient restClient = RestClient.builder()
.baseUrl("https://docs.almani-center.com/open/v1")
.defaultHeader(HttpHeaders.AUTHORIZATION, "Bearer ak_live_xxxx")
.defaultHeader("X-Site-Id", "1024")
.build();
record TemplateButton(String name, String type, String urlMobile) {}
record ReviewRequest(List<TemplateButton> buttons, String comments) {}
var request = new ReviewRequest(
List.of(new TemplateButton("주문 상세 보기", "WL",
"https://shop.example.com/orders/#{주문번호}")),
"버튼 링크를 주문 상세 페이지로 수정했습니다.");
String response = restClient.post()
.uri("/templates/{templateCode}/review", "TPL_20260725143012")
.header("Idempotency-Key", UUID.randomUUID().toString())
.contentType(MediaType.APPLICATION_JSON)
.body(request)
.retrieve()
.body(String.class);
HTTP/1.1 200 OK
{
"code": "0000",
"message": "OK",
"data": {
"templateCode": "TPL_20260725143012",
"approveStatus": "REQ",
"requestedAt": "2026-07-25T15:12:03+09:00"
},
"meta": {
"requestId": "req_d40a7c55",
"quota": {
"rateLimit": {
"limit": 120,
"remaining": 115,
"resetAt": "2026-07-25T15:13:00+09:00"
},
"templateReview": {
"dailyLimit": 30,
"usedToday": 6,
"remaining": 24,
"resetAt": "2026-07-26T00:00:00+09:00"
}
}
}
}
HTTP/1.1 409 Conflict
{
"code": "4090",
"message": "현재 상태(APR)에서는 검수를 요청할 수 없습니다. 재검수는 REG·REJ·UPT 상태에서만 가능합니다.",
"data": null,
"meta": {
"requestId": "req_92c6e0af",
"quota": {
"rateLimit": {
"limit": 120,
"remaining": 114,
"resetAt": "2026-07-25T15:13:00+09:00"
}
}
}
}
신청 전 사전 점검 — 정책 검증
/templates/validate
필요 스코프: template:read
템플릿을 등록하지 않고 반려 위험을 사전 진단합니다.
요청 바디는 템플릿 신청과
동일하며(autoReview는 무시), 검수 쿼터를 소모하지 않습니다.
ERROR는 반려 사유이므로 반드시 수정하고,
WARN은 심사 지연 가능성이 있으므로 검토 후 진행하세요.
Request Body
템플릿 신청과 동일 규격입니다.
autoReview는 무시됩니다.
Response
정책 위반이 발견되지 않으면 true입니다.
발견된 위반 목록입니다.
ERROR(반려 사유) 또는 WARN(심사 지연 가능).
위반 규칙 코드입니다 (예: AD_WORDING).
위반이 발견된 요청 필드입니다.
위반 내용 설명입니다.
curl -X POST "https://docs.almani-center.com/open/v1/templates/validate" \
-H "Authorization: Bearer ak_live_xxxx" \
-H "X-Site-Id: 1024" \
-H "Content-Type: application/json" \
-d '{
"templateName": "여름 특가 안내",
"templateType": "BA",
"category": "004001",
"content": "#{고객명}님, 지금 여름 특가 할인 중입니다!",
"buttons": [
{
"name": "지금 확인하기",
"type": "WL",
"urlMobile": "https://event.example.com/summer"
}
]
}'
// Java 17 + Spring Framework 6 RestClient
RestClient restClient = RestClient.builder()
.baseUrl("https://docs.almani-center.com/open/v1")
.defaultHeader(HttpHeaders.AUTHORIZATION, "Bearer ak_live_xxxx")
.defaultHeader("X-Site-Id", "1024")
.build();
record TemplateButton(String name, String type, String urlMobile) {}
record ValidateRequest(
String templateName, String templateType, String category,
String content, List<TemplateButton> buttons) {}
var request = new ValidateRequest(
"여름 특가 안내", "BA", "004001",
"#{고객명}님, 지금 여름 특가 할인 중입니다!",
List.of(new TemplateButton("지금 확인하기", "WL",
"https://event.example.com/summer")));
String response = restClient.post()
.uri("/templates/validate")
.contentType(MediaType.APPLICATION_JSON)
.body(request)
.retrieve()
.body(String.class);
HTTP/1.1 200 OK
{
"code": "0000",
"message": "OK",
"data": {
"valid": false,
"violations": [
{
"severity": "ERROR",
"rule": "AD_WORDING",
"field": "content",
"message": "'특가', '할인' 등 광고성 표현은 정보성 알림톡에서 반려 사유입니다."
},
{
"severity": "WARN",
"rule": "BUTTON_URL_MISMATCH",
"field": "buttons[0].urlMobile",
"message": "버튼 URL 도메인이 사이트 도메인과 다릅니다. 심사 지연 가능성이 있습니다."
}
]
},
"meta": {
"requestId": "req_e5c31d78",
"quota": {
"rateLimit": {
"limit": 120,
"remaining": 118,
"resetAt": "2026-07-25T14:31:00+09:00"
}
}
}
}
HTTP/1.1 400 Bad Request
{
"code": "4000",
"message": "templateType은 필수입니다.",
"data": null,
"meta": {
"requestId": "req_17c95ab0",
"quota": {
"rateLimit": {
"limit": 120,
"remaining": 117,
"resetAt": "2026-07-25T14:31:00+09:00"
}
}
}
}
일일 검수 한도
템플릿 검수 요청은 사이트 단위로 하루 요청 수가 제한됩니다(일자 집계).
템플릿 신청·재검수·취소 응답의 meta.quota.templateReview에서
잔여 한도를 확인할 수 있고,
쿼터 전체 조회(GET /quota)로
사이트 기준 전체 쿼터를 한 번에 조회할 수도 있습니다.
meta.quota.templateReview 필드
하루에 허용되는 템플릿 검수 요청 수입니다.
오늘 사용한 검수 요청 수입니다.
오늘 남은 검수 요청 수입니다.
한도가 초기화되는 다음 날 자정입니다. ISO-8601, KST 오프셋 포함.
-
한도를 초과하면 검수 요청이
4291에러(HTTP 429)로 거절됩니다 — 다음 날 자정 이후 다시 시도하세요. 현재 한도는 사이트당 하루 30회로 고정됩니다. - 사전 정책 검증과 조회 API는 검수 한도를 소모하지 않습니다.
-
API 키 단위 분당 요청 한도(
meta.quota.rateLimit)는 모든 응답에 항상 포함되며, 동일 정보가 RateLimit-* 헤더로도 제공됩니다.
meta.quota에 포함되므로,
쿼터 확인을 위해 별도 API를 짧은 주기로 반복 호출하지 마세요.
반복 호출도 분당 요청 한도를 소모합니다.
HTTP/1.1 200 OK
{
"code": "0000",
"message": "OK",
"data": {
"templateId": 5811,
"templateCode": "TPL_20260725143012",
"approveStatus": "REQ",
"requestedAt": "2026-07-25T14:30:12+09:00"
},
"meta": {
"requestId": "req_9f83ab21",
"quota": {
"rateLimit": {
"limit": 120,
"remaining": 117,
"resetAt": "2026-07-25T14:31:00+09:00"
},
"templateReview": {
"dailyLimit": 30,
"usedToday": 5,
"remaining": 25,
"resetAt": "2026-07-26T00:00:00+09:00"
}
}
}
}
HTTP/1.1 429 Too Many Requests
{
"code": "4291",
"message": "일일 템플릿 검수 요청 한도를 초과했습니다. 다음 날 자정 이후 다시 시도해 주세요.",
"data": null,
"meta": {
"requestId": "req_66d1f0a3",
"quota": {
"rateLimit": {
"limit": 120,
"remaining": 116,
"resetAt": "2026-07-25T14:31:00+09:00"
},
"templateReview": {
"dailyLimit": 30,
"usedToday": 30,
"remaining": 0,
"resetAt": "2026-07-26T00:00:00+09:00"
}
}
}
}