dot2shape.blog
← posts

토스증권 Open API 연동기 — OAuth2 토큰 캐싱과 레이트리밋 대응

공식 토스증권 Open API(REST)를 붙이며 정리한 인증·안정성 노트. OAuth2 Client Credentials 토큰 캐싱·재발급, 그룹별 레이트리밋과 429 백오프, 에러 모델 추적까지.

·
  • #토스증권
  • #Open API
  • #OAuth2
  • #Rate Limit
  • #REST API

이 글의 모든 자격증명·계좌 식별자는 placeholder(client_id=xxx, client_secret=yyy, X-Tossinvest-Account: 1, 토큰 eyJhbGciOi...)로 표기했습니다. 실제 값은 절대 코드·로그·블로그에 노출하지 말고 환경변수/시크릿 매니저로 관리하세요.

TL;DR

  • 토스증권 Open API는 공식 REST API다. 비공식 엔드포인트 리버스엔지니어링이 아니라 공식 문서와 OpenAPI 스펙이 제공된다. Base는 https://openapi.tossinvest.com.
  • 인증은 OAuth 2.0 Client Credentials Grant 단일 방식. POST /oauth2/token 으로 access token을 받아 Authorization: Bearer 로 호출한다.
  • 시세·종목 정보는 토큰만으로 호출되지만, 계좌·자산·주문X-Tossinvest-Account 헤더가 추가로 필요하다.
  • 토큰은 매 호출마다 발급하지 말고 expires_in 만큼 캐싱해 재사용한다. 401(expired-token/invalid-token)을 받으면 그때 재발급 후 1회 재시도.
  • 레이트리밋은 그룹별 초당 한도 + 토큰 버킷이다. 429를 받으면 Retry-After 만큼 대기하고 지수 백오프 + jitter로 재시도, X-RateLimit-Remaining 으로 선제 감속한다.
  • 에러는 error.{requestId, code, message, data} 구조. requestId(=X-Request-Id)를 로깅해두면 CS 추적이 된다.

개요 — 무엇을 붙이는가

토스증권 Open API는 국내(KRX·NXT) 및 미국 주식의 시세·종목정보·환율·장 운영시간, 그리고 본인 계좌의 보유주식·주문을 다루는 REST API다. 기능은 네 카테고리로 나뉜다.

카테고리대표 엔드포인트인증 요건
인증 (Auth)POST /oauth2/tokenclient_id / client_secret
시세·종목 정보 (Market Data · Stock Info · Market Info)GET /api/v1/prices, /orderbook, /candles, /stocks, /exchange-rate토큰만
계좌·자산 (Account · Asset)GET /api/v1/accounts, /holdings토큰 + X-Tossinvest-Account
주문 (Order · History · Info)POST /api/v1/orders, GET /api/v1/buying-power토큰 + X-Tossinvest-Account

연동 방식은 현재 REST만 제공된다. 즉 실시간 시세도 WebSocket이 아니라 폴링으로 구현해야 하며, 이 점이 뒤의 레이트리밋 설계와 직결된다.

클라이언트 등록은 토스증권 WTS 로그인 후 설정 > Open API 메뉴에서 client_id / client_secret 을 발급받는다.

인증 — 토큰을 캐싱해서 재사용한다

가장 먼저 부딪히는 지점이다. 인증은 OAuth 2.0 Client Credentials Grant 하나뿐이다. 사용자 로그인·리다이렉트·refresh token이 없고, 서버 대 서버 자격증명만 있으면 된다.

# 1) 토큰 발급
curl -s -X POST 'https://openapi.tossinvest.com/oauth2/token' \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  -d 'grant_type=client_credentials' \
  -d 'client_id=xxx' \
  -d 'client_secret=yyy'

응답은 공통 envelope이 아니라 OAuth 2.0 표준 토큰 응답 형식으로 내려온다(이 토큰 엔드포인트만 예외). 필수 필드는 access_token·token_type·expires_in 세 가지다.

{
  "access_token": "eyJraWQiOiIyMDI2LTA0LTAxLWtleSIsImFsZyI6IlJTMjU2In0...",
  "token_type": "Bearer",
  "expires_in": 86400
}
  • access_tokenJWT다. 모든 API 요청의 Authorization: Bearer 헤더에 담는다.
  • expires_in토큰 만료까지 남은 초(정수)다. 스펙 예시는 86400(=24시간)이다 — 즉 한 번 받으면 하루 단위로 재사용할 수 있다. 캐싱이 특히 유효한 이유다.

