> ## Documentation Index
> Fetch the complete documentation index at: https://wallet-docs.iexexchanger.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Аутентификация

> Подпись запросов HMAC-SHA256, проверка времени и IP-whitelist

Каждый запрос к `/v1/public/*` подписывается на вашей стороне ключом `api_secret`. Платформа
проверяет подпись на сервере — сам `api_secret` **никогда не передаётся по сети** после момента
выдачи. Базовый URL индивидуален для вашего инстанса, например `https://wallet.your-exchange.com`.

<Info>
  В этих примерах используются единые значения во всех разделах документации:
  базовый URL `https://wallet.your-exchange.com`, `X-Api-Id` = `pk_live_a1b2c3d4`,
  `order_id` в формате `INV-2026-000042`. Подставьте свои реальные значения.
</Info>

## Обязательные заголовки

Эти заголовки должны присутствовать в **каждом** запросе.

<ParamField header="X-Api-Id" type="string" required>
  Публичный идентификатор ключа. Выдаётся в админ-кабинете при создании API-ключа.
  Не является секретом — это лишь указатель, по какому ключу проверять подпись.
</ParamField>

<ParamField header="X-Timestamp" type="string" required>
  Unix-время в **секундах** на момент формирования запроса. Сервер принимает значение
  только в пределах **±300 секунд** от своего времени. Часть подписываемого сообщения.
</ParamField>

<ParamField header="X-Signature" type="string" required>
  `HMAC-SHA256` в нижнем регистре hex от сообщения `{X-Timestamp}.{raw_body}`
  с ключом `api_secret`. Подробности — ниже.
</ParamField>

<ParamField header="X-Idempotency-Key" type="string">
  Опционально, только для write-операций (создание депозита, выплаты и т. п.). UUID,
  который гарантирует, что безопасный повтор того же запроса не создаст дубликат.
  См. [Идемпотентность](/idempotency).
</ParamField>

<Warning>
  Подписывается **сырое тело запроса** — ровно те байты, которые уходят на сервер. Сначала
  сериализуйте JSON в строку, подпишите эту строку и отправьте **её же** в теле. Любое расхождение
  (лишний пробел, другой порядок ключей, повторная сериализация) сломает подпись и вернёт
  `INVALID_SIGNATURE`.
</Warning>

## Как формируется подпись

Сообщение для HMAC — это конкатенация трёх частей: значение `X-Timestamp`, символ-разделитель
точка `.` и сырое тело запроса.

```
message   = X-Timestamp + "." + raw_body
signature = HMAC_SHA256_hex( api_secret, message )
```

Для запросов **без тела** (`GET`, `HEAD`, `DELETE`) сырое тело — это пустая строка `""`
(именно пустая строка, а **не** `{}`). Поэтому сообщение оканчивается на точку:

```
message = "<timestamp>."
```

<Steps>
  <Step title="Соберите тело">
    Сериализуйте JSON в строку — это и есть `raw_body`. Для запросов без тела используйте пустую строку.
  </Step>

  <Step title="Возьмите timestamp">
    Текущее Unix-время в **секундах** (не миллисекундах). Поместите его в `X-Timestamp`.
  </Step>

  <Step title="Постройте сообщение">
    Соедините: `message = timestamp + "." + raw_body`.
  </Step>

  <Step title="Подпишите">
    `signature = HMAC_SHA256_hex(api_secret, message)` — результат в нижнем регистре hex.
  </Step>

  <Step title="Отправьте">
    Заголовки `X-Api-Id`, `X-Timestamp`, `X-Signature` (и тело, если оно есть). Тело отправляйте
    байт-в-байт тем же, что подписали.
  </Step>
</Steps>

## Полный разбор на конкретных числах

Возьмём заведомо известные входные данные и проверим, что у вас получается тот же результат.
Эти значения детерминированы — повторите расчёт у себя и сверьте подпись.

