AlgoLab Blog · 토스증권 Open API · 2026

토스증권 API 파이썬 첫 주문 — 토큰부터 체결 확인까지

토스증권 · 실행 코드 2026-08-19 · 약 9분 읽기 · 알고랩 AlgoLab
한 줄 요약 파이썬으로 토스증권 첫 주문까지 필요한 호출은 네 개입니다 — POST /oauth2/token(토큰) → GET /api/v1/accounts(계좌) → GET /api/v1/prices(현재가) → POST /api/v1/orders(주문). requests 하나면 되고 별도 SDK는 필요 없습니다. 처음 막히는 자리는 둘입니다. 계좌 관련 API의 X-Tossinvest-Account 헤더에는 계좌번호가 아니라 accountSeq(정수)를 넣어야 하고, 주문 응답에는 orderId만 오기 때문에 체결은 GET /api/v1/orders/{orderId}로 따로 확인해야 합니다. 서버 주소는 https://openapi.tossinvest.com 하나입니다.

토스증권 오픈API 스펙 정리를 읽고 나면 다음 질문은 대개 하나입니다. "그래서 파이썬으로 어떻게 한 번 사 보나요?"

스펙 문서는 필드를 하나씩 설명하지만 실제로 필요한 건 순서입니다. 앞 호출의 어떤 값이 다음 호출의 어디로 들어가는지가 안 보이면 필드를 다 읽어도 첫 주문이 안 나갑니다. 이 글은 토큰 발급부터 체결 수량 확인까지 한 파일로 이어 붙입니다.

이 글의 순서

  1. 준비물 — 키 두 개와 requests
  2. 토큰 — client_credentials 한 번
  3. accountSeq — 계좌번호가 아니다
  4. 주문 전에 두 가지를 먼저 본다
  5. 주문 — clientOrderId를 반드시 넣는다
  6. 체결 확인 — statusexecution
  7. 전체 코드 한 파일
  8. 처음 붙일 때 막히는 자리 5곳

1. 준비물 — 키 두 개와 requests

필요한 것은 client_idclient_secret 두 개뿐입니다. 토스증권 계좌로 로그인한 뒤 설정의 Open API 메뉴에서 직접 발급합니다. 발급 절차와 허용 IP 설정은 허브 글에 정리해 뒀으니 여기서는 넘어갑니다.

파이썬 쪽 준비물은 더 적습니다. 표준 REST API라 requests 하나면 끝입니다. 윈도우 전용 모듈이나 32비트 파이썬을 맞출 일이 없어서 키움 OpenAPI+ 시절의 환경 문제가 여기서는 아예 없습니다.

pip install requests
키는 코드에 박지 마십시오. client_secret은 계좌에 주문을 낼 수 있는 값입니다. 환경변수나 별도 설정 파일로 빼고 저장소에 올리지 않는 것이 기본입니다. 자세한 기준은 API 키 보안 5가지 안전장치에 정리했습니다.

2. 토큰 — client_credentials 한 번

토큰 발급은 OAuth 2.0 Client Credentials 방식입니다. 사용자 로그인 화면을 거치지 않고 서버끼리 주고받는 형태라 봇에 그대로 맞습니다.

주의할 점은 JSON이 아니라 application/x-www-form-urlencoded로 보낸다는 것입니다. requests에서는 json=이 아니라 data=를 씁니다.

import requests

BASE = "https://openapi.tossinvest.com"

res = requests.post(BASE + "/oauth2/token", data={
    "grant_type": "client_credentials",   # 이 값만 지원한다
    "client_id": CLIENT_ID,
    "client_secret": CLIENT_SECRET,
}, timeout=10)                            # json= 이 아니라 data=

응답은 이렇게 옵니다.

{
  "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
  "token_type": "Bearer",
  "expires_in": 3600
}

expires_in남은 초입니다. 매 요청마다 토큰을 새로 받으면 금방 호출 한도에 걸리므로 만료 시각을 계산해 두고 재사용합니다. 실패했을 때는 400·401·403과 함께 errorinvalid_client·unsupported_grant_type 같은 값으로 내려옵니다.

KIS·키움에서 넘어왔다면 — 토큰을 캐싱해야 한다는 점은 같습니다. 다만 KISappkey·appsecret을 JSON 바디로 보내고 토스증권은 폼 인코딩입니다. json=으로 보내면 발급 단계에서 바로 막힙니다.

3. accountSeq — 계좌번호가 아니다

여기가 첫 주문을 막는 1번 자리입니다. 시세 조회는 토큰만으로 되지만 계좌·주문 계열 API는 어느 계좌인지를 헤더로 알려 줘야 합니다. 그 헤더가 X-Tossinvest-Account입니다.

