Bit
Синхронизируем рынок
Для разработчиков

Публичный API v1 — руководство интегратора

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

Контур /api/v1 предназначен для ботов и внешних сервисов: рыночные данные без ключа, приватные ручки — по ключу с HMAC-подписью каждого запроса.

Машиночитаемое описание всех ручек — справочник ручек, он же спека public-v1.ru.yaml (OpenAPI 3.0.3): её можно скормить openapi-generator и получить готовый клиент. Здесь — то, чего в спеке нет: как подписать запрос, как устроены лимиты и где контур ведёт себя не так, как кажется на первый взгляд.

Ключи не для браузера. CORS на /api/v1 выключен намеренно, и включать его не планируется: секрет, попавший во фронтенд, утекает вместе со страницей — его увидит и расширение браузера, и любой, кому пользователь покажет DevTools. Контур строго server-to-server. Веб-интерфейсу нужен приватный /api/* под сессией, а не ключ.


1. Ключ и секрет

Ключи выдаются в личном кабинете, на странице API-ключи. Под ней — ручки приватного контура, работающие под сессией (Sanctum, cookie), а не через /api/v1:

Метод и путь Что делает
GET /api/api-keys Список ваших ключей (без секретов)
POST /api/api-keys Создать ключ; единственный ответ, где есть секрет
PUT /api/api-keys/{id} Переименовать, поправить ip_allowlist / expires_at
DELETE /api/api-keys/{id} Отозвать (идемпотентно)

Создание:

POST /api/api-keys
{
  "name": "market-maker-1",
  "scopes": ["read", "trade"],       // read | trade | withdraw
  "ip_allowlist": ["203.0.113.7"],   // опционально; для withdraw — обязательно
  "expires_at": null,                // ISO-8601 или null
  "code": "123456"                   // TOTP: нужен для trade/withdraw-ключа
}
201 Created
{
  "key": { "id": 7, "public_id": "ek_9f1c…", "scopes": ["read","trade"], "is_active": true, … },
  "secret": "…",
  "secret_shown_once": true
}

Что важно знать заранее:

  • secret показывается ровно один раз. Ручки «показать секрет ещё раз» не существует и не будет: иначе угнанная сессия ЛК давала бы доступ ко всем ранее выданным ключам. Потеряли — отзывайте ключ и выпускайте новый.
  • Скоупы неизменяемы. PUT со scopes вернёт 422. Нужен другой набор прав — новый ключ.
  • Выдача trade/withdraw-ключа требует второго фактора (TOTP, а если 2FA выключена — пароля). Ключ только на чтение — без подтверждения.
  • withdraw-ключ невозможно выпустить без 2FA и без непустого ip_allowlist — см. раздел 8.
  • Активных (неотозванных) ключей на аккаунт — не больше PUBLIC_API_MAX_KEYS (по умолчанию 10), иначе 422 key_limit_reached.
  • Ключи привязаны к APP_KEY приложения: её ротация делает все секреты нерасшифровываемыми, и все ключи придётся перевыпустить.

2. Подпись запроса

Каждый приватный запрос несёт три заголовка (плюс один необязательный):

Заголовок Значение
X-API-KEY public_id ключа (ek_…)
X-API-TIMESTAMP Время клиента в миллисекундах epoch
X-API-SIGNATURE HMAC-SHA256, hex в нижнем регистре
X-API-RECV-WINDOW Окно приёма в мс: по умолчанию 5000, максимум 60000

Каноническая строка

canonical = TIMESTAMP + "\n" + METHOD + "\n" + PATH + "\n" + QUERY + "\n" + BODY
signature = lowercase(hex(hmac_sha256(api_secret, canonical)))

Правила по каждому полю — их несоблюдение и есть 99% причин invalid_signature:

Часть Как ровно
TIMESTAMP Та же строка, что уйдёт в X-API-TIMESTAMP, символ в символ
METHOD Верхний регистр: GET, POST, DELETE
PATH Путь с ведущим слэшем и с префиксом контура: /api/v1/orders. Без схемы, хоста и без ?
QUERY Сырая query-строка без ?; пустая строка, если параметров нет
BODY Сырое тело ровно теми байтами, что уйдут в сокет; пустая строка для GET/DELETE без тела

Разделитель — один \n (0x0A). Строк всегда пять, даже когда QUERY и BODY пусты — значит, канон заканчивается на \n\n или на \n перед телом.

Отдельно про QUERY: сервер берёт её как есть и ничего не сортирует. Подписывайте ту строку, которую реально отправляете, включая порядок параметров и URL-кодирование. Библиотека, которая пересобирает URL после подписи (или добавляет свой параметр), сломает подпись.

Пример 1 — GET с параметрами

Секрет для примеров (использовать только для самопроверки):

c2VjcmV0LWRvLW5vdC11c2UtaW4tcHJvZA

Запрос GET /api/v1/ticker?symbol=BTC_USDT, метка 1757500000123. Канон (показан с явными \n):

1757500000123\nGET\n/api/v1/ticker\nsymbol=BTC_USDT\n

Подпись:

5414b01b270ef6e5518005d2c23b6bd8118300ebcfc789c2bf21b884c25838b5

Проверить свою реализацию побайтно:

printf '1757500000123\nGET\n/api/v1/ticker\nsymbol=BTC_USDT\n' \
  | openssl dgst -sha256 -hmac 'c2VjcmV0LWRvLW5vdC11c2UtaW4tcHJvZA' -r

Пример 2 — POST с телом

Запрос POST /api/v1/orders, метка 1757500000456, query пустая, тело (136 байт, ровно одной строкой, без переводов строки и лишних пробелов):

{"symbol":"BTC_USDT","side":"buy","type":"limit","tif":"gtc","price":"64150.10","quantity":"0.015","client_order_id":"mm-1757500000123"}

Канон:

1757500000456\nPOST\n/api/v1/orders\n\n{"symbol":"BTC_USDT",…,"client_order_id":"mm-1757500000123"}

Подпись:

db9795f324e502083ee00fef5b6bd66ecaa34666b3526b07497b58cd0c50f1fb

Если ваш JSON-сериализатор ставит пробелы после : или меняет порядок ключей — это другой байтовый поток и другая подпись. Сериализуйте тело один раз, в строку/буфер, подпишите именно её и её же отправьте.

Окно времени и защита от повтора

  • Окно двустороннее: метка из будущего отбивается так же, как из прошлого (stale_timestamp). Съехавшие часы клиента — вторая по частоте причина отказов после кривого канона; сверьтесь с GET /api/v1/time.
  • X-API-RECV-WINDOW больше 60000, ноль или не число — 401 bad_recv_window. Расширять окно «на всякий случай» вредно: оно же определяет, сколько живёт защита от повтора.
  • Каждая принятая подпись запоминается на время окна. Повтор той же подписи — 401 replayed_request.

Острый угол. Подпись — функция от (timestamp, method, path, query, body). Два одинаковых запроса с одинаковой меткой времени дают одну и ту же подпись, и второй будет отбит как повтор — хотя это не атака, а честные два ордера подряд. На быстрой машине два вызова легко попадают в одну миллисекунду.

Поэтому клиент обязан гарантировать строго возрастающие метки. Приём, которым пользуются собственные тесты биржи:

timestamp = max(current_time_ms, last_timestamp + 1)
last_timestamp = timestamp

Метка, ушедшая на пару миллисекунд вперёд, безопасна — окно двустороннее. Альтернатива — добавлять в каждый запрос уникальное поле (client_order_id для ордеров), но она работает не везде, а монотонный счётчик — всегда.


3. Рабочие примеры

Все три примера делают одно и то же: читают /account/balances, затем ставят лимитный ордер. Достаточно подставить API_KEY, API_SECRET и BASE.

bash + curl + openssl

#!/usr/bin/env bash
set -euo pipefail

BASE="https://wallex.zone"
API_KEY="ek_…"
API_SECRET="…"

# Строго монотонная метка: два запроса в одну миллисекунду дали бы одну подпись.
LAST_TS=0
next_ts() {
  local now
  now=$(date +%s%3N)
  if (( now <= LAST_TS )); then now=$(( LAST_TS + 1 )); fi
  LAST_TS=$now
  printf '%s' "$now"
}

signed() {
  local method="$1" path="$2" query="${3:-}" body="${4:-}"
  local ts sig url
  ts=$(next_ts)
  sig=$(printf '%s\n%s\n%s\n%s\n%s' "$ts" "$method" "$path" "$query" "$body" \
        | openssl dgst -sha256 -hmac "$API_SECRET" -r | cut -d' ' -f1)

  url="$BASE$path"
  [[ -n "$query" ]] && url="$url?$query"

  curl -sS -X "$method" "$url" \
    -H "X-API-KEY: $API_KEY" \
    -H "X-API-TIMESTAMP: $ts" \
    -H "X-API-SIGNATURE: $sig" \
    ${body:+-H 'Content-Type: application/json' --data-raw "$body"}
}

signed GET /api/v1/account/balances

ORDER='{"symbol":"BTC_USDT","side":"buy","type":"limit","price":"50000.00","quantity":"0.001","client_order_id":"bash-'"$(date +%s%3N)"'"}'
signed POST /api/v1/orders '' "$ORDER"

--data-raw здесь принципиален: --data вырезает переводы строк и может изменить байты тела, а подпись считается по ним.

Python

import hashlib
import hmac
import json
import time

import requests

BASE = "https://wallex.zone"
API_KEY = "ek_…"
API_SECRET = "…"


class Client:
    def __init__(self, base: str, key: str, secret: str) -> None:
        self.base, self.key, self.secret = base, key, secret
        self.session = requests.Session()
        self._last_ts = 0

    def _timestamp(self) -> str:
        # Строго монотонно: одинаковая метка на двух одинаковых запросах даёт
        # одну подпись, и второй прилетит обратно как replayed_request.
        ts = max(int(time.time() * 1000), self._last_ts + 1)
        self._last_ts = ts
        return str(ts)

    def request(self, method: str, path: str, params: dict | None = None, body: dict | None = None):
        query = "&".join(f"{k}={v}" for k, v in (params or {}).items())
        # Тело сериализуем ОДИН раз и отправляем ровно те же байты, что подписали.
        payload = json.dumps(body, separators=(",", ":")) if body is not None else ""
        ts = self._timestamp()

        canonical = "\n".join([ts, method.upper(), path, query, payload])
        signature = hmac.new(self.secret.encode(), canonical.encode(), hashlib.sha256).hexdigest()

        headers = {
            "X-API-KEY": self.key,
            "X-API-TIMESTAMP": ts,
            "X-API-SIGNATURE": signature,
            "Accept": "application/json",
        }
        if payload:
            headers["Content-Type"] = "application/json"

        url = f"{self.base}{path}" + (f"?{query}" if query else "")
        response = self.session.request(method.upper(), url, headers=headers, data=payload or None)

        if response.status_code >= 400:
            error = response.json()
            raise RuntimeError(f"{response.status_code} {error['code']}: {error['message']}")

        return response.json()


client = Client(BASE, API_KEY, API_SECRET)

print(client.request("GET", "/api/v1/account/balances"))

order = client.request("POST", "/api/v1/orders", body={
    "symbol": "BTC_USDT",
    "side": "buy",
    "type": "limit",
    "price": "50000.00",       # деньги — строками, всегда
    "quantity": "0.001",
    "client_order_id": f"py-{int(time.time() * 1000)}",
})
print(order["order"]["id"], order["order"]["status"])

Node.js (18+, без зависимостей)

import { createHmac } from 'node:crypto';

const BASE = 'https://wallex.zone';
const API_KEY = 'ek_…';
const API_SECRET = '…';

let lastTimestamp = 0;

function nextTimestamp() {
  // Node легко делает два запроса в одну миллисекунду — тогда совпадут и подписи.
  const now = Math.max(Date.now(), lastTimestamp + 1);
  lastTimestamp = now;
  return String(now);
}

async function signed(method, path, { params = {}, body = null } = {}) {
  const query = new URLSearchParams(params).toString();
  const payload = body === null ? '' : JSON.stringify(body);
  const timestamp = nextTimestamp();

  const canonical = [timestamp, method.toUpperCase(), path, query, payload].join('\n');
  const signature = createHmac('sha256', API_SECRET).update(canonical).digest('hex');

  const headers = {
    'X-API-KEY': API_KEY,
    'X-API-TIMESTAMP': timestamp,
    'X-API-SIGNATURE': signature,
    Accept: 'application/json',
  };
  if (payload) headers['Content-Type'] = 'application/json';

  const url = `${BASE}${path}${query ? `?${query}` : ''}`;
  const response = await fetch(url, { method: method.toUpperCase(), headers, body: payload || undefined });
  const data = await response.json();

  if (!response.ok) {
    throw new Error(`${response.status} ${data.code}: ${data.message}`);
  }

  return data;
}

console.log(await signed('GET', '/api/v1/account/balances'));

const { order } = await signed('POST', '/api/v1/orders', {
  body: {
    symbol: 'BTC_USDT',
    side: 'buy',
    type: 'limit',
    price: '50000.00',
    quantity: '0.001',
    client_order_id: `node-${Date.now()}`,
  },
});
console.log(order.id, order.status);

Отлаживать подпись удобнее всего на GET /api/v1/ping: она отвечает только на корректно подписанный запрос, но ничем не рискует.


4. Лимиты

Ведро считается по ключу (для публичных ручек — по IP). Значения по умолчанию; переменные окружения перечислены для тех, кто разворачивает биржу у себя:

Ведро Что покрывает Лимит Переменная окружения
api-v1-public /time, /symbols, /ticker, /depth, /trades, /klines, /funding 1200 запросов / 1 мин PUBLIC_API_LIMIT_PUBLIC, PUBLIC_API_LIMIT_PUBLIC_PER
api-v1-read /ping и все ручки скоупа read 600 / 1 мин PUBLIC_API_LIMIT_READ, PUBLIC_API_LIMIT_READ_PER
api-v1-trade POST /orders, DELETE /orders, DELETE /orders/{id} 300 / 1 мин PUBLIC_API_LIMIT_TRADE, PUBLIC_API_LIMIT_TRADE_PER
api-v1-withdraw POST /withdrawals 10 / 1 мин PUBLIC_API_LIMIT_WITHDRAW, PUBLIC_API_LIMIT_WITHDRAW_PER
отказы аутентификации неудачные попытки с одного IP 60 / 5 мин PUBLIC_API_AUTH_FAILURE_LIMIT, PUBLIC_API_AUTH_FAILURE_WINDOW

Прочие настройки контура: PUBLIC_API_MAX_KEYS (10 активных ключей на аккаунт), PUBLIC_API_ADDRESS_COOLDOWN_HOURS (24 часа охлаждения адреса вывода).

Особенности:

  • Ответ на превышение — 429 rate_limit_exceeded с заголовками Retry-After, X-RateLimit-Limit, X-RateLimit-Remaining. Ждите Retry-After, а не фиксированную паузу.
  • Ведро отказов аутентификации считает неудачи, а не запросы, и живёт по IP. Успешный запрос его не обнуляет. Практический вывод: не гоняйте в проде бота с заведомо неверной подписью — после 60 отказов с адреса перестанут отвечать по существу все ключи, в том числе исправные.
  • Отдельного ведра «ордеров в секунду» нет: api-v1-trade общий на постановку и отмену.

5. Ошибки

Формат один на весь контур, включая 401/403/404/429/500:

{ "code": "unknown_symbol", "message": "Unknown symbol `BTC_USD`. See GET /api/v1/symbols for the list." }

У ошибок валидации (422) добавляется третье поле:

{
  "code": "invalid_parameter",
  "message": "The request body did not pass validation.",
  "errors": { "quantity": ["The quantity must be sent as a JSON string (e.g. \"0.015\") to avoid float rounding."] }
}

Опирайтесь на code: message человеческий и может измениться.

Аутентификация и доступ

code HTTP Что случилось
missing_credentials 401 Нет одного из трёх обязательных заголовков
invalid_key 401 Ключа с таким public_id нет
key_revoked 401 Ключ отозван владельцем или админом
key_expired 401 Прошёл expires_at
invalid_signature 401 Подпись не совпала — проверьте канон побайтно
stale_timestamp 401 Метка вне окна (в любую сторону)
replayed_request 401 Такая подпись уже принималась — см. про монотонные метки
bad_recv_window 401 X-API-RECV-WINDOW не число, ноль или больше 60000
unauthorized 401 Общий отказ авторизации
ip_not_allowed 403 Адрес запроса не в ip_allowlist ключа
insufficient_scope 403 У ключа нет нужного скоупа
account_blocked 403 Аккаунт заблокирован — в поддержку
kyc_required 403 Нужна пройденная верификация (вывод)
forbidden 403 Общий запрет
rate_limit_exceeded 429 Лимит ведра или лимит отказов аутентификации

Параметры и ресурсы

code HTTP Что случилось
unknown_symbol 404 Рынка нет либо он делистнут
order_not_found 404 Ордера нет у этого аккаунта
unknown_asset 404 Актива с таким символом нет
not_found 404 Неизвестный путь
method_not_allowed 405 Путь есть, метод не тот
not_acceptable 406 Запрошен формат ответа, которого контур не отдаёт
csrf_token_mismatch 419 Признак того, что запрос ушёл в приватный контур, а не в /api/v1
invalid_parameter 422 Параметр или тело не прошли валидацию (errors)
service_unavailable 503 Зависимость временно недоступна
server_error / request_failed 5xx Наша ошибка; повторять с backoff

Отказы торгового домена (POST /api/v1/orders)

code HTTP Что случилось
trading_paused 503 Торги на паузе — единственный отказ, который пройдёт сам
market_not_tradable 422 Рынок не в торгуемом состоянии
perp_unavailable 422 Перп-контур недоступен
no_liquidity 422 Рыночному ордеру не с чем сводиться
tick_lot_mismatch 422 Цена не кратна tick_size либо количество — lot_size
order_too_small 422 Количество меньше минимального
notional_too_small 422 Объём в котируемом активе ниже min_notional
notional_too_large 422 Объём выше max_notional
insufficient_funds 422 Не хватает свободного баланса
reduce_only_invalid 422 reduce_only без подходящей позиции
order_rejected 422 Прочий отказ домена

Вывод (POST /api/v1/withdrawals)

code HTTP Что случилось
ip_allowlist_required 403 У ключа пустой ip_allowlist
two_factor_required 403 У аккаунта выключена 2FA
address_not_whitelisted 403 Адреса нет в адресной книге для этой пары актив+сеть
address_in_cooldown 403 Адрес ещё отлёживается (в message — время готовности)
demo_withdraw_forbidden 422 Демо-аккаунт не выводит
asset_not_withdrawable 422 Актив нельзя вывести в блокчейн-сеть
insufficient_funds 422 Не хватает доступного баланса

6. Формат данных и пагинация

  • Время — целое число миллисекунд epoch. Никаких ISO-строк в /api/v1.
  • Деньги и количества — строки, и в ответах, и в запросах. "quantity": 0.015 вместо "0.015" — 422: число в JSON означает, что сумма уже прошла через double, а такие ошибки на бирже видны только в отчётности.
  • Пагинация одна на все истории (/orders, /orders/open, /my-trades, /deposits, /withdrawals): курсор from_id (строго больше) + limit (1..1000, по умолчанию 100), порядок по возрастанию id. Offset-а нет — на живой ленте он пропускает записи.
  • Признак конца — короткая страница: элементов меньше, чем limit. Отдельного поля has_more нет.
  • limit вне границ — 422, а не молчаливое обрезание до 1000.

Типовой цикл выгрузки:

cursor = 0
while True:
    page = client.request("GET", "/api/v1/my-trades", {"from_id": cursor, "limit": 1000})["trades"]
    if not page:
        break
    handle(page)
    cursor = page[-1]["id"]

7. Торговля: острые углы

client_order_id и идемпотентность

client_order_id — ваш ключ идемпотентности, уникальный в пределах аккаунта. Повтор запроса с уже использованным значением не создаёт второй ордер: биржа возвращает ранее созданный и отвечает 200 OK вместо 201 Created.

Различайте создание и повтор по коду ответа: тело в обоих случаях одинаковое. Это делает безопасным retry по таймауту — сеть оборвалась, ответ не дошёл, повтор вернёт тот же ордер, а не удвоит позицию. Пользуйтесь этим всегда, когда у запроса возможен ретрай.

Пространство идентификаторов своё у каждого аккаунта: чужой client_order_id никогда не вернёт чужой ордер.

post_only отклоняется асинхронно

post_only-ордер, скрестившийся с книгой, нельзя отбить в ответе на POST: решение принимает движок, а не API. Такой ордер получает 201 со статусом pending, а rejected появится позже.

Следствие: 201 на post_only не означает, что ордер встал в книгу. Если логика на это опирается (мейкер-стратегия), статус надо опросить — GET /api/v1/orders/{id} или GET /api/v1/orders/open.

Ровно так же асинхронна отмена: DELETE /api/v1/orders/{id} отвечает 200 и может вернуть ещё прежний статус — команда ушла в движок. Повторный DELETE — тоже 200 (идемпотентно), а не 404 и не 409.

/my-trades — это только исполнения ордеров

Ликвидаций и списаний/начислений funding в /my-trades нет вовсе. У них нет ордера, а ручка отдаёт исполнения ордеров.

Практически: перп-PnL, посчитанный по одним лишь сделкам, разойдётся с балансом — тем сильнее, чем дольше держится позиция. Полную картину дают realized_pnl из GET /api/v1/account/positions и движения в /account/balances.

Делистнутые пары: асимметрия

Делистнутый рынок для контура не существует: его нет в /symbols, и любой запрос с таким symbol — 404 unknown_symbol.

Но ордера и сделки по нему остаются в истории и приходят в общей выдаче без фильтра. Значит, GET /api/v1/orders вернёт ордер по паре FOO_USDT, а GET /api/v1/orders?symbol=FOO_USDT ответит 404. Клиент, который для каждого символа из истории делает уточняющий запрос, на этом падает — выгружайте историю без фильтра и группируйте на своей стороне.

Прочее

  • GET /api/v1/ticker всегда возвращает список, даже для одного символа.
  • DELETE /api/v1/orders требует symbol; без него 422, а не «снять всё». В ответе — списки canceled и failed: то, что попало в failed, надо перечитать, оно осталось активным.
  • Демо-аккаунт торгует тем же API; сделки и балансы помечены is_demo, а вывод для него закрыт.

8. Вывод средств

TOTP-код бот ввести не может, поэтому второй фактор на выводе заменён набором условий. Все четыре проверяются до любого движения денег:

  1. У ключа непустой ip_allowlist. Ключ без ограничения по адресу выводить не может в принципе — 403 ip_allowlist_required. Выпустить withdraw-ключ без allowlist ЛК тоже не даст.
  2. У аккаунта включена и подтверждена 2FA — иначе 403 two_factor_required.
  3. Адрес есть в адресной книге ЛК для этой пары актив+сеть, иначе 403 address_not_whitelisted. Добавление адреса само стоит второго фактора (POST /api/withdrawal-addresses с code/password).
  4. Адрес отлежался 24 часа (PUBLIC_API_ADDRESS_COOLDOWN_HOURS) с момента добавления, иначе 403 address_in_cooldown; время готовности есть в message и в поле usable_at записи книги. Это то, что реально спасает при угоне сессии: у владельца есть сутки заметить чужой адрес.

Сверху — общие гейты: пройденный KYC (403 kyc_required), незаблокированный аккаунт (403 account_blocked), запрет для демо (422 demo_withdraw_forbidden).

Банковского вывода через API нет: у реквизитов нет адреса, а значит и заменять второй фактор нечем. Такие заявки создаются только в ЛК; в истории GET /api/v1/withdrawals они видны с type: bank, но без реквизитов.

Запрос:

POST /api/v1/withdrawals
{
  "asset": "USDT",
  "network": "trx",       // легаси-написания вроде "TRC-20" нормализуются
  "address": "T…",        // должен быть в адресной книге
  "amount": "100.5"       // строкой
}

Ответ — 201 с телом {"withdrawal": {…}}. В заявку попадает написание адреса из адресной книги, а не из запроса.

Заявка асинхронна: status идёт pending → processing → completed | rejected, опрашивайте GET /api/v1/withdrawals.


9. Чек-лист интеграции

  1. Выпустить ключ с минимальным набором скоупов; для вывода — обязательно с ip_allowlist.
  2. Проверить подпись на GET /api/v1/ping, только потом идти в торговые ручки.
  3. Синхронизировать часы (GET /api/v1/time) и сделать метки строго монотонными.
  4. Прочитать /symbols при старте: tick_size, lot_size, min_notional, max_leverage — оттуда, а не из конфига бота.
  5. Ставить client_order_id на каждом ордере и повторять запрос при таймауте.
  6. Все ошибки разбирать по code; на 429 ждать Retry-After.
  7. Для перпов считать PnL по позициям, а не по /my-trades.