# Endpoint (API v2): Ротация секрета подписи вебхуков

> ℹ️ **EasyPay API v2.** Это ручка **второго поколения** EasyPay API (база `https://api.appsign.me`), пришедшего на смену legacy-API v1 (`https://n8n.thenextgen.store/webhook/*`). Отличия v2 от v1: авторизация заголовком `X-Partner-Api-Key`, **плоское** тело запроса, идемпотентность через `client_token` (полный ключ собирает сервер), коды ошибок — в `UPPER_CASE`.

## Зачем это нужно

EasyPay подписывает каждый исходящий вебхук HMAC-секретом (см. [гайд по проверке подписи](https://docs.thenextgen.store/s/635a43c1-37fa-463b-bc7e-7ede7884fce2/doc/webhook-easypay-proverka-podpisi-YS2uy0j2It)). Секрет вы получаете при [регистрации вебхука](https://docs.thenextgen.store/s/635a43c1-37fa-463b-bc7e-7ede7884fce2/doc/endpoint-api-v2-registraciya-vebhuka-uvedomlenij-yMW2D2iTuK).

Эта ручка **меняет** секрет для одной среды: генерит новый и оставляет прежний действительным на короткий grace-период. Нужна когда:

* есть **подозрение на утечку** секрета;
* вы делаете **плановую ротацию** по расписанию безопасности.

> 💡 **Просто потеряли секрет? Ротация не нужна.** Ручка [регистрации](https://docs.thenextgen.store/s/635a43c1-37fa-463b-bc7e-7ede7884fce2/doc/endpoint-api-v2-registraciya-vebhuka-uvedomlenij-yMW2D2iTuK) — get-or-create: повторный вызов **возвращает текущий секрет**. Ротация нужна именно чтобы **сменить** секрет, а не прочитать его.

### Grace-период — переход без потери событий

Ротация мгновенно генерирует новый секрет (`current`) и сохраняет прежний как `previous` до момента `previous_valid_until` (по умолчанию **7 дней**). Пока идёт grace-период, EasyPay подписывает каждый вебхук **обоими** секретами — в заголовке `EasyPay-Signature` появляются два значения через пробел: `v1,<hex> v1,<hex>`. Вы успеваете раскатить новый секрет по всем инстансам, не теряя ни одного события.

## Как подключиться


1. У вас уже должен быть **зарегистрирован вебхук** для нужной среды.
2. Тот же API-ключ и то же разрешение `webhook_config`, что и для регистрации.

## Endpoint

```
POST https://api.appsign.me/rotate-partner-webhook-secret
Content-Type: application/json
X-Partner-Api-Key: ваш-api-ключ
```

## Формат запроса

```json
{
  "environment": "live",
  "client_token": "rotate-2026-07-10-leak"
}
```

**Параметры:**

| Параметр | Обязательный | Описание |
|----------|--------------|----------|
| `environment` | Да           | Среда, для которой ротировать секрет: `"test"` или `"live"`. У test и live **разные** секреты — ротируйте нужную; другая не затрагивается. |
| `client_token` | Да           | Идемпотентный токен ротации (см. ниже). |

## Идемпотентность

Ротация требует `client_token` — **в отличие от ручек создания**, где он опционален. Причина: ротация **не идемпотентна по содержимому** (каждый вызов обязан выдать **новый** секрет), поэтому безопасного «дедупа по содержимому» здесь нет.

* Каждый **уникальный** `client_token` = **одна** ротация.
* Повтор **того же** `client_token` **не** ротирует повторно и возвращает текущий секрет (`rotated: false`, `secret_status: "returned"`) — безопасный retry.
* Чтобы ротировать **ещё раз**, передайте **новый** `client_token`.

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

### Успешный ответ (200)

```json
{
  "success": true,
  "environment": "live",
  "rotated": true,
  "secret_status": "issued",
  "signing_secret": "whsec_live_044a6b63…",
  "previous_valid_until": "2026-07-17T12:00:00.000Z"
}
```

| Поле | Описание |
|------|----------|
| `signing_secret` | **Новый** секрет (`whsec_live_…` / `whsec_test_…`). Сохраните немедленно. Присутствует при `secret_status` `"issued"` (новый) или `"returned"` (повтор — текущий). |
| `secret_status` | `"issued"` — новый секрет выдан на этом вызове; `"returned"` — повтор того же `client_token`, возвращён текущий секрет. |
| `previous_valid_until` | ISO8601 — до этого момента **прежний** секрет ещё проверяет подпись (grace-период). |
| `rotated` | `true` — ротация произошла; `false` — идемпотентный повтор (новой ротации не было). |
| `environment` | Среда, для которой ротировали. |

### Ошибки

Все ошибки возвращаются с **HTTP 200** + полем `success: false` и `error_code`. Проверяйте `success`, а не HTTP-статус.

| `error_code` | Когда срабатывает |
|------------|-------------------|
| `INVALID_INPUT` | `environment` не `"test"`/`"live"`, **или** отсутствует `client_token`. |
| `AUTH_REQUIRED` / `INVALID_API_KEY` | Не передан или невалиден API-ключ. |
| `PERMISSION_DENIED` | У ключа нет разрешения `webhook_config`. |
| `IDEMPOTENCY_KEY_REUSED_WITH_DIFFERENT_PAYLOAD` | Тот же `client_token` использован с другим `environment`. Новый токен. |
| `PREVIOUS_ATTEMPT_FAILED` | Прошлая попытка с этим токеном упала. Новый токен. |
| `OPERATION_IN_PROGRESS` | Другой запрос с тем же токеном ещё обрабатывается. Повторите чуть позже. |
| `INTERNAL_ERROR` | Внутренний сбой EasyPay. Команда заботы уведомлена. |

## Как перейти на новый секрет


1. Вызовите ротацию, получите `signing_secret` и разверните его **рядом** со старым секретом.
2. Принимайте вебхук, если проходит **любой** из ваших секретов — готовые сниппеты в [гайде](https://docs.thenextgen.store/s/635a43c1-37fa-463b-bc7e-7ede7884fce2/doc/webhook-easypay-proverka-podpisi-YS2uy0j2It) уже перебирают все значения `v1,` в заголовке.
3. Уберите старый секрет **до** `previous_valid_until`.

## Ограничения

* Ротация — **по одной среде за вызов** (`test` или `live` отдельно).
* Нужен уже зарегистрированный вебхук и разрешение `webhook_config`.
* `client_token` **обязателен** (в отличие от ручек создания).
* Grace-период (действие старого секрета) — по умолчанию 7 дней.

## Пример на Python

```python
import urllib.request
import json
import uuid

url = "https://api.appsign.me/rotate-partner-webhook-secret"
payload = {
    "environment": "live",
    "client_token": f"rotate-{uuid.uuid4().hex[:12]}",
}

req = urllib.request.Request(
    url,
    data=json.dumps(payload).encode("utf-8"),
    headers={"Content-Type": "application/json", "X-Partner-Api-Key": "ваш-api-ключ"},
    method="POST",
)

with urllib.request.urlopen(req) as resp:
    result = json.loads(resp.read().decode("utf-8"))

if result.get("success") and result.get("secret_status") == "issued":
    print("НОВЫЙ секрет (сохраните немедленно!):", result["signing_secret"])
    print("Старый секрет действует до:", result.get("previous_valid_until"))
else:
    print("Ответ:", result.get("error_code") or result.get("secret_status"))
```

## Пример на cURL

```bash
curl -X POST "https://api.appsign.me/rotate-partner-webhook-secret" \
  -H "Content-Type: application/json" \
  -H "X-Partner-Api-Key: ваш-api-ключ" \
  -d '{
    "environment": "live",
    "client_token": "rotate-2026-07-10-leak"
  }'
```

## См. также

* [Endpoint (API v2): Регистрация вебхука уведомлений](https://docs.thenextgen.store/s/635a43c1-37fa-463b-bc7e-7ede7884fce2/doc/endpoint-api-v2-registraciya-vebhuka-uvedomlenij-yMW2D2iTuK) — как зарегистрировать вебхук и получить/восстановить секрет.
* [Webhook EasyPay: проверка подписи](https://docs.thenextgen.store/s/635a43c1-37fa-463b-bc7e-7ede7884fce2/doc/webhook-easypay-proverka-podpisi-YS2uy0j2It) — как проверить подпись вебхука.