
## Проверить купон

`POST /v1/cowork/coupon/preview`

Проверяет купон Cowork/Code и показывает, какой тариф и на какой срок он даёт. Место пользователя только читается, купон остаётся непогашенным.

Пользователь и аккаунт Битрикс24 берутся из привязки ключа, параметров пути нет.

## Поля запроса (body)

| Поле | Тип | Обяз. | Описание |
|------|-----|:-----:|----------|
| `code` | string | да | Купон в виде `ПРЕФИКС-ТЕЛО-СУММА`, от 1 до 64 символов. Регистр и пробелы приводятся к канонической форме на нашей стороне, дефисы разных начертаний тоже. Других полей тело не принимает: любое лишнее поле, в том числе `action`, даёт `400 INVALID_CODE` |

## Примеры

Ось авторизации одна: эндпоинт принимает только ключ десктопа Cowork/Code. Личный ключ и ключ приложения получают `403 INSUFFICIENT_SCOPE` либо `403 COWORK_DESKTOP_KEY_REQUIRED`.

### curl — ключ десктопа Cowork/Code

```bash
curl -X POST https://vibecode.bitrix24.tech/v1/cowork/coupon/preview \
  -H "X-Api-Key: YOUR_COWORK_KEY" \
  -H "Content-Type: application/json" \
  -d '{"code": "PRAKTIKUM-7HKM4TQZ2RVW-45C80C"}'
```

### JavaScript — ключ десктопа Cowork/Code

```javascript
const res = await fetch('https://vibecode.bitrix24.tech/v1/cowork/coupon/preview', {
  method: 'POST',
  headers: {
    'X-Api-Key': 'YOUR_COWORK_KEY',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ code: 'PRAKTIKUM-7HKM4TQZ2RVW-45C80C' }),
})

if (!res.ok) {
  const { error } = await res.json()
  console.error(error.code)
} else {
  const { data } = await res.json()

  if (!data.valid) {
    // Текст для пользователя выбирают по data.reason — таблица «Причины отказа».
    console.log(data.reason)
  } else {
    console.log(`${data.grant.tier} на ${data.grant.termMonths} мес.`)
    console.log('Что можно сделать:', data.decision.kind)
  }
}
```

## Поля ответа

| Поле | Тип | Описание |
|------|-----|----------|
| `success` | boolean | `true` при успешном ответе. Отказ по самому купону тоже приходит с `true` — он живёт в `data.valid` |
| `data.valid` | boolean | Купон годен: код живой, кампания открыта |
| `data.reason` | string или null | Причина отказа при `valid: false` — одно из значений, перечисленных в разделе «Причины отказа». При `valid: true` приходит `null` |
| `data.grant` | object или null | Что даёт купон. При `valid: false` приходит `null` |
| `data.grant.tier` | string | Тариф подарка: `PRO`, `MAX` или `ULTRA` |
| `data.grant.termMonths` | number | Срок подарка в месяцах |
| `data.grant.campaignName` | string | Название кампании — его показывают пользователю в окне подтверждения |
| `data.decision` | object | Что произойдёт с местом пользователя и что ему можно предложить. Приходит только при `valid: true`, и там приходит всегда. При `valid: false` поля в ответе нет вовсе — проверяйте его через `data.decision?.kind`, а не сравнением с `null` |
| `data.decision.kind` | string | Форма решения: `apply`, `extend`, `choose` или `refuse`. У `apply` и `extend` это же значение отправляют в поле `action` при погашении, у `choose` действие берут из `options`. Значения `choose` и `refuse` в `action` не отправляют |
| `data.decision.addsMonths` | number | Только у `extend`: на сколько месяцев продлится действующий тариф |
| `data.decision.reason` | string | Только у `refuse`: код отказа, по которому берётся текст для пользователя |
| `data.decision.facts` | object | Только у `choose`: факты о месте, которые окно обязано показать до выбора |
| `data.decision.options` | array | Только у `choose`: доступные действия. Каждый элемент несёт `kind` — его и отправляют в поле `action` при погашении |

### Формы решения

Поле `data.decision` описывает, что случится с местом пользователя, если купон погасить сейчас.

