# Endpoint (API v2): Payment Link для корзины — несколько продуктов

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

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

Обычная ручка создаёт платёжную ссылку на **один** продукт. Эта — собирает **корзину**: одну Stripe Payment Link сразу из **нескольких** уже одобренных продуктов партнёра, с указанием **количества** каждого. Клиент оплачивает весь набор за один чекаут.

Типичные сценарии: бандл «3 товара по акции», набор «пакет + дополнения», заказ из нескольких позиций с разными количествами, **тариф подписки, собранный из частей** — «базовый план + места + дополнительные аккаунты».

Повторно проходить модерацию не нужно — ручка переиспользует уже одобренные продукты и создаёт только новые Stripe Price + одну Payment Link с несколькими позициями.

**Разовые и подписочные корзины (с 16.09.2026).** Тип корзины определяется продуктами, передавать его не нужно:

| Продукты в корзине | Что получит клиент |
|--------------------|--------------------|
| все **разовые**    | одну оплату всей корзины |
| все **подписочные** с одинаковым периодом (например, все помесячные) | **одну подписку** из нескольких позиций: каждый период списывается сумма всех позиций × количества |

Разовые и подписочные продукты в одной корзине смешивать нельзя, как и подписки с разным периодом (месяц + год) — вернётся `INVALID_INPUT`. Пробного периода у корзины нет: если нужен trial, выпускайте ссылку на один продукт.

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


1. Запросите API-ключ у вашего менеджера EasyPay (или скопируйте в мини-аппе, «Show key» на первом шаге онбординга).
2. Менеджер активирует разрешение `product_create` для вашего аккаунта (то же, что и для создания продукта — отдельного разрешения не нужно).
3. Узнайте `stripe_product_id` каждого нужного **одобренного** продукта (есть в мини-аппе в карточке продукта).

## Endpoint

```
POST https://api.appsign.me/create-partner-stripe-cart-payment-link
Content-Type: application/json
X-Partner-Api-Key: ваш-api-ключ
```

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

```json
{
  "items": [
    { "stripe_product_id": "prod_AAA", "unit_amount": 1875, "quantity": 2 },
    { "stripe_product_id": "prod_BBB", "unit_amount": 3750, "quantity": 1 }
  ],
  "currency": "USD",
  "payment_method_types": ["card", "link"],
  "allow_promotion_codes": false,
  "success_url": "https://example.com/thanks",
  "client_token": "order-42"
}
```

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

| Параметр | Обязательный | Описание |
|----------|--------------|----------|
| `items`  | Да           | Массив позиций корзины, от 1 до **20** (лимит Stripe на line items). |
| `items[].stripe_product_id` | Да           | ID одобренного продукта партнёра (`prod_…`). Все продукты должны принадлежать вам и быть одобрены. Дубликаты одного `stripe_product_id` в корзине запрещены — объедините в одну позицию через `quantity`. |
| `items[].unit_amount` | Да           | Цена за единицу в наименьшей единице валюты (центы): `1875` = $18.75. Для подписки — цена единицы за период. Целое от `50` до `99999999`. |
| `items[].quantity` | Да           | Количество единиц этой позиции. Целое от `1` до `999`. |
| `currency` | Нет          | `"USD"`, `"EUR"` или `"GBP"`. Если указана — должна совпадать с валютой **всех** продуктов (Stripe Payment Link использует одну валюту). Если не указана — берётся из продуктов (она у всех должна быть одинаковой). |
| `payment_method_types` | Нет          | Массив способов оплаты для ссылки. Если не указан — Stripe подберёт по умолчанию. Значения проверяются: неподключённый или несовместимый с валютой способ вернёт `invalid_input`. См. раздел «Способы оплаты» ниже. |
| `allow_promotion_codes` | Нет          | Разрешить ввод промокода на чекауте. По умолчанию `false`. |
| `success_url` | Нет          | HTTPS-URL для редиректа после оплаты. Максимум 2048 символов. |
| `client_token` | Нет          | Ваш идемпотентный токен (см. ниже). |

**Способы оплаты.** Проверенные и работающие: `card`, `link` (под USD, EUR, GBP, BRL), `sepa_debit` (только EUR), `pix` (только BRL), `us_bank_account` (только USD).

Два способа **не подключены** в Stripe-аккаунте EasyPay и отклоняются с ошибкой `invalid_input`: `zip` и `bacs_debit`. Нужны — напишите команде заботы EasyPay.

Остальные способы Stripe передавайте, если нужны: мы их не блокируем, но и не обещаем — совместимость определяет Stripe. Способ, несовместимый с валютой корзины, отклоняется до обращения в Stripe, с текстом, называющим и способ, и валюту.

**Что наследуется от продуктов** (нельзя переопределить): тип корзины (разовая или подписка) и период подписки, тестовый/боевой режим, базовая валюта. Все продукты корзины должны быть в **одной** валюте и **одном** режиме (все live или все test).

## Идемпотентность — `client_token`

Вам **не нужно** конструировать сложный ключ идемпотентности — сервер собирает его сам. Достаточно опционального `client_token`:

* **Передали** `**client_token**` → повтор того же запроса с тем же токеном вернёт **ту же** ссылку (поле `duplicate: true`), без создания дублей. Используйте, если хотите безопасный retry — например, привяжите токен к ID заказа.
* **Не передали** `**client_token**` → одинаковые по составу корзины в пределах окна дедупятся автоматически (по содержимому).

