템플릿 검수 가이드

알림톡 템플릿은 등록 후 카카오 심사를 통과(승인)해야 발송에 사용할 수 있습니다. 이 가이드는 검수 상태가 어떻게 바뀌는지, 반려되면 사유를 어디서 확인하고 어떤 순서로 대응하는지, 그리고 반려를 미리 줄이는 방법(사전 점검)과 일일 검수 한도를 설명합니다.

시작 전에 Base URL은 https://docs.almani-center.com/open/v1 입니다. 모든 요청에는 인증에서 발급한 API 키(Authorization: Bearer ak_live_xxxx — 예시는 더미 키)와 대상 사이트를 지정하는 X-Site-Id 헤더가 필요합니다. 템플릿 API 전체 규격은 템플릿 API 레퍼런스를 참고하세요.

검수 상태 흐름

일반적인 흐름은 REGREQAPR 또는 REJ 입니다. 반려된 템플릿은 수정 후 재검수를 요청해 다시 REQ가 되고, 검수중(REQ) 요청을 취소하면 REG로 되돌아갑니다.

템플릿 검수 상태 흐름도: REG에서 검수 요청으로 REQ, 카카오 심사 결과에 따라 APR(승인) 또는 REJ(반려), REJ는 수정 후 재검수로 REQ 복귀, REQ에서 취소하면 REG 복귀 검수 요청 취소 승인 반려 수정 후 재검수 REG 등록 · 초안 REQ 검수중 (카카오 심사) APR 승인 · 발송 가능 REJ 반려

수정중(UPT) 상태의 템플릿도 재검수 요청 대상입니다. 재검수는 REG·REJ·UPT 상태에서만 요청할 수 있습니다.

상태별 의미

상태의미설명
REG 등록 검수 요청 전 초안 상태입니다. autoReview: false로 등록하거나 검수 요청을 취소하면 이 상태가 됩니다.
REQ 검수중 카카오 심사가 진행 중입니다.
APR 승인 심사를 통과했습니다. 발송에 사용할 수 있습니다(sendable: true).
REJ 반려 심사에서 반려되었습니다. rejectReason으로 사유를 확인하고 수정 후 재검수를 요청하세요.
UPT 수정중 템플릿을 수정하고 있는 상태입니다. 재검수 요청 대상입니다.

상태별로 모아 보려면 템플릿 목록 조회status 필터를 사용하세요. 복수 상태는 콤마로 구분합니다 (예: status=REQ,REJ).

반려 사유 확인법

반려(REJ) 사유는 세 곳에서 확인할 수 있습니다.

  1. 템플릿 조회data.rejectReason에 카카오 심사 코멘트 전문(최신)이 담깁니다. recentHistory로 최근 검수 이력 5건도 함께 확인합니다.
  2. 템플릿 목록 조회status=REJ 필터로 반려된 템플릿을 모아 보고, items[].rejectReason에서 최신 반려 사유 요약을 확인합니다.
  3. 검수 이력 — 여러 번 반려·재검수를 거친 템플릿은 review-history로 전체 이력을 시간순으로 확인합니다. 카카오 심사 코멘트는 items[].reviewer"카카오"인 항목입니다.
상세 조회는 AL_TEMPLATE의 저장 상태를 반환합니다 템플릿 조회는 REQ 상태에서도 요청 중 비즈톡을 실시간으로 재조회하지 않습니다.

반려 사유 확인 — 템플릿 조회

GET /templates/{templateCode}

필요 스코프: template:read

템플릿 단건 상세와 검수 상태를 조회합니다. 반려된 템플릿이면 rejectReason에 카카오 심사 코멘트 전문(최신)이 담깁니다. 전체 응답 필드는 템플릿 조회 레퍼런스를 참고하세요.

Path 파라미터

templateCode 필수 string

템플릿 코드입니다 (예: TPL_20260725143012).

Response (반려 확인 관련 필드)

approveStatus string

현재 상태입니다 — REG/REQ/APR/REJ/UPT.

rejectReason string | null

REJ일 때 카카오 심사 코멘트 전문(최신)입니다.

sendable boolean

APR 여부 — 발송에 사용할 수 있는지를 나타냅니다.

recentHistory[] array

최근 검수 이력 5건입니다 — { status, comment, at }. 전체 이력은 검수 이력 조회를 사용합니다.

참고 우측 응답 예시는 반려(REJ) 케이스입니다. 버튼 링크가 메시지 내용과 무관한 페이지로 연결되어 반려된 사례로, 아래 반려 대응 절차에서 이 사례를 이어서 처리합니다.
요청 예시
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"
      }
    }
  }
}

검수 이력 조회

GET /templates/{templateCode}/review-history

필요 스코프: template:read

템플릿의 전체 검수 이력을 시간순으로 반환합니다. 반려가 반복된 템플릿에서 이전 반려 사유와 수정 내역을 되짚어볼 때 사용합니다. 커서 페이지네이션(공통 규약 참조)을 지원합니다.

Path 파라미터

templateCode 필수 string

템플릿 코드입니다.

Query 파라미터

limit 선택 int