그런데 여기 들어가는 값은 계좌번호가 아니라 accountSeq라는 정수 식별키입니다. GET /api/v1/accounts를 한 번 불러서 꺼내 옵니다.

requests.get(BASE + "/api/v1/accounts",
             headers={"Authorization": f"Bearer {tok}"}, timeout=10).json()

{
  "result": [
    {
      "accountNo": "12345678901",
      "accountSeq": 1,
      "accountType": "BROKERAGE"
    }
  ]
}

accountNo는 사람이 보는 계좌번호이고 API에 넣는 값은 accountSeq입니다. accountTypeBROKERAGE(종합매매)·OVERSEAS_DERIVATIVES· PENSION_SAVINGS·RESHORING_INVESTMENT가 정의돼 있고, 공식 스펙 기준 현재 지원되는 것은 BROKERAGE입니다.

여기서 나는 두 가지 오류. 헤더를 아예 빼면 account-header-required, 없는 계좌를 지정하면 account-not-found가 내려옵니다. 계좌번호 문자열을 그대로 넣었을 때도 같은 자리에서 막힙니다. 응답 코드별 대응은 토스증권 Open API 에러코드 정리를 보십시오.
POST /oauth2/token client_credentials -> access_token GET /api/v1/accounts 계좌 목록 -> accountSeq GET /api/v1/prices symbols=005930 -> lastPrice POST /api/v1/orders side · quantity · price -> orderId GET /api/v1/orders/{orderId} status · execution.filledQuantity X-Tossinvest-Account 헤더로 재사용 주문 응답은 접수 확인일 뿐, 체결은 별도 조회
첫 주문까지의 호출 순서 — 앞 호출의 값이 다음 호출의 어디로 들어가는가

4. 주문 전에 두 가지를 먼저 본다

바로 주문을 던져도 되지만 봇으로 돌릴 거라면 두 가지를 먼저 확인하는 편이 낫습니다. 살 돈이 있는지지금 얼마인지입니다.

H = {"Authorization": f"Bearer {tok}", "X-Tossinvest-Account": str(seq)}

# 매수 가능 금액 — 통화별로 따로 조회한다
requests.get(BASE + "/api/v1/buying-power", headers=H,
             params={"currency": "KRW"}, timeout=10).json()["result"]
# {'currency': 'KRW', 'cashBuyingPower': '5000000'}

# 현재가 — symbols 는 콤마로 최대 200개까지 (계좌 헤더 불필요)
requests.get(BASE + "/api/v1/prices", headers={"Authorization": f"Bearer {tok}"},
             params={"symbols": "005930"}, timeout=10).json()["result"]
# [{'symbol': '005930', 'lastPrice': '72000', 'currency': 'KRW', ...}]

cashBuyingPower미수가 발생하지 않는 순수 현금 기준 매수 가능 금액입니다. 매도 쪽은 /api/v1/sellable-quantitysellableQuantity를 봅니다. 금액과 수량이 문자열로 오는 점에 유의하십시오 — 부동소수점 오차를 피하려고 전 구간을 문자열로 다룹니다. float()로 바꿔서 계산하면 원 단위에서 어긋날 수 있습니다.

5. 주문 — clientOrderId를 반드시 넣는다

주문은 POST /api/v1/orders 하나입니다. 매수·매도가 경로로 나뉘지 않고 side 값으로 갈립니다.

필드메모
symbol필수국내 6자리 숫자 005930, 미국 티커 AAPL
sideBUY · SELL주문 방향
orderTypeLIMIT · MARKET지정가 · 시장가
timeInForceDAY(기본) · CLSLIMIT+CLS = LOC (미국주식만)
quantity수량문자열. 기본은 양의 정수
priceLIMIT일 때 필수MARKET에 넣으면 400
clientOrderId선택(사실상 필수)멱등키. 최대 36자
confirmHighValueOrder기본 false1억원 이상 주문에 필요

성공하면 이것만 옵니다.

{
  "result": {
    "orderId": "0d5QIHjmtksbsmM-hBRAgP-ExI8iodGm9fAR5txelPfnMM8XQ_swoJdwL5RpGWMo",
    "clientOrderId": "my-order-001"
  }
}
clientOrderId는 사실상 필수입니다. 타임아웃이 났을 때 주문이 들어갔는지 안 들어갔는지 알 수 없는 상황이 반드시 옵니다. 이 값을 넣어 두면 같은 값으로 재요청했을 때 새 주문을 만들지 않고 이전 결과를 그대로 돌려줍니다. 서버가 자동 생성해 주지 않으므로 직접 만들어야 하고, 공식 스펙 기준 유효 기간은 10분입니다. 그 뒤에는 같은 값이어도 새 주문이 됩니다.

6. 체결 확인 — status와 execution

주문 응답은 접수 확인이지 체결 확인이 아닙니다. orderId를 들고 GET /api/v1/orders/{orderId}를 불러야 결과가 보입니다.