| `kind` | Когда приходит | Что показать |
|--------|----------------|--------------|
| `apply` | Место свободное, бесплатное, закрытое либо его ещё нет | Подтверждение подарка и кнопку «Применить» |
| `extend` | Тариф подарка совпадает с действующим, место куплено не больше чем на месяц, и кампания разрешает продление срока | «Тариф продлится ещё на `addsMonths` мес.» Тариф не меняется, двигается только срок |
| `choose` | Место занято, но выбор у пользователя есть | Сначала факты из `facts`, ниже кнопки по `options` |
| `refuse` | Предложить нечего | Отказ. Текст берётся по коду из `reason` — это коды `COUPON_SEAT_*` и `COUPON_TIER_DOWNGRADE_BLOCKED`, их описания в таблице [Погасить купон](/docs/cowork/coupon-redeem) |

Состав `facts` у решения `choose`:

| Поле | Тип | Описание |
|------|-----|----------|
| `facts.currentTier` | string | Действующий тариф места |
| `facts.paidThroughAt` | string | До какой даты оплачено, ISO 8601 |
| `facts.isPaid` | boolean | У места есть живое списание |
| `facts.cancellationScheduled` | boolean | На месте назначена отмена |
| `facts.grantTier` | string | Тариф подарка |
| `facts.grantTermMonths` | number | Срок подарка в месяцах |

Состав элемента `options`:

| Поле | Тип | Описание |
|------|-----|----------|
| `options[].kind` | string | Действие: `force` — применить подарок сейчас вместо действующего тарифа, `resume-and-apply` — снять назначенную отмену и применить |
| `options[].losesDays` | number | Только у `force`: сколько целых суток оплаченного срока сгорит. Число приходит отсюда и на клиенте не пересчитывается |
| `options[].losesTier` | string | Только у `force`: тариф, оплаченный срок которого сгорит |

### Причины отказа

Значения поля `data.reason` при `valid: false`.

| Причина | Что показать пользователю |
|---------|---------------------------|
| `COUPON_INVALID` | «Код не подходит». Под этим значением собраны «кода не существует», «код отозван», «код уже погашен», «срок вышел», «кампания закончилась», «личный лимит выбран» и «лимит аккаунта выбран» |
| `COUPON_TOO_MANY_ATTEMPTS` | «Слишком много попыток. Попробуйте через полчаса» |
| `COUPON_ALREADY_REDEEMED_BY_YOU` | «Вы уже активировали этот купон». Не показывайте здесь «код не подходит» — подарок пользователю уже выдан |
| `COUPON_NOT_ASSIGNED_TO_YOU` | «Купон выписан на другой адрес электронной почты. Войдите под ним и повторите». Купон живой, исправить это пользователь может сам |
| `COUPON_PORTAL_ACCESS_GATED` | «Доступ к Cowork/Code на аккаунте закрыт — обратитесь к администратору» |
| `COUPON_TARIFF_NOT_ELIGIBLE` | «Купон действует не на этом тарифе Битрикс24». Кампания ограничена списком тарифов, и тариф аккаунта в него не входит. Отказ терминальный: на этом аккаунте купон не начнёт работать сам, повторять вызов незачем |
| `COUPON_TARIFF_UNKNOWN` | «Тариф аккаунта пока не определён. Попробуйте позже». Кампания ограничена списком тарифов, а тариф аккаунта платформа ещё не прочитала — так бывает на только что подключённом аккаунте. Купон цел, и попытка не засчитывается в лимит. Не показывайте здесь «код не подходит»: верное действие — повторить позже |

## Пример ответа

Купон годен, место пользователя свободно:

```json
{
  "success": true,
  "data": {
    "valid": true,
    "reason": null,
    "grant": {
      "tier": "PRO",
      "termMonths": 3,
      "campaignName": "Практикум партнёров, август"
    },
    "decision": { "kind": "apply" }
  }
}
```

Купон годен, но место занято старшим оплаченным тарифом — пользователь выбирает:

```json
{
  "success": true,
  "data": {
    "valid": true,
    "reason": null,
    "grant": {
      "tier": "PRO",
      "termMonths": 3,
      "campaignName": "Практикум партнёров, август"
    },
    "decision": {
      "kind": "choose",
      "facts": {
        "currentTier": "MAX",
        "paidThroughAt": "2026-09-14T10:00:00.000Z",
        "isPaid": true,
        "cancellationScheduled": false,
        "grantTier": "PRO",
        "grantTermMonths": 3
      },
      "options": [
        { "kind": "force", "losesDays": 24, "losesTier": "MAX" }
      ]
    }
  }
}
```

Купон не годен — статус ответа тот же `200`, поля `decision` в теле нет:

```json
{
  "success": true,
  "data": {
    "valid": false,
    "reason": "COUPON_INVALID",
    "grant": null
  }
}
```

## Пример ответа при ошибке

400 — тело без поля `code`:

