AlgoLab Blog · 키움 인증 트러블슈팅 · 2026

키움 REST API 토큰 유효기간 — expires_dt와 폐기

키움증권 · 인증 2026-08-16 · 약 7분 읽기 · 알고랩 AlgoLab
한 줄 요약 키움 REST API 접근토큰의 유효기간은 공식 명세에 "몇 시간"으로 적혀 있지 않습니다. 대신 발급(au10001) 응답에 expires_dt(만료일) 필드가 오고, 공식 예시 값은 "20241107083713" 같은 14자리 문자열입니다. 즉 키움은 남은 시간이 아니라 만료 시각을 알려 줍니다. 그러니 코드에 86400이나 24 * 3600을 박지 말고 이 값을 파싱해서 그 시각 이전에 재발급하십시오. 폐기가 필요하면 au10002POST /oauth2/revokeappkey·secretkey·token을 넣어 호출합니다.

키움 REST API 자동매매 가이드대로 OpenAPI+에서 넘어와 첫 호출까지 성공하고 나면, 다음으로 반드시 마주치는 질문이 이겁니다. "이 토큰, 언제까지 쓸 수 있지?"

검색해 보면 답이 제각각입니다. 어디는 하루, 어디는 몇 시간이라고 합니다. 그런데 키움증권 REST API 공식 가이드의 au10001 명세를 직접 열어 보면 기간을 나타내는 항목 자체가 없습니다. 응답 필드는 expires_dt·token_type·token 셋뿐입니다. 이 글은 그 사실에서 출발해 토큰 수명을 어떻게 다뤄야 하는지만 다룹니다.

목차

  1. 발급 요청과 응답 실물 (au10001)
  2. expires_dt 읽는 법 — 형식과 타임존 함정
  3. "24시간"을 코드에 박으면 안 되는 이유
  4. 캐싱 + 선제 재발급 구현
  5. 폐기 au10002 — 언제 쓰나
  6. 키움 vs 한국투자증권 KIS 필드명 대조
  7. 만료 증상과 재시도 설계

1. 발급 요청과 응답 실물 (au10001)

항목
API IDau10001 (접근토큰 발급)
Method / URLPOST /oauth2/token
실전 도메인https://api.kiwoom.com
모의투자 도메인https://mockapi.kiwoom.com
Content-Typeapplication/json;charset=UTF-8
요청 필드grant_type(=client_credentials) · appkey · secretkey
응답 필드expires_dt · token_type · token

공식 문서의 예시를 그대로 옮깁니다.

# 요청
{
  "grant_type": "client_credentials",
  "appkey":     "AxserEsdcredca.....",
  "secretkey":  "SEefdcwcforehDre2fdvc...."
}

# 응답
{
  "expires_dt": "20241107083713",
  "token_type": "bearer",
  "token": "WQJCwyqInphKnR3bSRtB9NE1lv...",
  "return_code": 0,
  "return_msg": "정상적으로 처리되었습니다"
}

여기서 이미 두 가지가 갈립니다. ① 토큰이 담기는 키 이름이 access_token이 아니라 그냥 token입니다. ② 만료 정보가 남은 초(expires_in)가 아니라 시각(expires_dt)입니다. 다른 증권사 예제를 복사해 온 코드가 KeyError: 'access_token'으로 죽는 가장 흔한 이유가 이겁니다.

2. expires_dt 읽는 법 — 형식과 타임존 함정

"20241107083713"14자리입니다. 앞에서부터 끊으면 이렇습니다.

구간자리
1~42024
5~611
7~807
9~1008
11~1237
13~1413
from datetime import datetime
from zoneinfo import ZoneInfo

KST = ZoneInfo("Asia/Seoul")

def parse_expires(expires_dt: str) -> datetime:
    """'20241107083713' -> tz-aware datetime (KST)"""
    return datetime.strptime(expires_dt, "%Y%m%d%H%M%S").replace(tzinfo=KST)

exp = parse_expires("20241107083713")
print(exp)                       # 2024-11-07 08:37:13+09:00
print(exp - datetime.now(KST))   # 남은 시간

타임존을 빠뜨리면 9시간이 어긋납니다. 국내 증권사 API가 돌려주는 시각은 한국 시간 기준입니다. 그런데 클라우드 인스턴스나 도커 컨테이너는 기본이 UTC인 경우가 많습니다. datetime.now()(naive)와 strptime 결과를 그대로 빼면 UTC 서버에서는 토큰이 아직 9시간 남았다고 착각하거나 이미 만료됐다고 착각합니다. 전자는 장중 인증 실패로, 후자는 불필요한 재발급 폭주로 나타납니다. 서버 타임존을 Asia/Seoul로 고정하거나 위 코드처럼 명시적으로 붙이십시오.

3. "24시간"을 코드에 박으면 안 되는 이유