requests.get(BASE + f"/api/v1/orders/{order_id}", headers=H, timeout=10).json()["result"]

{
  "orderId": "bAGzNvMOOTa5Uy0xVzYNbxDJ3Qpobwau...",
  "symbol": "005930",
  "side": "BUY",
  "orderType": "LIMIT",
  "timeInForce": "DAY",
  "status": "FILLED",
  "price": "70000",
  "quantity": "10",
  "currency": "KRW",
  "orderedAt": "2026-08-19T09:30:00+09:00",
  "execution": {
    "filledQuantity": "10",
    "averageFilledPrice": "70000",
    "filledAmount": "700000",
    "commission": "1400",
    "tax": "0",
    "filledAt": "2026-08-19T09:31:15+09:00",
    "settlementDate": "2026-08-21"
  }
}

status에 정의된 값은 열 가지입니다. 봇을 만들 때는 이 분기를 미리 짜 둬야 합니다.

status봇 동작
PENDING체결 대기다시 조회
PARTIAL_FILLED부분 체결filledQuantity만 반영, 잔량 관리
FILLED전량 체결종료
CANCELED취소 완료filledQuantity로 부분체결 여부 확인
REJECTED거부사유 확인 후 재주문 판단
PENDING_CANCEL · PENDING_REPLACE취소·정정 대기확정까지 재주문 금지
CANCEL_REJECTED · REPLACE_REJECTED취소·정정 거부원주문은 이전 상태로 복귀
REPLACED정정됨새 주문으로 추적 이관

CANCELED가 떴다고 "하나도 안 샀다"로 처리하면 안 됩니다. 부분 체결 뒤 나머지가 취소된 경우도 CANCELED입니다. 수량은 항상 execution.filledQuantity로 판단하십시오. 부분체결이 왜 계속 생기는지는 체결·슬리피지 글에 정리해 뒀습니다.

7. 전체 코드 한 파일

위 조각을 이어 붙이면 이렇게 됩니다. 지정가 매수 한 건을 넣고 체결까지 지켜보는 최소 구성입니다.

import os, time, uuid, requests

BASE = "https://openapi.tossinvest.com"
CID  = os.environ["TOSS_CLIENT_ID"]
SEC  = os.environ["TOSS_CLIENT_SECRET"]

class Toss:
    def __init__(self):
        self._tok, self._exp, self.seq = None, 0, None

    def token(self):
        if self._tok and time.time() < self._exp - 60:
            return self._tok
        r = requests.post(BASE + "/oauth2/token", data={
            "grant_type": "client_credentials",
            "client_id": CID, "client_secret": SEC}, timeout=10)
        r.raise_for_status()
        b = r.json()
        self._tok, self._exp = b["access_token"], time.time() + int(b["expires_in"])
        return self._tok

    def _headers(self):
        return {"Authorization": f"Bearer {self.token()}",
                "X-Tossinvest-Account": str(self.account_seq())}

    def account_seq(self):
        if self.seq is None:
            r = requests.get(BASE + "/api/v1/accounts",
                             headers={"Authorization": f"Bearer {self.token()}"}, timeout=10)
            r.raise_for_status()
            self.seq = r.json()["result"][0]["accountSeq"]
        return self.seq

    def call(self, method, path, **kw):
        r = requests.request(method, BASE + path, headers=self._headers(), timeout=10, **kw)
        if r.status_code >= 400:
            err = r.json().get("error", {})
            raise RuntimeError(f"{r.status_code} {err.get('code')} {err.get('message')} {err.get('data')}")
        return r.json()["result"]

    def buy_limit(self, symbol, qty, price, coid=None):
        return self.call("POST", "/api/v1/orders", json={
            "clientOrderId": coid or uuid.uuid4().hex[:32],
            "symbol": symbol, "side": "BUY", "orderType": "LIMIT",
            "quantity": str(qty), "price": str(price)})

    def wait_fill(self, order_id, timeout=60):
        deadline = time.time() + timeout
        while time.time() < deadline:
            o = self.call("GET", f"/api/v1/orders/{order_id}")
            if o["status"] in ("FILLED", "CANCELED", "REJECTED"):
                return o
            time.sleep(1)          # 초당 한도가 있으므로 간격을 둔다
        return self.call("GET", f"/api/v1/orders/{order_id}")

if __name__ == "__main__":
    t = Toss()
    res = t.buy_limit("005930", 1, 70000)
    print("접수:", res["orderId"])
    done = t.wait_fill(res["orderId"])
    print("결과:", done["status"], done["execution"]["filledQuantity"])
🛠️
첫 주문까지는 됐는데 그 다음이 막히셨다면

