AlgoLab Blog · 토스증권 트러블슈팅 · 2026

토스증권 오픈API 에러코드 12가지 — 401·422 해결

토스증권 · 트러블슈팅 2026-08-04 · 약 8분 읽기 · 알고랩 AlgoLab
한 줄 요약 토스증권 OpenAPI의 오류는 HTTP 상태가 아니라 본문의 error.code로 분기해야 합니다. 같은 edge-blocked401이면 Authorization 헤더 누락, 403이면 허용 IP 미등록, 404면 없는 경로를 뜻하고, 반대로 422 하나에 잔고 부족·장 시간 외·호가 범위 초과가 뒤섞여 있기 때문입니다. 봇이 실제로 만나는 것은 12개이고, 그중 봇을 멈춰야 하는 것(invalid-token·account-restricted)과 기다렸다 재시도하면 되는 것(rate-limit-exceeded·order-hours-closed)을 나누는 것이 핵심입니다.

토스증권 오픈API 자동매매 가이드에서 발급과 주문까지 다뤘다면, 이 글은 그다음에 실제로 마주치는 실패 응답만 봅니다. 한국투자증권 KIS API를 써 보신 분이라면 KIS API 에러코드 11가지와 같은 성격의 글이라고 생각하시면 됩니다. 다만 KIS가 EGW00201처럼 영숫자 코드를 쓰는 것과 달리, 토스증권은 insufficient-buying-power처럼 읽히는 문자열을 씁니다.

먼저 — 실패 응답의 생김새

성공은 {"result": ...}, 실패는 {"error": {...}}입니다. 둘은 절대 같이 오지 않습니다.

{
  "error": {
    "requestId": "01HXYZABCDEFG123456789",
    "code": "invalid-request",
    "message": "주문 방향이 올바르지 않습니다.",
    "data": { "field": "side", "allowedValues": ["BUY", "SELL"] }
  }
}

data가 특히 유용합니다. 어떤 필드가 문제인지, 허용되는 값이 무엇인지 서버가 직접 알려줍니다. 로그에 message만 남기지 말고 code·data·requestId를 통째로 남기세요. requestId는 응답 헤더 X-Request-Id와 같은 값이고, 고객센터 문의 시 그대로 첨부하는 용도입니다.

import requests

def call(method, path, **kw):
    r = requests.request(method, BASE + path, timeout=5, **kw)
    if r.status_code >= 400:
        e = r.json().get("error", {})
        raise TossApiError(code=e.get("code"), status=r.status_code,
                           msg=e.get("message"), data=e.get("data"),
                           rid=e.get("requestId") or r.headers.get("X-Request-Id"))
    return r.json()["result"]

봇이 실제로 만나는 12가지

  1. edge-blocked — 같은 이름, 세 가지 뜻
  2. invalid-token · expired-token
  3. account-header-required
  4. account-not-found
  5. invalid-request
  6. confirm-high-value-required
  7. insufficient-buying-power
  8. order-hours-closed
  9. price-out-of-range
  10. opposite-pending-order-exists
  11. idempotency-key-conflict · request-in-progress
  12. rate-limit-exceeded

인증 계열 — 401 · 403

1. edge-blocked — 같은 이름인데 상태별로 뜻이 다르다

이 코드 하나가 세 개의 HTTP 상태에 걸쳐 나타납니다. 여기서 헷갈리면 엉뚱한 곳을 고치게 됩니다.

상태실제 의미고칠 곳
401Authorization 헤더가 아예 없음헤더 조립 로직 — 토큰 갱신 실패로 None이 들어간 경우가 많음
403허용되지 않은 요청허용 IP 미등록이 1순위
404지원하지 않는 API 경로경로 오타 · /api/v1 누락

403이 뜨면 코드부터 보지 말고 IP부터 확인하세요. 토스증권 OpenAPI는 WTS 설정의 허용 IP 관리에 등록된 IP에서만 호출이 됩니다. 로컬에서 되던 봇을 VPS·클라우드로 옮기면 공인 IP가 바뀌므로 반드시 다시 등록해야 합니다. 유동 IP 환경이라면 재부팅만으로도 막힐 수 있습니다.

