KIS API hashkey — 안 넣어도 주문되는데 왜 쓰나
hashkey는 주문 헤더의 필수값이 아닙니다.
한국투자증권 공식 깃허브 저장소(koreainvestment/open-trading-api)의 인증 모듈 kis_auth.py에도
"hash key 필수 사항아님, 생략가능"이라는 주석이 그대로 달려 있고, 해시 처리 분기 자체가 기본 상태에서 꺼진 채 배포됩니다.
그래서 주문이 안 나가는 원인은 대부분 hashkey가 아니라 access_token·tr_id·계좌번호 쪽입니다.
hashkey는 POST 본문의 위·변조를 막는 선택적 장치이고, 쓰려면
POST /uapi/hashkey에 주문 본문을 그대로 보내 응답의 HASH 값을 받아 주문 헤더에 얹으면 됩니다.
다만 이때 발급에 쓴 본문 문자열과 주문에 보내는 본문 문자열이 한 글자라도 다르면 안 됩니다.
KIS API 발급 가이드를 따라 appkey와 appsecret을 받고
첫 주문 코드를 짜다 보면 반드시 마주치는 단어가 있습니다. hashkey입니다.
공식 예제에도 나오고, 블로그 코드에도 나오고, 파이썬 래퍼 라이브러리에도 나옵니다.
그런데 정작 이게 뭔지, 꼭 필요한지를 설명해 주는 곳은 드뭅니다.
실제로 알고랩에서 주문·정정·취소 코드를 다루다 보면 "해시키를 넣었더니 오히려 주문이 거부된다"는 질문을 자주 받습니다. 결론부터 말하면 해시키는 없어도 되는 값이고, 있어서 문제가 되는 경우가 오히려 더 많습니다. 이 글은 그 하나만 다룹니다.
이 글의 순서
- hashkey가 막으려는 것은 무엇인가
- 발급 —
POST /uapi/hashkey실제 요청과 응답 - 주문 헤더에 얹는 자리
- 공식
kis_auth.py는 실제로 어떻게 처리하나 - 넣을 것인가 말 것인가 — 판단 기준
- 막히는 지점 5가지
- 체크리스트
1. hashkey가 막으려는 것은 무엇인가
KIS API에서 조회 계열은 GET이고, 주문·정정·취소 계열은 POST입니다.
POST는 본문(body)에 종목코드·수량·단가가 실려 나갑니다.
hashkey는 그 본문이 전송 도중에 바뀌지 않았음을 증명하는 값입니다.
쉽게 말하면 봉인 스티커입니다. 주문서를 봉투에 넣기 전에 내용물로 지문을 하나 떠서 봉투 겉면에 붙여 보내고, 받는 쪽이 봉투를 열어 다시 지문을 떠서 비교합니다. 두 지문이 다르면 누군가 중간에 봉투를 열었다는 뜻입니다. 수량을 10주에서 1,000주로 바꿔치기하는 종류의 공격을 겨냥한 장치입니다.
그래서 hashkey는 인증(누구인가)이 아니라 무결성(내용이 그대로인가)을 담당합니다.
누구인가는 이미 authorization 헤더의 access_token과 appkey·appsecret이 답하고 있습니다.
토큰 쪽 규칙은 access_token 만료·재발급 규칙에 따로 정리해 두었습니다.
2. 발급 — POST /uapi/hashkey 실제 요청과 응답
발급 방식은 단순합니다. 주문에 보낼 본문을 그대로 해시키 엔드포인트에 한 번 더 보내면 됩니다. 별도의 서명 알고리즘을 직접 구현할 필요가 없습니다. 바이낸스나 HTX(후오비)처럼 HMAC 서명을 손으로 만드는 방식과는 다릅니다.
import json, requests
# 실전 도메인 (모의투자는 openapivts.koreainvestment.com:29443)
BASE = "https://openapi.koreainvestment.com:9443"
order_body = {
"CANO": "12345678", # 계좌번호 앞 8자리
"ACNT_PRDT_CD": "01", # 계좌상품코드 뒤 2자리
"PDNO": "005930", # 종목코드
"ORD_DVSN": "01", # 01 = 시장가
"ORD_QTY": "1",
"ORD_UNPR": "0", # 시장가면 0
}
# ★ 핵심: 직렬화 결과를 변수 하나에 담아 둔다
body_str = json.dumps(order_body)
hash_headers = {
"content-type": "application/json; charset=utf-8",
"appkey": APP_KEY,
"appsecret": APP_SECRET,
}
res = requests.post(f"{BASE}/uapi/hashkey",
data=body_str, # dict가 아니라 '문자열'을 그대로
headers=hash_headers)
print(res.status_code, res.json())
돌려받는 응답은 이런 모양입니다.
200 {
"BODY": {
"CANO": "12345678",
"ACNT_PRDT_CD": "01",
"PDNO": "005930",
"ORD_DVSN": "01",
"ORD_QTY": "1",
"ORD_UNPR": "0"
},
"HASH": "b3a1f0c9e27d4a8b5c6e1f2a3d4b5c6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c"
}
꺼내 쓸 것은 HASH 필드 하나입니다.
(위 HASH 문자열은 형태를 보여 주기 위한 예시이며 실제 값은 요청 본문마다 달라집니다.)
BODY는 서버가 인식한 본문을 되돌려 준 것이라 디버깅에 유용합니다 —
내가 보낸 값과 다르게 찍혔다면 직렬화나 인코딩 어딘가가 틀어진 것입니다.
3. 주문 헤더에 얹는 자리
받은 HASH를 주문 요청 헤더의 hashkey 키에 넣습니다.
그리고 본문은 방금 해시를 뜬 그 문자열을 그대로 보냅니다. 이 부분이 이 글의 전부라고 해도 됩니다.
order_headers = {
"content-type": "application/json; charset=utf-8",
"authorization": f"Bearer {ACCESS_TOKEN}",
"appkey": APP_KEY,
"appsecret": APP_SECRET,
"tr_id": "TTTC0802U", # 실전 현금 매수
"custtype": "P", # 개인
"hashkey": res.json()["HASH"],
}
order = requests.post(
f"{BASE}/uapi/domestic-stock/v1/trading/order-cash",
data=body_str, # ★ 해시 뜰 때 쓴 그 문자열 그대로
headers=order_headers)
print(order.json()["rt_cd"], order.json()["msg1"])
| 헤더 | 역할 | 없으면 |
|---|---|---|
authorization | 내가 누구인가 (Bearer + access_token) | 주문 불가 |
appkey / appsecret | 어떤 앱의 호출인가 | 주문 불가 |
tr_id | 무슨 거래인가 (매수 TTTC0802U / 매도 TTTC0801U) | 주문 불가 |
custtype | 개인 P / 법인 B | 거부될 수 있음 |
hashkey | 본문이 변조되지 않았는가 | 주문 됨 (선택값) |
표에서 보이듯 hashkey만 성격이 다릅니다.
나머지는 없으면 주문 자체가 성립하지 않지만, hashkey는 빠져도 주문이 나갑니다.
모의투자에서 실전으로 넘어갈 때 tr_id가 VTTC0802U로 바뀌는 문제는
모의투자 실전 전환 체크리스트에 따로 정리해 두었습니다.
4. 공식 kis_auth.py는 실제로 어떻게 처리하나
한국투자증권이 공개한 깃허브 저장소 koreainvestment/open-trading-api의 인증 모듈에는
해시키 전용 함수가 들어 있습니다. 구조를 보면 이 글의 설명이 그대로 확인됩니다.
# koreainvestment/open-trading-api · kis_auth.py (구조)
def set_order_hash_key(h, p):
url = f"{getTREnv().my_url}/uapi/hashkey"
res = requests.post(url, data=json.dumps(p), headers=h)
if res.status_code == 200:
h["hashkey"] = _getResultObject(res.json()).HASH
주목할 점이 셋입니다.
data=json.dumps(p)— 딕셔너리를json=으로 넘기지 않고 직접 직렬화한 문자열을 보냅니다.- 응답에서
HASH만 꺼내 넘겨받은 헤더 딕셔너리h에hashkey키로 꽂습니다. 즉 헤더를 제자리에서 수정합니다. - 이 함수를 호출할지 말지가 플래그로 갈립니다. 저장소에 올라온 상태에서는 해시 분기가 기본으로 활성화되어 있지 않고, 함수 주석에도 "hash key 필수 사항아님, 생략가능"이라고 적혀 있습니다.
정리. 증권사가 직접 배포한 참조 구현이 기본값에서 해시키를 쓰지 않는다는 것은
꽤 분명한 신호입니다. hashkey는 보안을 한 겹 더 얹고 싶을 때 켜는 옵션이지
주문의 전제 조건이 아닙니다.
5. 넣을 것인가 말 것인가
대부분의 개인 자동매매 봇에는 넣지 않아도 됩니다. 이유는 단순합니다.
hashkey가 막으려는 위협은 내 봇과 증권사 서버 사이에서 통신 내용을 가로채 바꿔치기하는 공격인데,
KIS API는 이미 HTTPS로 암호화된 채널을 씁니다. 봉투 자체가 이미 봉인되어 있는 셈입니다.
반대로 넣는 쪽이 합리적인 경우도 있습니다.
| 상황 | 권장 | 이유 |
|---|---|---|
| 혼자 쓰는 개인 봇 (VPS·집 PC) | 생략 | 왕복 2회 → 1회. 지연·호출량 절감 |
| 주문 요청이 사내 프록시·게이트웨이를 경유 | 사용 | 중간 구간이 늘어나면 무결성 검증 값이 생김 |
| 여러 사람이 만지는 서버에서 운영 | 사용 | 본문 변조 시점을 응답으로 잡아낼 수 있음 |
| 납품 프로그램 (감사·기록 요건 있음) | 사용 | "주문 본문을 검증했다"는 근거를 남김 |
| 초단타·다전략 동시 운용 | 생략 권장 | 호출 횟수가 2배 → EGW00201 위험 |
가장 자주 놓치는 것. hashkey를 켜면 주문 1건당 API 호출이 2회가 됩니다.
전략 3개가 동시에 주문을 던지는 봇이라면 초당 호출량이 그대로 2배가 됩니다.
한국투자증권의 초당 거래건수 제한은 계정 단위로 공유되므로,
해시키를 켠 뒤 갑자기 EGW00201이 뜨기 시작했다면 원인은 전략이 아니라 이것입니다.
호출량 설계는 API 호출 제한 설계를 참고하십시오.
6. 막히는 지점 5가지
① 직렬화 불일치 — 압도적 1위
해시를 뜰 때는 json.dumps(order_body)를 보내고, 주문할 때는 requests의 json=order_body를 쓰는 코드입니다.
두 경우 실제로 전송되는 바이트가 다릅니다. requests의 json= 인자는 자체 규칙으로 직렬화하며,
구분자 뒤 공백 처리가 json.dumps 기본값과 어긋날 수 있습니다.
# ✗ 틀린 코드 — 두 번 직렬화되고 결과가 달라질 수 있다
requests.post(hash_url, data=json.dumps(body), headers=h1)
requests.post(order_url, json=body, headers=h2) # ← 여기서 어긋남
# ✓ 맞는 코드 — 문자열 하나를 두 번 쓴다
body_str = json.dumps(body)
requests.post(hash_url, data=body_str, headers=h1)
requests.post(order_url, data=body_str, headers=h2)
② 발급 후에 본문을 다시 손댐
해시키를 받아 놓고 그 아래에서 order_body["ORD_QTY"] = str(qty)처럼 값을 갱신하는 코드입니다.
봉인을 붙인 다음 봉투를 열어 내용을 바꾼 것과 같습니다. 수량 계산은 반드시 해시키 발급 전에 끝내야 합니다.
③ 실전·모의 도메인 혼용
실전은 openapi.koreainvestment.com의 9443 포트, 모의투자는 openapivts.koreainvestment.com의 29443 포트입니다.
해시키 발급도 주문과 같은 환경의 도메인으로 보내야 합니다.
앱키·앱시크릿도 환경별로 별도 발급이므로, 도메인만 바꾸고 키를 그대로 두면 인증 단계에서 걸립니다.
④ content-type에 charset 누락
한글 종목명이 본문에 들어가는 API에서는 application/json만 적고 charset=utf-8을 빠뜨리면
인코딩 해석이 갈릴 수 있습니다. 발급 요청과 주문 요청 양쪽에 동일하게 적어 두는 편이 안전합니다.
⑤ 해시키를 만능 해결책으로 오해
주문이 거부될 때 해시키부터 붙여 보는 경우가 많은데, 대체로 무관합니다.
거부 사유는 rt_cd와 msg1에 그대로 내려오므로 그 메시지를 먼저 읽는 것이 순서입니다.
자주 나오는 코드는 KIS API 에러코드 11가지에 정리해 두었습니다.
주문 헤더 구성·토큰 캐싱·호출량 설계는 한 번 제대로 잡아 두면 다시 건드릴 일이 거의 없는 영역입니다. 알고랩은 KIS·키움 REST API 연동을 포함한 자동매매 프로그램을 맞춤 제작합니다.
상담해 보기 →7. 체크리스트
- 직렬화 문자열을 변수 하나로 고정했는가 (
body_str) - 해시키 발급 이후에 본문을 수정하지 않았는가
- 발급과 주문의 도메인·포트가 같은 환경인가
content-type에charset=utf-8이 들어갔는가- 주문 1건당 호출이 2회로 늘어난 것을 초당 제한 계산에 반영했는가
- 애초에 해시키가 정말 필요한 구성인가 — 아니면 빼는 것이 정답
가장 빠른 진단법. 해시키를 일단 빼고 주문을 한 번 던져 보십시오.
그래도 실패한다면 원인은 확실히 해시키가 아닙니다 — 토큰·tr_id·계좌번호를 보십시오.
해시키를 뺐더니 성공한다면, 위 ①~④ 중 하나에 걸린 것입니다.
확인 캐치. 이 글의 엔드포인트·헤더·tr_id 값은 2026년 8월 10일 기준
한국투자증권 공식 깃허브 저장소(koreainvestment/open-trading-api)의 인증 모듈과 공개 문서를 근거로 정리했습니다.
API 스펙과 필수 여부 정책은 증권사 사정으로 예고 없이 바뀔 수 있으므로 구현 전 공식 개발자센터에서 최신 값을 확인하십시오.
본 글은 특정 종목이나 수익률에 대한 어떠한 예측이나 권유도 담고 있지 않습니다.
마무리
정리하면 한 줄입니다. hashkey는 KIS 주문의 필수값이 아니고, 켜면 호출이 2배가 되며,
켤 거면 본문 문자열 하나를 두 번 쓰면 됩니다.
증권사가 배포한 참조 구현조차 기본값에서 이 기능을 켜 두지 않았다는 사실이 가장 확실한 답입니다.
KIS와 키움 중 무엇으로 시작할지 아직 정하지 못했다면 키움 vs KIS API 비교부터 읽어 보시는 편이 순서에 맞습니다.