인증 토큰 생성하기
빗썸 Private API 호출에 필요한 JWT 인증 토큰의 구조와 생성 규칙을 설명합니다
API Key가 아직 없다면 빠른 시작 가이드 > API Key 발급을 먼저 확인하세요.
인증 흐름
Private API는 요청마다 JWT(JSON Web Token) 인증 토큰을 생성하여 Authorization 헤더에 포함해야 합니다.
- API Key와 Secret Key 준비
- JWT Payload 구성
- Secret Key로 서명 (HS256)
- Authorization 헤더에 Bearer 토큰 포함
- API 요청 전송
GET /v1/accounts HTTP/1.1
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Content-Type: application/json; charset=utf-8JWT 구조
빗썸 API의 JWT는 세 부분으로 구성됩니다.
| 구성 | 내용 |
|---|---|
| Header | {"alg": "HS256", "typ": "JWT"} — 서명 알고리즘 |
| Payload | 인증 정보를 담는 클레임(아래 상세) |
| Signature | Secret Key로 생성한 HMAC-SHA256 서명 |
Payload 필드
요청에 파라미터가 있는지 여부에 따라 포함하는 필드가 달라집니다.
| 필드 | 타입 | 필수 | 설명 |
|---|---|---|---|
access_key | string | ✅ 항상 | 발급받은 API Key |
nonce | string | ✅ 항상 | 요청마다 고유한 값(UUID 권장) |
timestamp | integer | ✅ 항상 | 현재 시각(밀리초 단위 Unix timestamp) |
query_hash | string | 파라미터 있을 때 | 쿼리 문자열의 SHA-512 해시값 |
query_hash_alg | string | 파라미터 있을 때 | 해시 알고리즘("SHA512" 고정) |
파라미터가 없는 경우
access_key, nonce, timestamp 3개 필드만 포함합니다.
{
"access_key": "L7rVaYfBIc2BDsnlQGfkR93d6DoOAJCw7mJr5Eso",
"nonce": "6f5570df-d8bc-4daf-85b4-976733feb624",
"timestamp": 1712230310689
}파라미터가 있는 경우
쿼리 문자열을 SHA-512로 해싱하여 query_hash와 query_hash_alg를 추가합니다.
{
"access_key": "L7rVaYfBIc2BDsnlQGfkR93d6DoOAJCw7mJr5Eso",
"nonce": "6f5570df-d8bc-4daf-85b4-976733feb624",
"timestamp": 1712230310689,
"query_hash": "1c2362ca9d79947582cae192acf63efb8756caa49af1eb64b12ba45617165431...",
"query_hash_alg": "SHA512"
}코드 예시
파라미터 없는 요청
GET /v1/accounts(전체 계좌 조회)와 같이 쿼리 파라미터가 없는 요청에 사용합니다.
const jwt = require('jsonwebtoken');
const { v4: uuidv4 } = require('uuid');
const axios = require('axios')
const accessKey = '발급받은 API KEY'
const secretKey = '발급받은 SECRET KEY'
const apiUrl = 'https://api.bithumb.com'
// Generate access token
const payload = {
access_key: accessKey,
nonce: uuidv4(),
timestamp: Date.now()
};
const jwtToken = jwt.sign(payload, secretKey)
const config = {
headers: {
Authorization: `Bearer ${jwtToken}`
}
}
// Call API
axios.get(apiUrl + '/v1/accounts', config)
.then((response) => {
// handle to success
console.log('status: ', response.status)
console.log('data: ', response.data)
})
.catch((error) => {
// handle to fail
console.log(error.response.status)
console.log(error.response.data)
});# Python 3
# pip3 installl pyJwt
import jwt
import uuid
import time
import requests
# Set API parameters
accessKey = '발급받은 API KEY'
secretKey = '발급받은 SECRET KEY'
apiUrl = 'https://api.bithumb.com'
# Generate access token
payload = {
'access_key': accessKey,
'nonce': str(uuid.uuid4()),
'timestamp': round(time.time() * 1000)
}
jwt_token = jwt.encode(payload, secretKey)
authorization_token = 'Bearer {}'.format(jwt_token)
headers = {
'Authorization': authorization_token
}
try:
# Call API
response = requests.get(apiUrl + '/v1/accounts', headers=headers)
# handle to success or fail
print(response.status_code)
print(response.json())
except Exception as err:
# handle exception
print(err)package com.example.sample;
// https://mvnrepository.com/artifact/com.auth0/java-jwt
import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
// https://mvnrepository.com/artifact/org.apache.httpcomponents/httpclient
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import java.nio.charset.StandardCharsets;
import java.util.UUID;
public class GETNoArgs {
public static void main(String[] args) {
String accessKey = "발급받은 API KEY";
String secretKey = "발급받은 SECRET KEY";
String apiUrl = "https://api.bithumb.com";
// Generate access token
Algorithm algorithm = Algorithm.HMAC256(secretKey);
String jwtToken = JWT.create()
.withClaim("access_key", accessKey)
.withClaim("nonce", UUID.randomUUID().toString())
.withClaim("timestamp", System.currentTimeMillis())
.sign(algorithm);
String authenticationToken = "Bearer " + jwtToken;
// Call API
final HttpGet httpRequest = new HttpGet(apiUrl + "/v1/accounts");
httpRequest.addHeader("Authorization", authenticationToken);
try (CloseableHttpClient client = HttpClients.createDefault();
CloseableHttpResponse response = client.execute(httpRequest)) {
// handle to response
int httpStatus = response.getStatusLine().getStatusCode();
String responseBody = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8);
System.out.println(httpStatus);
System.out.println(responseBody);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}파라미터 있는 요청
쿼리 파라미터가 있는 요청(예: GET /v1/orders/chance?market=KRW-BTC)의 경우, 파라미터를 해싱하여 JWT payload에 포함해야 합니다. 아래는 주문 가능 정보 조회 API 호출 예시입니다.
const jwt = require('jsonwebtoken');
const { v4: uuidv4 } = require('uuid');
const crypto = require('crypto');
const querystring = require('querystring');
const axios = require('axios')
const accessKey = '발급받은 API KEY'
const secretKey = '발급받은 SECRET KEY'
const apiUrl = 'https://api.bithumb.com'
// Set API parameters
const query = 'market=KRW-BTC'
// Generate access token
const alg = 'SHA512'
const hash = crypto.createHash(alg)
const queryHash = hash.update(query, 'utf-8').digest('hex')
const payload = {
access_key: accessKey,
nonce: uuidv4(),
timestamp: Date.now(),
query_hash: queryHash,
query_hash_alg: alg
}
const jwtToken = jwt.sign(payload, secretKey)
const config = {
headers: {
Authorization: `Bearer ${jwtToken}`
}
}
// Call API
axios.get(apiUrl + '/v1/orders/chance?' + query, config)
.then((response) => {
// handle to success
console.log('status: ', response.status)
console.log('data: ', response.data)
})
.catch((error) => {
// handle to fail
console.log(error.response.status)
console.log(error.response.data)
})# Python 3
# pip3 installl pyJwt
import jwt
import uuid
import hashlib
import time
from urllib.parse import urlencode
import requests
accessKey = '발급받은 API KEY'
secretKey = '발급받은 SECRET KEY'
apiUrl = 'https://api.bithumb.com'
# Set API parameters
param = dict( market='KRW-BTC' )
# Generate access token
query = urlencode(param).encode()
hash = hashlib.sha512()
hash.update(query)
query_hash = hash.hexdigest()
payload = {
'access_key': accessKey,
'nonce': str(uuid.uuid4()),
'timestamp': round(time.time() * 1000),
'query_hash': query_hash,
'query_hash_alg': 'SHA512',
}
jwt_token = jwt.encode(payload, secretKey)
authorization_token = 'Bearer {}'.format(jwt_token)
headers = {
'Authorization': authorization_token
}
try:
# Call API
response = requests.get(apiUrl + '/v1/orders/chance', params=param, headers=headers)
# handle to success or fail
print(response.status_code)
print(response.json())
except Exception as err:
# handle exception
print(err)package com.example.sample;
// https://mvnrepository.com/artifact/com.auth0/java-jwt
import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
// https://mvnrepository.com/artifact/org.apache.httpcomponents/httpclient
import org.apache.http.NameValuePair;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.utils.URLEncodedUtils;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.util.EntityUtils;
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
public class GETArgs {
public static void main(String[] args) throws NoSuchAlgorithmException {
String accessKey = "발급받은 API KEY";
String secretKey = "발급받은 SECRET KEY";
String apiUrl = "https://api.bithumb.com";
// Set API parameters
List<NameValuePair> queryParams = new ArrayList<>();
queryParams.add(new BasicNameValuePair("market", "KRW-BTC"));
// Generate access token
String query = URLEncodedUtils.format(queryParams, StandardCharsets.UTF_8);
MessageDigest md = MessageDigest.getInstance("SHA-512");
md.update(query.getBytes(StandardCharsets.UTF_8));
String queryHash = String.format("%0128x", new BigInteger(1, md.digest()));
Algorithm algorithm = Algorithm.HMAC256(secretKey);
String jwtToken = JWT.create()
.withClaim("access_key", accessKey)
.withClaim("nonce", UUID.randomUUID().toString())
.withClaim("timestamp", System.currentTimeMillis())
.withClaim("query_hash", queryHash)
.withClaim("query_hash_alg", "SHA512")
.sign(algorithm);
String authenticationToken = "Bearer " + jwtToken;
// Call API
final HttpGet httpRequest = new HttpGet(apiUrl + "/v1/orders/chance?" + query);
httpRequest.addHeader("Authorization", authenticationToken);
try (CloseableHttpClient client = HttpClients.createDefault();
CloseableHttpResponse response = client.execute(httpRequest)) {
// handle to response
int httpStatus = response.getStatusLine().getStatusCode();
String responseBody = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8);
System.out.println(httpStatus);
System.out.println(responseBody);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}query_hash 상세 규칙
query_hash는 GET 쿼리 파라미터와 POST/DELETE body 파라미터 모두에 적용됩니다. 파라미터를 쿼리 문자열 형태로 변환한 뒤 SHA-512로 해싱합니다.
POST 요청 — body 파라미터
body의 JSON 파라미터를 쿼리 문자열 형태로 변환한 뒤 동일하게 해싱합니다.
POST /v2/orders
Body: {"market": "KRW-BTC", "side": "bid", "order_type": "limit", "price": "80000000", "volume": "0.001"}
→ 쿼리 문자열로 변환: "market=KRW-BTC&side=bid&order_type=limit&price=80000000&volume=0.001"
→ 이 문자열을 SHA-512 해싱
const crypto = require('crypto');
const requestBody = {
market: 'KRW-BTC',
side: 'bid',
order_type: 'limit',
price: '80000000',
volume: '0.001',
};
// body 파라미터를 쿼리 문자열로 변환
const query = Object.entries(requestBody)
.map(([key, value]) => `${key}=${value}`)
.join('&');
const hash = crypto.createHash('SHA512');
const queryHash = hash.update(query, 'utf-8').digest('hex');import hashlib
from urllib.parse import urlencode
request_body = {
'market': 'KRW-BTC',
'side': 'bid',
'order_type': 'limit',
'price': '80000000',
'volume': '0.001',
}
# body 파라미터를 쿼리 문자열로 변환
query = urlencode(request_body)
hash_obj = hashlib.sha512()
hash_obj.update(query.encode('utf-8'))
query_hash = hash_obj.hexdigest()import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.LinkedHashMap;
import java.util.Map;
Map<String, String> requestBody = new LinkedHashMap<>();
requestBody.put("market", "KRW-BTC");
requestBody.put("side", "bid");
requestBody.put("order_type", "limit");
requestBody.put("price", "80000000");
requestBody.put("volume", "0.001");
// body 파라미터를 쿼리 문자열로 변환
String query = requestBody.entrySet().stream()
.map(entry -> entry.getKey() + "=" + entry.getValue())
.collect(java.util.stream.Collectors.joining("&"));
MessageDigest md = MessageDigest.getInstance("SHA-512");
md.update(query.getBytes(StandardCharsets.UTF_8));
String queryHash = String.format("%0128x", new BigInteger(1, md.digest()));배열 파라미터 처리
파라미터에 배열이 포함된 경우, 쿼리 문자열 형태에 주의해야 합니다. 배열의 각 요소를 key[]=value 형태로 개별 전개합니다.
예를 들어 여러 주문을 조회할 때:
파라미터: { "order_ids": ["id1", "id2", "id3"] }
✅ 올바른 쿼리 문자열:
order_ids[]=id1&order_ids[]=id2&order_ids[]=id3
❌ 잘못된 쿼리 문자열:
order_ids=id1,id2,id3
const crypto = require('crypto');
const querystring = require('querystring');
const params = {
order_ids: ['id1', 'id2', 'id3']
};
// querystring.stringify는 배열을 자동으로 key=v1&key=v2 형태로 변환
// 하지만 빗썸 API는 key[]=v1&key[]=v2 형태가 필요
const query = params.order_ids
.map(id => `order_ids[]=${id}`)
.join('&');
const hash = crypto.createHash('SHA512');
const queryHash = hash.update(query, 'utf-8').digest('hex'); import hashlib
order_ids = ['id1', 'id2', 'id3']
# 배열 파라미터를 key[]=value 형태로 변환
query = '&'.join([f'order_ids[]={oid}' for oid in order_ids])
hash_obj = hashlib.sha512()
hash_obj.update(query.encode('utf-8'))
query_hash = hash_obj.hexdigest()import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.List;
List<String> orderIds = List.of("id1", "id2", "id3");
// 배열 파라미터를 key[]=value 형태로 변환
String query = orderIds.stream()
.map(id -> "order_ids[]=" + id)
.collect(java.util.stream.Collectors.joining("&"));
MessageDigest md = MessageDigest.getInstance("SHA-512");
md.update(query.getBytes(StandardCharsets.UTF_8));
String queryHash = String.format("%0128x", new BigInteger(1, md.digest()));주의사항
| 항목 | 설명 |
|---|---|
| 토큰 재사용 금지 | 요청마다 새로운 nonce와 timestamp로 토큰을 생성하세요. 동일 토큰을 재사용하면 인증이 거부됩니다. |
| Secret Key 보안 | Secret Key는 발급 시 한 번만 표시됩니다. 소스 코드에 직접 작성하지 말고 환경 변수나 .env 파일로 관리하세요. (개발 환경 설정하기 참고) |
| Content-Type | POST, DELETE 요청 시 body가 있으면 Content-Type: application/json; charset=utf-8 헤더를 포함하세요. |
| query_hash 누락 | 파라미터가 있는데 query_hash를 빠뜨리면 401 Unauthorized 에러가 발생합니다. |
| 배열 파라미터 | key[]=value 형태로 전개해야 합니다. 쉼표 구분(key=v1,v2)은 올바른 해시를 생성하지 않습니다. |
자주 발생하는 에러와 해결 방법
인증 관련 오류는 대부분 JWT 서명이 올바르게 되지 않았을 때 발생합니다.
| HTTP 상태 코드 | 에러 코드 | 원인 | 해결 방법 |
|---|---|---|---|
| 401 | invalid_query_payload | JWT query 검증 실패 | query_hash가 요청 파라미터와 일치하는지 확인하세요. |
| 401 | jwt_verification | JWT 토큰 검증 실패 | Secret Key와 서명 알고리즘(HS256)을 확인하세요. |
| 401 | expired_jwt | JWT 만료 | 요청마다 새로운 nonce와 timestamp를 생성하세요. |
| 401 | NotAllowIP | 허용되지 않은 IP 접근 | 빗썸 사이트에서 등록한 IP 주소를 확인하세요. |
| 401 | out_of_scope | API Key 권한 부족 | 빗썸 사이트에서 API 활성화 항목을 확인하세요. |
전체 에러 코드는 API 주요 에러 코드 문서를 참고하세요.
다음 단계
Updated 13 days ago
