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

Этот документ описывает, как сервер мерчанта аутентифицируется в API ключом API, и связанные с ключом лимиты запросов, ротацию и сетевые правила. Прочитайте его до первого запроса.

## Ключ API

Каждый запрос к маршрутам `/v1` передаёт ключ API мерчанта в заголовке `X-Api-Key`. Единственное исключение — `GET /v1/openapi.json`, машиночитаемое описание API: ему ключ не нужен.

```sh
curl -s https://api.stons.io/v1/invoices/6f1d3c1e-2f7a-4f0e-9a51-0c8b3e2d7a44 -H "X-Api-Key: $API_KEY"
```

Ключ принадлежит мерчанту — в кабинете он называется проектом. Первый ключ владелец проекта получает при подаче заявки на проект в кабинете, следующие выпускает в разделе проекта «Ключи API» кнопкой «Выпустить новый ключ». Ключ имеет вид `sk_<16 шестнадцатеричных символов>_<43 символа base64url>`: первая часть — публичный идентификатор ключа, который кабинет показывает в списке ключей, вторая — секрет.

Сервис хранит не ключ, а хэш argon2id его секрета, и сравнивает хэши за постоянное время. Отсюда два следствия:

- утерянный ключ нельзя восстановить, только выпустить новый;
- ключ показывается один раз, в момент выпуска, — сохраните его в менеджер секретов.

Правила обращения с ключом:

- Ключ предназначен только для запросов сервер-сервер. Он не должен попадать в браузер, мобильное приложение, репозиторий или логи.
- Ключ видит только данные своего мерчанта: инвойса другого мерчанта для него не существует (`404`).
- Ключ работает с момента выпуска. Пока проект не одобрен после проверки, `POST /v1/invoices` и `POST /v1/addresses` отвечают `403 merchant_not_approved`, а чтение инвойсов работает.
- Успешную проверку ключа сервис помнит до 60 секунд. Поэтому отозванный ключ и ключ отключённого мерчанта отклоняются не позже чем через 60 секунд после изменения.

## Ошибки

Ключ проверяется раньше тела запроса, поэтому запрос с неверным ключом получает `401`, даже если и тело неверное.

