Upbit Python API: Quotes, Correct Signing and Order Validation
TECH INSIGHT / EXPLAINED
Build the integration in observable stages: public quote, authenticated read, locally validated order payload, then a documented order test. A successful HTTP response and a completed trade are different milestones.
Start with a public quote
This example targets Upbit Korea at api.upbit.com. KRW-BTC means BTC quoted in KRW. A current-price request does not need a private key. Validate the returned market and price instead of assuming the first JSON element is always present.
import requests
from decimal import Decimal
def read_quote(session, market='KRW-BTC'):
with session.get('https://api.upbit.com/v1/ticker',
params={'markets': market}, timeout=(3, 10)) as response:
response.raise_for_status()
rows = response.json(parse_float=Decimal)
if not isinstance(rows, list) or len(rows) != 1:
raise ValueError('unexpected ticker response')
row = rows[0]
if not isinstance(row, dict) or row.get('market') != market:
raise ValueError('market mismatch')
price = Decimal(str(row['trade_price']))
if not price.is_finite() or price <= 0:
raise ValueError('invalid price')
return price
# No request is made until read_quote(session) is called.Signing is a serialization contract
For authenticated calls, follow the current official JWT guide: fresh nonce, matching access/secret keys, HS512 as recommended, and SHA512 query_hash when parameters or a body exist. Keep secrets outside code and logs.
The hashing input is the documented non-URL-encoded query form. Preserve order and repeated array keys. For JSON POST bodies, derive that form from the same values you send; hashing the literal JSON bytes is different. The following deliberately uses a fake parameter set to make the array behavior testable offline.
import hashlib
from urllib.parse import urlencode, unquote
def signing_query(params):
return unquote(urlencode(params, doseq=True))
params = [('market', 'KRW-BTC'), ('states[]', ['wait', 'watch'])]
query = signing_query(params)
assert query == 'market=KRW-BTC&states[]=wait&states[]=watch'
query_hash = hashlib.sha512(query.encode('utf-8')).hexdigest()
assert len(query_hash) == 128Separate the three basic order shapes
A limit order uses price and volume; a market buy uses ord_type=price and a total quote-currency amount; a market sell uses ord_type=market and base-asset volume. These are basic forms, not a complete list of supported options. Check the current market’s available-order information, price increments, minimum value and fees.
Use decimal strings and the required rounding policy. A universal round(qty, 8) rule cannot validate every market constraint. A fixed 0.1-second sleep likewise does not coordinate shared quota usage.
Test the boundary before creating exposure
The documented /v1/orders/test endpoint validates an order request without creating a real order; its returned identifiers cannot be used as live orders. It still requires the relevant permission. It is not a simulation of fills or profitability.
For actual orders, persist the returned order ID and reconcile fills and remaining quantity. If submission times out, query the documented identifier before deciding whether to send again. For 401, inspect the documented error name and permissions/IP/signing inputs; do not assume most errors have one cause. Keep withdrawal permissions outside this tutorial’s requirements.
Sources and further reading
Upbit authentication · Current ticker API · Create order · Test order · Rate limits
Content and examples reviewed: 2026-09-29
댓글
댓글 쓰기