# Endpoint: Создание промокода Stripe для существующих продуктов

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

EasyPay позволяет создавать **процентные промокоды Stripe** (Coupon + Promotion Code) и привязывать их сразу к 1..N вашим уже одобренным продуктам — одним запросом. Промокод — это **текст, который клиент вводит на чекауте Stripe** (например, `BLACKFRIDAY`, `SUMMER25`), чтобы получить процентную скидку на конкретные продукты.

Используйте этот API когда:

* хотите запустить рекламную кампанию с промокодом на несколько ваших продуктов разом (например, скидка 20% на «Pro plan» и «Pro Annual» одновременно);
* партнёрский маркетолог хочет выпускать промокоды самостоятельно, не дёргая команду заботы;
* AI-агент партнёра должен оформить промокод по запросу клиента прямо в чате (через MCP).

Запрос идемпотентный: повторите с тем же `idempotency_key` и тем же телом — вернётся тот же промокод, без дублей в Stripe.

**MVP — только процентная скидка** (`percent_off`). Фиксированная сумма (`amount_off`) — в будущей версии.

**Internal beta.** Сохраняйте `code`, `stripe_coupon_id` и `stripe_promotion_code_id` из ответа на своей стороне — это рабочие идентификаторы промокода для дальнейшего использования. Отдельная ручка для деактивации промокода (`deactivate_partner_stripe_promotion_code`) появится позже.

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


1. Запросите API-ключ у вашего менеджера EasyPay — или скопируйте его из мини-апп («Show key» на первом шаге онбординга).
2. Менеджер активирует разрешение `product_create` для вашего аккаунта (тот же permission, что и для создания продукта / Payment Link — отдельного не нужно).
3. Узнайте `stripe_product_id` нужных одобренных продуктов — они есть в мини-апп в карточках продуктов или в вашей Internal Tariffs Google-таблице. Все привязываемые продукты должны принадлежать вам и быть **одного окружения** (все live ИЛИ все test) — смешанный список вернёт `mixed_environment`.

## Endpoint

```
POST https://n8n.thenextgen.store/webhook/promotion-code-create
X-Partner-Api-Key: ваш-api-ключ
Content-Type: application/json
```

> Ключ передаётся **в header** `X-Partner-Api-Key` — это unified стандарт EasyPay для MCP-совместимых ручек. Для совместимости с legacy-клиентами поле `api_key` в теле тоже поддерживается, но header — рекомендуемый способ.

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

```json
{
  "stripe_product_ids": ["prod_XXXXXXXXXXXX", "prod_YYYYYYYYYYYY"],
  "code": "BLACKFRIDAY",
  "percent_off": 25,
  "duration": "once",
  "max_redemptions": 100,
  "expires_at": "2026-12-01T00:00:00Z",
  "idempotency_key": "ваш-уникальный-ключ-этого-запроса"
}
```

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

| Параметр | Обязательный | Описание |
|----------|--------------|----------|
| `stripe_product_ids` | Да           | Массив из 1..50 ID одобренных продуктов партнёра в Stripe (`prod_…`). Все продукты должны быть одного окружения (live ИЛИ test). |
| `code`   | Да           | Текст, который клиент введёт на чекауте Stripe (1..500 символов; разрешены `A-Z`, `a-z`, `0-9`, `_`, `-`). Stripe нормализует в верхний регистр и матчит case-insensitive. Текст должен быть **уникальным в рамках вашего аккаунта** — если такой код уже занят, вернётся `code_already_exists`. |
| `percent_off` | Да           | Размер скидки в процентах. Целое число 1..100. |
| `duration` | Нет          | Длительность скидки Stripe Coupon. Возможные значения: `"once"` (по умолчанию) — применяется к одному следующему платежу; `"forever"` — применяется к каждому биллинг-циклу подписки; `"repeating"` — применяется N месяцев (требует `duration_in_months`). |
| `duration_in_months` | Только если `duration=repeating` | Целое число ≥1. Сколько месяцев действует скидка для `repeating`. Для `once` / `forever` — поле запрещено. |
| `max_redemptions` | Нет          | Максимальное число использований промокода всеми клиентами суммарно. Целое ≥1. Не передавайте — без лимита. |
| `expires_at` | Нет          | Дата и время в ISO8601, когда Stripe перестанет принимать промокод на чекауте. Должно быть минимум 60 секунд в будущем. Не передавайте — без срока. |
| `idempotency_key` | Да           | Партнёрский уникальный ключ запроса (1..128 символов). Один и тот же ключ + то же тело = безопасный replay. **НЕ должен начинаться с** `ingest:` — этот префикс зарезервирован под внутренние записи EasyPay. |
| `source` | Нет          | Идентификатор источника для аналитики (`"api"` или `"mcp"`). По умолчанию `"api"`. MCP-агент проставит `"mcp"` автоматически. |

**Что наследуется от продуктов** (нельзя переопределить): `is_test` — все продукты должны быть одного окружения, оно и определяет, будет ли промокод создан в Stripe test mode или live mode.

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

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

