KAT Cloud로그인

API 에러 처리와 Rate Limit 대응: 429 재시도와 지수 백오프

KAT Cloud API 에러는 status, code, message 형식으로 오고 Rate Limit은 1,200 requests/60초입니다. 상태 코드별 처리 기준, AU002·AU017·CH001·CH002 에러 코드, 재시도·지수 백오프 코드를 정리했습니다.

가이드KAT Cloud 팀10분 읽기
API 에러 처리와 Rate Limit 대응: 429 재시도와 지수 백오프

KAT Cloud API는 요청이 실패하면 {"status", "code", "message"} 형식의 에러 본문과 HTTP 상태 코드를 함께 돌려주며, Rate Limit은 1,200 requests / 60초입니다. 429(한도 초과)와 500(서버 오류), 네트워크 오류는 지수 백오프로 재시도하고, 400·401·403·404는 재시도해도 결과가 같으므로 바로 실패로 처리하는 것이 기본 원칙입니다. 이 글은 문서에 나온 에러 코드와 함께 Python·Node.js 재시도 코드를 정리합니다.

KAT Cloud API 에러 형식과 상태 코드

KAT Cloud(캣클라우드) API의 성공 응답은 {"code": "0", "message": "Success", "result": ...} 구조입니다. 실패하면 아래처럼 HTTP 상태를 담은 status, 에러 코드 code, 사람이 읽는 message가 옵니다.

JSON
{
  "status": 401,
  "code": "AU002",
  "message": "올바르지 않은 토큰입니다."
}

API 문서의 HTTP 상태 코드 정의는 다음과 같습니다.

HTTP의미
400잘못된 요청(파라미터 오류)
401인증 실패(API Key 없음·무효)
403권한 없음(그룹 접근 불가 또는 요금제·허용 조건 미충족, AU017)
404리소스 없음
429Rate Limit 초과
500서버 오류

문서에 구체적으로 나온 에러 코드는 아래와 같습니다. CH001, CH002, Validation Error는 POST /message/send 요청 실패 시의 코드입니다.

HTTPcode문서 설명대응
401AU002올바르지 않은 토큰입니다.키 문자열과 x-api-key 헤더 확인
403AU017문자 발신 외 Studio API는 BUSINESS 요금제이거나 별도 허용된 그룹만 사용할 수 있습니다.키에 연결된 그룹의 조건 확인
400Validation Error요청 형식 오류(전화번호 형식, 본문 길이, 제목 길이, imageList 항목 길이 등)요청 값 수정
404CH001발신에 사용할 채널을 찾을 수 없습니다.발신 번호가 내 그룹 번호인지 확인
503CH002문자 발송에 실패했습니다.잠시 뒤 확인 후 재시도

로그에는 상태 코드만 남기지 말고 code와 message, 요청 메서드와 경로, 재시도 횟수를 함께 남깁니다. 같은 403이라도 AU017인지 다른 원인인지 로그만 보고 구분할 수 있어야 합니다. AU017의 원인 찾기는 API 키 발급과 인증에서 자세히 다룹니다.

재시도할 에러와 하지 않을 에러

재시도는 시간이 지나면 결과가 달라질 수 있는 에러에만 합니다. 키가 틀렸거나 요청 값이 잘못됐다면 몇 번을 다시 보내도 같은 에러가 나고, 그동안 Rate Limit만 소모합니다.

429, 500, 네트워크 오류는 재시도하고, 503 CH002와 POST 요청은 중복 여부를 확인한 뒤 재시도하며, 400, 401, 403, 404는 재시도하지 않는 기준을 세 열로 비교한 그림
재시도해도 결과가 같은 에러는 바로 실패로 처리합니다.

조회(GET)는 몇 번을 보내도 상태가 바뀌지 않으므로 자유롭게 재시도할 수 있습니다. 반면 POST는 첫 요청이 실제로는 처리됐는데 응답만 늦게 끊긴 경우가 있어, 그냥 다시 보내면 그룹이 두 개 생기거나 문자가 두 번 나갈 수 있습니다. POST를 안전하게 재시도하는 방법은 마지막 절에서 다룹니다.

Rate Limit 1,200 requests / 60초 다루기

KAT Cloud API의 Rate Limit은 60초에 1,200건, 평균으로 초당 20건입니다. 한도가 API 키 단위인지 계정 단위인지는 문서에 명시되어 있지 않으므로, 같은 키를 쓰는 서버·배치·워커의 요청을 합쳐서 한도 안에 들어가도록 설계하는 편이 안전합니다. 429 응답에 대기 시간을 알려 주는 헤더가 오는지도 문서에 나와 있지 않으므로, 대기 시간은 클라이언트가 지수 백오프로 정합니다.

