Аутентификация
Каждый аутентифицированный запрос подписывается секретом вашего API-ключа. Сам секрет никогда не передается по сети: сервер заново вычисляет подпись из тех же байтов запроса и сверяет результат.
Перед началом работы
| Тип учетных данных | Формат | Назначение | Срок действия |
|---|---|---|---|
| API-ключ + секрет | ak_live_… / sk_live_… (в тестнете Nile: ak_test_… / sk_test_…) | Каждый вызов с вашего бэкенда | Бессрочно, пока не отозван или не наступил expires_at |
| Bootstrap token | Authorization: Bearer abt_… | Создание аккаунта, адрес для депозита при регистрации, GET /v1/account и выпуск первого ключа — больше ничего | 15 минут |
| Без учетных данных (публичные) | — | GET /v1/prices, GET /v1/estimate, GET /v1/orderbook, GET /v1/resources/{address} и получение челленджа | Ограничено лимитами запросов на IP-адрес |
API-ключ предназначен исключительно для машинного взаимодействия: никогда не используйте его в коде веб-страниц. Ключ можно создать до внесения первого депозита, чтобы ваш сервис мог получить адрес пополнения (GET /v1/deposit-addresses) и пополнить баланс самостоятельно; до поступления средств заказы будут отклоняться с ошибкой 4001 insufficient_funds. Секрет ключа отображается ровно один раз при создании — сохраните его в надежном месте.
Три заголовка запроса
| Заголовок | Значение |
|---|---|
X-API-KEY | Идентификатор ключа, как указано в кабинете. Не является секретом. |
X-API-TIMESTAMP | Текущее время UTC в формате ISO 8601 с миллисекундами, например 2026-09-11T18:04:05.123Z. |
X-API-SIGN | base64(HMAC_SHA256(api_secret, canonical_string)) |
Каноническая строка (Canonical string)
canonical_string = timestamp + METHOD + path + query + body
| Часть строки | Требования |
|---|---|
timestamp | Значение заголовка X-API-TIMESTAMP, байт в байт. |
METHOD | HTTP-метод заглавными буквами: GET, POST, PATCH, DELETE. |
path | Путь запроса с префиксом /v1, закодированный точно так же, как отправляется по сети, например /v1/orders. |
query | Пустая строка "", если параметров нет; при наличии параметров — символ ? и строка параметров в исходном виде (без пересортировки и повторного кодирования). |
body | Сырое тело запроса в кодировке UTF-8; пустая строка "", если тела нет (например, при запросах GET). |
Никаких разделителей между частями нет. Сериализуйте тело запроса один раз и отправляйте именно эти байты: подписание форматированного JSON с отступами и отправка компактного JSON — самая частая причина ошибки 1002 invalid_signature.
Шаг 1 — Сформируйте и подпишите строку
Пример подписания запроса GET /v1/balance с секретом sk_test_example во время 2026-09-11T18:04:05.123Z:
2026-09-11T18:04:05.123ZGET/v1/balance
TS=2026-09-11T18:04:05.123Z
printf '%s' "${TS}GET/v1/balance" | openssl dgst -sha256 -hmac "sk_test_example" -binary | base64
import { createHmac } from "node:crypto";
const ts = "2026-09-11T18:04:05.123Z";
const sign = createHmac("sha256", "sk_test_example").update(`${ts}GET/v1/balance`).digest("base64");
console.log(sign);
import base64, hashlib, hmac
ts = "2026-09-11T18:04:05.123Z"
digest = hmac.new(b"sk_test_example", f"{ts}GET/v1/balance".encode(), hashlib.sha256).digest()
print(base64.b64encode(digest).decode())
Во всех трех случаях вывод будет идентичен: zqttXJ133TRJ7aj14dJ3jamfeCG2mDj+TbNPrrBJl2g=. Сверьтесь с этим значением перед отладкой других параметров.
Шаг 2 — Отправьте запрос
TS=$(date -u +%Y-%m-%dT%H:%M:%S.000Z)
SIGN=$(printf '%s' "${TS}GET/v1/balance" | openssl dgst -sha256 -hmac "$TENERGY_SECRET" -binary | base64)
curl -s https://api-nile.tenergy.me/v1/balance \
-H "X-API-KEY: $TENERGY_KEY" -H "X-API-TIMESTAMP: $TS" -H "X-API-SIGN: $SIGN"
import { createHmac } from "node:crypto";
const ts = new Date().toISOString();
const sign = createHmac("sha256", process.env.TENERGY_SECRET!)
.update(`${ts}GET/v1/balance`)
.digest("base64");
const res = await fetch("https://api-nile.tenergy.me/v1/balance", {
headers: { "X-API-KEY": process.env.TENERGY_KEY!, "X-API-TIMESTAMP": ts, "X-API-SIGN": sign },
});
console.log(res.status, await res.json());
import base64, hashlib, hmac, json, os, urllib.error, urllib.request
from datetime import datetime, timezone
ts = datetime.now(timezone.utc).isoformat(timespec="milliseconds").replace("+00:00", "Z")
sign = base64.b64encode(hmac.new(os.environ["TENERGY_SECRET"].encode(),
f"{ts}GET/v1/balance".encode(), hashlib.sha256).digest()).decode()
req = urllib.request.Request("https://api-nile.tenergy.me/v1/balance", headers={
"X-API-KEY": os.environ["TENERGY_KEY"], "X-API-TIMESTAMP": ts, "X-API-SIGN": sign})
try:
with urllib.request.urlopen(req) as res:
print(res.status, json.load(res))
except urllib.error.HTTPError as err:
print(err.code, json.load(err))
В руководстве быстрого старта эта логика обернута в удобную функцию-хелпер tenergy(method, path, body).
Смещение часов, защита от повторов и правила IP
| Правило | Допустимое значение | Ошибка при нарушении |
|---|---|---|
| Смещение часов (Clock skew) | ±5 секунд от серверного времени | 1003 signature_timestamp_skew — поле details.server_time содержит точное время сервера |
| Защита от повторов (Replay) | Подпись действует ровно один раз в рамках окна | 1009 replayed_signature — для каждого запроса (включая повторные попытки) создавайте новое время и подпись |
| Белый список IP | Опционально для ключа; если пуст — разрешены любые адреса | 1004 ip_not_allowed — поле details.source_ip содержит зафиксированный IP-адрес |
| Окружение | Ключи ak_live_ — только для основного хоста, ak_test_ — только для Nile | 1012 key_environment_mismatch, отклоняется еще до поиска ключа в базе |
Обязательно настройте службу синхронизации времени NTP на всех серверах, генерирующих подпись. Рассинхронизация часов приводит к ошибке 1003 на каждом запросе; увеличение количества повторов здесь не поможет.
Области видимости ключей (Scopes)
API-ключ получает только те разрешения, которые были явно указаны при его создании: поле scopes обязательно, и система не имеет предустановленных наборов разрешений. Названия прав соответствуют формату area.action (тип ApiKeyScope в openapi.yaml); в справочнике по API-ключам подробно описано назначение каждого скоупа. Вызов операции вне прав ключа вернет статус 403 с кодом 1005 insufficient_scope и указанием недостающего скоупа в поле details.required_scope. Запрашивайте только минимально необходимые для интеграции права.
Идемпотентность
| Запрос | Ключ идемпотентности | Поведение при повторном вызове |
|---|---|---|
POST /v1/orders | Поле client_order_id в теле запроса | Возвращает исходный заказ, статус 200, без повторного списания средств |
| Другие изменяющие вызовы | Заголовок Idempotency-Key, от 8 до 128 символов | Возвращает исходный результат выполнения |
История идемпотентности хранится 24 часа. Отправка того же ключа с другим телом запроса отклоняется с ошибкой 3010 idempotency_conflict.