인터넷에서 특정 시간 수치를 본 적이 있을 겁니다. 그 값이 지금 맞을 수도 있습니다. 문제는 그게 공식 명세에 적힌 값이 아니라 관측값이라는 점입니다. 관측값을 상수로 박아 두면 세 가지 상황에서 조용히 깨집니다.

규칙 — 유효기간은 추측하지 말고 받아서 쓰십시오. 키움은 expires_dt, 한국투자증권 KIS는 access_token_token_expired입니다. 둘 다 "언제까지"를 알려 주는 값이라 코드가 상수에 의존할 이유가 없습니다.

캐시된 토큰 재사용 (호출 0회) 선제 재발급 au10001 발급 expires_dt 만료 시각 만료 시각에서 거꾸로 여유(예: 120초)를 빼고 재발급 — 발급 시각에 상수를 더하지 않는다
토큰 수명 관리 — 기준점은 발급 시각이 아니라 만료 시각

4. 캐싱 + 선제 재발급 구현

구조는 짧습니다. 토큰과 만료 시각을 한 쌍으로 들고 다니고, 요청 직전에 남은 시간을 확인해 부족하면 새로 받습니다.

import requests
from datetime import datetime, timedelta
from zoneinfo import ZoneInfo

KST  = ZoneInfo("Asia/Seoul")
HOST = "https://api.kiwoom.com"        # 모의: https://mockapi.kiwoom.com

class KiwoomToken:
    def __init__(self, appkey, secretkey, margin_sec=120):
        self.appkey, self.secretkey = appkey, secretkey
        self.margin = timedelta(seconds=margin_sec)
        self._token, self._exp = None, None

    def _issue(self):
        r = requests.post(
            f"{HOST}/oauth2/token",
            headers={"Content-Type": "application/json;charset=UTF-8"},
            json={"grant_type": "client_credentials",
                  "appkey": self.appkey, "secretkey": self.secretkey},
            timeout=10)
        r.raise_for_status()
        body = r.json()
        if body.get("return_code") != 0:
            raise RuntimeError(f"au10001 실패: {body.get('return_msg')}")
        self._token = body["token"]                      # access_token 아님
        self._exp   = datetime.strptime(body["expires_dt"], "%Y%m%d%H%M%S") \
                              .replace(tzinfo=KST)
        return self._token

    def get(self):
        if self._token and self._exp - self.margin > datetime.now(KST):
            return self._token                            # 캐시 재사용
        return self._issue()

    def revoke(self):
        """au10002 — 키 유출이 의심될 때만"""
        if not self._token:
            return
        requests.post(
            f"{HOST}/oauth2/revoke",
            headers={"Content-Type": "application/json;charset=UTF-8"},
            json={"appkey": self.appkey, "secretkey": self.secretkey,
                  "token": self._token},
            timeout=10)
        self._token, self._exp = None, None

실제 요청에는 token_typebearer이므로 authorization 헤더에 Bearer {token} 형태로 싣고, 키움은 여기에 어떤 TR을 부르는지 알리는 api-id 헤더가 추가로 필요합니다. 연속조회를 쓸 때 같이 나가는 cont-yn·next-key도 이 헤더 묶음에 들어갑니다.

인증까지는 됐는데 그 다음이 막혔다면

토큰·요청 제한·재접속은 봇 안정성의 기본 3종입니다. 어디까지 직접 만들고 어디부터 맡길지 먼저 정리해 드립니다.

30초 상담으로 범위 잡기 →

5. 폐기 au10002 — 언제 쓰나

항목
API IDau10002 (접근토큰폐기)
Method / URLPOST /oauth2/revoke
요청 필드appkey · secretkey · token
응답 필드return_code · return_msg (토큰 정보 없음)
# 요청
{
  "appkey":    "AxserEsdcredca.....",
  "secretkey": "SEefdcwcforehDre2fdvc....",
  "token":     "WQJCwyqInphKnR3bSRtB9NE1lv..."
}

# 응답
{
  "return_code": 0,
  "return_msg": "정상적으로 처리되었습니다"
}

평소에는 부를 일이 없습니다. 폐기가 정답인 상황은 하나입니다 — 앱키·시크릿키·토큰이 밖으로 샜을 가능성이 있을 때. 로그에 토큰을 통째로 찍어 뒀거나, 설정 파일을 저장소에 커밋했거나, 화면 공유 중에 노출된 경우입니다.

봇 종료 시 자동 폐기는 신중하게. "프로세스 끝날 때 revoke"를 넣어 두는 구성이 깔끔해 보이지만, 같은 앱키로 여러 프로세스가 도는 구조라면 한쪽의 종료가 다른 쪽이 쓰던 토큰까지 무효화할 수 있습니다. 장중에 조회 프로세스와 주문 프로세스를 분리해 돌리는 봇에서 실제로 겪는 사고입니다. 멀티 프로세스라면 폐기는 수동 운영 명령으로만 두세요.