2. invalid-token · expired-token (401)

두 개를 다르게 처리해야 합니다. expired-token은 정상적인 수명 만료이므로 토큰을 다시 받아 1회 재시도하면 됩니다. 반면 invalid-token은 토큰 형식이 잘못됐거나 폐기된 것이라, 재발급을 반복해도 같은 결과가 나옵니다 — 봇을 멈추고 알림을 보내야 하는 쪽입니다.

ONCE_RETRY = {"expired-token"}          # 토큰 갱신 후 1회 재시도
STOP       = {"invalid-token", "login-user-not-found",
              "forbidden", "account-restricted", "prerequisite-required"}
WAIT       = {"rate-limit-exceeded", "edge-rate-limit-exceeded",
              "order-hours-closed", "internal-error", "maintenance"}

access_tokenexpires_in은 86400초입니다. 만료 5분 전에 선제 갱신하면 이 오류를 거의 보지 않게 됩니다. client_secret 자체가 유출·재발급된 상황도 invalid-token으로 나타나므로, 키 관리는 API 키 보안의 원칙을 그대로 적용하세요.

계좌 헤더 계열 — 400 · 404

3. account-header-required (400)

계좌·자산·주문·조건주문 계열 호출에 X-Tossinvest-Account를 빼먹은 경우입니다. 시세 조회는 토큰만으로 되기 때문에, 시세 코드를 복사해 주문 코드를 만들다가 이 헤더를 빠뜨리는 일이 흔합니다. 헤더 조립 함수를 하나로 만들고 계좌 계열이면 무조건 붙게 강제하는 편이 안전합니다.

4. account-not-found (404)

헤더는 보냈는데 그 값에 해당하는 계좌가 없는 경우입니다. 3번과 헷갈리기 쉬운데, 3번은 "안 보냄", 4번은 "보냈는데 틀림"입니다. 계좌 식별자는 GET /api/v1/accounts로 확인하되, 이 엔드포인트는 ACCOUNT 그룹이라 초당 1회 제한입니다. 루프 안에서 부르지 말고 기동 시 한 번만 읽어 캐시하세요.

주문 계열 — 400 · 422

5. invalid-request (400)

가장 자주 보는 코드이고, 원인은 data가 알려 줍니다. 실무에서 잦은 세 가지는 열거형 대소문자(buyBUY여야 함), 숫자를 숫자로 보낸 것(quantity·price는 문자열), 필수 필드 누락입니다. orderTypeLIMIT인데 price가 없으면 여기로 떨어집니다.

// 잘못된 요청
{"symbol":"005930","side":"buy","orderType":"LIMIT","quantity":10}

// 응답
{"error":{"code":"invalid-request","message":"주문 방향이 올바르지 않습니다.",
  "data":{"field":"side","allowedValues":["BUY","SELL"]}}}

6. confirm-high-value-required (400)

주문 금액이 1억원 이상인데 confirmHighValueOrdertrue가 아닐 때입니다. 이건 버그가 아니라 안전장치입니다. 봇에서 이 오류가 났다면 대개 수량 계산이 잘못돼 0이 하나 더 붙은 것이므로, 자동으로 true를 붙여 재시도하도록 만들면 안 됩니다. 알림을 보내고 멈추는 쪽이 맞습니다 — 킬 스위치·서킷브레이커 설계와 같은 맥락입니다.

7. insufficient-buying-power (422)

매수 가능 금액 부족입니다. 주문 직전에 GET /api/v1/buying-power로 확인하면 줄일 수 있지만, 이 엔드포인트가 속한 ORDER_INFO 그룹은 오전 9시~9시 10분에 초당 6회에서 3회로 줄어듭니다. 장 시작 직후 모든 주문마다 잔고를 확인하는 구조라면 가장 바쁜 시간에 429를 맞습니다. 잔고는 주기적으로 갱신해 캐시하고, 이 오류가 나면 그때 재조회하는 편이 낫습니다. 매도 쪽 대응 코드는 insufficient-sellable-quantity입니다.

8. order-hours-closed (422)

