KIS API 토큰 만료 — access_token 재발급 규칙
access_token은 매번 새로 받는 값이 아니라 저장해 두고 재사용하는 값입니다. 공식 인증 예제(kis_auth.py) 기준 규칙은 넷입니다 —
① 유효시간 1일(응답의 access_token_token_expired에 만료 일시가 들어옴),
② 발급은 1분에 1회(넘기면 차단),
③ 6시간 이내 재신청은 기존 토큰 값이 그대로 유지,
④ 발급할 때마다 알림이 발송.
그래서 정답은 파일 캐싱입니다 — POST /oauth2/tokenP로 받은 토큰과 만료 일시를 저장하고, 만료 전이면 저장된 값을 그대로 쓰십시오. 루프마다 재발급하는 코드는 몇 초 만에 막힙니다.
KIS API 키 발급 가이드대로 appkey와 appsecret을 받고 첫 주문까지 성공하면, 그다음 날 아침 이런 일이 벌어집니다. 어제 잘 되던 코드가 인증 오류를 뱉습니다. 토큰이 만료된 겁니다.
여기서 가장 흔한 대응이 "그럼 호출할 때마다 새로 받자"인데, 이게 두 번째 사고를 부릅니다. 이 글은 그 사이에서 정확히 무슨 규칙이 걸려 있는지, 그리고 어떻게 짜야 하는지에 대한 글입니다.
목차
1. appkey와 access_token은 뭐가 다른가
처음 헷갈리는 지점입니다. KIS Developers에서 앱을 등록하면 appkey와 appsecret 두 개를 받는데, 이것만으로는 조회도 주문도 되지 않습니다.
appkey / appsecret | access_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_type은 client_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_type | Bearer — 헤더 접두어 |
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절 참고).
④ 발급하면 알림이 발송된다
공식 예제 주석은 토큰 발급 시 알림이 발송된다고 안내합니다. 개발 중 재발급을 반복하면 알림이 계속 쌓입니다. 캐싱을 해야 할 이유가 하나 더 있는 셈입니다.
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번 안에 잡힙니다.
- 실전/모의 환경이 섞였는가. 실전은
openapi.koreainvestment.com:9443, 모의는openapivts.koreainvestment.com:29443이고appkey도 별도 발급입니다. 모의 토큰으로 실전 도메인을 때리면 통과하지 못합니다. 환경 전환 시 도메인·키·tr_id세 가지를 함께 바꿔야 합니다 — 모의투자 실전 전환 체크리스트에 정리돼 있습니다. - 헤더가 빠졌는가.
authorization만 넣고appkey·appsecret·tr_id를 빠뜨리는 경우가 많습니다. KIS는 넷을 다 요구합니다. Bearer접두어를 빠뜨렸는가.authorization값은Bearer {토큰}이며 공백 한 칸이 들어갑니다.- 토큰이 만료됐는가. 저장된
access_token_token_expired를 눈으로 확인하십시오.EGW00121처럼 토큰 유효성 관련 코드가 오면 이 경우입니다. - 캐시 파일이 환경별로 분리돼 있는가. 실전·모의 토큰을 같은 파일에 덮어쓰면 어제는 되고 오늘은 안 되는 재현 불가 버그가 됩니다.
응답 본문의 msg_cd·msg1을 로그에 그대로 남기는 것이 가장 빠른 길입니다. 에러코드별 의미는 KIS API 에러코드 정리에, 로그 설계는 거래 로그 설계에 있습니다.
6. 웹소켓 approval_key는 별개다
실시간 시세를 받으려고 웹소켓에 붙을 때 access_token을 넣으면 실패합니다. 웹소켓 접속에는 approval_key라는 별도 값이 필요하고, 발급 경로도 다릅니다.
access_token | approval_key | |
|---|---|---|
| 쓰는 곳 | REST 조회·주문 | WebSocket 접속 |
| 전달 위치 | authorization 헤더 | 웹소켓 요청 본문 |
| 서로 대체 | 불가 — 각각 관리해야 함 | |
실시간 시세를 쓰는 봇이라면 기동 시 두 값을 모두 준비하는 절차를 두십시오. 웹소켓 쪽 상세는 KIS API 웹소켓 실시간 시세에서 다룹니다.
7. 토큰을 다루는 보안 원칙
토큰은 만료 전까지 "주문이 가능한 값"입니다. 유출되면 그 시간 동안 계좌에 주문을 넣을 수 있다는 뜻입니다. 최소한 이 셋은 지키십시오.
kis_token.json·appkey·appsecret을 깃 저장소에 올리지 않기..gitignore에 먼저 넣으십시오.- 블로그·질문 게시판·AI 챗봇에 코드 붙여넣을 때 토큰 문자열을 지우기. 실제로 가장 흔한 유출 경로입니다.
- 노출이 의심되면 KIS Developers에서 앱키를 즉시 재발급. 토큰은 자연 만료를 기다리지 말 것.
키·토큰 관리 전반은 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시간 이내 재신청은 같은 값, 발급하면 알림. 이 넷을 알면 캐싱 말고 다른 답이 없다는 게 저절로 나옵니다.
그리고 이 문제로 며칠을 태우는 분이 많습니다. 전략과는 아무 상관없는 배관인데도 그렇습니다.