매 호출마다 발급하지 않는다

여기서 처음에 하기 쉬운 실수가, API를 호출할 때마다 토큰을 새로 발급하는 것이다. 토큰 발급(AUTH) 자체에도 레이트리밋(초당 5회)이 걸려 있어서, 호출량이 늘면 발급 단계에서 먼저 막힌다.

그래서 토큰은 한 번 받아 expires_in 만큼 메모리에 캐싱하고 재사용한다. 만료 약간 전(예: 만료 30~60초 전)에 선제 재발급하면 만료 경계에서의 401을 줄일 수 있다. 의사코드로 옮기면 다음 한 가지 규칙으로 압축된다.

  • 캐시에 유효한 토큰이 있으면 그대로 쓴다.
  • 없거나 만료 임박이면 1회만 발급하고 캐시에 저장한다(동시 요청이 몰릴 때 발급이 중복되지 않도록 한 번에 하나만 발급).

401은 재발급 후 1회 재시도

캐싱을 해도 토큰은 어쨌든 만료된다. 토스증권은 토큰 관련 실패를 401로 구분해서 내려준다.

HTTPcode의미대응
401expired-token액세스 토큰 만료재발급 후 재시도
401invalid-token토큰이 유효하지 않거나 형식 오류재발급 후 재시도
401edge-blockedAuthorization 헤더 누락헤더 추가 (재발급 불필요)
401login-user-not-found토큰에 대응하는 로그인 정보 없음자격증명·계정 상태 점검

expired-token / invalid-token토큰을 버리고 재발급한 뒤 같은 요청을 1회만 재시도하면 된다. 무한 재시도 루프를 막기 위해 재시도 횟수는 1회로 제한하는 게 안전하다.

계좌·주문 — X-Tossinvest-Account 를 빠뜨리지 않기

시세·종목 정보는 토큰만으로 호출된다.

# 시세·종목 정보 (토큰만 필요) — 005930: 삼성전자
curl -s 'https://openapi.tossinvest.com/api/v1/stocks?symbols=005930' \
  -H 'Authorization: Bearer eyJhbGciOi...'

반면 계좌·자산·주문 카테고리는 토큰에 더해 계좌 식별 헤더가 필요하다.

# 계좌·자산 / 주문 (토큰 + 계좌 헤더)
curl -s 'https://openapi.tossinvest.com/api/v1/holdings' \
  -H 'Authorization: Bearer eyJhbGciOi...' \
  -H 'X-Tossinvest-Account: 1'

이 헤더를 빠뜨리면 400 account-header-required 가 떨어진다. 계좌 번호 원문이 아니라 accountSeq(계좌 목록 조회로 얻는 순번)를 넣는다는 점을 기억하면 된다. 시세 코드와 계좌 코드를 같은 HTTP 클라이언트로 공유할 때, 계좌 계열 요청에만 이 헤더를 주입하도록 분기해두는 게 깔끔하다.

레이트리밋·안정성 — REST 폴링과 그룹별 한도

REST만 제공되므로 실시간 시세는 결국 폴링이다. 폴링 주기를 무작정 줄이면 레이트리밋에 걸리고, 늘리면 실시간성이 떨어진다. 토스증권의 한도는 엔드포인트 그룹별 초당 한도로 나뉘어 있어, 균형점을 그룹 단위로 잡아야 한다.

Rate Limit Group요청 한도피크시간 한도
AUTH초당 5회--
ACCOUNT초당 1회--
ASSET초당 5회--
STOCK초당 5회--
MARKET_INFO초당 3회--
MARKET_DATA초당 10회--
MARKET_DATA_CHART초당 5회--
ORDER초당 6회09:00–09:10 KST: 초당 3회
ORDER_HISTORY초당 5회--
ORDER_INFO초당 6회09:00–09:10 KST: 초당 3회