주문을 접수할 수 없는 시간입니다. 봇을 24시간 띄워 두면 필연적으로 만납니다. 에러로 재시도 루프를 도는 대신 GET /api/v1/market-calendar/KR·/US로 장 운영 시간을 먼저 읽고 스케줄을 짜는 것이 정석입니다. 미국 주식은 데이마켓·프리마켓·정규장·애프터마켓이 나뉘어 있어, 정규장에만 되는 금액 주문을 프리마켓에 내면 amount-order-outside-regular-hours가 뜹니다.

9. price-out-of-range (422)

주문 가격이 상·하한가 범위를 벗어났습니다. 전일 종가 기준으로 계산한 지정가를 그대로 던졌을 때 나옵니다. GET /api/v1/price-limits로 그날의 상·하한가를 먼저 읽어 클램프하세요. 사촌 격인 invalid-tick-size호가 단위에 맞지 않는 가격입니다 — 가격대별로 호가 단위가 달라지므로 반올림 규칙을 넣어야 합니다. 주문 유형별 성질 차이는 시장가·지정가·조건부 주문에 정리해 두었습니다.

10. opposite-pending-order-exists (422)

같은 종목에 반대 방향의 미체결 주문이 남아 있을 때 새 주문이 거부됩니다. 처음 보면 당황스럽지만, 봇 입장에서는 흔한 상황입니다 — 매수 지정가를 걸어 두고 전략이 뒤집혀 매도를 내는 경우가 그렇습니다. 새 주문을 내기 전에 해당 종목의 미체결을 먼저 취소하는 순서를 강제하면 사라집니다.

# 반대 방향 주문 전에 미체결 정리
def place_safe(symbol, side, **body):
    for o in call("GET", "/api/v1/orders",
                  params={"status": "OPEN"}, headers=H)["items"]:
        if o["symbol"] == symbol and o["side"] != side:
            call("POST", f"/api/v1/orders/{o['orderId']}/cancel", headers=H)
    return call("POST", "/api/v1/orders", json={"symbol": symbol,
                "side": side, **body}, headers=H)

국내 주식에서만 나오는 investor-exchange-not-integrated도 같은 계열입니다. 투자자지시 거래소 설정이 통합(SOR)이 아닐 때 발생하므로, 계좌 설정 문제이지 코드 문제가 아닙니다.

멱등키와 한도 — 409 · 422 · 429

11. idempotency-key-conflict · request-in-progress · already-*

clientOrderId중복 주문을 막는 멱등키입니다. 최대 36자, ^[a-zA-Z0-9\-_]+$, 유효 기간 10분이고 서버가 자동 생성해 주지 않습니다. 관련 오류가 셋으로 갈립니다.

재시도할 때 키를 새로 만들면 멱등성이 무의미해집니다. 주문 1건에 키 1개를 만들어 두고, 네트워크 타임아웃으로 재시도할 때는 같은 키를 그대로 보내세요. 그래야 "주문이 들어갔는지 모르겠다"는 상황에서 중복 체결이 안 납니다.

12. rate-limit-exceeded · edge-rate-limit-exceeded (429)

한도는 클라이언트 × API 그룹의 초당 요청 수로 걸립니다. 응답 헤더를 보고 판단하세요.

X-RateLimit-Limit: 6        # 현재 허용 초당 요청 수
X-RateLimit-Remaining: 0    # 남은 토큰 (429면 0)
X-RateLimit-Reset: 1        # 토큰 1개 재충전까지 예상 초
Retry-After: 1              # 429에만 포함

공식 권장 대응은 Retry-After만큼 대기 후 재시도, 지수 백오프(1s → 2s → 4s)에 지터, 그리고 X-RateLimit-Remaining이 줄어들면 429가 나기 전에 선제적으로 속도를 늦추는 것입니다. 전략을 여러 개 돌리면 호출량이 합산된다는 점까지 포함한 설계는 API 호출 제한 설계에서 다뤘습니다. 원리는 KIS의 EGW00201과 같습니다.

🛠️
에러 하나 때문에 봇이 며칠째 멈춰 있다면

알고랩은 봇 제작 시 code 기준 분기·재시도·알림을 기본으로 넣습니다. 이미 만들던 코드가 있으면 그 상태로 가져오셔도 됩니다.

상담 시작하기 →

