업비트 API 에러코드 24개 — 재시도할 것과 멈출 것
업비트 API 에러는 HTTP 상태코드가 아니라 응답 JSON의 error.name 문자열로 판정해야 합니다. 주문 계열 400(under_min_total_bid·insufficient_funds_bid·create_bid_error 등)과 인증 계열 401(invalid_query_payload·jwt_verification·nonce_used·no_authorization_ip)은 같은 요청을 다시 보내도 결과가 같으므로 재시도하지 말고 멈춰야 합니다. 재시도해도 되는 것은 429(다음 초 경계까지 대기)·500·market_offline 정도이고, 418은 429를 무시하고 계속 보낸 결과인 일시 차단이라 안내된 시간까지 호출을 완전히 멈춰야 합니다.
목차
- 에러 응답의 모양 — name이 숫자일 때와 문자열일 때
- 업비트 API 에러코드 24개 전체표
- 주문에서 가장 자주 나는 6개
- 인증 에러 7개 — JWT·nonce·IP·권한
- 429·418·500 — 속도 제한과 차단
- 봇의 분기 코드 — 재시도 / 중단 / 알림
- 실주문 전에 걸러내기 — /v1/orders/test 와 /v1/orders/chance
- 자주 묻는 질문
이 글의 표와 메시지는 업비트 개발자 센터(docs.upbit.com)의 REST API 사용 및 에러 안내·요청 수 제한(Rate Limits)·주문 생성 레퍼런스와 REST API 연동 Best Practice 문서를 2026-10-03에 대조해 정리했습니다. 업비트 봇의 전체 구조는 허브 글 업비트 자동매매 — REST만 쓰면 절반은 손해본다에 있고, 이 글은 그중 "에러가 났을 때 봇이 무엇을 해야 하나" 하나만 다룹니다.
1. 에러 응답의 모양 — name이 숫자일 때와 문자열일 때
업비트는 에러가 나면 error 객체 안에 name과 message를 돌려줍니다. 함정은 API 종류에 따라 name의 타입이 다르다는 점입니다. 공식 문서는 시세 조회(Quotation) API는 정수, 거래·자산(Exchange) API는 문자열을 반환한다고 적고 있습니다.
// Quotation API (시세: /v1/ticker, /v1/candles ...) — name 이 정수
{
"error": {
"name": 400,
"message": "ERROR_MESSAGE"
}
}
// Exchange API (주문·잔고: /v1/orders, /v1/accounts ...) — name 이 문자열
{
"error": {
"name": "insufficient_funds_bid",
"message": "주문 가능한 금액(KRW)이 부족합니다."
}
}
비교 전에 str(err.get("name"))로 타입을 맞춰 두십시오.
또 하나, 공식 문서는 out_of_scope 항목에서 "API 도메인(포켓·입출금·주문)마다 HTTP 상태 코드(401, 403)가 일관되지 않을 수 있으니 에러코드 문자열 기준으로 검증하라"고 권장합니다. 상태코드 분기는 큰 묶음(4xx/5xx)에만 쓰고, 실제 대응은 error.name으로 정하라는 뜻입니다.
2. 업비트 API 에러코드 24개 전체표
공식 에러 안내 표(20개)에 POST /v1/orders 레퍼런스의 응답 예시에만 나오는 4개(invalid_time_in_force·over_krw_funds_bid·notfoundmarket·market_offline)를 더했습니다. 마지막 열이 봇이 할 일입니다.
| HTTP | error.name | 뜻 | 봇의 대응 |
|---|---|---|---|
| 400 | create_ask_error · create_bid_error | 주문 요청 정보가 올바르지 않음 | 중단 · 파라미터 조합 수정 |
| 400 | insufficient_funds_ask · insufficient_funds_bid | 매도/매수 가능 잔고 부족 | 중단 · 잔고 재조회 후 수량 재계산 |
| 400 | under_min_total_ask · under_min_total_bid | 최소 주문 금액 미달 | 중단 · 주문 전 금액 필터 |
| 400 | over_krw_funds_bid | 최대 주문 가능 금액(예시: 1,000,000,000 KRW) 초과 | 중단 · 분할 주문 |
| 400 | invalid_time_in_force | 주문 조건(ioc·fok·post_only) 조합 오류 | 중단 · 코드 수정 |
| 400 | validation_error | 필수 파라미터 누락 | 중단 · 코드 수정 |
| 400 | invaild_parameter / invalid_parameter | 잘못된 파라미터 (철자 주의 — 아래 설명) | 중단 · 코드 수정 |
| 400 | duplicated_identifier | 이미 쓴 identifier | 중단 · ID 생성 규칙 수정 |
| 400 | notfoundmarket | 없는 페어(예: KRW-BTCs) | 중단 · 종목 목록 갱신 |
| 400 | withdraw_address_not_registered | 허용되지 않은 출금 주소 | 중단 (출금 봇) |
| 401 | invalid_query_payload | JWT 페이로드 오류 | 중단 · query_hash 생성 점검 |
| 401 | jwt_verification | JWT 검증 실패 | 중단 · 서명·키 점검 |
| 401 | expired_access_key | API 키 만료 | 중단 · 키 재발급 알림 |
| 401 | nonce_used | 이미 사용된 nonce | 토큰 새로 서명 후 1회 재시도 |
| 401 | no_authorization_ip | 등록되지 않은 IP | 중단 · 서버 IP 점검 |
| 401 | no_authorization_token | 인증 헤더 누락 | 중단 · 코드 수정 |
| 403 | out_of_scope | 키에 해당 권한 없음 | 중단 · 키 권한 확인 |
| 403 | open_api_withdraw_locked | 출금 안심차단 미해지 | 중단 · 앱에서 해제 |
| 403 | market_offline | 시스템 점검 중 | 대기 후 재시도 |
| 404 | pocket_not_found · currency_not_found | 포켓/자산 코드 없음 | 중단 · UUID·대문자 코드 확인 |
| 429 | — | 초당 요청 한도 초과 | 다음 초 경계까지 대기 후 재시도 |
| 418 | — | 429 누적으로 일시 차단 | 전면 정지 · 차단 시간 후 재개 |
| 500 | — | 서버 내부 오류·점검 | 짧은 백오프 후 재시도 |
철자 함정 — 공식 에러 안내 표에는 invaild_parameter(i와 a가 뒤바뀐 철자)로, POST /v1/orders 응답 예시에는 invalid_parameter로 적혀 있습니다. 어느 쪽이 실제로 오는지 문서만으로는 단정할 수 없으므로, 분기 코드에는 두 철자를 모두 넣어 두십시오.
3. 주문에서 가장 자주 나는 6개
① under_min_total_bid — 5,000원 미만
원화(KRW) 마켓의 최소 주문 가능 금액은 공식 문서 기준 5,000 KRW입니다(BTC·USDT 마켓은 별도 문서). 분할 매수 봇이 "잔고의 10%씩"처럼 비율로 금액을 정하면 잔고가 줄어든 뒤 어느 순간부터 이 에러가 반복됩니다. 주문 직전에 총액을 계산해 5,000원 미만이면 아예 보내지 않는 필터가 정답이고, 마켓별 최소값은 GET /v1/orders/chance 응답의 min_total로 확인할 수 있습니다.
② insufficient_funds_bid — 잔고가 있는데 부족하다고 할 때
주문 가능 금액은 GET /v1/accounts의 balance이고, 미체결 주문에 묶인 돈은 locked로 따로 잡힙니다. 지정가 매수를 걸어 둔 채 같은 원화로 또 매수하면 이 에러가 납니다. 매수 수수료(/v1/orders/chance의 bid_fee)도 총액에 더해져야 하므로, 잔고를 100% 쓰는 주문은 수수료만큼 모자라 거절되기 쉽습니다.
③ create_bid_error — 시장가 주문 파라미터 조합
공식 문서가 직접 드는 원인은 "시장가 주문임에도 가격을 입력하는 경우"입니다. 업비트의 시장가는 매수와 매도가 다른 ord_type을 씁니다.
| 주문 | ord_type | 넣는 값 | 넣지 않는 값 |
|---|---|---|---|
| 지정가 매수/매도 | limit | price + volume | — |
| 시장가 매수 | price | price = 원화 총액 | volume |
| 시장가 매도 | market | volume = 코인 수량 | price |
| 최유리 지정가 | best | 매수 price / 매도 volume + time_in_force | 반대쪽 값 |
④ invalid_time_in_force — post_only와 SMP
post_only는 ord_type=limit에서만 쓸 수 있고 smp_type과 함께 쓸 수 없습니다. ioc·fok는 지정가와 최유리 지정가에서 사용합니다. 자전거래 방지 옵션은 업비트 SMP 글에 정리해 두었습니다.
⑤ duplicated_identifier — 실패한 주문의 ID도 재사용 불가
identifier는 계정 내 전체 주문 기준으로 고유해야 하고, 생성·체결 여부와 관계없이 한 번 쓴 값은 다시 쓸 수 없으며 최대 64자입니다. 그래서 같은 identifier로 재전송하면 이중 주문 대신 이 에러가 나고, 첫 요청의 접수 여부는 identifier로 조회해 확인합니다.
⑥ 호가 단위 위반
원화 마켓은 가격 구간마다 주문 단위가 다릅니다(예: 10원 이상~100원 미만은 0.1원). 15원짜리 코인에 15.01원으로는 주문할 수 없다는 것이 공식 예시입니다. 이때의 error.name은 공식 표에 없으므로 주문 전에 가격을 tick_size(호가 정책 조회 API) 단위로 내림하고 실제 응답을 로그로 남기십시오.
4. 인증 에러 7개 — JWT·nonce·IP·권한
401 계열은 거의 전부 코드나 설정 문제라 재시도로 풀리지 않습니다. 공식 Best Practice 예제의 토큰 생성부는 이렇습니다.
import hashlib, uuid, jwt
from urllib.parse import urlencode, unquote
def make_token(access_key, secret_key, params=None):
payload = {"access_key": access_key, "nonce": str(uuid.uuid4())} # 요청마다 새 nonce
if params:
query_string = unquote(urlencode(params, doseq=True)) # 실제로 보내는 값과 같아야 함
payload["query_hash"] = hashlib.sha512(query_string.encode("utf-8")).hexdigest()
payload["query_hash_alg"] = "SHA512"
token = jwt.encode(payload, secret_key, algorithm="HS512")
return token if isinstance(token, str) else token.decode("utf-8")
invalid_query_payload—query_hash를 만든 문자열과 실제로 보낸 파라미터가 다를 때가 대표적입니다. 해시를 만든 뒤 파라미터를 하나 더 붙이거나, 숫자를float로 바꿔"5000"이"5000.0"이 되면 어긋납니다. 배열 파라미터는 이름의[]를 인코딩하지 않는다는 규칙도 공식 문서에 있습니다.jwt_verification— Secret Key가 틀렸거나 서명 과정이 깨진 경우. Access Key와 Secret Key를 뒤바꿔 넣는 실수가 흔합니다.nonce_used— 재시도 루프가 이미 만든 토큰을 재사용할 때 납니다. 재시도마다make_token()을 다시 호출하십시오.no_authorization_ip— API 키는 등록한 IP에서만 쓸 수 있습니다. 클라우드 서버를 재시작해 공인 IP가 바뀌면 그 순간부터 모든 주문이 이 에러로 막힙니다. 클라우드 서버는 고정 IP를 쓰십시오.expired_access_key— 업비트 공지 기준 API 키 유효기간은 1년이고 연장할 수 없습니다. 만료 전 삭제·재발급이 유일한 방법이니 발급일을 봇 설정에 기록해 두고 30일 전 알림을 거는 것이 좋습니다.no_authorization_token—Authorization: Bearer {토큰}헤더 누락.out_of_scope(403) — 조회 권한만 준 키로 주문을 보낸 경우. 키 권한은 포켓별 API Key 목록 조회로 확인할 수 있습니다. 키 관리 원칙은 API 키 보안 글에 있습니다.
5. 429·418·500 — 속도 제한과 차단
업비트는 모든 REST 응답 헤더에 그룹과 잔여 요청 수를 실어 보냅니다.
Remaining-Req: group=default; min=1800; sec=29
sec가 이번 1초에 남은 횟수이고, min은 공식 문서에서 Deprecated(참조하지 말 것)로 표시돼 있습니다. 그룹별 한도는 공식 Rate Limits 문서 기준으로 시세 그룹(market·candle·trade·ticker·orderbook) 각 초당 10회(IP 단위), default 초당 30회, order 초당 12회(2026-08-21에 8회에서 상향), order-test 초당 8회, order-cancel-all 2초당 1회입니다. 거래·자산 그룹은 2026-06-25부터 계정이 아니라 포켓 단위로 셉니다.
- 429 — 초당 한도 초과. 공식 대응은 "다음 초 경계까지 대기 후 재시도"입니다.
- 418 — 429를 받고도 계속 보낸 결과인 일시 차단입니다. 응답에 담긴 차단 시간이 지날 때까지 재시도하지 말아야 하고, 공식 문서는 위반이 반복되면 차단 시간이 점진적으로 늘어난다고 적고 있습니다.
- 500 — 서비스 점검 또는 시스템 오류. Best Practice는 5xx 같은 일시 장애는 호출 목적에 맞게 재시도할 수 있다고 안내합니다. 단, 주문 생성 요청의 500은 "접수됐는지 모르는 상태"이므로 재전송 전에
identifier로 조회부터 하십시오.
여러 거래소·증권사 봇에 공통으로 쓰는 스로틀 설계는 API 호출 제한 설계 글에 정리했습니다.
6. 봇의 분기 코드 — 재시도 / 중단 / 알림
위 표를 그대로 코드로 옮기면 이렇습니다. requests 기준이며, 알림 함수는 텔레그램 등 운영 중인 채널로 바꾸면 됩니다.
import time, requests
STOP = { # 같은 요청을 다시 보내도 결과가 같은 에러 → 멈추고 사람에게 알림
"create_ask_error", "create_bid_error",
"insufficient_funds_ask", "insufficient_funds_bid",
"under_min_total_ask", "under_min_total_bid", "over_krw_funds_bid",
"invalid_time_in_force", "validation_error",
"invaild_parameter", "invalid_parameter", # 공식 문서 철자 두 가지
"duplicated_identifier", "notfoundmarket",
"invalid_query_payload", "jwt_verification", "expired_access_key",
"no_authorization_ip", "no_authorization_token", "out_of_scope",
}
def call(method, url, make_headers, max_retry=3, **kw):
for attempt in range(max_retry):
r = requests.request(method, url, headers=make_headers(), timeout=5, **kw) # 매번 새 토큰
if r.ok:
return r.json()
if r.status_code == 418:
raise SystemExit("418 일시 차단 — 차단 시간까지 봇 전체 정지: " + r.text)
if r.status_code == 429:
time.sleep(1.0 - (time.time() % 1.0) + 0.05) # 다음 초 경계까지
continue
name = str((r.json().get("error") or {}).get("name", ""))
if name in STOP:
notify(f"[업비트] {r.status_code} {name} — 재시도 안 함: {r.text}")
return None
if name == "nonce_used" or name == "market_offline" or r.status_code >= 500:
time.sleep(0.5 * (2 ** attempt))
continue
notify(f"[업비트] 미분류 에러 {r.status_code} {name}")
return None
notify(f"[업비트] 재시도 {max_retry}회 실패: {url}")
return None
핵심은 make_headers를 함수로 넘겨 재시도마다 토큰을 새로 서명하는 것(nonce_used 방지)과, 418을 예외로 올려 봇 전체를 세우는 것입니다. 장애 상황별 복구 순서는 봇 장애 복구 플레이북을 함께 보십시오.
7. 실주문 전에 걸러내기 — /v1/orders/test 와 /v1/orders/chance
업비트는 모의투자 서버가 없는 대신 주문 생성 테스트 API(POST /v1/orders/test)를 제공합니다. 공식 설명은 "실제 주문과 동일한 검증 과정을 거치지만 주문이 실제로 생성되지는 않는다"이고, 수수료도 들지 않습니다. 위 400 계열 에러 대부분을 실돈 없이 미리 확인할 수 있다는 뜻입니다.
POST https://api.upbit.com/v1/orders/test
Authorization: Bearer {JWT_TOKEN}
Content-Type: application/json
{"market": "KRW-BTC", "side": "bid", "ord_type": "price", "price": "4000"}
# → 원화 마켓 최소 5,000 KRW 미달이면 under_min_total_bid 가 이 단계에서 걸린다
- 테스트 응답의
uuid·identifier는 조회·취소에 쓸 수 없습니다(실제 주문이 아니므로). order-test그룹은 초당 8회로 실주문(order12회)과 한도를 따로 셉니다.- 마켓별 최소·최대 주문 금액(
min_total·max_total)과 수수료(bid_fee·ask_fee)는GET /v1/orders/chance?market=KRW-BTC로 받아 두고, 봇 시작 시 한 번 캐시하는 구성이 깔끔합니다.
업비트 봇을 처음 만든다면 파이썬 업비트 봇 30분에서 기본 골격을, 취소·정정 흐름은 업비트 취소 후 재주문 API에서 이어서 볼 수 있습니다. 증권사 쪽 에러 체계와 비교하고 싶다면 KIS API 에러코드 정리가 같은 구조로 되어 있습니다.
에러코드·한도·최소 주문 금액은 업비트 정책에 따라 공지 후 바뀔 수 있습니다. 이 글은 2026-10-03 기준 업비트 개발자 센터 문서를 대조한 것이므로, 실서비스 적용 전 공식 문서를 다시 확인하십시오. 투자 손익에 대한 판단이나 권유가 아니라 프로그램 오류 처리에 관한 기술 정리입니다.
8. 자주 묻는 질문
업비트 API에서 under_min_total_bid 에러는 왜 나나요?
주문 총액이 마켓별 최소 주문 금액보다 작을 때 납니다. 원화(KRW) 마켓의 최소 주문 가능 금액은 업비트 개발자 문서 기준 5,000 KRW입니다. 지정가는 price × volume, 시장가 매수(ord_type=price)는 price 값이 총액이므로 이 값을 주문 전에 계산해 걸러야 합니다. 정책은 바뀔 수 있으니 공식 문서와 GET /v1/orders/chance 응답을 확인하십시오.
잔고가 있는데 insufficient_funds_bid가 뜨는 이유는 무엇인가요?
주문 가능 금액은 보유 원화 전체가 아니라 미체결 주문에 묶인 금액(locked)을 뺀 값입니다. 매수 수수료까지 더한 금액이 남은 balance보다 크면 거절됩니다. GET /v1/accounts의 balance와 locked, GET /v1/orders/chance의 bid_fee를 함께 보고 주문 금액을 정하십시오.
업비트 API 에러가 나면 봇이 재시도해야 하나요?
공식 Best Practice는 사용자 행동 없이 해결되기 어려운 4xx 오류는 즉시 호출을 중단하고 원인을 식별하라고 안내합니다. 재시도 대상은 429(다음 초 경계까지 대기), 500, market_offline 정도이고, 418은 429를 계속 보낸 결과인 일시 차단이라 안내된 차단 시간이 지날 때까지 멈춰야 합니다.
nonce_used 에러는 어떻게 해결하나요?
JWT 페이로드의 nonce는 요청마다 새 값이어야 합니다. 재시도할 때 이전에 만든 JWT 토큰을 그대로 다시 보내면 nonce가 같아 거절됩니다. 재시도마다 uuid4로 nonce를 새로 만들고 query_hash도 다시 계산해 토큰을 새로 서명하십시오.