Сменили состав корзины — используйте новый `client_token` (тот же токен с другим составом вернёт `IDEMPOTENCY_KEY_REUSED_WITH_DIFFERENT_PAYLOAD`).

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

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

```json
{
  "success": true,
  "payment_link_id": "plink_XXXXXXXXXXXX",
  "short_url": "https://short.appsign.me/abc",
  "short_url_id": "lnk_5hfk_...",
  "currency": "USD",
  "mode": "one_time",
  "interval": null,
  "interval_count": null,
  "line_item_count": 2,
  "items": [
    { "stripe_product_id": "prod_AAA", "stripe_price_id": "price_YYY", "quantity": 2 },
    { "stripe_product_id": "prod_BBB", "stripe_price_id": "price_ZZZ", "quantity": 1 }
  ],
  "duplicate": false
}
```

| Поле | Описание |
|------|----------|
| `short_url` | Короткая ссылка (`short.appsign.me`) — **единственная партнёр-смотрящая ссылка для чекаута**. Отправляйте её клиенту. Сохраните на своей стороне — отдельного метода для повторного чтения нет. |
| `short_url_id` | ID короткой ссылки (на случай будущего удаления/обновления). |
| `payment_link_id` | ID Stripe Payment Link (`plink_…`) — для сверки в Stripe Dashboard. Полный `buy.stripe.com` URL партнёру не возвращается. |
| `items[].stripe_price_id` | Созданный Stripe Price для каждой позиции. |
| `mode` | `one_time` — разовая оплата, `subscription` — одна подписка из всех позиций. |
| `interval` / `interval_count` | Период подписки (например, `month` и `1`). У разовой корзины — `null`. |
| `line_item_count` | Число позиций в корзине. |
| `duplicate` | `true`, если запрос с этим `client_token` уже обрабатывался и вернулся закэшированный результат. |

### Ошибки

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

| `error_code` | Когда срабатывает |
|------------|-------------------|
| `INVALID_INPUT` | Невалидное тело: пустой/слишком большой `items` (>20), дубликат продукта, `unit_amount`/`quantity` вне диапазона, несовпадение валюты, разовые и подписочные продукты вместе, подписки с разным периодом, смешанные валюты или режимы (live/test) в корзине. |
| `AUTH_REQUIRED` / `INVALID_API_KEY` | Не передан или невалиден API-ключ. |
| `PRODUCT_NOT_FOUND` | Один из `stripe_product_id` не принадлежит партнёру или не существует. |
| `PRODUCT_NOT_APPROVED` | Один из продуктов ещё не прошёл модерацию. |
| `CROSS_TENANT_ATTEMPT` | Один из `stripe_product_id` принадлежит другому партнёру. |
| `IDEMPOTENCY_KEY_REUSED_WITH_DIFFERENT_PAYLOAD` | Тот же `client_token` использован с другим составом корзины. Сгенерируйте новый токен. |
| `PREVIOUS_ATTEMPT_FAILED` | Прошлая попытка с этим токеном завершилась ошибкой. Используйте новый `client_token`. |
| `OPERATION_IN_PROGRESS` | Другой запрос с тем же токеном ещё обрабатывается. Повторите чуть позже. |
| `SHORT_URL_UNAVAILABLE` | Payment Link создан, но короткая ссылка временно недоступна. Напишите в команду заботы с вашим `client_token`. |
| `INTEGRATION_TIMEOUT` / `INTEGRATION_UPSTREAM_ERROR` | Временный сбой Stripe. Повторите с новым `client_token`. |
| `INTERNAL_ERROR` | Внутренний сбой EasyPay. Команда заботы уведомлена. |

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

* От 1 до **20** позиций в корзине.
* Только **одобренные** продукты (первая ссылка на новый продукт — через мини-апп, чтобы пройти модерацию).
* Либо все продукты разовые, либо все подписочные с одним периодом. Пробного периода у корзины нет.
* Состав выданной ссылки не редактируется — изменилось количество, создайте новую корзину.
* Все продукты — в **одной** валюте (`USD`/`EUR`/`GBP`) и **одном** режиме (все live или все test).
* `unit_amount` за единицу: от `50` до `99999999` (центы). `quantity`: от `1` до `999`.
* Сохраните `short_url` сразу после получения ответа.

## Пример на Python

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

url = "https://api.appsign.me/create-partner-stripe-cart-payment-link"
payload = {
    "items": [
        {"stripe_product_id": "prod_AAA", "unit_amount": 1875, "quantity": 2},
        {"stripe_product_id": "prod_BBB", "unit_amount": 3750, "quantity": 1},
    ],
    "client_token": f"order-{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"):
    print("Ссылка для клиента:", result["short_url"])
    print("Позиций:", result["line_item_count"])
else:
    print("Ошибка:", result.get("error_code"), "—", result.get("error_message"))
```

## Пример на cURL

```bash
curl -X POST "https://api.appsign.me/create-partner-stripe-cart-payment-link" \
  -H "Content-Type: application/json" \
  -H "X-Partner-Api-Key: ваш-api-ключ" \
  -d '{
    "items": [
      { "stripe_product_id": "prod_AAA", "unit_amount": 1875, "quantity": 2 },
      { "stripe_product_id": "prod_BBB", "unit_amount": 3750, "quantity": 1 }
    ],
    "client_token": "order-42"
  }'
```