API ключи

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Крипто депозиты/выводыда
p2pP2P-сделкида

Уровни: none (запрещено) · read_only (только GET) · full (все операции).


Базовый URL

Все запросы с API-ключом идут на шлюз, а не на основной API-домен.

СредаBase URL
productionhttps://api.abcex.io

Пути те же, что и в обычном API (/api/v1/..., /api/v2/exchange/...).


Заголовки запроса

К каждому запросу, подписанному ключом, обязательны три заголовка:

ЗаголовокЗначение
X-API-KeypublicKey (64 hex-символа)
X-API-TimestampТекущее Unix-время в миллисекундах, строкой
X-API-SignatureHMAC-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
productionwss://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>@depthpublicстакан
<SYMBOL>@tradespublicлента сделок
<SYMBOL>.1m@candlespublicсвечи
<SYMBOL>@tickerpublicтикер
CLNTF@my-balancesprivateспот балансы
HLORD@my-balancesprivateфьюч балансы
<SYMBOL>@order-infoprivateордера
<SYMBOL>@order-historyprivateистория ордеров
<SYMBOL>@my-tradesprivateсвои сделки
position-infoprivateпозиции

Ping

Клиент периодически шлёт { "method": "ping" } и получает { "pong": <ms> }. Рекомендуется раз в ~30 секунд.

Минимальный пример (Node.js 18/20)

npm install ws
const 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 formatX-API-Key не 64 hex-символа
missing X-API-Timestamp or X-API-SignatureНет одного из заголовков
invalid timestamp formatX-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

  1. Метод в верхнем регистре.
  2. В подписи и в URL — одинаковый путь вместе с query-string.
  3. Тело в подписи — байт-в-байт то же, что отправляется (не сериализуйте дважды с разным порядком полей).
  4. GET без тела: в подписи пустая строка "", в запросе нет body.
  5. Разделитель — \n (0x0A), не \r\n и не литералы \\n.
  6. secretKey — hex-строка как есть, без декодирования.
  7. timestamp в подписи и в заголовке — одно и то же значение.
  8. Проверьте часы на клиенте (расхождение > 30 сек).

Ограничения

  • Некоторые эндпоинты намеренно закрыты для ключей и вернут 401 route not allowed for API key access. Если нужной вам ручки нет — обратитесь в поддержку.
  • Не делитесь secretKey, не коммитьте его в git, не логируйте. В случае утечки — удалите ключ в ЛК и выпустите новый.