| Параметр      | Значение                                                                           |
| ------------- | ---------------------------------------------------------------------------------- |
| `api_secret`  | `s3cr3t_ApiSecret_ExampleOnly_DoNotUse`                                            |
| `X-Timestamp` | `1735680000`                                                                       |
| `raw_body`    | `{"assetCode":"USDT_TRC20","orderId":"INV-2026-000042","expectedAmount":"100.50"}` |

Сообщение для подписи:

```text theme={null}
1735680000.{"assetCode":"USDT_TRC20","orderId":"INV-2026-000042","expectedAmount":"100.50"}
```

Результат `HMAC-SHA256` в hex:

```text theme={null}
58d7e5946e50f93551ac9d48728b5d66308c5328241044a0e7ef03528f0bc6ed
```

<Check>
  Если ваша реализация выдаёт ровно
  `58d7e5946e50f93551ac9d48728b5d66308c5328241044a0e7ef03528f0bc6ed` —
  алгоритм подписи у вас собран правильно.
</Check>

Для пустого тела (`GET`) с тем же секретом и тем же `X-Timestamp = 1735680000` сообщение —
`1735680000.`, а подпись:

```text theme={null}
6f52d4e629b5daa5789bdc218214e5f4d80586cde1100e5a2211294d6b0d2317
```

## Готовый запрос

