AlgoLab Blog · KIS API 트러블슈팅 · 2026

KIS API 토큰 만료 — access_token 재발급 규칙

한국투자증권 · 인증 2026-08-09 · 약 8분 읽기 · 알고랩 AlgoLab
한 줄 요약 KIS API의 access_token매번 새로 받는 값이 아니라 저장해 두고 재사용하는 값입니다. 공식 인증 예제(kis_auth.py) 기준 규칙은 넷입니다 — ① 유효시간 1일(응답의 access_token_token_expired에 만료 일시가 들어옴), ② 발급은 1분에 1회(넘기면 차단), ③ 6시간 이내 재신청은 기존 토큰 값이 그대로 유지, ④ 발급할 때마다 알림이 발송. 그래서 정답은 파일 캐싱입니다 — POST /oauth2/tokenP로 받은 토큰과 만료 일시를 저장하고, 만료 전이면 저장된 값을 그대로 쓰십시오. 루프마다 재발급하는 코드는 몇 초 만에 막힙니다.

KIS API 키 발급 가이드대로 appkeyappsecret을 받고 첫 주문까지 성공하면, 그다음 날 아침 이런 일이 벌어집니다. 어제 잘 되던 코드가 인증 오류를 뱉습니다. 토큰이 만료된 겁니다.

여기서 가장 흔한 대응이 "그럼 호출할 때마다 새로 받자"인데, 이게 두 번째 사고를 부릅니다. 이 글은 그 사이에서 정확히 무슨 규칙이 걸려 있는지, 그리고 어떻게 짜야 하는지에 대한 글입니다.

목차

  1. appkeyaccess_token은 뭐가 다른가
  2. 발급 실물 — POST /oauth2/tokenP 요청·응답
  3. 규칙 4가지 — 1일 · 1분 1회 · 6시간 · 알림
  4. 정답 코드 — 만료 일시 기준 캐싱
  5. 인증이 실패할 때 확인 순서
  6. 웹소켓 approval_key는 별개다
  7. 토큰을 다루는 보안 원칙

1. appkeyaccess_token은 뭐가 다른가

처음 헷갈리는 지점입니다. KIS Developers에서 앱을 등록하면 appkeyappsecret 두 개를 받는데, 이것만으로는 조회도 주문도 되지 않습니다.

appkey / appsecretaccess_token
비유신분증출입증
얻는 곳KIS Developers 포털에서 1회 발급POST /oauth2/tokenP로 매번 받음
수명재발급 전까지 유지1일
보내는 위치appkey·appsecret 헤더authorization: Bearer ... 헤더
노출되면치명적 — 즉시 재발급치명적 — 만료 전까지 주문 가능

신분증을 보여주고 출입증을 받아, 그 출입증으로 하루를 다닌다고 보면 됩니다. 조회·주문 요청에는 appkey·appsecret·authorization·tr_id함께 들어갑니다.

2. 발급 실물 — POST /oauth2/tokenP 요청·응답

공식 저장소 koreainvestment/open-trading-api의 인증 모듈 kis_auth.py가 실제로 하는 일은 이렇습니다.

import json, requests

BASE = "https://openapi.koreainvestment.com:9443"      # 실전
# BASE = "https://openapivts.koreainvestment.com:29443" # 모의투자

def issue_token(appkey, appsecret):
    url = BASE + "/oauth2/tokenP"
    payload = {
        "grant_type": "client_credentials",
        "appkey": appkey,
        "appsecret": appsecret,
    }
    headers = {"content-type": "application/json; charset=utf-8"}
    res = requests.post(url, data=json.dumps(payload), headers=headers, timeout=10)
    res.raise_for_status()
    return res.json()

grant_typeclient_credentials 고정입니다. 그리고 정상 응답은 이렇게 생겼습니다 — 여기서 봐야 할 필드가 두 개입니다.

{
  "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ0b2tlbiIs ...(생략)",
  "access_token_token_expired": "2026-08-10 09:14:22",
  "token_type": "Bearer",
  "expires_in": 86400
}
필드쓰임
access_token이후 모든 요청의 authorization: Bearer {값}
access_token_token_expired만료 일시. 이 값을 저장해 두고 현재 시각과 비교
token_typeBearer — 헤더 접두어
expires_in남은 초

24시간을 직접 계산하지 마십시오. datetime.now() + timedelta(hours=24)로 만료를 추정하는 코드가 흔한데, 응답이 만료 일시를 문자열로 그대로 알려줍니다. 공식 예제도 access_token_token_expired를 파일에 적어 두고 다음 실행 때 현재 시각과 문자열 비교로 판정합니다. 정책이 바뀌어도 서버가 준 값을 쓰면 코드가 따라갑니다.

3. 규칙 4가지 — 1일 · 1분 1회 · 6시간 · 알림

① 유효시간은 1일

공식 인증 모듈 주석에 토큰 유효시간 1일로 명시돼 있습니다. 하루가 지나면 같은 토큰으로 보낸 요청은 인증에서 막힙니다.

