AI와 함께 만드는 삶

PM으로 일하며 AI와 함께 무언가를 만들어가는 삶을 기록합니다.

프로젝트 기록/주식 자동매매 개발기

토스증권 Open API 파이썬 연동 가이드 — 키 발급부터 일봉 수집까지 삽질기

makewithai 2026. 7. 24. 09:49

이번 편은 이야기보다 실전 가이드에 가깝다. 한국 주식 데이터를 파이썬으로 직접 가져와보고 싶은 사람을 위해, 실제로 연동하면서 겪은 과정과 막혔던 지점을 순서대로 정리했다.

결론부터 말하면 막힌 지점은 세 곳이었다. 401 인증 오류, KRX 세션 만료, 그리고 업종 분류 API가 빈 값을 돌려주는 문제. 각각 어떻게 뚫었는지가 이 글의 핵심이다.

버전 주의: 토스증권 Open API는 2026년 5월 사전 신청을 시작해 단계적으로 롤아웃 중인 서비스다. 스펙과 제공 범위가 계속 바뀔 수 있으니, 최신 사양은 반드시 공식 문서를 확인할 것. 아래 내용은 2026년 7월 기준 직접 호출해서 확인한 결과다.

토스증권 Open API 연동 전체 흐름도, 사전 신청부터 일봉 수집까지 7단계와 401 오류 발생 지점 표시

1. 토스증권 Open API

1-1. 어떤 API인가

토스증권 Open API는 국내(KRX)와 미국 주식의 시세·종목정보·환율·계좌·주문 기능을 제공하는 REST API다. 

모든 호출에 OAuth 2.0 토큰이 필요하다.

기존 증권사 API와 비교했을 때 진입 장벽이 낮은 편이다. 윈도우 전용 OCX 설치 같은 게 필요 없고, HTTP 요청만 보낼 수 있으면 파이썬이든 자바스크립트든 상관없다. 클라우드 서버나 라즈베리파이에서도 돌릴 수 있다.

한 가지 특이한 점은 AI 코딩 도구를 염두에 둔 문서 구성이다. 사람이 읽는 문서 외에 llms.txt, overview.md, 그리고 기계가 파싱하는 OpenAPI JSON 스펙을 함께 공개한다. AI 에이전트에게 "토스 API로 시세 받아줘"라고 시키면 스키마를 정확히 읽어서 코드를 짜준다.

1-2. 신청부터 승인까지

토스증권 Open API는 사전 신청 후 순차 승인 방식이었다. 신청하고 실제로 키는 얼마지나지 않아 생성되었다

https://corp.tossinvest.com/ko/open-api  open API 요청을 할수있고 

https://developers.tossinvest.com/docs 상세 내용은 가이드 문서 확인하면된다.

 

승인되면 WTS(PC 웹) 로그인 후 설정 메뉴에서 클라이언트를 등록하고, 앱키(client_id)와 시크릿(client_secret)을 발급받는다.

https://www.tossinvest.com/ 토스증권 페이지에서 오른쪽 하던의 설정 버튼 클릭시 발급된 내용확인이 가능하다 

 

* 주의 : client Id는 한번만 보여지므로 바로 복사해서 넣어두어야 사용이 가능하다. 다시 보기는 없으니 주의 해야한다.

 

1-3. 허용 IP 등록

발급받은 키는 등록한 IP에서만 동작한다. 이 PC의 공인 IP를 확인해서 등록해야 한다.

https://www.tossinvest.com/ 토스증권 페이지에서 오른쪽 하던의 설정 버튼 클릭 후 Open API 탭 클릭에서 확인이 가능하다.

 

가정용 인터넷은 공인 IP가 바뀔 수 있으니, 어느 날 갑자기 호출이 막히면 IP부터 다시 확인해보는 게 좋다.


2. 첫 번째 삽질 — 401 invalid_client

등록 전에는 API 호출이 전부 막히는데, 여기서 첫 번째 삽질이 있었다. 처음 발급받은 키로 인증을 시도했더니 이런 에러가 났다.

401 Unauthorized
{"error":"invalid_client","error_description":"Client authentication failed"}