읽어둘 포인트가 몇 개 있다.

  • ACCOUNT 은 초당 1회로 가장 빡빡하다. 계좌 목록은 자주 부를 정보가 아니므로 캐싱 대상이다.
  • MARKET_DATA 는 초당 10회로 가장 여유롭다. 현재가/호가 폴링은 여기에 들어간다. 반면 캔들(MARKET_DATA_CHART)은 초당 5회로 절반이라 따로 관리한다.
  • 주문(ORDER/ORDER_INFO)은 장 시작 직후 09:00–09:10에 한도가 절반(6→3회)으로 줄어든다. 개장 직후 주문이 몰리는 시간대에 더 조이는 구조이므로, 이 구간을 코드에서 별도로 인지하지 않으면 정확히 가장 바쁠 때 막힌다.

응답 헤더로 백오프한다

429를 받기 전에도, 받은 후에도 판단 근거는 응답 헤더다.

헤더의미
X-RateLimit-Limit현재 허용된 초당 요청 수 (burst capacity)
X-RateLimit-Remaining버킷에 남은 토큰 수 (429 시 0)
X-RateLimit-Reset토큰 1개 재충전까지 예상 초
Retry-After재시도 권장 초 (429 응답에만 포함)

대응 전략은 문서 권장안을 그대로 따르는 게 가장 안정적이었다.

  • 429 수신 시 Retry-After 값만큼 대기한 뒤 재시도한다.
  • 그 위에 지수 백오프(1s → 2s → 4s …) + jitter를 얹는다. jitter가 없으면 여러 워커가 동시에 깨어나 같은 순간 재요청하며 429를 재생산한다.
  • X-RateLimit-Remaining 이 낮아지면 클라이언트 측에서 선제적으로 호출 속도를 늦춘다. 429를 맞고 회복하는 것보다, 맞기 전에 감속하는 쪽이 체감 안정성이 높다.
# 429 응답에 포함되는 재시도 힌트 확인
curl -s -D - -o /dev/null 'https://openapi.tossinvest.com/api/v1/prices?symbols=005930' \
  -H 'Authorization: Bearer eyJhbGciOi...' \
  | grep -i -E 'x-ratelimit|retry-after'

에러 모델 — requestId 를 로깅한다

모든 에러는 동일한 봉투 구조로 내려온다.

{
  "error": {
    "requestId": "01HXYZABCDEFG123456789",
    "code": "invalid-request",
    "message": "주문 방향이 올바르지 않습니다.",
    "data": {
      "field": "side",
      "allowedValues": ["BUY", "SELL"]
    }
  }
}
  • code 가 분기 기준이다(예: invalid-tick-size, order-not-found, invalid-token). HTTP status가 아니라 이 code 로 처리 로직을 나누면 깔끔하다.
  • data 는 에러 해결 힌트로, 코드별로 포함 여부와 키 구조가 다르다. 위 예처럼 allowedValues 를 그대로 보여주면 디버깅이 빨라진다.
  • requestId 는 응답 헤더 X-Request-Id 와 같은 값이다. 요청 로그에 항상 같이 남겨두면 CS 문의 시 그대로 첨부할 수 있다. requestId 가 누락된 응답이면 응답 헤더의 cf-ray 값으로 대체한다.

주문 쪽에서 한 번 짚어둘 만한 코드:

  • 400 account-header-requiredX-Tossinvest-Account 누락.
  • 400 confirm-high-value-required — 주문 금액이 1억원 이상인데 confirmHighValueOrdertrue 가 아닐 때. 고액 주문은 의도 확인 플래그를 명시적으로 요구한다.

배운 점 체크리스트

  • 토큰은 expires_in 만큼 캐싱·재사용, 만료 임박 시 선제 재발급(한 번에 하나만 발급).
  • 401 expired-token/invalid-token → 재발급 후 1회만 재시도.
  • 계좌·자산·주문 요청에만 X-Tossinvest-Account: {accountSeq} 주입.
  • 폴링 주기는 그룹별 초당 한도 기준으로 설계 — ACCOUNT(1/s) 캐싱, 시세는 MARKET_DATA(10/s) 활용.
  • 주문 한도는 09:00–09:10 KST에 절반으로 줄어든다 — 개장 직후 구간을 코드에서 인지.
  • 429 → Retry-After 대기 + 지수 백오프 + jitter, X-RateLimit-Remaining 으로 선제 감속.
  • 모든 요청에 requestId(=X-Request-Id, 없으면 cf-ray) 로깅.
  • 자격증명은 환경변수/시크릿으로만 — 코드·로그·글에 절대 노출 금지.

참고: 토스증권 Open API 공식 문서 · AI/에이전트용 /llms.txt · Overview Markdown