한국투자증권 오픈API(KIS Developers)는 국내에서 가장 널리 쓰이는 증권사 API입니다. 문서도 예제도 많죠.
그런데 실계좌로 몇 달 돌려보면 문서 바깥의 세계를 만납니다. “API는 성공이라는데 현실은 아닌” 순간들입니다.
이 글은 두 부분입니다. 앞은 처음 붙이는 순서(발급 → 토큰 → 첫 조회), 뒤는 실제로 밟은 함정과 호출 제한 대응입니다.
[30초 요약] 앱키·시크릿을 발급받아 토큰을 받고, 모든 요청 헤더에 `tr_id`를 넣으면 조회가 됩니다. 실전과 모의는 도메인과 TR ID가 다릅니다. 운영에서 가장 자주 부딪히는 건 호출 제한(EGW00201) 이고, 이때 주문(POST)은 함부로 재시도하면 안 됩니다 — 중복 체결 위험 때문입니다.
사용 순서 — 신청부터 첫 조회까지
| 단계 | 할 일 |
|---|---|
| 1. 신청 | 한국투자증권 계좌 개설 후 KIS Developers에서 API 신청 |
| 2. 발급 | 앱키(APP_KEY)·앱시크릿(APP_SECRET) 발급 — 실전·모의 따로 |
| 3. 토큰 | `/oauth2/tokenP` 로 접근 토큰 발급 |
| 4. 조회 | 헤더에 토큰·앱키·`tr_id` 넣고 요청 |
실전과 모의투자는 서버 주소부터 다릅니다.
| 구분 | 도메인 | 잔고 조회 TR |
|---|---|---|
| 실전 | `openapi.koreainvestment.com:9443` | `TTTC8434R` |
| 모의 | `openapivts.koreainvestment.com:29443` | `VTTC8434R` |
모의에서 통과한 코드를 실전으로 옮길 때 TR ID 앞글자(T/V)를 안 바꿔서 헤매는 경우가 흔합니다. 설정 한 곳에서 같이 바뀌게 만들어 두세요.
토큰 발급
import os, requests
BASE = "https://openapivts.koreainvestment.com:29443" # 모의. 실전은 위 표 참고
APP_KEY = os.environ["KIS_APP_KEY"]
APP_SECRET = os.environ["KIS_APP_SECRET"]
r = requests.post(
f"{BASE}/oauth2/tokenP",
headers={"content-type": "application/json"},
json={"grant_type": "client_credentials",
"appkey": APP_KEY, "appsecret": APP_SECRET},
timeout=15,
)
r.raise_for_status()
token = r.json()["access_token"]
토큰은 파일에 저장해서 재사용하세요. 스크립트를 돌릴 때마다 새로 받으면 짧은 간격의 재발급이 거부되는 경우가 있습니다. 저는 만료가 가까울 때만 새로 받게 해뒀습니다.
공통 헤더와 잔고 조회
모든 요청에 같은 헤더 묶음이 들어가고, `tr_id`가 “무슨 요청인지”를 결정합니다.
def headers(tr_id: str) -> dict:
return {
"Content-Type": "application/json",
"authorization": f"Bearer {token}",
"appKey": APP_KEY,
"appSecret": APP_SECRET,
"tr_id": tr_id,
"custtype": "P", # 개인
}
res = requests.get(
f"{BASE}/uapi/domestic-stock/v1/trading/inquire-balance",
headers=headers("VTTC8434R"), # 실전은 TTTC8434R
params={
"CANO": os.environ["KIS_CANO"], # 계좌번호 앞 8자리
"ACNT_PRDT_CD": "01",
"AFHR_FLPR_YN": "N", "OFL_YN": "", "INQR_DVSN": "02",
"UNPR_DVSN": "01", "FUND_STTL_ICLD_YN": "N",
"FNCG_AMT_AUTO_RDPT_YN": "N", "PRCS_DVSN": "01",
"CTX_AREA_FK100": "", "CTX_AREA_NK100": "",
},
timeout=15,
).json()
if res.get("rt_cd") == "0":
row = res["output2"][0]
cash = row.get("ord_psbl_cash") or row.get("dnca_tot_amt") or "0"
두 가지를 챙기세요. 성공 판정은 HTTP 200이 아니라 `rt_cd == “0”` 입니다. 그리고 주문가능현금(`ord_psbl_cash`)은 장 마감 등 일부 시점에 비어서 옵니다 — 예수금 총액(`dnca_tot_amt`)으로 받아내는 폴백이 필요합니다.
호출 제한(EGW00201) — 한도가 고정이 아닙니다
가장 자주 만나는 오류입니다. “초당 거래건수 초과”라는 뜻이죠.
직접 운영해보니 한도가 고정값이 아니었습니다. 같은 호출 간격이어도 통과하는 날과 튕기는 날이 있었고, KIS 쪽 부하에 따라 달라지는 것으로 보입니다.
실제로 호출 간격을 0.3초로 두고 재시도 없이 돌렸을 때, 장 시작 직후 30분(09:01~09:30) 동안 이 오류가 6건 났습니다. 지금은 이렇게 받습니다.
import time
RATE_LIMIT_DELAYS = (1.0, 3.0, 9.0) # 유량 제한은 짧게 재시도하면 계속 튕긴다
def get_with_retry(url, tr_id, params, max_retries=3):
for attempt in range(max_retries):
res = requests.get(url, headers=headers(tr_id), params=params, timeout=15).json()
if res.get("rt_cd") == "0":
return res
if res.get("msg_cd") != "EGW00201" or attempt == max_retries - 1:
return res
time.sleep(RATE_LIMIT_DELAYS[attempt])
return res
기본 간격은 0.5초, 튕기면 1·3·9초로 늘려가며 다시 시도합니다. 짧게 연타하면 계속 튕기니 간격을 벌리는 게 핵심입니다.
주문은 유량 제한일 때만 재시도합니다
조회는 여러 번 재시도해도 문제가 없습니다. 주문은 다릅니다.
주문 POST를 “아무 오류에나” 재시도하면, 실제로는 첫 요청이 접수됐는데 응답만 늦은 경우에 같은 주문이 두 번 나갑니다. 그래서 저는 EGW00201(요청 자체가 거부된 게 확실한 경우)에만 주문을 재시도합니다. 나머지 오류는 재시도하지 않고, 잔고를 다시 조회해서 실제 상태를 확인합니다.
실계좌에서 밟은 함정 다섯
함정 1 — 해외 잔고가 두 배로 보입니다. 같은 종목이 거래소별로 중복 반환되는 경우가 있습니다. 응답을 그대로 믿지 말고 심볼 기준으로 합산한 뒤 로직에 넘기세요.
함정 2 — 체결 조회만 영원히 죽는 계좌가 있습니다. 해외주식 체결내역 TR이 “처리계좌 ID와 사용자정보 상이”(APTR0058)만 반환했습니다. 잔고 조회는 멀쩡한데요. 초기 코드는 조회 실패 시 “체결됐다고 가정”했고 — 이게 유령 매매 기록을 만들었습니다. 우회는 주문 전후 잔고 비교입니다. 수량이 변했으면 체결, 그대로면 미체결.
함정 3 — 매도 지정가를 ‘현재가’로 걸면 안 팔립니다. 매도는 매수 1호가(bid), 매수는 매도 1호가(ask) 기준으로 걸어야 즉시 체결됩니다.
함정 4 — 취소는 취소 전용 TR로. 조회용 TR ID로 취소를 보내면 원인을 알려주지 않는 오류가 옵니다. 조회 계열과 주문 계열은 TR이 다릅니다.
함정 5 — ‘오늘’이 미국엔 아직 안 왔습니다. 한국 새벽에 해외 일봉을 오늘 날짜로 조회하면 빈 응답이 옵니다. 날짜를 하루씩 물려 재시도하고, 받은 최신 봉의 날짜를 반드시 검증하세요.
다섯 함정의 공통 교훈
“API가 성공을 반환했다”와 “내가 원한 일이 일어났다”는 다른 문장입니다. 중요한 동작 뒤에는 반드시 상태를 다시 조회해서 현실을 확인하세요. 응답 코드보다 잔고를 믿는 게 맞았습니다.
자주 묻는 질문
API 사용 수수료가 따로 있나요? API 이용 자체에 별도 요금은 없고, 매매 수수료는 계좌 조건을 따릅니다. 조건은 바뀔 수 있으니 신청 시 확인하세요.
github 예제를 그대로 써도 되나요? 구조를 익히기엔 좋습니다. 다만 재시도·잔고 재확인·토큰 캐시가 빠진 예제가 많아, 실계좌에 붙이기 전에 이 세 가지는 직접 넣는 걸 권합니다.
APTR0058 오류는 해결되나요? 저는 해결하지 못하고 우회했습니다(잔고 비교). 키 재발급·고객센터 문의를 시도해볼 수 있지만, 어떤 경우든 잔고 비교 폴백은 넣어두세요.
모의투자로 충분히 검증되나요? 시작으로는 필수입니다. 다만 호가·체결 지연과 호출 제한은 실전에서 다르게 나옵니다.
토스증권 API와 비교하면요? 토스는 인증이 더 단순하고 엔드포인트가 적습니다. 함정의 종류가 다를 뿐 어디나 있습니다.
함께 읽으면 좋은 글
다른 증권사의 함정은 토스증권 OpenAPI 파이썬 실전 가이드에, 이런 수집기를 24시간 돌리는 구조는 라즈베리파이 자동화 서버에 정리했습니다.
자동매매의 적은 시장이 아니라 “성공했다고 믿은 실패”였습니다. 확인하는 코드가 수익 코드보다 먼저입니다.
※ 본 글은 2026년 기준 실사용 경험이며 API 사양은 변경될 수 있습니다. 앱키·계좌번호는 반드시 환경변수로 관리하세요. 특정 증권사·상품의 권유가 아닙니다.