한도에 부딪히지 않는 가장 좋은 방법은 요청 수 자체를 줄이는 것입니다.

  • 새 통화를 확인하려고 목록을 반복 조회하지 말고 CALL_COMPLETED 웹훅을 받습니다.
  • 웹훅을 쓸 수 없다면 updatedAfter 증분 조회로 갱신된 기록만 가져옵니다. 방법은 통화 기록 증분 동기화에 정리했습니다.
  • GET /call-history는 limit을 최대값 100으로 올려 페이지 수를 줄입니다.
  • GET /group처럼 자주 바뀌지 않는 값은 몇 분 단위로 캐시합니다.
  • 대량 작업은 클라이언트 쪽에서 속도를 제한합니다. 아래 코드는 60초에 1,000건으로 여유를 두었습니다.

Python: 재시도와 지수 백오프 구현

아래 코드는 KAT Cloud 요청을 한곳에서 처리하는 함수입니다. 60초 슬라이딩 윈도로 요청 속도를 제한하고, 재시도 대상 에러는 대기 시간을 1초, 2초, 4초처럼 두 배씩 늘리되 최대 60초로 묶고 무작위 지터를 섞습니다. 지터는 여러 워커가 같은 순간에 일제히 재시도해 다시 한도에 걸리는 것을 막아 줍니다.

내 서버가 요청을 보내 429를 받으면 1초 안팎 기다렸다가 다시 보내고, 또 429를 받으면 2초 안팎 기다렸다가 다시 보내 성공 응답을 받는 지수 백오프 순서
대기 시간을 두 배씩 늘리고 무작위 지터를 섞습니다.
Python
import os
import random
import time
from collections import deque

import requests

BASE_URL = "https://studio-gateway.zerocall.kr"
RETRYABLE = {429, 500, 502, 503, 504}

session = requests.Session()
session.headers.update({
    "x-api-key": os.environ["KAT_API_KEY"],
    "Content-Type": "application/json",
})


class KatApiError(Exception):
    def __init__(self, status, code, message):
        super().__init__(f"{status} {code} {message}")
        self.status, self.code, self.message = status, code, message


class RateLimiter:
    """period초 동안 max_calls건 이하로 보내는 슬라이딩 윈도"""

    def __init__(self, max_calls=1000, period=60.0):
        self.max_calls, self.period = max_calls, period
        self.calls = deque()

    def wait(self):
        now = time.monotonic()
        while self.calls and now - self.calls[0] >= self.period:
            self.calls.popleft()
        if len(self.calls) >= self.max_calls:
            time.sleep(self.period - (now - self.calls[0]))
        self.calls.append(time.monotonic())


limiter = RateLimiter(max_calls=1000)  # 문서 한도 1,200보다 여유 있게


def to_error(resp):
    try:
        body = resp.json()
    except ValueError:
        body = {}
    return KatApiError(body.get("status", resp.status_code), body.get("code"), body.get("message"))


def kat_request(method, path, *, retry=None, max_retries=5, **kwargs):
    if retry is None:
        retry = method.upper() == "GET"  # 기본은 조회만 재시도
    for attempt in range(max_retries + 1):
        limiter.wait()
        try:
            resp = session.request(method, BASE_URL + path, timeout=15, **kwargs)
        except (requests.ConnectionError, requests.Timeout):
            if not retry or attempt == max_retries:
                raise
        else:
            if resp.ok:
                if not resp.content:
                    return None  # 204 No Content
                body = resp.json()
                return body["result"] if isinstance(body, dict) and "result" in body else body
            if not retry or resp.status_code not in RETRYABLE or attempt == max_retries:
                raise to_error(resp)
        delay = min(60.0, 1.0 * 2 ** attempt)
        time.sleep(delay / 2 + random.uniform(0, delay / 2))  # 지수 백오프 + 지터


try:
    groups = kat_request("GET", "/group")
except KatApiError as e:
    if e.code == "AU017":
        print("비즈니스 기능을 쓸 수 없는 그룹이 키에 연결되어 있습니다.")
    raise

Node.js: fetch로 같은 전략 구현

Node.js 18 이상에서는 내장 fetch와 AbortSignal.timeout으로 같은 전략을 만들 수 있습니다.

JavaScript
const BASE_URL = 'https://studio-gateway.zerocall.kr';
const RETRYABLE = new Set([429, 500, 502, 503, 504]);
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

class KatApiError extends Error {
  constructor(status, code, message) {
    super(`${status} ${code} ${message}`);
    this.status = status;
    this.code = code;
  }
}