페이지 크기입니다. 기본 20, 최대 100.

cursor 선택 string

이전 응답의 data.nextCursor 값입니다.

Response

items[].status string

REQ | APR | REJ.

items[].comment string

검수 요청 의견 또는 카카오 심사 코멘트입니다.

items[].reviewer string | null

코멘트 작성 주체입니다. 카카오 심사 코멘트는 "카카오"입니다.

items[].at string

이력 발생 시각입니다.

nextCursor string | null

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

반려 대응 절차

  1. 사유 확인템플릿 조회rejectReason(전문)과 recentHistory를 확인합니다. 반려가 반복된 템플릿은 검수 이력 조회로 전체 흐름을 되짚습니다.
  2. 템플릿 수정 — 사유에 해당하는 요소를 수정합니다. 예를 들어 '특가', '할인' 등 광고성 표현은 정보성 알림톡에서 반려 사유이고, 버튼 URL이 메시지 내용과 무관한 페이지로 연결되면 반려됩니다. 변수만으로 구성된 본문도 반려됩니다.
  3. 사전 점검 (권장) — 수정본을 사전 정책 검증으로 먼저 진단합니다. 검수 쿼터를 소모하지 않습니다.
  4. 재검수 요청재검수 요청 바디에 수정할 필드를 함께 보내면 수정과 재검수를 한 번에 처리합니다. comments에 반려 사유에 대한 소명을 담는 것을 권장합니다.
  5. 결과 확인템플릿 조회에서 알마니에 저장된 현재 상태와 반려 사유를 확인합니다. 이 조회는 비즈톡 상태를 실시간으로 재조회하지 않습니다.
주의 재검수 요청은 REG·REJ·UPT 상태에서만 가능하며, 그 외 상태에서 호출하면 4090 에러가 반환됩니다. 재검수 요청도 일일 검수 한도를 소모하므로, 신청 전 사전 점검으로 반려 가능성을 먼저 줄이세요.

수정 후 재검수 요청

POST /templates/{templateCode}/review

필요 스코프: template:write

반려(REJ) · 초안(REG) · 수정중(UPT) 상태 템플릿의 검수를 (재)요청합니다. 요청 바디에 수정할 필드를 함께 보내면 수정과 재검수를 한 번에 처리합니다. 성공 시 상태는 REQ가 됩니다.

Path 파라미터

templateCode 필수 string

템플릿 코드입니다.

Request Body

content / buttons / emphasis / image / itemList / mappingData 선택

템플릿 신청과 동일 규격입니다. 보낸 필드만 수정한 뒤 검수를 요청합니다.

comments 선택 string

카카오 심사 담당자에게 전달되는 의견입니다. 반려 사유에 대한 소명을 권장합니다.

Response

templateCode string

템플릿 코드입니다.

approveStatus string

성공 시 REQ가 됩니다.

requestedAt string

검수 요청 시각입니다.

주요 에러

코드HTTP설명
4090409 상태 전이 불가 — REG·REJ·UPT 이외 상태에서 요청 (message에 현재 상태 포함)
4291429일일 템플릿 검수 요청 한도 초과
요청 예시
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"
      }
    }
  }
}

신청 전 사전 점검 — 정책 검증

POST /templates/validate

필요 스코프: template:read

템플릿을 등록하지 않고 반려 위험을 사전 진단합니다. 요청 바디는 템플릿 신청과 동일하며(autoReview는 무시), 검수 쿼터를 소모하지 않습니다.

권장 사용법 템플릿 신청·재검수 요청 전에 이 API로 먼저 검증하면 반려로 인한 재작업과 일일 검수 한도 소모를 줄일 수 있습니다. ERROR는 반려 사유이므로 반드시 수정하고, WARN은 심사 지연 가능성이 있으므로 검토 후 진행하세요.

Request Body

templateName / templateType / category / content / …

템플릿 신청과 동일 규격입니다. autoReview는 무시됩니다.

Response

valid boolean

정책 위반이 발견되지 않으면 true입니다.

violations[] array of object

발견된 위반 목록입니다.

violations[].severity string

ERROR(반려 사유) 또는 WARN(심사 지연 가능).

violations[].rule string

위반 규칙 코드입니다 (예: AD_WORDING).

violations[].field string

위반이 발견된 요청 필드입니다.

violations[].message string

위반 내용 설명입니다.

요청 예시
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"
      }
    }
  }
}

일일 검수 한도

템플릿 검수 요청은 사이트 단위로 하루 요청 수가 제한됩니다(일자 집계). 템플릿 신청·재검수·취소 응답의 meta.quota.templateReview에서 잔여 한도를 확인할 수 있고, 쿼터 전체 조회(GET /quota)로 사이트 기준 전체 쿼터를 한 번에 조회할 수도 있습니다.

meta.quota.templateReview 필드

dailyLimit int

하루에 허용되는 템플릿 검수 요청 수입니다.

usedToday int

오늘 사용한 검수 요청 수입니다.

remaining int

오늘 남은 검수 요청 수입니다.

resetAt string

한도가 초기화되는 다음 날 자정입니다. 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"
      }
    }
  }
}