```json
{
  "success": true,
  "stripe_coupon_id": "AB12CD34",
  "stripe_promotion_code_id": "promo_1ABCDEF...",
  "code": "BLACKFRIDAY",
  "percent_off": 25,
  "duration": "once",
  "duration_in_months": null,
  "max_redemptions": 100,
  "expires_at": "2026-12-01T00:00:00Z",
  "stripe_product_ids": ["prod_XXXXXXXXXXXX", "prod_YYYYYYYYYYYY"],
  "is_test": false,
  "idempotency_key": "ваш-уникальный-ключ-этого-запроса",
  "created_at": "2026-05-13T16:41:20.096Z",
  "is_replay": false,
  "is_retry": false,
  "retry_count": 0
}
```

| Поле | Описание |
|------|----------|
| `code` | Финальный (нормализованный к верхнему регистру) текст промокода, который клиент вводит на чекауте. На случай разницы регистров отдавайте клиенту именно это значение. |
| `stripe_coupon_id` | ID Stripe Coupon — внутренний объект скидки в Stripe. Может пригодиться для сверки в Stripe Dashboard или будущей деактивации. |
| `stripe_promotion_code_id` | ID Stripe Promotion Code (`promo_...`) — это и есть «текст-обёртка» поверх Coupon. Сохраните его на своей стороне — он нужен для деактивации промокода через будущую ручку `deactivate_partner_stripe_promotion_code`. |
| `is_test` | `true`, если промокод создан в Stripe test mode (вы передавали `prod_*` тестовых продуктов). На реальных клиентах работать не будет — для production нужны live-продукты. |
| `is_replay` | `true`, если запрос с этим `idempotency_key` уже был обработан и вы получили закэшированный результат. Полезно для отладки. |
| `is_retry` | `true`, если EasyPay делал внутренние retry-попытки до Stripe (типично при сетевых сбоях). На корректность ответа не влияет. |
| `retry_count` | Сколько внутренних retry-попыток было сделано до успеха (`0` для чистой первой попытки). |
| `created_at` | UTC-таймстамп первоначального создания row в EasyPay (не Stripe). На replay не меняется. |

### Ошибки

Все ошибки возвращаются с **HTTP 200** + полем `success: false` — это нужно, чтобы AI-агенты и n8n-флоу не путали клиентскую ошибку с сетевым сбоем.

| `error_code` | Когда срабатывает |
|------------|-------------------|
| `invalid_input` | Невалидное тело запроса: пропущенные поля, `percent_off` вне 1..100, `code` не соответствует `^[A-Za-z0-9_-]+$`, `expires_at` в прошлом, `duration_in_months` без `duration=repeating`, `idempotency_key` начинается с `ingest:` и т.п. |
| `unauthorized` | Невалидный API-ключ или партнёр-аккаунт неактивен |
| `forbidden` | У партнёра нет разрешения `product_create` |
| `product_not_found` | Один из `stripe_product_ids` не найден в вашей Internal Tariffs (продукт не ваш или не существует). Сообщение содержит конкретный `prod_…`, который не прошёл. |
| `product_not_approved` | Один из продуктов ещё не прошёл модерацию (нет `Stripe_Product_ID` в Internal Tariffs). |
| `product_data_corrupt` | У продукта повреждены данные в Internal Tariffs — обратитесь в команду заботы. |
| `mixed_environment` | Переданы `prod_*` из разных окружений (часть test, часть live). Разделите на два запроса. |
| `code_already_exists` | Текст промокода уже занят на вашем Stripe-аккаунте. Возьмите более уникальный (например, с префиксом бренда). |
| `idempotency_conflict` | Тот же `idempotency_key` уже использовался с **другим** телом запроса. Сгенерируйте новый ключ для нового тела. |
| `stripe_error` | Сбой на стороне Stripe (4xx — детали в `error_message`; 5xx — EasyPay автоматически ретраит и не вернёт эту ошибку, если успели восстановиться). |
| `internal_error` | Сбой EasyPay. Безопасно повторить запрос с тем же `idempotency_key` — это либо вернёт уже созданный промокод, либо продолжит работу с той точки, где она прервалась. |

## Идемпотентность и повторы

Сервис рассчитан на безопасные повторы.

* **Тот же** `**idempotency_key**` **+ то же тело запроса.** Возвращается тот же промокод. В ответе `is_replay: true`. Никаких дублей в Stripe. Лимит на 24 часа — после этого та же комбинация ключ+тело может создать новый промокод.
* **Тот же** `**idempotency_key**` **+ другое тело.** Возвращается `idempotency_conflict`. Никаких изменений в Stripe. Сгенерируйте новый ключ для нового тела.
* **Сетевой сбой посреди обработки** (вы не получили ответ). Повторите с тем же ключом и телом — EasyPay подхватит работу с того места, где она прервалась.
* **Лимит ретраев.** На один `idempotency_key` допустимо 5 неудачных попыток. После шестой ключ блокируется как `failed_terminal` — следующие запросы вернут закэшированную ошибку. Чтобы повторить — сгенерируйте новый `idempotency_key`.