| HTTP | `error.code` | Когда |
| --- | --- | --- |
| 401 | `unauthorized` | Заголовка нет, ключ неверного формата, неизвестен, отозван или секрет не совпадает. Ответ не уточняет, что именно |
| 403 | `merchant_disabled` | Ключ верный, но мерчант отключён — оператором или владельцем в кабинете |
| 429 | `rate_limited` | Превышен лимит запросов — см. [лимиты запросов](#лимиты-запросов) |

Пример: запрос без ключа.

```sh
curl -s https://api.stons.io/v1/invoices/6f1d3c1e-2f7a-4f0e-9a51-0c8b3e2d7a44
```

`401`:

```json
{"error":{"code":"unauthorized","message":"Missing or invalid X-Api-Key header"}}
```

## Лимиты запросов

Запросы к маршрутам `/v1` ограничены дважды, в окне 60 секунд: для IP-адреса клиента и для ключа API. Значения лимитов задаёт оператор установки.

- Лимит по IP проверяется первым, до ключа: в него засчитываются и запросы без ключа или с неверным ключом.
- Лимит по ключу проверяется после ключа: он считает запросы одного ключа с любых адресов.
- `GET /v1/openapi.json` и несуществующие пути не ограничены.

Ответы ограниченных маршрутов содержат заголовки:

| Заголовок | Смысл |
| --- | --- |
| `x-ratelimit-limit` | Сколько запросов разрешено в окне. Запрос, прошедший оба лимита, сообщает лимит по ключу; запрос, отклонённый лимитом, — этот лимит |
| `x-ratelimit-remaining` | Сколько запросов осталось в окне |
| `x-ratelimit-reset` | Через сколько секунд окно начнётся заново |
| `retry-after` | Только в ответе `429`: через сколько секунд повторить |

## Условные чтения

Каждое простое чтение отвечает заголовком `ETag` — метку именно этого ответа дают `GET /v1/assets`, `GET /v1/invoices`, `GET /v1/invoices/{id}` и `GET /v1/deposits/{tx_hash}`. Пришлите метку обратно в `If-None-Match`, и ответ, который не изменился, придёт как `304 Not Modified` без тела. Это нужно продукту, который опрашивает инвойс, пока покупатель платит, или проходит [ленту изменений](https://stons.io/ru/docs/invoices.md#лента-изменений) между вебхуками.

```http
GET /v1/invoices/6f1d3c1e-2f7a-4f0e-9a51-0c8b3e2d7a44
X-Api-Key: <ваш ключ>
If-None-Match: "LXEWQrcmsEQBYnyp-6wy9chTD7GQPMTbAiWHF5IaSIE"
```

- `304` приходит с той же меткой и без тела: то, что у вас есть, актуально. `200` приходит с новой меткой — храните её рядом с ответом и присылайте со следующим запросом.
- Метка считается по всему телу, поэтому меняется от любого изменения ответа: пришёл перевод, сменился статус, пересчиталась сумма, тот же инвойс запросили вместе с QR-кодом. Это не номер версии инвойса, и у двух разных запросов об одном инвойсе метки разные.
- Метка непрозрачна: это строка в кавычках, которую надо сохранить и вернуть, читать из неё нечего. `If-None-Match: *` совпадает с любым ответом, который есть у сервиса.
- Условный запрос — это запрос: он считается в лимитах выше и стоит сервису той же работы. Экономия на вашей стороне — тело по сети и его разбор.
- Метку получает только `200`. Ошибка метки не получает, и `304` её не заменяет: запрос, который ответил бы `404`, отвечает `404`.

## Настройки проекта

Интеграция настраивает проект сама, своим ключом: указывает, куда присылать
[вебхуки](https://stons.io/ru/docs/webhooks.md), куда [страница оплаты](https://stons.io/ru/docs/checkout.md#адреса-возврата)
возвращает покупателя, и какие токены проект принимает. В кабинет заходить для
этого не нужно (ADR-0067).

Секрет подписи вебхуков эти маршруты не возвращают. Он показывается один раз,
при создании проекта; ключ API — не доступ к секрету.

### `GET /v1/project`

```json
{
  "project_id": "7f2c1b94-3d5a-4e86-9a2f-6c0b1d8e4a37",
  "name": "Shop",
  "address_mode": "per_invoice",
  "webhook_url": "https://shop.example/hooks",
  "return_url": "https://shop.example/thanks",
  "cancel_url": null,
  "review_status": "approved"
}
```

- `address_mode` решает, что этот API принимает: `POST /v1/addresses` работает
  только в `per_user`, а счёт в `per_user` без `external_user_id` отклоняется —
  см. [адреса](https://stons.io/ru/docs/addresses.md).
- `review_status` = `pending` или `rejected` означает, что счета и постоянные
  адреса отвечают `403 merchant_not_approved`: проект ещё не одобрен.

### `PATCH /v1/project`

Меняет одно или несколько полей; пустое тело и любое другое поле — `400`:

```http
PATCH /v1/project
X-Api-Key: <ваш ключ>
Content-Type: application/json

{ "webhook_url": "https://shop.example/hooks", "return_url": "https://shop.example/thanks" }
```

- `webhook_url` можно задать и заменить, **но не удалить**: `null` — `400`.
  События, уже поставленные в очередь, должны куда-то уходить.
- `return_url` и `cancel_url`: адрес задаёт, `null` очищает, отсутствие поля
  оставляет как есть. Правила адресов — у [страницы оплаты](https://stons.io/ru/docs/checkout.md#адреса-возврата).
- Режим адресов этим маршрутом не меняется: его разрешает оператор.
- Ответ — объект проекта после изменения. Изменение попадает в журнал аудита с
  именем ключа, которым сделано.

### `GET /v1/project/assets`

Какие активы проект принимает, с двумя переключателями — платформы и своим:

```json
{
  "assets": [
    {
      "asset": "USDT_TRON",
      "network": "tron",
      "symbol": "USDT",
      "decimals": 6,
      "min_deposit": "1000000",
      "platform_enabled": true,
      "enabled": true,
      "accepting": true
    }
  ]
}
```

`accepting` — можно ли создать счёт в этом активе сейчас: платформа принимает
его **и** проект его не выключил.

### `PUT /v1/project/assets`

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

```http
PUT /v1/project/assets
X-Api-Key: <ваш ключ>
Content-Type: application/json

{ "assets": [{ "asset": "USDT_TRON", "enabled": true }, { "asset": "TRX_TRON", "enabled": false }] }
```

Актив, которого сервис не принимает, — `422 asset_not_supported`, и из запроса
не записывается ничего. Ответ — весь список после изменения. Переключатель,
который ничего не меняет, аудита не создаёт.

## Ротация

У мерчанта может быть несколько действующих ключей одновременно, поэтому ключ заменяется без простоя:

1. Владелец проекта выпускает новый ключ в разделе проекта «Ключи API» в кабинете.
2. Продукт переключается на новый ключ.
3. Владелец там же отзывает старый ключ. API отклоняет отозванный ключ не позже чем через 60 секунд после отзыва. Отозванный ключ не восстанавливается.

## Сеть

API мерчантов отвечает по адресу `https://api.stons.io/v1`. В боевой установке перед ним стоит HTTPS-прокси:

- из интернета доступны только пути, начинающиеся с `/v1/`; любой другой путь отвечает `404` с пустым телом;
- запрос по `http://` перенаправляется на `https://` кодом `308`. К моменту перенаправления заголовок с ключом уже ушёл по сети открытым текстом, поэтому в клиенте указывайте адрес с `https://`.

Маршруты `/admin/v1`, `/cabinet/v1` и `/checkout/v1` обслуживают админку сотрудников, приложение кабинета и сервер страницы оплаты STONS. Они не часть интеграции мерчанта и из интернета недоступны.