```json
{
  "success": false,
  "error": {
    "code": "INVALID_CODE",
    "message": "Field `code` is required"
  }
}
```

## Ошибки

| HTTP | Код | Описание |
|------|-----|----------|
| 400 | `INVALID_CODE` | В теле нет строкового поля `code`, оно пустое, длиннее 64 символов либо рядом лежит лишнее поле. Поле `action` принимает только погашение, здесь оно даёт этот же код ошибки |
| 400 | `INVALID_JSON_BODY` | Тело передано, но не читается как JSON |
| 401 | `MISSING_API_KEY` | Не передан заголовок `X-Api-Key` |
| 401 | `INVALID_API_KEY` | Ключ не опознан: такой строки на платформе нет |
| 403 | `INSUFFICIENT_SCOPE` | У ключа нет скоупа `vibe:cowork` |
| 403 | `COWORK_DESKTOP_KEY_REQUIRED` | Ключ другого класса: личный ключ, ключ приложения, ключ места агента или проектный ключ деплоя |
| 403 | `COUPON_FEATURE_DISABLED` | Купоны на аккаунте не включены. Поле ввода стоит скрыть целиком |
| 403 | `KEY_NOT_BOUND_TO_USER` | Ключ не привязан к пользователю аккаунта |
| 403 | `WRITE_BLOCKED_READONLY_KEY` | Ключ выпущен только для чтения |
| 429 | `RATE_LIMITED` | Суммарный лимит платформы — 20 запросов в минуту. Действующее для вашего ключа значение приходит в заголовке `x-ratelimit-limit` — оно ниже суммарного, поскольку лимит делится между репликами |

Полный список общих ошибок API — [Ошибки](/docs/errors).

Отказ по самому купону ошибкой не считается и приходит статусом `200` с `data.valid: false`.

## Известные особенности

**Проверка не отвечает на вопрос «погашение пройдёт».** Поле `data.valid` смотрит только на купон и кампанию: код живой, срок не вышел, кампания открыта, личный лимит и лимит аккаунта не выбраны. Условия самого места отвечают отдельным полем `data.decision`, но и оно не гарантия: штучный потолок кампании проверяется только при погашении, а место могло измениться между двумя вызовами. Готовьте экран отказа на шаге погашения даже после зелёной проверки.

**Место читается, но не создаётся.** Погашение перед проверкой правил заводит пользователю бесплатное место, проверка — нет: иначе просмотр купона менял бы состояние. Поэтому у пользователя без места решение вырождается в `apply`, а не в пустое значение.

**Неудачные попытки жгут тот же счётчик, что и погашение.** Счёт ведётся по паре «аккаунт плюс пользователь», коллеги по аккаунту чужими попытками не страдают. Десять неудач за час закрывают попытки на тридцать минут. Успешное погашение счётчик обнуляет.

**Опечатка счётчик не тратит.** Хвост купона — контрольная сумма над префиксом и телом. Ввод, который её не прошёл, до базы не доходит, поэтому пользователь, переписывающий код с раздатки или диктующий его по телефону, ничего не теряет.

**Не вызывайте проверку на каждое нажатие клавиши.** Она дешевле погашения и повторяема, поэтому для подбора опаснее него. Вешайте вызов на кнопку либо на задержку от 500 мс после последнего нажатия.

**Семь разных отказов схлопнуты в `COUPON_INVALID` намеренно, и различать их не понадобится.** Отдельная причина на каждый случай подсказывала бы подбирающему, какие префиксы живые: чтобы увидеть ответ «лимит выбран», надо предъявить настоящий код этой кампании. Ветки под «код отозван» и «лимит выбран» заводить не нужно — таких значений в ответе не появится.

**Отказ занимает не меньше четырёхсот миллисекунд.** Время ответа выровнено по нижней границе, чтобы секундомер не отличал «такого кода нет» от «код есть, но отозван». Ответ, который сам по себе занял дольше, не укорачивается.

**Отложенного применения не существует.** Действия «применить подарок в конце оплаченного срока» в `options` не бывает. Появление такого варианта означает ошибку на нашей стороне, а не сигнал рисовать под него кнопку.

## Смотрите также

- [Погасить купон](/docs/cowork/coupon-redeem)
- [Состояние подписки Cowork/Code](/docs/cowork/state)
- [Сводка по подписке Cowork/Code](/docs/cowork/me)
- [Предрасчёт смены тарифа](/docs/cowork/subscription-preview)
- [Cowork/Code](/docs/cowork)
- [Ошибки](/docs/errors)