## Привязка к нескольким продуктам

Промокод применяется на чекауте только к тем продуктам, чьи `prod_*` перечислены в `stripe_product_ids`. Например, если у вас три продукта (`Basic`, `Pro`, `Enterprise`) и вы создали промокод с `stripe_product_ids: ["prod_Pro", "prod_Enterprise"]`, то клиент введёт код только при покупке Pro или Enterprise — на Basic он не сработает.

Это удобно для маркетинговых акций «скидка на премиум-планы» без необходимости создавать отдельный промокод под каждый продукт.

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

* Endpoint доступен только для **уже одобренных продуктов** партнёра. Для нового продукта сначала проведите его через мини-апп (модерация).
* 1..50 продуктов в одном промокоде.
* `code` должен быть уникальным в рамках вашего Stripe-аккаунта.
* `idempotency_key` 1..128 символов, не начинается с `ingest:`.
* Все привязываемые продукты — одного окружения (test ИЛИ live).
* Сохраняйте `code` и `stripe_promotion_code_id` на своей стороне сразу после создания промокода.
* Деактивация — через будущую ручку `deactivate_partner_stripe_promotion_code` (пока — через команду заботы EasyPay).
* MVP: только `percent_off`. Фиксированной суммы (`amount_off`), бесплатной доставки, currency restrictions — пока нет.

## Пример на Python

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

url = "https://n8n.thenextgen.store/webhook/promotion-code-create"
payload = {
    "stripe_product_ids": ["prod_XXXXXXXXXXXX", "prod_YYYYYYYYYYYY"],
    "code": "BLACKFRIDAY",
    "percent_off": 25,
    "duration": "once",
    "max_redemptions": 100,
    "expires_at": "2026-12-01T00:00:00Z",
    "idempotency_key": str(uuid.uuid4())
}

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

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

if result.get("success"):
    print(f"Promo code to share with customers: {result['code']}")
    print(f"Stripe promo id: {result['stripe_promotion_code_id']}")
    print(f"is_replay: {result.get('is_replay')}")
else:
    print(f"Error: {result.get('error_code')} — {result.get('error_message')}")
```

## Пример на cURL

```bash
curl -X POST https://n8n.thenextgen.store/webhook/promotion-code-create \
  -H "X-Partner-Api-Key: ваш-api-ключ" \
  -H "Content-Type: application/json" \
  -d '{
    "stripe_product_ids": ["prod_XXXXXXXXXXXX"],
    "code": "SUMMER25",
    "percent_off": 25,
    "duration": "once",
    "idempotency_key": "summer-2026-promo-001"
  }'
```

## Подписки: пример с длительной скидкой

Для продуктов-подписок промокод может действовать несколько биллинг-циклов:

```json
{
  "stripe_product_ids": ["prod_SUBSCRIPTION_PRODUCT_ID"],
  "code": "FIRST3MONTHS",
  "percent_off": 50,
  "duration": "repeating",
  "duration_in_months": 3,
  "max_redemptions": 200,
  "idempotency_key": "summer-2026-3mo-discount"
}
```

С таким промокодом первые 3 ежемесячных списания будут со скидкой 50%; дальше — полная цена.

## Через MCP-агента

Если у вас подключён EasyPay MCP-сервер (см. статью про MCP), можно создать промокод одной фразой в чате:

```
Создай промокод BLACKFRIDAY со скидкой 25% на продукты prod_XXX и prod_YYY,
лимит 100 использований, до 1 декабря.
```

Агент сам соберёт payload и вызовет `create_partner_stripe_promotion_code`. Ключ передаётся транспортно через MCP-config — модель его не видит.

## Если что-то пошло не так

* `**unauthorized**` — обновите ключ через мини-апп («Show again» на первом шаге онбординга или у вашего менеджера).
* `**product_not_found**` **для продукта, который вы видите в мини-апп** — проверьте, что копируете именно `stripe_product_id` (формат `prod_…`), а не внутренний номер из Internal Tariffs. И что окружение совпадает.
* `**mixed_environment**` — разбейте запрос на два: отдельный для test-продуктов, отдельный для live.
* `**code_already_exists**` — текст уже занят. Возьмите более уникальный (например, добавьте префикс с названием бренда: `MYBRAND-BLACKFRIDAY`).
* `**idempotency_conflict**` — сгенерируйте новый ключ. Не пытайтесь «починить» существующий: каждое тело запроса должно иметь свой ключ.
* `**invalid_input**` **с сообщением про reserved prefix** — `idempotency_key` не должен начинаться с `ingest:`. Сгенерируйте новый ключ (например, `uuid.uuid4()`).
* **Любая другая ситуация** — напишите в вашу EasyPay-группу с командой заботы или попросите MCP-агента передать запрос команде заботы.