② 발급은 1분에 1회

여기서 대부분 사고가 납니다. 토큰 발급 호출 자체에 빈도 제한이 있어서 1분에 1회를 넘겨 시도하면 차단됩니다. 함수 안에서 매번 issue_token()을 부르는 구조는 몇 초 만에 이 제한에 도달하고, 그 순간부터 발급도 안 되고 주문도 못 넣는 상태가 됩니다.

이 패턴을 피하십시오.

# ❌ 최악 — 호출마다 재발급
def get_price(code):
    token = issue_token(APPKEY, APPSECRET)["access_token"]   # 매번 발급
    ...

for code in universe:          # 종목 30개 루프
    get_price(code)            # → 30회 발급 시도 → 즉시 차단

이건 초당 거래건수 초과(EGW00201)와는 별개의 제한입니다. 그쪽은 조회·주문의 초당 한도, 이쪽은 토큰 발급 자체의 분당 한도입니다. 둘 다 막히면 원인 찾기가 훨씬 어려워집니다.

③ 6시간 이내 재신청은 기존 값이 유지된다

공식 예제 주석에 6시간 이내 발급 신청 시 기존 토큰 값이 유지된다는 안내가 있습니다. 즉 짧은 간격으로 다시 요청해도 새 문자열이 아니라 같은 값이 돌아옵니다.

이 사실을 모르면 이런 오진을 합니다 — "재발급했는데 토큰이 안 바뀌네? 발급이 실패했나 보다" 하고 다시 재발급을 시도합니다. 아무 효과 없이 ②의 분당 제한에만 걸립니다. 토큰이 안 먹히는 원인은 대개 토큰이 아니라 헤더 구성이나 환경 불일치입니다(5절 참고).

④ 발급하면 알림이 발송된다

공식 예제 주석은 토큰 발급 시 알림이 발송된다고 안내합니다. 개발 중 재발급을 반복하면 알림이 계속 쌓입니다. 캐싱을 해야 할 이유가 하나 더 있는 셈입니다.

API 호출 필요 저장된 만료 일시 access_token_token_expired > 현재 시각 ? Y N 저장된 토큰 재사용 발급 호출 0회 tokenP 1회 호출 → 파일에 저장 Bearer 헤더 첨부 발급은 1분 1회 · 6시간 이내 재신청은 같은 값
토큰은 받는 것이 아니라 확인하고 필요할 때만 받는 것

4. 정답 코드 — 만료 일시 기준 캐싱

공식 kis_auth.py가 쓰는 방식 그대로입니다. 토큰과 만료 일시를 파일에 적어 두고, 실행할 때 현재 시각과 비교합니다.

import json, os
from datetime import datetime

TOKEN_FILE = "kis_token.json"

def load_cached_token():
    if not os.path.exists(TOKEN_FILE):
        return None
    with open(TOKEN_FILE, encoding="utf-8") as f:
        c = json.load(f)
    # 공식 예제와 동일하게 'YYYY-MM-DD HH:MM:SS' 문자열 비교
    now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
    if c.get("expired", "") > now:
        return c["token"]
    return None

def get_token(appkey, appsecret):
    cached = load_cached_token()
    if cached:
        return cached                      # ← 대부분 여기서 끝난다

    data = issue_token(appkey, appsecret)  # 하루 1~2회만 실제 호출
    token = data["access_token"]
    with open(TOKEN_FILE, "w", encoding="utf-8") as f:
        json.dump({"token": token,
                   "expired": data["access_token_token_expired"]}, f)
    return token

이제 조회·주문 함수는 get_token()만 부르면 됩니다.

def build_headers(appkey, appsecret, tr_id):
    return {
        "content-type": "application/json; charset=utf-8",
        "authorization": f"Bearer {get_token(appkey, appsecret)}",
        "appkey": appkey,
        "appsecret": appsecret,
        "tr_id": tr_id,
        "custtype": "P",
    }

여기서 끝나면 안 되는 한 가지. 봇이 24시간 넘게 켜져 있는 구조라면, 프로세스가 살아 있는 채로 토큰이 만료되는 순간이 옵니다. 그래서 인증 오류를 받았을 때 캐시를 버리고 한 번만 재발급한 뒤 그 요청을 재시도하는 경로를 함께 두십시오. 단, 재시도는 1회만입니다 — 무한 재시도는 그대로 분당 발급 제한 사고가 됩니다. VPS 24시간 운영에서 상시 구동 구조를 다룹니다.

💬
인증에서 며칠째 막혀 계신다면

토큰·헤더·환경 분리는 전략과 무관한 배관인데 초보자가 가장 오래 붙잡는 지점입니다. 알고랩은 인증부터 주문·운영까지 포함해 제작합니다. 24시간 빠른 답변 가능합니다.

무료 상담 시작하기 →

5. 인증이 실패할 때 확인 순서