그리고 당연한 이야기지만, 앱키·시크릿키·토큰은 코드에 넣지 말고 환경변수나 별도 설정 파일로 분리하고 그 파일은 버전관리에서 제외하십시오. 폐기를 부를 일 자체를 안 만드는 것이 가장 좋습니다.

6. 키움 vs 한국투자증권 KIS 필드명 대조

두 증권사를 함께 쓰는 봇이 늘었습니다. 그런데 인증 부분이 이름부터 전부 다릅니다.

구분키움 REST API한국투자증권 KIS
발급 URL/oauth2/token/oauth2/tokenP
API ID / 식별au10001tr_id 체계와 별도
시크릿 키 이름secretkeyappsecret
토큰 필드tokenaccess_token
만료 필드expires_dt (시각)access_token_token_expired (시각)
폐기au10002 · /oauth2/revoke별도 폐기 절차 확인 필요
결과 코드return_code · return_msgrt_cd · msg1 · msg_cd

이 표가 말하는 건 하나입니다 — 브로커별 어댑터를 두고 내부에서는 하나의 형태로 정규화하라. 키움과 KIS 비교에서 다룬 구조 차이가 인증 계층에서 가장 먼저 드러납니다. KIS 쪽 토큰 수명 처리는 KIS access_token 재발급 규칙에 따로 정리해 뒀습니다.

7. 만료 증상과 재시도 설계

만료된 토큰으로 조회나 주문을 부르면 인증 실패로 떨어집니다. 여기서 코드가 흔히 저지르는 실수는 이 실패를 일시적 네트워크 오류로 취급하는 것입니다.

# 나쁜 예 — 같은 만료 토큰으로 계속 두드린다
for _ in range(10):
    r = call_api(token)          # 계속 인증 실패
    if r.ok: break
    time.sleep(1)                # 실패 10회 + 호출 제한 소모

# 좋은 예 — 한 번만 재발급하고 한 번만 재시도
r = call_api(tokens.get())
if is_auth_error(r):
    r = call_api(tokens._issue())   # 강제 재발급 후 1회 재시도
    if is_auth_error(r):
        raise RuntimeError("재발급 후에도 인증 실패 — 키 상태 확인 필요")

무한 재시도가 위험한 이유는 실패가 해결되지 않는데 호출 횟수만 늘기 때문입니다. 키움 REST API 429 — 허용된 요청 개수 초과가 이 패턴에서 자주 따라옵니다. 인증 실패와 호출 제한이 겹치면 원인 파악이 훨씬 어려워집니다.

토큰 발급 자체도 아껴야 할 호출입니다. 매 요청마다 발급받는 구현은 초보 단계에서 놀랄 만큼 흔한데, 장중 수백 회 호출하는 봇이라면 그것만으로 제한에 닿습니다. VI 발동 구간처럼 이벤트가 몰리는 시간대에 이 문제가 먼저 터집니다.

자주 묻는 것

모의투자와 실전의 토큰을 같이 쓸 수 있나요?

안 됩니다. api.kiwoom.commockapi.kiwoom.com은 별개 환경이라 앱키도 토큰도 각각 받아야 합니다. 설정에서 도메인만 바꾸고 토큰을 재사용하면 인증 실패가 납니다. 키움 OpenAPI 입문 단계에서 모의로 붙였다가 실전 전환할 때 가장 자주 걸리는 지점입니다.

토큰을 파일에 저장해도 되나요?

프로세스를 자주 재시작한다면 저장하는 편이 낫습니다. 매번 새로 발급받는 것보다 호출을 아낍니다. 다만 토큰은 비밀번호와 같은 등급으로 다루십시오 — 권한을 제한한 경로에 두고, 로그에 찍지 말고, 저장소에 올리지 마십시오. 저장 시에는 tokenexpires_dt를 같이 저장해야 의미가 있습니다. 토큰만 저장하면 만료 판단을 못 해서 결국 매번 재발급하게 됩니다.

이 값들을 그대로 믿어도 되나요?

이 글의 API ID·URL·요청/응답 필드명·예시 값은 2026-08-16 시점 키움증권 REST API 공식 가이드에서 확인한 것입니다. 한국투자증권 쪽 필드명은 공개된 예제 코드 기준입니다. 증권사 명세는 예고 없이 바뀌므로, 운영에 넣기 전 각 개발자 포털의 현재 명세로 대조하시고 도메인·필드명은 코드 상수 대신 설정으로 빼 두시기 바랍니다.

인증부터 주문까지 한 번에 만들고 싶다면

토큰 관리·재시도·호출 제한까지 묶어서 실제로 도는 봇으로 만들어 드립니다. 24시간 빠른 답변 가능합니다.

무료 상담 시작하기