IP 문제인 줄 알고 한참 확인했는데, 원인은 다른 데 있었다. 키 자체가 등록 과정에서 온전히 발급되지 않은 상태였다. 콘솔에서 키를 다시 발급받으니 바로 해결됐다.

401과 403은 다른 문제다

이걸 구분하는 게 시간을 크게 아껴준다.

코드의미먼저 볼 곳

401 Unauthorized 인증 자체가 실패 키 값, 키 상태 (재발급이 답인 경우가 많다)
403 Forbidden 인증은 됐는데 권한 없음 허용 IP, 스코프, 계좌 권한

401이 뜨면 IP를 뒤지기 전에 키 상태부터 의심하는 게 낫다.

참고 — 인증 방식도 의심해봤지만 원인은 아니었다

401을 처음 만났을 때, 키 문제인 줄 모르고 자격증명을 실어 보내는 방식을 바꿔서도 시도해봤다. OAuth2 client_credentials는 두 가지 전달 방식을 허용하는데, 서버에 따라 한쪽만 받는 경우가 있기 때문이다.

방식 A — 폼 파라미터로 전달 (내가 처음부터 쓰던 방식)

import requests

r = requests.post(
    "https://openapi.tossinvest.com/oauth2/token",
    data={
        "grant_type": "client_credentials",
        "client_id": "YOUR_APP_KEY",
        "client_secret": "YOUR_APP_SECRET",
    },
)
token = r.json()["access_token"]

방식 B — HTTP Basic 인증 헤더로 전달 (공식 문서 예제 형태)

r = requests.post(
    "https://openapi.tossinvest.com/oauth2/token",
    data={"grant_type": "client_credentials"},
    auth=("YOUR_APP_KEY", "YOUR_APP_SECRET"),   # ← Basic 인증
)

결과부터 말하면 방식은 원인이 아니었다. 실제 순서는 이랬다.

  1. 방식 A로 시도 → 401
  2. 방식 B로도 시도 → 똑같이 401, 에러 메시지도 동일
  3. 콘솔에서 키 재발급
  4. 방식 A로 다시 시도 → 200 성공

즉 문제였던 건 키 자체였고, 방식 A는 유효한 키였다면 처음부터 성공했을 코드였다. 방식 B는 문제 있는 키로만 테스트했고 새 키로 재검증하지 않았으므로, "B가 안 된다"고 말할 근거도 없다.

교훈은 이쪽이다. 두 방식 모두 같은 에러가 났다는 건 애초에 인증 방식 문제가 아니라는 신호였는데, 그걸 못 읽고 계속 방식만 바꿔봤다. 변수를 바꿨는데 결과가 똑같으면, 그 변수는 원인이 아니다. 다음 변수로 넘어가야 한다.

토큰을 받은 뒤에는 모든 요청에 Authorization: Bearer {token} 헤더를 붙이면 된다.

401 Unauthorized와 403 Forbidden 오류의 원인 구분 및 각각의 점검 순서 정리표


3. 호출 제한과 응답 구조

3-1. 초당 호출 제한

엔드포인트마다 다르다. 확인한 범위는 대략 이랬다.

구분제한

인증(토큰 발급) 초당 5회
시세 조회 초당 10회
캔들(일봉) 조회 초당 5회

전 종목을 순회하는 작업이라면 이 숫자가 바로 병목이 된다. 요청 사이에 time.sleep()을 넣거나, 실패 시 대기 후 재시도하는 로직을 처음부터 넣어두는 게 낫다. 나중에 넣으려면 구조를 다시 짜야 한다.

3-2. 문서에 없는 캔들 응답 구조

캔들(일봉) API의 실제 응답 형식은 공식 문서에 명확히 나와 있지 않았다. 직접 호출해서 확인한 구조는 이랬다.

{
  "result": {
    "candles": [
      {
        "timestamp": "2026-07-16T00:00:00.000+09:00",
        "openPrice": "269000",
        "highPrice": "269000",
        "lowPrice": "256500",
        "closePrice": "257000",
        "volume": "11701935"
      }
    ],
    "nextBefore": "2026-07-13T00:00:00.000+09:00"
  }
}