토큰을 다시 받아도 안 될 때, 이 순서로 확인하십시오. 대부분 3번 안에 잡힙니다.

  1. 실전/모의 환경이 섞였는가. 실전은 openapi.koreainvestment.com:9443, 모의는 openapivts.koreainvestment.com:29443이고 appkey별도 발급입니다. 모의 토큰으로 실전 도메인을 때리면 통과하지 못합니다. 환경 전환 시 도메인·키·tr_id 세 가지를 함께 바꿔야 합니다 — 모의투자 실전 전환 체크리스트에 정리돼 있습니다.
  2. 헤더가 빠졌는가. authorization만 넣고 appkey·appsecret·tr_id를 빠뜨리는 경우가 많습니다. KIS는 넷을 다 요구합니다.
  3. Bearer 접두어를 빠뜨렸는가. authorization 값은 Bearer {토큰}이며 공백 한 칸이 들어갑니다.
  4. 토큰이 만료됐는가. 저장된 access_token_token_expired를 눈으로 확인하십시오. EGW00121처럼 토큰 유효성 관련 코드가 오면 이 경우입니다.
  5. 캐시 파일이 환경별로 분리돼 있는가. 실전·모의 토큰을 같은 파일에 덮어쓰면 어제는 되고 오늘은 안 되는 재현 불가 버그가 됩니다.

응답 본문의 msg_cd·msg1로그에 그대로 남기는 것이 가장 빠른 길입니다. 에러코드별 의미는 KIS API 에러코드 정리에, 로그 설계는 거래 로그 설계에 있습니다.

6. 웹소켓 approval_key는 별개다

실시간 시세를 받으려고 웹소켓에 붙을 때 access_token을 넣으면 실패합니다. 웹소켓 접속에는 approval_key라는 별도 값이 필요하고, 발급 경로도 다릅니다.

access_tokenapproval_key
쓰는 곳REST 조회·주문WebSocket 접속
전달 위치authorization 헤더웹소켓 요청 본문
서로 대체불가 — 각각 관리해야 함

실시간 시세를 쓰는 봇이라면 기동 시 두 값을 모두 준비하는 절차를 두십시오. 웹소켓 쪽 상세는 KIS API 웹소켓 실시간 시세에서 다룹니다.

7. 토큰을 다루는 보안 원칙

토큰은 만료 전까지 "주문이 가능한 값"입니다. 유출되면 그 시간 동안 계좌에 주문을 넣을 수 있다는 뜻입니다. 최소한 이 셋은 지키십시오.

키·토큰 관리 전반은 API 키 보안과 계좌 보호에 따로 정리해 뒀습니다.

자주 묻는 질문

토큰 유효기간이 정확히 몇 시간인가요?

공식 인증 예제 주석 기준 1일이고, 응답의 access_token_token_expired에 만료 일시가 함께 옵니다. 상수로 계산하지 말고 응답 값을 저장해 쓰십시오.

재발급했는데 토큰이 안 바뀝니다.

정상입니다. 6시간 이내 재신청은 기존 값이 유지됩니다. 계속 재발급을 시도하면 분당 1회 제한에만 걸립니다. 원인은 다른 데 있을 가능성이 큽니다 — 5절 순서대로 확인하십시오.

모의투자 토큰을 실전에 쓸 수 있나요?

안 됩니다. 도메인·appkey가 모두 별도이며 tr_id도 다른 경우가 있습니다. 캐시 파일도 환경별로 나누십시오.

여러 프로세스가 같은 계정으로 돌면 어떻게 하나요?

각자 발급을 시도하면 분당 제한에 함께 걸립니다. 토큰 파일을 공유 경로 하나로 두고 모두 그 파일을 읽게 하거나, 발급 전담 프로세스를 두는 구조가 안전합니다. 다중 전략의 합산 호출량 설계는 API 호출 제한 설계를 참고하십시오.

확인 캐치. 이 글의 엔드포인트·필드명·발급 규칙은 2026년 8월 9일 기준 한국투자증권 공식 깃허브 저장소(koreainvestment/open-trading-api)의 인증 모듈 kis_auth.py 코드와 주석을 근거로 정리했습니다. 유효기간·발급 제한·알림 정책은 증권사 사정으로 예고 없이 변경될 수 있으므로 구현 전 공식 개발자센터에서 최신 값을 반드시 확인하십시오. 본 글은 특정 종목이나 수익률에 대한 어떤 예측이나 권유도 담고 있지 않습니다.

마무리

정리하면 한 줄입니다. access_token은 받는 값이 아니라 저장했다가 확인하고 쓰는 값입니다. 유효시간 1일, 발급은 1분 1회, 6시간 이내 재신청은 같은 값, 발급하면 알림. 이 넷을 알면 캐싱 말고 다른 답이 없다는 게 저절로 나옵니다.

그리고 이 문제로 며칠을 태우는 분이 많습니다. 전략과는 아무 상관없는 배관인데도 그렇습니다.

인증·주문·운영까지 통째로 맡기고 싶다면

토큰 관리부터 장애 처리까지 포함해 설계합니다. 24시간 빠른 답변 가능합니다.

무료 상담 시작하기