Upbit API with Python: From Quotes to Placing Orders

🌐 한국어

Go looking for how to build a crypto trading bot and almost everyone starts with "strategy." Then you sit down to write code and get stuck immediately on something more basic: how do I actually send a request to the exchange? This article covers that first gate only — connecting to the Upbit API from Python to read quotes, check balances, and place an order.

0. What You Need

Just two things.

  • Python packages: pip install requests pyjwt — requests for HTTP, PyJWT for building the auth token.
  • Upbit API keys: issued from the Upbit website under My Page → Open API Management. When you issue them, always register an allowed IP, and start with asset-inquiry and order-inquiry permissions only. Turn on order permissions after your code has proven itself — there's no rush.

The secret key you're issued cannot be viewed again once you leave that screen. And never write it directly into your source. Keep it in environment variables or a config file.

import os

ACCESS_KEY = os.environ["UPBIT_ACCESS_KEY"]   # real keys live in env vars

SECRET_KEY = os.environ["UPBIT_SECRET_KEY"]

SERVER_URL = "https://api.upbit.com"

1. Fetching Quotes — No Authentication Required

Start with the easy part. Current-price lookups need no authentication. A single GET request is the whole thing.

import requests

def get_price(market="KRW-BTC"):

    url = f"{SERVER_URL}/v1/ticker"

    res = requests.get(url, params={"markets": market}, timeout=5)

    res.raise_for_status()          # raises on 4xx/5xx

    data = res.json()[0]

    return {

        "market":  data["market"],

        "price":   data["trade_price"],               # current price

        "change":  data["signed_change_rate"] * 100,  # % vs. previous close

    }

print(get_price("KRW-BTC"))

# {'market': 'KRW-BTC', 'price': 95000000.0, 'change': 1.23}

Note that market follows a "settlement currency-target coin" format. Buying Bitcoin with Korean won is KRW-BTC; Ethereum with won is KRW-ETH. Reversing the order is a surprisingly common slip.

You can also fetch several markets at once — just comma-join them in the markets parameter. Don't make 20 calls for 20 tickers; batch them. It's the easiest way to conserve your rate limit.

2. Authenticated Requests — Building a JWT

From balance lookups onward you have to prove who you are. Upbit uses JWT. In plain terms: you sign your access key plus a one-time value with your secret key, and send the resulting string in a header.

There are two rules.

  • Requests without parameters: sign only access_key and nonce (a random string for replay protection).
  • Requests with parameters: SHA512-hash the query string and sign it as query_hash alongside the rest.
import jwt, uuid, hashlib

from urllib.parse import urlencode

def make_token(params=None):

    payload = {

        "access_key": ACCESS_KEY,

        "nonce": str(uuid.uuid4()),   # a fresh value on every request

    }

    if params:

        query = urlencode(params)

        payload["query_hash"] = hashlib.sha512(query.encode()).hexdigest()

        payload["query_hash_alg"] = "SHA512"

    token = jwt.encode(payload, SECRET_KEY, algorithm="HS256")

    return {"Authorization": f"Bearer {token}"}

One thing to watch. The query string you hash into query_hash and the query string you actually send must be identical down to the character. Change the ordering or the encoding and the server will reject the signature. That's why reusing the exact value from urlencode(params), as above, is the safe move.

3. Checking Balances

Now that you have a token, you can see your account. This one takes no parameters, so it's the simplest possible authenticated call.

def get_balances():

    res = requests.get(f"{SERVER_URL}/v1/accounts",

                       headers=make_token(), timeout=5)

    res.raise_for_status()

    return res.json()

for acc in get_balances():

    print(acc["currency"], acc["balance"], acc["avg_buy_price"])

# KRW 150000.0 0

# BTC 0.00123 94000000

If this works, your authentication is wired correctly. If you get a 401, nine times out of ten it's an unregistered IP or whitespace around your key. Throw a .strip() on the values when you read them from a config file.

4. Placing an Order

Orders are POSTs with parameters, so they need query_hash. Three combinations cover Upbit ordering:

  • Limit order: ord_type="limit" + price + volume
  • Market buy: ord_type="price" + price (the amount of cash to spend) — no volume
  • Market sell: ord_type="market" + volume (the quantity to sell) — no price

The confusing part is market buys, where price means "how much won to spend," not "how many coins."

def buy_market(market, krw_amount):

    """Market buy — spend krw_amount worth of Korean won"""

    params = {

        "market":   market,

        "side":     "bid",        # bid=buy, ask=sell

        "ord_type": "price",

        "price":    str(krw_amount),

    }

    headers = make_token(params)

    headers["Content-Type"] = "application/json"

    res = requests.post(f"{SERVER_URL}/v1/orders",

                        json=params, headers=headers, timeout=5)

    if res.status_code >= 400:

        print("order failed:", res.status_code, res.text)

        return None

    return res.json()

# order = buy_market("KRW-BTC", 6000)   # buy 6,000 KRW worth

Notice that query_hash is built from urlencode(params) while the request body is JSON. Upbit accepts that combination. But the keys and values in params must match exactly, so once you've built the dictionary, don't touch it.

5. What Bites You in Production

  • Minimum order size: KRW markets require at least 5,000 won. Plenty of people test with 1,000 won and wonder why nothing goes through.
  • Rate limits: there's a cap on requests per second, and exceeding it returns a 429. Put time.sleep(0.1) in your loops as a baseline.
  • Quantity precision: coin quantities go to 8 decimal places. Feeding a raw computed value straight in can get rejected due to floating-point error, so clean it up with round(qty, 8).
  • An order is not a fill: a 200 from the order API means "accepted," not "executed." You have to confirm the actual fill via /v1/order?uuid=....
  • Read-only first: when you first wire up an order function, keep a dry-run mode that only prints instead of calling. Most logic bugs surface within the first week.

Summary

To restate the order of operations: issue keys → fetch quotes (unauthenticated) → build a JWT → check balances → place orders. Don't move to the next step before the current one is confirmed. Writing order code before balance lookups work is especially wasteful — when it fails, you can't tell whether authentication is broken or your order parameters are wrong.

That's the "hands and feet" of an automated trading system. Strategy — the brain — comes after, and honestly, strong hands and feet are what keep a bot alive longer.

This article is for educational and informational purposes and is not investment advice. Automated crypto trading carries a risk of principal loss, and careless API key management can lead to loss of assets. All decisions and consequences are your own.

댓글

이 블로그의 인기 게시물

한국투자증권 KIS API로 실시간 시세 받기 (WebSocket 실전)

파이썬으로 업비트 API 연동하기 — 시세 조회부터 주문까지 기초

Go로 자동매매 신호봇 프레임워크 설계하기