주의할 점 두 가지.

  • 가격이 문자열로 온다. "269000" 이지 269000 이 아니다. 그대로 계산에 넣으면 문자열 연결이 되거나 타입 오류가 난다. 받는 즉시 숫자로 변환해두는 게 좋다.
  • 한 번에 최대 200개까지만 준다. 그보다 긴 기간이 필요하면 nextBefore 값을 이용해 이전 구간을 이어 받아야 한다.
def fetch_candles(ticker, start_date):
    all_candles, before = [], None
    while True:
        resp = call_candle_api(ticker, before=before)
        candles = resp["result"]["candles"]
        if not candles:
            break
        all_candles.extend(candles)
        before = resp["result"].get("nextBefore")
        if not before or candles[-1]["timestamp"] < start_date:
            break
        time.sleep(0.2)          # 초당 5회 제한 대응
    return all_candles

4. 실패에 대비한 폴백 구조

API 호출은 반드시 가끔 실패한다. 네트워크 문제일 수도, 일시적 서버 오류일 수도 있다. 자동으로 매일 돌아가는 시스템이라면 이 실패가 전체를 멈춰 세운다.

그래서 토스 API가 실패하면 자동으로 다른 공개 데이터 소스(pykrx)로 넘어가도록 만들었다.

def get_price(ticker):
    try:
        return toss_client.get_candle(ticker)
    except TossAPIError:
        logger.warning(f"{ticker}: 토스 API 실패, pykrx로 폴백")
        return pykrx_fallback(ticker)

한쪽이 막혀도 시스템 전체가 멈추지 않는다.

다만 폴백에는 조건이 있다. 두 소스의 데이터 형식이 다르면(가격 단위, 날짜 형식, 수정주가 반영 여부) 조용히 잘못된 값이 섞여 들어간다. 폴백을 넣을 때는 정규화 함수를 하나 거치게 만들고, 폴백이 발동했다는 사실을 로그로 남겨야 한다. 그래야 나중에 이상한 결과가 나왔을 때 원인을 추적할 수 있다.

토스 API 실패 시 pykrx로 자동 전환되는 데이터 소스 폴백 구조 다이어그램


5. KRX 데이터 (pykrx)

5-1. 계정이 필요한 이유

시가총액, PBR 같은 재무 데이터는 pykrx 라이브러리로 가져올 수 있는데, 일부 데이터는 KRX 정보데이터시스템(data.krx.co.kr) 계정 로그인이 있어야 막힘없이 조회된다.

📸 [스크린샷: data.krx.co.kr 회원가입 화면]

계정 정보는 코드에 직접 적지 않고 .env 파일에 따로 저장해서 불러오는 방식으로 관리했다.

import os
from dotenv import load_dotenv

load_dotenv()
krx_id = os.environ["KRX_ID"]
krx_pw = os.environ["KRX_PW"]

.env 파일은 반드시 .gitignore 에 추가해둘 것. GitHub에 올라간 자격증명은 몇 분 안에 스캔당한다.

# .gitignore
.env
*.key
config/secrets.*

5-2. 두 번째 삽질 — 로그인 세션이 자꾸 끊긴다

KRX 로그인 세션은 1시간이면 만료된다.

9년치 전 종목 일봉(2,334거래일)을 하루씩 받는 작업은 몇 시간이 걸린다. 당연히 중간에 세션이 끊겼다. 그것도 여러 번.

그래서 하루 단위로 진행 상황을 저장하고, 끊기면 마지막으로 받은 날짜부터 이어받는 방식으로 만들었다.

def collect_daily(start, end, out_dir):
    done = {f.stem for f in Path(out_dir).glob("*.parquet")}   # 이미 받은 날짜
    failed = []
    for d in trading_days(start, end):
        if d in done:
            continue                                          # 건너뛰기
        try:
            df = fetch_krx_ohlcv(d)
            df.to_parquet(f"{out_dir}/{d}.parquet")            # 즉시 저장
        except SessionExpired:
            relogin()                                         # 세션 갱신 후 재시도
            failed.append(d)
        except Exception:
            failed.append(d)
    return failed                                             # 마지막에 재시도