async function katRequest(method, path, { body, retry = method === 'GET', maxRetries = 5 } = {}) {
  for (let attempt = 0; ; attempt++) {
    let res;
    try {
      res = await fetch(`${BASE_URL}${path}`, {
        method,
        headers: { 'x-api-key': process.env.KAT_API_KEY, 'Content-Type': 'application/json' },
        body: body ? JSON.stringify(body) : undefined,
        signal: AbortSignal.timeout(15000),
      });
    } catch (networkError) {
      if (!retry || attempt >= maxRetries) throw networkError;
    }
    if (res) {
      if (res.ok) {
        const text = await res.text();
        if (!text) return null; // 204 No Content
        const json = JSON.parse(text);
        return json && typeof json === 'object' && 'result' in json ? json.result : json;
      }
      const err = await res.json().catch(() => ({}));
      if (!retry || !RETRYABLE.has(res.status) || attempt >= maxRetries) {
        throw new KatApiError(err.status ?? res.status, err.code, err.message);
      }
    }
    const delay = Math.min(60000, 1000 * 2 ** attempt);
    await sleep(delay / 2 + Math.random() * (delay / 2)); // 지수 백오프 + 지터
  }
}

katRequest('GET', '/call-history?groupId=1&limit=100')
  .then((page) => console.log(page.total))
  .catch((e) => console.error(e.status, e.code, e.message));

cURL로 스크립트를 짤 때도 원칙은 같습니다. -w "%{http_code}"로 상태 코드를 받아 429나 5xx일 때만 대기 후 다시 호출합니다.

Shell
for i in 1 2 3 4 5; do
  code=$(curl -s -o resp.json -w "%{http_code}" \
    "https://studio-gateway.zerocall.kr/group" -H "x-api-key: $KAT_API_KEY")
  case "$code" in
    2*) cat resp.json; break ;;
    429|5*) sleep $((2 ** i)) ;;
    *) cat resp.json; exit 1 ;;
  esac
done

POST 요청을 안전하게 재시도하는 법

POST는 기본값으로 재시도하지 않고, 엔드포인트마다 중복을 막을 장치를 확인한 뒤에만 재시도합니다.

요청중복 방지 방법
POST /chat(문자 발송)문서에 중복 방지용으로 설명된 externalMessageId를 업무 키로 만들어, 재시도 때도 같은 값을 보냅니다.
POST /group재시도 전에 GET /group으로 같은 title의 그룹이 이미 생겼는지 확인합니다.
POST /webhook재시도 전에 GET /webhook으로 같은 eventType·url이 있는지 확인합니다.
POST /message/sendDeprecated API이며 중복 방지 필드가 없습니다. 새 연동은 채팅 API를 씁니다.
Python
# 주문번호처럼 업무에서 유일한 값으로 외부 ID를 만들면, 재시도해도 같은 값이 나갑니다.
kat_request("POST", "/chat", retry=True, json={
    "chatRoomId": "yyyyyyyyyyyyyyyyyyyyyyyyyyyyyy",
    "content": "예약이 확정되었습니다.",
    "externalMessageId": "order-20261029-0001-confirm",
})

재시도 후에도 실제 발송 여부가 애매하다면 채팅방의 대화 조회(GET /chat)로 확인합니다. 채팅 API로 문자를 보내는 전체 흐름은 채팅 API로 문자 보내기에 정리했습니다. 에러 형식 원문은 API 문서의 에러 처리에 있습니다.

자주 묻는 질문

KAT Cloud API에서 429를 받으면 얼마나 기다려야 하나요?

KAT Cloud API 문서는 Rate Limit을 1,200 requests / 60초로 밝히고 있지만, 429 응답 후 대기 시간이나 관련 헤더는 따로 정하지 않았습니다. 1초에서 시작해 두 배씩 늘리고 최대 60초로 묶은 지수 백오프에 무작위 지터를 섞어 재시도하고, 근본적으로는 요청 속도를 한도 아래로 낮춥니다.

403 AU017 에러도 재시도하면 해결되나요?

KAT Cloud의 403 AU017은 요청이 접근하는 그룹이 비즈니스 요금제 또는 별도 허용 조건을 충족하지 못했다는 뜻이라 재시도로 해결되지 않습니다. API 키에 연결된 그룹의 요금제를 확인하거나, 비즈니스 기능용 키에 조건을 충족하는 그룹만 연결해야 합니다.

문자 발송 요청을 재시도하면 문자가 두 번 나가지 않나요?

KAT Cloud 채팅 API의 POST /chat은 중복 방지용 externalMessageId 필드를 제공하므로, 재시도할 때 같은 값을 넣어 보내는 방식으로 중복 발송을 막는 데 씁니다. Deprecated된 POST /message/send에는 이런 필드가 없으므로 새 연동은 채팅 API를 쓰는 것이 좋습니다.

  • #에러 처리
  • #Rate Limit
  • #재시도
  • #지수 백오프
  • #KAT Cloud API

Get started

만든 AI에게 전화번호를 주세요.

070 번호 발급, SIP 직접 연결, 음성 AI 모델 연결, 문자·웹훅까지 API 하나로. 월 10,000원부터.