키움 REST API 연속조회 — cont-yn·next-key
잔고가 50종목인데 20개만 왔다면 API가 고장 난 것이 아니라
나머지를 아직 안 준 것입니다.
키움 REST API는 목록이 길면 나눠서 주고,
다음 페이지 정보를 응답 헤더에 담습니다.
cont-yn이 Y면 다음 데이터가 있다는 뜻이고,
next-key(최대 50자)가 이어받을 지점입니다.
이 두 값을 다음 요청의 헤더에 그대로 실어 같은 엔드포인트를 다시 부르면 이어집니다.
⚠️ 응답 바디에는 next-key가 없습니다.
response.json()이 아니라 response.headers를 보십시오.
키움 REST API를 붙일 때 인증과 주문은 문서대로 하면 대체로 넘어갑니다. 그런데 조회에서 한 번씩 멈추는 지점이 있습니다. 분명히 계좌에 종목이 더 있는데 응답에 일부만 담겨 오는 것입니다.
에러도 없습니다. return_code는 0이고
return_msg는 "정상적으로 처리되었습니다"입니다.
정상 응답인데 데이터가 모자란 상태라, 조회 조건을 의심하며 시간을 보내기 쉽습니다.
원인은 하나입니다. 연속조회를 안 돌렸기 때문입니다.
1. 헤더 세 개만 알면 끝난다
키움 REST API 요청·응답 헤더에서 연속조회와 관련된 값은 셋입니다.
| 헤더 | 방향 | 의미 |
|---|---|---|
api-id |
요청 | 8자리 TR코드. 어떤 조회인지 지정 (예: kt00018) |
cont-yn |
요청 · 응답 | 연속조회 여부. 응답이 Y면 다음 데이터가 있다 |
next-key |
요청 · 응답 | 연속조회 키. 최대 50자. 응답 값을 다음 요청에 그대로 세팅 |
규칙은 문서 표현 그대로입니다.
"응답 Header의 연속조회여부 값이 Y일 경우, 다음 데이터 요청 시
응답 Header의 next-key 값을 세팅"합니다.
즉 헤더에서 받아 헤더로 되돌려주는 구조입니다.
2. 첫 요청 — 평범하게 한 번 부른다
예시로 계좌평가잔고내역요청(kt00018)을 씁니다.
국내주식 계좌 계열이라 경로는 /api/dostk/acnt이고,
POST로 조회 조건을 바디에 담아 보냅니다.
import requests
HOST = "https://api.kiwoom.com" # 모의투자는 별도 도메인
PATH = "/api/dostk/acnt"
headers = {
"Content-Type": "application/json;charset=UTF-8",
"authorization": f"Bearer {access_token}",
"api-id": "kt00018",
"cont-yn": "N", # 첫 요청은 연속조회가 아니다
"next-key": "", # 비워 둔다
}
body = {
"qry_dt": "20260813", # 조회 기준일자
# 그 밖의 조회 조건은 TR 명세에 따른다
}
res = requests.post(HOST + PATH, headers=headers, json=body, timeout=10)
응답 헤더에는 이런 값들이 실려 옵니다.
>>> {k: res.headers.get(k) for k in ("api-id", "cont-yn", "next-key")}
{'api-id': 'kt00018', 'cont-yn': 'Y', 'next-key': 'MTIzNDU2Nzg5MA=='}
>>> res.json()["return_code"], res.json()["return_msg"]
(0, '정상적으로 처리되었습니다')
여기서 대부분이 멈춥니다.
return_code가 0이라 성공으로 보이고,
바디 JSON에는 next-key라는 키가 없습니다.
그래서 "키움 REST API에는 페이징이 없나 보다"라고 결론 내리고
조회 조건만 계속 바꾸게 됩니다.
정보는 처음부터 헤더에 있었습니다.
3. 두 번째 요청 — 받은 값을 그대로 되돌려준다
바디는 첫 요청과 똑같이 두고, 헤더 두 개만 바꿉니다.
headers["cont-yn"] = res.headers.get("cont-yn") # "Y"
headers["next-key"] = res.headers.get("next-key") # 응답에서 받은 값 그대로
res2 = requests.post(HOST + PATH, headers=headers, json=body, timeout=10)
바디의 조회 조건을 같이 바꾸지 마십시오. 연속조회는 같은 조건의 다음 구간을 받는 동작입니다. 기준일자나 조회 구분을 함께 바꾸면 이어받는 지점과 조건이 어긋나 중복이나 누락이 생깁니다. 바뀌는 것은 헤더 두 개뿐입니다.
4. 끝까지 받는 반복 코드
실제로 쓰는 형태는 cont-yn이 Y가 아닐 때까지 도는 반복문입니다.
다만 안전장치 세 개를 같이 넣어야 합니다.
import time
def fetch_all(api_id, body, access_token, max_pages=50, pause=0.2):
"""cont-yn 이 N 이 될 때까지 모든 페이지를 모아서 돌려준다"""
headers = {
"Content-Type": "application/json;charset=UTF-8",
"authorization": f"Bearer {access_token}",
"api-id": api_id,
"cont-yn": "N",
"next-key": "",
}
pages, seen_keys = [], set()
for page in range(max_pages): # ① 페이지 상한
res = requests.post(HOST + PATH, headers=headers,
json=body, timeout=10)
res.raise_for_status()
data = res.json()
if data.get("return_code") != 0: # ② 실패면 즉시 중단
raise RuntimeError(
f"{api_id} 실패: {data.get('return_code')} "
f"{data.get('return_msg')}"
)
pages.append(data)
cont = (res.headers.get("cont-yn") or "N").strip().upper()
nkey = res.headers.get("next-key") or ""
if cont != "Y" or not nkey:
break # 마지막 페이지
if nkey in seen_keys: # ③ 같은 키 재등장 = 무한루프
raise RuntimeError(f"next-key 반복 감지: {nkey[:12]}...")
seen_keys.add(nkey)
headers["cont-yn"] = "Y"
headers["next-key"] = nkey
time.sleep(pause) # 호출 간격
return pages
안전장치 세 개는 각각 다른 사고를 막습니다.
| 장치 | 막는 사고 |
|---|---|
max_pages 상한 |
서버가 계속 Y를 주는 상황에서 무한 호출로 한도 소진 |
return_code 검사 |
중간 페이지가 실패했는데 빈 데이터를 정상으로 합쳐 버리는 것 |
seen_keys 중복 검사 |
같은 next-key가 반복돼 같은 구간을 무한히 다시 받는 것 |
페이지를 다 받은 뒤에 처리하십시오. 페이지마다 주문을 내거나 무거운 계산을 끼워 넣으면, 조회가 길어질수록 호출량과 실행 시간이 같이 늘어납니다. 모으기와 처리를 분리하면 중간에 끊겨도 재시도가 쉬워집니다.
5. 자주 나는 실수 5가지
| 실수 | 증상 | 고치는 법 |
|---|---|---|
바디에서 next-key를 찾는다 |
페이징 기능이 없다고 결론 | res.headers에서 읽는다 |
| 요청 헤더 키에 언더스코어 사용 | 계속 첫 페이지만 온다 | cont_yn이 아니라 cont-yn |
첫 요청부터 cont-yn: Y |
이어받을 지점이 없어 결과가 어긋남 | 첫 요청은 N + 빈 next-key |
| 반복 중 바디 조건 변경 | 중복·누락 데이터 | 바디 고정, 헤더 둘만 교체 |
| 간격 없이 연속 호출 | 호출 제한에 걸려 중단 | 간격 + 실패 시 점진적 재시도 |
마지막 항목은 특히 조심해야 합니다. 연속조회는 짧은 시간에 같은 엔드포인트를 반복 호출하는 작업이라 호출 제한에 가장 먼저 부딪히는 구간입니다. 실제로 거부가 뜨는 경우의 대응은 키움 REST API 429 — 허용된 요청 개수 초과에, 애초에 한도 안에서 돌게 만드는 설계는 API 호출 제한 설계에 정리해 두었습니다.
6. 한국투자증권 KIS와는 방식이 다르다
두 증권사를 함께 붙이는 봇이라면 이 차이를 미리 알아 두는 편이 좋습니다. 같은 "연속조회"라는 이름인데 옮기는 위치가 다릅니다.
| 키움 REST API | 한국투자증권 KIS API | |
|---|---|---|
| 다음 페이지 정보 위치 | 응답 헤더 | 응답 바디(연속조회 검색조건) |
| 다음 요청에 싣는 곳 | 요청 헤더 (next-key) |
요청 쿼리 파라미터 |
| 연속 여부 표시 | cont-yn |
tr_cont |
| 키 형태 | 단일 키값(최대 50자) | 검색조건 값 여러 개 |
KIS 쪽 구현은 KIS API 잔고조회 연속조회에 따로 정리해 두었습니다. 두 방식을 상위 코드에서 직접 다루면 분기가 지저분해지므로, 증권사별 어댑터 안에 가두고 바깥에는 "전체 목록"만 돌려주는 구조를 권합니다.
인증·조회·주문까지 실제로 도는 상태로 만들어 드립니다.
무료 상담 시작하기 →자주 묻는 질문
next-key는 응답의 어디에 있나요?
응답 헤더입니다. res.json()이 아니라
res.headers.get("next-key")로 꺼내십시오.
함께 오는 cont-yn이 Y면 다음 데이터가 있다는 뜻입니다.
첫 요청에도 cont-yn을 넣어야 하나요?
첫 요청은 cont-yn: N, next-key: ""로 둡니다.
처음부터 Y를 보내면서 키가 비어 있으면 이어받을 지점이 없습니다.
헤더 이름을 cont_yn으로 써도 되나요?
안 됩니다. HTTP 헤더 키는 하이픈인 cont-yn·next-key입니다.
파이썬 변수명으로 cont_yn을 쓰는 것은 무관하지만
헤더 키에 언더스코어가 들어가면 인식되지 않습니다.
KIS API와 무엇이 다른가요?
키움은 헤더에서 헤더로, KIS는 바디에서 쿼리로 옮깁니다.
연속 표시도 cont-yn 대 tr_cont로 다릅니다.
연속조회 중 요청이 거부되면?
즉시 재시도하지 말고 간격을 늘려 가며 다시 시도하십시오. 페이지마다 무거운 작업을 끼우지 말고 모으기와 처리를 분리하는 편이 안전합니다.
확인 캐치. 이 글의 헤더명·TR코드·경로는
2026년 8월 13일 기준 키움증권 REST API 문서를 근거로 작성했습니다.
코드 예시의 next-key 값과 조회 조건 필드는 형식을 보여주기 위한 예시이며,
TR별 요청 바디 항목과 응답 필드는 각 TR 명세에 따라 다릅니다.
증권사 API 규격·호출 한도·도메인은 개정될 수 있으므로
구현 전 키움 REST API 공식 문서에서 현재 값을 확인하십시오.
본 글은 기술 자료이며 특정 종목이나 수익률에 대한 예측·권유를 담고 있지 않습니다.
마무리
정리하면 한 줄입니다.
목록이 모자라면 응답 헤더의 cont-yn을 보고,
next-key를 그대로 되돌려주면 됩니다.
바디를 아무리 뒤져도 답이 없는 이유는 처음부터 거기에 없었기 때문입니다.