핵심은 세 가지다.

  • 하루치 받을 때마다 즉시 저장 — 메모리에 쌓아두면 중단 시 전부 날아간다
  • 시작할 때 이미 받은 날짜를 스캔해서 건너뜀 — 재실행이 곧 이어받기가 된다
  • 실패한 날짜는 목록에 쌓아두고 마지막에 재시도 — 중간에 멈추지 않는다

이 구조가 아니었으면 수집에 며칠이 아니라 몇 주가 걸렸을 것이다.

5-3. 세 번째 삽질 — 업종 분류가 빈 값으로 온다

종목별 업종을 가져오는 공식 함수가 있었는데, 호출하면 빈 결과만 돌아왔다.

우회로를 찾았다. 업종별 지수(은행, 전기전자, 화학 등)의 구성 종목 목록을 받아서, 그걸 거꾸로 뒤집어 "이 종목은 이 업종"이라는 표를 직접 만들었다.

sector_map = {}
for sector_code, sector_name in SECTOR_INDEXES.items():
    tickers = get_index_portfolio(sector_code)   # 지수 → 종목 목록
    for t in tickers:
        sector_map[t] = sector_name              # 종목 → 업종 (역방향)

공식 API가 막혀 있어도 우회로가 있는 경우였다. 같은 데이터를 다른 각도에서 제공하는 엔드포인트가 있는지 먼저 훑어보는 게, 라이브러리를 갈아엎는 것보다 훨씬 빠르다.

참고로 5편에서 다룬 "업종 쏠림 실험"이 이 표로 돌린 것이다. 이 우회로가 없었으면 그 검증 자체를 못 했다.


6. 그 와중에 만난 딴 데서 온 버그

API 연동은 아니지만 같이 겪은 문제라 적어둔다.

자동 실행용 배치파일에 한글 설명을 적어뒀는데, 실행 환경의 콘솔 설정에 따라 한글이 깨지면서 그 줄만이 아니라 다음 명령어까지 통째로 인식 못 하는 현상이 있었다.

증상이 고약한 이유는, 문제의 원인(한글 주석)과 증상이 나타나는 위치(그 다음 줄)가 달라서다. 다음 줄 명령어를 아무리 들여다봐도 오타가 없으니 한참을 헤맸다.

해결: 실행되는 문구는 전부 영어로, 설명은 주석으로만 한글을 쓰는 식으로 분리했다.

@echo off
chcp 65001 > nul
REM 매일 장 마감 후 일봉 수집 (한글은 주석에만)
echo Starting daily collection...
python collect_daily.py

자동화 스크립트를 짤 때는 실행되는 부분과 사람이 읽는 설명 부분을 언어로도 분리해두는 게 안전하다.


7. 정리 — 처음 연동한다면 이 순서로

  •  토스증권 Open API 사전 신청 (승인까지 며칠 소요)
  •  WTS에서 클라이언트 등록 → client_id / client_secret 발급
  •  공인 IP 확인 후 허용 IP 등록
  •  토큰 발급 테스트 — 401이면 키 재발급부터 시도 (인증 방식보다 키 상태가 원인인 경우가 많다)
  •  캔들 API 한 종목으로 응답 구조 확인 (가격이 문자열로 오는 점 주의)
  •  200개 제한 → nextBefore 페이징 구현
  •  초당 호출 제한 대응 (sleep + 재시도)
  •  폴백 소스 연결 + 정규화 함수 + 로그
  •  .env 분리 및 .gitignore 등록
  •  장기 수집은 이어받기 구조로 (즉시 저장 + 완료 스캔 + 실패 목록)

참고 링크


다음 편 예고

여기까지 만든 시스템으로 실제 페이퍼 트레이딩을 몇 주 돌렸다.

그런데 이 프로젝트를 만들다 보니 정작 눈을 돌리게 된 건 자동매매 계좌가 아니라 원래 갖고 있던 내 계좌였다. 레버리지 상품 하나가 계좌를 갉아먹고 있었다는 걸 뒤늦게 발견한 이야기를 다음 편에서 다룬다.


이 글은 투자 권유가 아니며, 개인적인 AI·자동매매 시스템 개발 과정의 기록입니다. API 스펙·정책·제공 범위는 빠르게 바뀔 수 있으므로, 실제 적용 시에는 각 서비스의 공식 문서에서 최신 사양을 확인하시기 바랍니다.