덜 흔하지만 알아 둘 것

코드상태의미
stock-restricted422거래 제한 종목 (정리매매·투자위험 등)
account-restricted422계좌 상태가 해당 주문을 허용하지 않음 (연금·RIA 등)
prerequisite-required422약관 동의·위험 고지 미완료
order-limit-exceeded422주문 설정 한도 초과
duplicate-conditional-order422OCO·OTO는 종목당 1개 (SINGLE은 무제한)
condition-already-met422조건주문 감시가가 이미 충족된 가격
unsupported-content-type415본문은 application/json
maintenance500시스템 점검 — 재시도 대상

stock-restricted·account-restricted·prerequisite-required 세 개는 재시도해도 소용없습니다. 계좌나 종목 쪽 상태 문제라 사람이 처리해야 하므로, 봇은 즉시 멈추고 알림을 보내는 것이 맞습니다.

KIS·키움에서 넘어왔다면 — 판정 지점이 다르다

다른 증권사 봇을 옮겨 올 때 가장 많이 나는 사고는 "실패를 실패로 못 읽는 것"입니다. 한국투자증권 KIS API는 HTTP 200으로 내려온 본문 안의 rt_cd·msg_cd를 봐야 실패를 알 수 있는 경우가 있어서, raise_for_status()만 믿으면 실패한 주문을 성공으로 처리합니다. 토스증권 OpenAPI는 그 지점이 훨씬 단순합니다.

구분토스증권 OpenAPI한국투자증권 KIS API키움 REST API
실패 판정HTTP 4xx·5xx + error 봉투HTTP 200에도 rt_cd로 판정응답 코드 필드 확인
코드 형식읽히는 문자열
insufficient-buying-power
영숫자
EGW00201
업무 코드 체계
업무 구분URL 경로tr_id 헤더api-id 헤더
인증 만료expired-tokenaccess_token 만료 재발급토큰 재발급
호출 초과rate-limit-exceeded (429)EGW00201TR 요청 제한
실시간없음 (REST API 전용)WebSocket 제공WebSocket 제공

바꿔 말하면, KIS나 키움에서 쓰던 appkey·tr_id 기반 헤더 조립과 rt_cd 검사 코드는 전부 걷어내야 합니다. 대신 토스증권 쪽은 상태코드로 1차 분기, error.code로 2차 분기라는 두 단계만 지키면 됩니다.

자주 묻는 질문

어떤 오류를 재시도하고 어떤 것을 멈춰야 하나요?

세 갈래로 나누면 충분합니다. 대기 후 재시도rate-limit-exceeded·order-hours-closed·internal-error·maintenance, 토큰 갱신 후 1회 재시도expired-token, 나머지 즉시 중단·알림invalid-token·account-restricted·confirm-high-value-required처럼 사람이 손대야 하는 것들입니다. 이 분류표를 코드 상단에 상수로 두면 분기가 흐트러지지 않습니다.

같은 오류가 계속 나는데 로그에 뭘 남겨야 하나요?

code·data·requestId 세 개는 필수입니다. requestId는 응답 헤더 X-Request-Id와 같은 값이며, 일부 응답에서 누락되면 referenceIdx-amz-cf-id 헤더를 대신 첨부하라고 공식 문서가 안내합니다. 고객센터 문의는 이 값 하나로 조회가 되므로, 없으면 재현부터 다시 해야 합니다.

KIS나 키움의 에러 처리 코드를 그대로 쓸 수 있나요?

구조가 다릅니다. KIS API는 rt_cd·msg_cd·msg1HTTP 200 본문 안에서 확인해야 하는 경우가 있어 상태코드만 보면 실패를 놓칩니다. 토스증권 OpenAPI는 실패가 4xx·5xx로 명확히 내려오고 본문이 error 봉투로 통일돼 있어 오히려 단순합니다. 증권사별 규격 차이는 증권사 API 비교에서 정리했습니다.

토스증권 자동매매 봇 맞춤 제작

에러 코드별 분기·재시도·텔레그램 알림·24시간 무중단 운영까지 — 알고랩이 통합 패키지로 제작합니다.
24시간 빠른 답변 가능합니다.

무료 상담 시작하기