API-ключи позволяют ботам и внешним приложениям выполнять запросы от имени
пользователя без логина и пароля. Каждый ключ — пара publicKey + secretKey:
первый передаётся в заголовке запроса, вторым подписывается тело сообщения.
secretKey показывается только один раз при создании. Восстановить его
нельзя — потеряли, создавайте новый.
Получение ключа
Ключ создаётся в личном кабинете или через REST:
POST https://hub.abcex.io/api/v1/identity/client/api-key/create
Authorization: Bearer <JWT из логина>
Content-Type: application/json
{
"name": "my-trading-bot",
"auth2fa_code": "123456",
"permissions": [
{ "category": "trading_spot", "accessLevel": "full" },
{ "category": "trading_futures", "accessLevel": "full" },
{ "category": "dictionary", "accessLevel": "read_only" },
{ "category": "transfer", "accessLevel": "full" },
{ "category": "fiat_processing", "accessLevel": "none" },
{ "category": "crypto_processing","accessLevel": "none" },
{ "category": "p2p", "accessLevel": "none" }
],
"ipAddresses": ["1.2.3.4"]
}Ответ:
{
"publicKey": "94becfa6dc2ded...",
"secretKey": "89ba273ce5ec52..."
}Управление ключами (выпуск, удаление, изменение) идёт только под JWT + 2FA — из личного кабинета или соответствующих REST-эндпоинтов. Самим API-ключом управлять ключами нельзя.
Правила создания
- Максимум 10 активных ключей на пользователя.
- Если не передать
ipAddresses— ключ живёт 90 дней и торговать им можно с любого IP (но часть прав будет недоступна, см. ниже). - Если передать
ipAddresses— ключ бессрочный, запросы принимаются только с этих IP. - Для операций с деньгами (
fiat_processing,crypto_processing,p2p) IP-whitelist обязателен — без него можно ставить толькоnone.
Категории и уровни доступа
| Категория | Что включает | IP обязателен? |
|---|---|---|
trading_spot | Спотовая торговля | нет |
trading_futures | Фьючерсная торговля | нет |
transfer | Внутренние переводы между счетами | нет |
dictionary | Справочники, тикеры, инструменты | нет |
fiat_processing | Фиатные депозиты/выводы | да |
crypto_processing | Крипто депозиты/выводы | да |
p2p | P2P-сделки | да |
Уровни: none (запрещено) · read_only (только GET) · full (все
операции).
Базовый URL
Все запросы с API-ключом идут на шлюз, а не на основной API-домен.
| Среда | Base URL |
|---|---|
| production | https://api.abcex.io |
Пути те же, что и в обычном API (/api/v1/..., /api/v2/exchange/...).
Заголовки запроса
К каждому запросу, подписанному ключом, обязательны три заголовка:
| Заголовок | Значение |
|---|---|
X-API-Key | publicKey (64 hex-символа) |
X-API-Timestamp | Текущее Unix-время в миллисекундах, строкой |
X-API-Signature | HMAC-SHA256 от сообщения (ниже), в hex |
Заголовок Authorization при этом передавать не нужно.
Подпись запроса
Формула
message = timestamp + "\n" + METHOD + "\n" + path + "\n" + body
signature = HMAC-SHA256(secretKey, message) → hex-строка (64 символа)
Правила
timestamp— та же самая строка, что уходит вX-API-Timestamp.METHOD— заглавными буквами:GET,POST,PATCH,DELETE,PUT.path— путь запроса вместе с query-string. Пример:/api/v1/orders?limit=10&side=buy— именно так, как стоит в URL.body— тело запроса как есть (та же строка, что отправляется). Для GET/DELETE без тела — пустая строка"".- Разделитель — реальный перевод строки
\n(0x0A). secretKeyподаётся в HMAC как 64-символьная hex-строка (не декодируйте в байты).- Окно времени — сервер примет запрос, если
|сейчас − timestamp| ≤ 30 секунд. Считайте timestamp непосредственно перед отправкой.
Полный пример
Node.js
const crypto = require('node:crypto');
const GATEWAY_URL = 'https://api.abcex.io';
const PUBLIC_KEY = '<publicKey>';
const SECRET_KEY = '<secretKey>';
function sign({ method, path, query, body }) {
const qs = query ? '?' + new URLSearchParams(query).toString() : '';
const fullPath = path + qs;
const bodyStr = body ? JSON.stringify(body) : '';
const timestamp = Date.now().toString();
const message = `${timestamp}\n${method.toUpperCase()}\n${fullPath}\n${bodyStr}`;
const signature = crypto
.createHmac('sha256', SECRET_KEY)
.update(message)
.digest('hex');
return { fullPath, bodyStr, timestamp, signature };
}
async function call({ method, path, query, body }) {
const { fullPath, bodyStr, timestamp, signature } = sign({ method, path, query, body });
const res = await fetch(GATEWAY_URL + fullPath, {
method,
headers: {
'Content-Type': 'application/json',
'X-API-Key': PUBLIC_KEY,
'X-API-Timestamp': timestamp,
'X-API-Signature': signature,
},
body: bodyStr || undefined,
});
return { status: res.status, body: await res.text() };
}
(async () => {
// GET
console.log(await call({ method: 'GET', path: '/api/v1/auth/me' }));
// GET с query
console.log(await call({
method: 'GET',
path: '/api/v1/currency-network/list',
query: { limit: 10 },
}));
})();Python
import hmac, hashlib, time, json, urllib.parse, requests
GATEWAY_URL = 'https://api.abcex.io'
PUBLIC_KEY = '<publicKey>'
SECRET_KEY = '<secretKey>'
def call(method, path, query=None, body=None):
qs = '?' + urllib.parse.urlencode(query) if query else ''
full_path = path + qs
body_str = json.dumps(body, separators=(',', ':')) if body else ''
timestamp = str(int(time.time() * 1000))
message = f'{timestamp}\n{method.upper()}\n{full_path}\n{body_str}'
signature = hmac.new(SECRET_KEY.encode(), message.encode(), hashlib.sha256).hexdigest()
return requests.request(
method,
GATEWAY_URL + full_path,
data=body_str if body_str else None,
headers={
'Content-Type': 'application/json',
'X-API-Key': PUBLIC_KEY,
'X-API-Timestamp': timestamp,
'X-API-Signature': signature,
},
)WebSocket
Тот же publicKey + secretKey подходит для WebSocket. Подпись здесь
короче: подписывается только факт аутентификации, не каждое сообщение.
URL
| Среда | URL |
|---|---|
| production | wss://hub.abcex.io/websocket/exchange |
Аутентификация
Первое сообщение после открытия соединения:
{
"method": "auth",
"key": "<publicKey>",
"timestamp": 1700000000000,
"signature": "<hex HMAC-SHA256>"
}Подпись:
message = timestamp + "\n" + publicKey
signature = HMAC-SHA256(secretKey, message) → hex
timestamp — Unix миллисекунды, числом. Окно валидности — ±30 сек.
Ответ сервера: { "method": "auth", "success": true } или
{ "method": "auth", "success": false, "error": "<код>" }.
Коды ошибок: invalid_key_format, invalid_signature,
timestamp_out_of_range, key_not_found, key_expired.
Подписки
После успешной аутентификации:
{ "id": 1, "method": "subscribe", "data": ["BTCUSDT@depth"] }
{ "id": 2, "method": "unsubscribe", "data": ["BTCUSDT@depth"] }
{ "id": 3, "method": "list_subscriptions" }Публичные каналы доступны и без auth. Приватные — только после него.
| Канал | Тип | Что |
|---|---|---|
<SYMBOL>@depth | public | стакан |
<SYMBOL>@trades | public | лента сделок |
<SYMBOL>.1m@candles | public | свечи |
<SYMBOL>@ticker | public | тикер |
CLNTF@my-balances | private | спот балансы |
HLORD@my-balances | private | фьюч балансы |
<SYMBOL>@order-info | private | ордера |
<SYMBOL>@order-history | private | история ордеров |
<SYMBOL>@my-trades | private | свои сделки |
position-info | private | позиции |
Ping
Клиент периодически шлёт { "method": "ping" } и получает
{ "pong": <ms> }. Рекомендуется раз в ~30 секунд.
Минимальный пример (Node.js 18/20)
npm install wsconst crypto = require('node:crypto');
const WebSocket = require('ws');
const WS_URL = 'wss://hub.abcex.io/websocket/exchange';
const PUBLIC_KEY = '<publicKey>';
const SECRET_KEY = '<secretKey>';
const ws = new WebSocket(WS_URL);
ws.on('open', () => {
const ts = Date.now();
const signature = crypto
.createHmac('sha256', SECRET_KEY)
.update(`${ts}\n${PUBLIC_KEY}`)
.digest('hex');
ws.send(JSON.stringify({ method: 'auth', key: PUBLIC_KEY, timestamp: ts, signature }));
});
ws.on('message', (raw) => {
const msg = JSON.parse(raw.toString());
console.log('<-', msg);
if (msg.method === 'auth' && msg.success) {
ws.send(JSON.stringify({
id: 1,
method: 'subscribe',
data: ['CLNTF@my-balances', 'BTCUSDT@depth'],
}));
setInterval(() => ws.send(JSON.stringify({ method: 'ping' })), 30_000);
}
});
ws.on('close', (code) => console.log('closed', code));
ws.on('error', (err) => console.error('error', err));На Node.js ≥ 22 и в браузере WebSocket встроен — тогда без npm i ws, с
addEventListener и event.data вместо raw.
Ошибки
Все ошибки авторизации приходят с кодом 401 Unauthorized:
| Сообщение | Что не так |
|---|---|
invalid API key format | X-API-Key не 64 hex-символа |
missing X-API-Timestamp or X-API-Signature | Нет одного из заголовков |
invalid timestamp format | X-API-Timestamp не число |
timestamp expired | Время запроса отличается от серверного > 30 сек |
api key not found | Такого publicKey нет |
api key is not active | Ключ заблокирован или удалён |
api key expired | Истёк срок ключа (90 дней без IP) |
invalid signature | Подпись не совпадает |
ip address not in whitelist | Запрос с IP не из whitelist |
route not allowed for API key access | Этот эндпоинт недоступен через API-ключи |
insufficient permissions for this route | У ключа не хватает уровня доступа для ручки |
Чек-лист при invalid signature
invalid signature- Метод в верхнем регистре.
- В подписи и в URL — одинаковый путь вместе с query-string.
- Тело в подписи — байт-в-байт то же, что отправляется (не сериализуйте дважды с разным порядком полей).
- GET без тела: в подписи пустая строка
"", в запросе нетbody. - Разделитель —
\n(0x0A), не\r\nи не литералы\\n. secretKey— hex-строка как есть, без декодирования.timestampв подписи и в заголовке — одно и то же значение.- Проверьте часы на клиенте (расхождение > 30 сек).
Ограничения
- Некоторые эндпоинты намеренно закрыты для ключей и вернут
401 route not allowed for API key access. Если нужной вам ручки нет — обратитесь в поддержку. - Не делитесь
secretKey, не коммитьте его в git, не логируйте. В случае утечки — удалите ключ в ЛК и выпустите новый.
