# REST API ## REST Request Rules - Public endpoints (mostly market data) do not require an API key. - Private endpoints require: the `X-KAPI-KEY` header, a `timestamp` parameter, and a `signature` parameter. - `GET` and `DELETE`: send input values as query string parameters. - `POST`: send input values in the body as `application/x-www-form-urlencoded` and include `Content-Type: application/x-www-form-urlencoded`. - Parameter order does not matter, but the signature MUST be computed over the exact encoded string that will be sent (excluding `signature` itself), then `signature` appended. - If a request has both query string and body parameters, sign `queryString + bodyString` with no extra `&` between them. Prefer keeping all signed parameters in one place to avoid mistakes. Example signing input (query string + body, concatenated): ```text timestamp=1719232467910symbol=btc_krw ``` ## Rate Limits Track quota with response headers: - `Ratelimit: limit=50, remaining=48, reset=1` - `Ratelimit-Policy: 50;w=1` - `Retry-After` accompanies HTTP 429. | API group | Limit | |---|---:| | Public REST | 50 req/sec per IP | | Order placement | 30 req/sec per account | | Order cancellation | 30 req/sec per account | | Deposit / withdrawal | 5 req/sec per account | | Other private REST | 50 req/sec per account | On HTTP 429, pause until `Retry-After` or the `Ratelimit` reset window before retrying. ## Timestamp Window For private requests include: - `timestamp`: current Unix time in milliseconds. - `recvWindow`: optional validity window in milliseconds. Default `5000`, maximum `60000`. The server accepts the request only when: ```text serverTime - timestamp <= recvWindow timestamp < serverTime + 1000 ``` If the client clock drifts, Korbit returns `EXCEED_TIME_WINDOW`. This window is **asymmetric** — `recvWindow` widens only the past side, while the future bound is a fixed `+1000` ms — so a clock even slightly *ahead* of the server fails every signed request and raising `recvWindow` cannot help. Sign against Korbit's server clock (from `GET /v2/time`), not the raw host clock; see resilience recipe 6. ## HMAC-SHA256 Helper Node.js example. This signs the exact `URLSearchParams` payload before appending `signature`. ```js import crypto from "node:crypto"; const BASE_URL = "https://api.korbit.co.kr"; const apiKey = process.env.KORBIT_API_KEY; const apiSecret = process.env.KORBIT_API_SECRET; function signHmac(encodedParams) { return crypto.createHmac("sha256", apiSecret).update(encodedParams, "utf8").digest("hex"); } async function korbitPrivate(method, path, params = {}) { const clean = Object.fromEntries( Object.entries(params).filter(([, v]) => v !== undefined && v !== null && v !== "") ); const p = new URLSearchParams({ ...clean, timestamp: String(Date.now()) }); const signature = signHmac(p.toString()); p.append("signature", signature); const headers = { "X-KAPI-KEY": apiKey }; let url = `${BASE_URL}${path}`; const init = { method, headers }; if (method === "GET" || method === "DELETE") { url += `?${p.toString()}`; } else { headers["Content-Type"] = "application/x-www-form-urlencoded"; init.body = p.toString(); } const res = await fetch(url, init); const json = await res.json().catch(() => ({})); if (!res.ok || json.success === false) { const err = new Error(`Korbit API error ${res.status}`); err.status = res.status; // numeric HTTP status err.code = json?.error?.message; // symbolic code, e.g. "DUPLICATE_CLIENT_ORDER_ID" err.body = json; throw err; } return json.data ?? json; } ``` On failure Korbit returns `{ "success": false, "error": { "code": , "message": "" } }`. The symbolic code you branch on (`EXCEED_TIME_WINDOW`, `DUPLICATE_CLIENT_ORDER_ID`, `NO_BALANCE`, …) is `error.message` — **not** `error.code`, which is the numeric HTTP status repeated. Nothing symbolic appears at the top level. The helper above surfaces the symbolic code as `err.code` so callers branch on that. ## ED25519 Helper The private key should be a PEM string stored outside the source code. ```js import crypto from "node:crypto"; const apiKey = process.env.KORBIT_API_KEY; const privateKeyPem = process.env.KORBIT_ED25519_PRIVATE_KEY; function signEd25519(encodedParams) { const sig = crypto.sign(null, Buffer.from(encodedParams), privateKeyPem); return sig.toString("base64"); } ``` When you append the Base64 signature via `URLSearchParams`, the value is URL-encoded automatically. ## Placing Orders `POST /v2/orders` with `symbol`, `side` (`buy`/`sell`), and `orderType`. How you size the order depends on the type — this is the most common mistake, so get it exactly right: | orderType | required sizing field(s) | omit | |---|---|---| | `limit` | `price` **and** `qty` | `amt` | | `market` / `best` **buy** | `amt` (purchase amount in KRW) | `price`, `qty` | | `market` / `best` **sell** | `qty` (base quantity) | `price`, `amt` | So a market BUY of 50,000 KRW of BTC sends `amt=50000` and sends **no** `qty` and **no** `price`. A market SELL of 0.01 BTC sends `qty=0.01` and no `amt`/`price`. `best` (BBO) orders additionally require `timeInForce` and `bestNth`. Pass `clientOrderId` (matching `[0-9a-zA-Z.:_-]{1,36}`) to make placement idempotent: repeated requests with the same `clientOrderId` are processed only once, and you can look the order up later with `GET /v2/orders?clientOrderId=...`. A successful placement returns the assigned `orderId`; read the rest of the order's state with a follow-up `GET /v2/orders` (see below). A `clientOrderId` effectively never frees up, so mint a fresh collision-resistant id per placement and persist it rather than reusing one across distinct orders — see Resilience #1. ## Reading Orders and Fills `GET /v2/orders` (by `orderId` or `clientOrderId`) returns the order, including its fill state. Money fields are strings — keep them as strings / decimals. | field | meaning | notes | |---|---|---| | `qty` | ordered quantity | absent for a market buy (which was sized by `amt`) | | `amt` | purchase amount (market buy) | counter asset, e.g. KRW | | `filledQty` | quantity filled so far | `"0"` until something fills | | `filledAmt` | counter-asset amount filled so far | e.g. KRW spent/received | | `avgPrice` | average execution price | **optional — absent until there is a fill.** Never read it unguarded | | `status` | order status | see Order Statuses below (`partiallyFilled` etc.) | To act on a fill safely: treat `avgPrice` as possibly missing (derive it as `filledAmt / filledQty` with decimal math when you need it and it is absent, and treat "nothing filled" as no average price — never `NaN`). The unfilled remainder is `qty - filledQty`; never let it go negative. A `partiallyFilled` order has a real `filledQty` smaller than `qty`, so size any follow-on action off `filledQty`/`filledAmt`, not the originally requested size. ## Balances `GET /v2/balance` (optionally `currencies=btc,eth`) returns an array, one object per asset. All amounts are strings: | field | meaning | |---|---| | `currency` | asset name, e.g. `krw`, `btc` | | `balance` | total = `available + tradeInUse + withdrawalInUse` | | `available` | quantity free to trade/withdraw right now | | `tradeInUse` | quantity locked in open orders | | `withdrawalInUse` | quantity locked in pending withdrawals | | `avgPrice` | average purchase price (optional) | Size new orders against `available`, not `balance` — the difference is already committed to open orders or withdrawals. ## REST Endpoints Tables below are grouped exactly as in the YAML spec. Required parameters are listed in the per-group reference files at the bottom of this document. ### Quotation | Method | Path | Permission | Purpose | |---|---|---|---| | `GET` | [`/v2/tickers`](/llms/en/rest_api/quotation.md#get-_v2_tickers) | (public) | Get latest price and trading volume for a symbol or symbols. | | `GET` | [`/v2/orderbook`](/llms/en/rest_api/quotation.md#get-_v2_orderbook) | (public) | Get orderbook data. | | `GET` | [`/v2/trades`](/llms/en/rest_api/quotation.md#get-_v2_trades) | (public) | Get recent trades. | | `GET` | [`/v2/candles`](/llms/en/rest_api/quotation.md#get-_v2_candles) | (public) | Get historical candlesticks (klines) data. | | `GET` | [`/v2/currencyPairs`](/llms/en/rest_api/quotation.md#get-_v2_currencyPairs) | (public) | | | `GET` | [`/v2/tickSizePolicy`](/llms/en/rest_api/quotation.md#get-_v2_tickSizePolicy) | (public) | Get tick size policy and orderbook grouping levels for a trading pair. | ### Trading | Method | Path | Permission | Purpose | |---|---|---|---| | `GET` | [`/v2/orders`](/llms/en/rest_api/trading.md#get-_v2_orders) | `readOrders` | Use either `orderId` or `clientOrderId` to query the status of an individual order. | | `GET` | [`/v2/openOrders`](/llms/en/rest_api/trading.md#get-_v2_openOrders) | `readOrders` | Query the list of open orders for a single trading pair. Only orders with the status `open` or `partiallyFilled` are queried. | | `GET` | [`/v2/allOrders`](/llms/en/rest_api/trading.md#get-_v2_allOrders) | `readOrders` | Query the recent order list for a single trading pair. Only orders created within 36 hours can be queried. | | `GET` | [`/v2/myTrades`](/llms/en/rest_api/trading.md#get-_v2_myTrades) | `readOrders` | Query the recent trades list for a single trading pair. Only trade history from the past 36 hours can be queried. | | `POST` | [`/v2/orders`](/llms/en/rest_api/trading.md#post-_v2_orders) | `writeOrders` | Place a new order. | | `DELETE` | [`/v2/orders`](/llms/en/rest_api/trading.md#delete-_v2_orders) | `writeOrders` | Requests to cancel an open order. | ### Asset | Method | Path | Permission | Purpose | |---|---|---|---| | `GET` | [`/v2/balance`](/llms/en/rest_api/asset.md#get-_v2_balance) | `readBalances` | Get balance. | ### Deposit (Crypto) | Method | Path | Permission | Purpose | |---|---|---|---| | `GET` | [`/v2/coin/depositAddresses`](/llms/en/rest_api/deposit-crypto.md#get-_v2_coin_depositAddresses) | `readDeposits` | Retrieve the list of cryptocurrency deposit addresses. | | `GET` | [`/v2/coin/depositAddress`](/llms/en/rest_api/deposit-crypto.md#get-_v2_coin_depositAddress) | `readDeposits` | Get the deposit address for a single cryptocurrency. | | `POST` | [`/v2/coin/depositAddress`](/llms/en/rest_api/deposit-crypto.md#post-_v2_coin_depositAddress) | `writeDeposits` | Generate a cryptocurrency deposit address. If a deposit address already exists, the existing address will be returned. | | `GET` | [`/v2/coin/recentDeposits`](/llms/en/rest_api/deposit-crypto.md#get-_v2_coin_recentDeposits) | `readDeposits` | Get recent deposit history. | | `GET` | [`/v2/coin/deposit`](/llms/en/rest_api/deposit-crypto.md#get-_v2_coin_deposit) | `readDeposits` | Check the status of cryptocurrency deposits. | ### Withdrawal (Crypto) | Method | Path | Permission | Purpose | |---|---|---|---| | `GET` | [`/v2/coin/withdrawableAddresses`](/llms/en/rest_api/withdrawal-crypto.md#get-_v2_coin_withdrawableAddresses) | `readWithdrawals` | Retrieve the list of addresses registered for API withdrawals. | | `GET` | [`/v2/coin/withdrawableAmount`](/llms/en/rest_api/withdrawal-crypto.md#get-_v2_coin_withdrawableAmount) | `readWithdrawals` | Get the available cryptocurrency withdrawal amount. | | `POST` | [`/v2/coin/withdrawal`](/llms/en/rest_api/withdrawal-crypto.md#post-_v2_coin_withdrawal) | `writeWithdrawals` | Request for cryptocurrency withdrawal. You need to register your withdrawal addresses for use with API in order to use this feature. | | `DELETE` | [`/v2/coin/withdrawal`](/llms/en/rest_api/withdrawal-crypto.md#delete-_v2_coin_withdrawal) | `writeWithdrawals` | Cancel a cryptocurrency withdrawal. | | `GET` | [`/v2/coin/recentWithdrawals`](/llms/en/rest_api/withdrawal-crypto.md#get-_v2_coin_recentWithdrawals) | `readWithdrawals` | Get recent cryptocurrency withdrawal history. | | `GET` | [`/v2/coin/withdrawal`](/llms/en/rest_api/withdrawal-crypto.md#get-_v2_coin_withdrawal) | `readWithdrawals` | Get the status of the requested withdrawal. | ### Deposit/Withdrawal (KRW) | Method | Path | Permission | Purpose | |---|---|---|---| | `POST` | [`/v2/krw/sendKrwDepositPush`](/llms/en/rest_api/krw.md#post-_v2_krw_sendKrwDepositPush) | `writeDeposits` | Send a notification for KRW deposit requests to your Korbit mobile app. | | `POST` | [`/v2/krw/sendKrwWithdrawalPush`](/llms/en/rest_api/krw.md#post-_v2_krw_sendKrwWithdrawalPush) | `writeWithdrawals` | Send a notification for KRW withdrawal requests to your Korbit mobile app. | | `GET` | [`/v2/krw/recentDeposits`](/llms/en/rest_api/krw.md#get-_v2_krw_recentDeposits) | `readDeposits` | Get recent KRW deposit history. | | `GET` | [`/v2/krw/recentWithdrawals`](/llms/en/rest_api/krw.md#get-_v2_krw_recentWithdrawals) | `readWithdrawals` | Get recent KRW withdrawal history. | ### Other Endpoints | Method | Path | Permission | Purpose | |---|---|---|---| | `GET` | [`/v2/currencies`](/llms/en/rest_api/other.md#get-_v2_currencies) | (public) | Get cryptocurrencies information. | | `GET` | [`/v2/time`](/llms/en/rest_api/other.md#get-_v2_time) | (public) | Get the current server time. | | `GET` | [`/v2/tradingFeePolicy`](/llms/en/rest_api/other.md#get-_v2_tradingFeePolicy) | `readOrders` | Get the trading fee rates applied to your account. | | `GET` | [`/v2/currentKeyInfo`](/llms/en/rest_api/other.md#get-_v2_currentKeyInfo) | signed (any key) | Get current API Key's information. | | `GET` | [`/v2/notices`](/llms/en/rest_api/other.md#get-_v2_notices) | (public) | Get the 20 most recent Korbit notices (announcements), most recent first. | | `GET` | [`/v2/marketAlerts`](/llms/en/rest_api/other.md#get-_v2_marketAlerts) | (public) | Get the current market alert (Market Warning System, 시장경보제) status for each trading pair. Returns only pairs that currently have active alerts. | ## Order Statuses | Value | Meaning | |---|---| | `pending` | Order pending. When the balance is insufficient or timeInForce condition is triggered, the order may fail and change to the `expired` status. | | `open` | Fully unfilled | | `filled` | Execution closed. An order whose unfilled remainder is returned instead of resting on the book (e.g. an `ioc` order, or a price-protected (`pp`) order trimmed by the protection range) also closes as `filled` even when less than the requested quantity executed. Confirm the executed amount with `filledQty`/`filledAmt`. | | `canceled` | Fully canceled | | `partiallyFilled` | Partially filled | | `partiallyFilledCanceled` | Partially filled and remaining amount canceled | | `expired` | Order submission failed (due to insufficient balance or timeInForce conditions) | ## Error Codes | Error | When | Path | |---|---|---| | `BAD_REQUEST` | Bad request. | `POST /v2/orders` | | `CANNOT_CANCEL_WITHDRAWAL` | The withdrawal cannot be canceled (likely because it's being processed) | `DELETE /v2/coin/withdrawal` | | `DAILY_LIMIT_EXCEEDED` | You have exceeded the daily withdrawal limit. | `POST /v2/coin/withdrawal` | | `DUPLICATE_CLIENT_ORDER_ID` | Request rejected due to duplicate `clientOrderId`. | `POST /v2/orders` | | `FORBIDDEN_WITHDRAWAL_ADDRESS` | Withdrawals to the address is forbidden due to policy. | `POST /v2/coin/withdrawal` | | `INVALID_CURRENCY` | Invalid currency | `POST /v2/coin/withdrawal` | | `INVALID_CURRENCY_PAIR` | Invalid symbol. | `POST /v2/orders` | | `INVALID_USER_STATUS` | Trading has been temporarily restricted according to Korbit's policy. | `POST /v2/coin/withdrawal`, `POST /v2/orders` | | `NOT_FOUND` | The withdrawal cannot be found | `DELETE /v2/coin/withdrawal` | | `NO_BALANCE` | Insufficient balance. | `POST /v2/coin/withdrawal`, `POST /v2/orders` | | `ONLY_SELL_LIMIT_ORDERS_ALLOWED` | Only limit sell orders are allowed during the initial listing period. | `POST /v2/orders` | | `ORDER_ALREADY_CANCELED` | Already canceled order | `DELETE /v2/orders` | | `ORDER_ALREADY_EXPIRED` | Already expired order | `DELETE /v2/orders` | | `ORDER_ALREADY_FILLED` | Already filled order | `DELETE /v2/orders` | | `ORDER_NOT_FOUND` | Not found order | `DELETE /v2/orders` | | `ORDER_VALUE_TOO_LARGE` | Order exceeds the maximum amount. Please adjust the `qty` * `price` to be 1 billion KRW or less. | `POST /v2/orders` | | `ORDER_VALUE_TOO_SMALL` | Order does not meet the minimum amount. Please adjust the `qty` * `price` to be at least 5,000 KRW. | `POST /v2/orders` | | `PRICE_OVER_UPPER_BOUND` | Above the upper price limit during the initial listing period. | `POST /v2/orders` | | `PRICE_TICK_SIZE_INVALID` | Invalid tick size. | `POST /v2/orders` | | `PRICE_UNDER_LOWER_BOUND` | Below the lower price limit during the initial listing period. | `POST /v2/orders` | | `TOO_MANY_OPEN_ORDERS` | Order quantity limit exceeded. | `POST /v2/orders` | | `TRY_AGAIN` | The order is currently being processed. Please try again in a few moments later. | `DELETE /v2/orders` | | `UNREGISTERED_WITHDRAWAL_ADDRESS` | The address hasn't been registered as an OpenAPI withdrawal address. | `POST /v2/coin/withdrawal` | | `WITHDRAWAL_ALREADY_FINISHED` | The withdrawal has already been finished | `DELETE /v2/coin/withdrawal` | | `WITHDRAWAL_ALREADY_IN_PROGRESS` | A withdrawal is already in progress. Please try again after the current transaction is completed. | `POST /v2/coin/withdrawal` | | `WITHDRAWAL_SUSPENDED` | Withdrawal suspended | `POST /v2/coin/withdrawal` |