Так выглядит подписанный `POST /v1/public/deposits` с данными из примера выше.

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST https://wallet.your-exchange.com/v1/public/deposits \
    -H "X-Api-Id: pk_live_a1b2c3d4" \
    -H "X-Timestamp: 1735680000" \
    -H "X-Signature: 58d7e5946e50f93551ac9d48728b5d66308c5328241044a0e7ef03528f0bc6ed" \
    -H "Content-Type: application/json" \
    -d '{"assetCode":"USDT_TRC20","orderId":"INV-2026-000042","expectedAmount":"100.50"}'
  ```

  ```javascript Node.js theme={null}
  import crypto from 'node:crypto';

  const BASE_URL = 'https://wallet.your-exchange.com';
  const API_ID = 'pk_live_a1b2c3d4';
  const API_SECRET = process.env.WALLET_API_SECRET; // никогда не хардкодьте секрет

  function signedHeaders(rawBody) {
    const ts = Math.floor(Date.now() / 1000).toString();
    const signature = crypto
      .createHmac('sha256', API_SECRET)
      .update(`${ts}.${rawBody}`)
      .digest('hex');
    return {
      'X-Api-Id': API_ID,
      'X-Timestamp': ts,
      'X-Signature': signature,
      'Content-Type': 'application/json',
    };
  }

  // Сериализуем тело ОДИН раз и используем ту же строку и для подписи, и для отправки.
  const rawBody = JSON.stringify({
    assetCode: 'USDT_TRC20',
    orderId: 'INV-2026-000042',
    expectedAmount: '100.50',
  });

  const res = await fetch(`${BASE_URL}/v1/public/deposits`, {
    method: 'POST',
    headers: signedHeaders(rawBody),
    body: rawBody, // те же байты, что подписали
  });
  const envelope = await res.json();
  ```

  ```python Python theme={null}
  import hmac, hashlib, json, time, requests

  BASE_URL = 'https://wallet.your-exchange.com'
  API_ID = 'pk_live_a1b2c3d4'
  API_SECRET = 'СЕКРЕТ_ИЗ_ОКРУЖЕНИЯ'  # храните в env / секрет-менеджере

  def signed_headers(raw_body: str):
      ts = str(int(time.time()))
      signature = hmac.new(
          API_SECRET.encode(),
          f"{ts}.{raw_body}".encode(),
          hashlib.sha256,
      ).hexdigest()
      return {
          'X-Api-Id': API_ID,
          'X-Timestamp': ts,
          'X-Signature': signature,
          'Content-Type': 'application/json',
      }

  # Сериализуем тело ОДИН раз, без лишних пробелов, и шлём ту же строку.
  raw_body = json.dumps(
      {'assetCode': 'USDT_TRC20', 'orderId': 'INV-2026-000042', 'expectedAmount': '100.50'},
      separators=(',', ':'),
  )
  res = requests.post(
      f'{BASE_URL}/v1/public/deposits',
      headers=signed_headers(raw_body),
      data=raw_body,  # ровно та строка, что подписали
  )
  ```

  ```php PHP theme={null}
  <?php
  $baseUrl = 'https://wallet.your-exchange.com';
  $apiId = 'pk_live_a1b2c3d4';
  $apiSecret = getenv('WALLET_API_SECRET');

  // Сериализуем тело один раз и используем эту же строку для подписи и отправки.
  $rawBody = json_encode(
      ['assetCode' => 'USDT_TRC20', 'orderId' => 'INV-2026-000042', 'expectedAmount' => '100.50'],
      JSON_UNESCAPED_SLASHES
  );
  $ts = (string) time();
  $signature = hash_hmac('sha256', $ts . '.' . $rawBody, $apiSecret);

  $ch = curl_init($baseUrl . '/v1/public/deposits');
  curl_setopt_array($ch, [
      CURLOPT_POST => true,
      CURLOPT_POSTFIELDS => $rawBody,
      CURLOPT_RETURNTRANSFER => true,
      CURLOPT_HTTPHEADER => [
          'X-Api-Id: ' . $apiId,
          'X-Timestamp: ' . $ts,
          'X-Signature: ' . $signature,
          'Content-Type: application/json',
      ],
  ]);
  $response = curl_exec($ch);
  ```
</CodeGroup>

<Tip>
  Для запросов без тела (например `GET /v1/public/deposits/by-order-id/{order_id}`) передайте в
  функцию подписи **пустую строку** — тогда сообщение будет `"<timestamp>."`, а тело запроса
  не отправляется.
</Tip>

## Формат ответа

Все ответы возвращаются в едином envelope.

```json Успех theme={null}
{ "ok": true, "data": { "uuid": "…", "status": "check" } }
```

```json Ошибка theme={null}
{ "ok": false, "error": { "code": "INVALID_SIGNATURE", "message": "X-Signature mismatch" } }
```

Суммы во всех полях — **строки** (например `"100.50"`), чтобы исключить потерю точности на
плавающей запятой. Полный справочник кодов — на странице [Ошибки](/errors).

## IP-whitelist

Помимо подписи платформа проверяет IP-адрес вызывающего. Если для сайта задан белый список,
запросы принимаются **только** с перечисленных адресов; иначе — `403 IP_NOT_WHITELISTED`.

<Steps>
  <Step title="Узнайте исходящий IP">
    Это адрес, с которого ваш бэкенд CMS обращается к API (а не IP браузера клиента).
  </Step>

  <Step title="Добавьте его в админ-кабинете">
    Белый список IP сайта управляется оператором в админ-панели платформы.
  </Step>

  <Step title="Учитывайте смену IP">
    При переезде сервера, смене провайдера или добавлении балансировщика обновите список —
    иначе запросы начнут отклоняться с `IP_NOT_WHITELISTED`.
  </Step>
</Steps>

<Note>
  Если белый список для сайта пуст, проверка IP не применяется. Для боевого окружения мы
  рекомендуем всегда настраивать whitelist.
</Note>

## Ротация ключей

`api_secret` показывается **один раз** — в момент создания ключа. Сохраните его сразу в
безопасное место (секрет-менеджер). Восстановить его позже нельзя; если секрет утерян или
скомпрометирован — выпустите новый ключ через ротацию.

<Steps>
  <Step title="Выпустите новый ключ">
    В админ-кабинете создайте новый credential или выполните ротацию существующего. Вы получите
    новый `X-Api-Id` и новый `api_secret` (последний — единожды).
  </Step>

  <Step title="Переключите интеграцию">
    Обновите `X-Api-Id` и `api_secret` в своей CMS. Старый ключ при ротации помечается как
    `rotated` и продолжает работать ограниченное время (grace period) — это позволяет выкатить
    смену без простоя.
  </Step>

  <Step title="Отзовите старый ключ">
    После того как весь трафик идёт на новый ключ, отзовите старый. Отозванный ключ сразу
    перестаёт проходить аутентификацию.
  </Step>
</Steps>

<Warning>
  Никогда не передавайте `api_secret` в теле запроса, в URL, в логах или клиентском коде. По сети
  должна уходить только подпись `X-Signature`. Платформа не принимает секрет в открытом виде.
</Warning>

## Возможные ошибки аутентификации

| HTTP | Код                  | Когда возникает                                                                                                            | Что проверить                                                                                |
| ---- | -------------------- | -------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- |
| 401  | `UNAUTHORIZED`       | Нет `X-Api-Id`, `X-Signature` или `X-Timestamp`; ключ не найден, неактивен, истёк; сайт не активен; `X-Timestamp` не число | Все три обязательных заголовка на месте; ключ действующий; сайт активен                      |
| 401  | `INVALID_SIGNATURE`  | Подпись не совпала                                                                                                         | Подписаны те же байты, что отправлены; верный секрет; сообщение `timestamp + "." + raw_body` |
| 401  | `TIMESTAMP_SKEW`     | `X-Timestamp` вне диапазона ±300 секунд                                                                                    | Синхронизируйте часы сервера по NTP; шлите время в **секундах**, не миллисекундах            |
| 403  | `IP_NOT_WHITELISTED` | IP вызывающего не в белом списке сайта                                                                                     | Добавьте исходящий IP бэкенда в whitelist                                                    |
| 429  | `RATE_LIMITED`       | Превышен лимит запросов для сайта                                                                                          | Снизьте частоту; учитывайте заголовок `Retry-After`. См. [Лимиты](/rate-limits)              |

## Частые вопросы

<AccordionGroup>
  <Accordion title="Получаю INVALID_SIGNATURE, хотя секрет верный" icon="signature">
    В 9 случаях из 10 причина — расхождение тела. Подписывайте и отправляйте **одну и ту же
    строку**. Не сериализуйте JSON дважды, не позволяйте HTTP-клиенту повторно кодировать тело,
    не добавляйте отступы. Сравните: сообщение для HMAC — это `X-Timestamp`, затем точка, затем
    байт-в-байт тело запроса.
  </Accordion>

  <Accordion title="Получаю TIMESTAMP_SKEW" icon="clock">
    Время сервера расходится с временем платформы более чем на 300 секунд. Включите синхронизацию
    по NTP. Также убедитесь, что отправляете Unix-время в **секундах**: `Math.floor(Date.now() / 1000)`
    в JS, `int(time.time())` в Python.
  </Accordion>

  <Accordion title="Как подписать GET-запрос?" icon="link">
    Тело пустое, поэтому `raw_body = ""`, а сообщение — `"<timestamp>."` (timestamp и точка).
    Заголовки `X-Api-Id`, `X-Timestamp`, `X-Signature` всё равно обязательны.
  </Accordion>

  <Accordion title="Нужно ли передавать X-Api-Key или X-Api-Secret?" icon="ban">
    Нет. Достаточно `X-Api-Id` плюс подпись. `api_secret` не передаётся по сети ни в каком виде —
    он используется только локально для вычисления `X-Signature`.
  </Accordion>

  <Accordion title="Заголовки регистрозависимы?" icon="font">
    Имена HTTP-заголовков нечувствительны к регистру. А вот сама подпись — hex в **нижнем**
    регистре, и сообщение для HMAC чувствительно к каждому байту тела.
  </Accordion>
</AccordionGroup>