전략 신호 연결, 미체결 관리, 재시작 복구, 장 마감 처리까지 붙여 실제로 도는 상태로 만들어 드립니다. 24시간 빠른 답변 가능합니다.

제작 상담하기 →

8. 처음 붙일 때 막히는 자리 5곳

① 호가 단위에 안 맞는 가격

국내주식 price는 원 단위 정수이면서 가격대별 호가 단위에 맞아야 합니다. 어긋나면 400 invalid-request가 나는데 친절하게도 error.datatickSizenearestPrices(근접한 유효 가격 두 개)가 실려 옵니다. 현재가에 임의로 곱해서 지정가를 만드는 코드라면 반드시 여기서 걸립니다. 응답의 nearestPrices로 보정해 재요청하는 처리를 넣어 두십시오.

② MARKET인데 price를 같이 보냄

orderTypeMARKET이면 price전달 자체가 불가합니다. None이나 0을 넣는 것도 아니고 키를 빼야 합니다. 반대로 LIMIT인데 price가 없으면 역시 400 invalid-request입니다. 위 코드에서 if price is not None으로 분기한 이유가 이것입니다.

③ 429 — 초당 한도

체결을 확인한다고 while True로 조회를 돌리면 429 rate-limit-exceeded가 납니다. 응답 헤더에 X-RateLimit-Limit·X-RateLimit-Remaining· X-RateLimit-Reset·Retry-After가 함께 오므로 고정 sleep이 아니라 Retry-After를 읽어 쉬는 편이 안전합니다. 여러 전략을 한 키로 돌린다면 호출량이 합산된다는 점도 같이 고려해야 합니다 — 설계 관점은 API 호출 제한 설계에 정리했습니다.

④ 1억원 이상 주문

주문 금액이 1억원 이상이면 confirmHighValueOrder: true를 함께 보내지 않는 한 400 confirm-high-value-required로 막힙니다. 착오주문 방지 장치입니다. 수량 계산에 버그가 있어 자릿수가 튀었을 때 이 응답이 마지막 방어선이 되므로 봇에서는 이 플래그를 기본 false로 두는 편이 낫습니다.

⑤ 장 시간이 아닐 때

장이 닫혀 있으면 order-hours-closed가 내려옵니다. 스케줄러를 만들 때는 휴장일도 함께 봐야 하는데 토스증권은 GET /api/v1/market-calendar/KR/US로 장 운영 정보를 제공합니다. 휴장일을 직접 계산하던 문제를 이 API 하나로 대신할 수 있습니다.

공식 스펙은 계속 바뀝니다. 이 글은 발행 시점의 토스증권 Open API 공개 스펙을 따라 썼습니다. 필드와 한도는 공식 개발자 문서에서 한 번 더 확인하십시오. 증권사 API 스펙은 예고 없이 갱신되는 편이라 도메인·필드명·한도는 코드에 상수로 박지 말고 설정으로 분리해 두는 편이 안전합니다.

다음 단계

주문이 한 번 나가고 나면 다음 문제는 낸 주문을 어떻게 거두느냐입니다. 토스증권은 정정·취소 결과로 orderId를 발급하기 때문에 주문 추적 구조가 KIS·키움과 다릅니다 — 이 부분만 따로 정리한 글이 토스증권 API 주문 취소·정정입니다.

자주 묻는 질문

Q. 토스증권 Open API를 파이썬에서 쓰려면 무슨 라이브러리가 필요한가요?

표준 REST API라 requests 하나면 충분합니다. 별도 SDK나 전용 모듈을 설치하지 않아도 됩니다. 한국투자증권 KIS나 키움 REST API처럼 윈도우 전용 모듈이나 ActiveX가 필요 없어서 리눅스 서버에서도 그대로 돕니다.

Q. X-Tossinvest-Account 헤더에 계좌번호를 넣으면 되나요?

아닙니다. accountNo가 아니라 accountSeq라는 정수 식별키를 넣습니다. 헤더를 아예 빼면 account-header-required, 없는 계좌를 지정하면 account-not-found가 내려옵니다.

Q. 주문을 넣었는데 체결됐는지는 어떻게 아나요?

POST /api/v1/orders의 응답에는 orderIdclientOrderId만 있습니다. 체결은 GET /api/v1/orders/{orderId}statusexecution.filledQuantity로 확인합니다.

Q. 같은 주문이 두 번 나가지 않게 하려면?

clientOrderId를 멱등키로 넣습니다. 같은 값으로 재요청하면 이전 주문 결과를 그대로 돌려줍니다. 공식 스펙 기준 유효 기간은 10분입니다.

토스증권 자동매매, 맡기고 싶다면

토스증권 Open API 연동부터 전략 신호·미체결 관리·무인 운영까지 실제로 도는 상태로 만들어 드립니다. 24시간 빠른 답변 가능합니다.

무료 상담 시작하기