
## Погасить купон

`POST /v1/cowork/coupon/redeem`

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

Пользователь и аккаунт берутся из привязки ключа, параметров пути нет. Операция необратима: погашенный купон обратно не возвращается.

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

| Поле | Тип | Обяз. | Описание |
|------|-----|:-----:|----------|
| `code` | string | да | Купон в виде `ПРЕФИКС-ТЕЛО-СУММА`, от 1 до 64 символов. Регистр, пробелы и начертание дефисов приводятся к канонической форме на нашей стороне |
| `action` | string | нет | Что выбрал пользователь в развилке: `apply`, `extend`, `force` или `resume-and-apply`. Значение берётся из `decision` [проверки купона](/docs/cowork/coupon-preview): при `kind` `apply` или `extend` — само это значение, при `kind: choose` — `kind` выбранного элемента `options`. Поле опущено — считается `apply`, то есть поведение до появления развилки |

## Примеры

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

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

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

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

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

if (!res.ok) {
  const { error } = await res.json()
  if (error.code === 'CONCURRENT_REDEMPTION') {
    // Единственный код, который повторяют автоматически.
  }
  console.error(error.code)
} else {
  const { data } = await res.json()
  if (data.accessGranted) {
    console.log(`${data.grantedTier} на ${data.grantedTermMonths} мес.`)
  } else {
    // Тариф выдан, доступа ещё нет — заявка ушла администратору аккаунта.
    console.log(`${data.grantedTier} активирован, ждём решения администратора`)
  }
}
```

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

| Поле | Тип | Описание |
|------|-----|----------|
| `success` | boolean | `true` при успешном погашении |
| `data.grantedTier` | string | Выданный тариф: `PRO`, `MAX` или `ULTRA` |
| `data.grantedTermMonths` | number | Срок, на который выдан тариф, в месяцах |
| `data.accessGranted` | boolean | `false` означает, что тариф выдан, а доступ к Cowork/Code администратор аккаунта ещё не открыл |

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

```json
{
  "success": true,
  "data": {
    "grantedTier": "PRO",
    "grantedTermMonths": 3,
    "accessGranted": true
  }
}
```

Тариф выдан, но доступ к Cowork/Code на аккаунте ещё не открыт:

```json
{
  "success": true,
  "data": {
    "grantedTier": "PRO",
    "grantedTermMonths": 3,
    "accessGranted": false
  }
}
```

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

409 — на месте пользователя уже стоит этот тариф:

```json
{
  "success": false,
  "error": {
    "code": "COUPON_SEAT_ALREADY_ON_TIER",
    "message": "The coupon cannot be redeemed"
  }
}
```

## Ошибки

Отказ по самому купону приходит статусом `409`, а `error.code` называет причину. Текст в `message` у всех таких отказов один, разбирать его не нужно — решение принимается по коду.

| HTTP | Код | Описание |
|------|-----|----------|
| 400 | `INVALID_CODE` | В теле нет строкового поля `code`, оно пустое, длиннее 64 символов либо рядом лежит поле, которого тело не принимает |
| 400 | `INVALID_ACTION` | Поле `action` есть, но его значение не входит в набор `apply`, `extend`, `force`, `resume-and-apply` |
| 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` | Ключ выпущен только для чтения |
| 409 | `COUPON_INVALID` | Купона не существует, он отозван, уже погашен, истёк, кампания закончилась, выбран её штучный потолок либо выбран лимит на пользователя или на аккаунт. Показывать одну формулировку — «Код не подходит» |
| 409 | `COUPON_TOO_MANY_ATTEMPTS` | Десять неудач за час. Попытки закрыты на тридцать минут |
| 409 | `COUPON_ALREADY_REDEEMED_BY_YOU` | Этот пользователь уже активировал этот купон |
| 409 | `COUPON_NOT_ASSIGNED_TO_YOU` | Купон выписан на другой адрес электронной почты |
| 409 | `COUPON_PORTAL_ACCESS_GATED` | Доступ к Cowork/Code на аккаунте закрыт |
| 409 | `COUPON_TARIFF_NOT_ELIGIBLE` | Кампания ограничена списком тарифов Битрикс24, и тариф аккаунта в него не входит. Отказ терминальный: повторный вызов вернёт то же самое |
| 409 | `COUPON_TARIFF_UNKNOWN` | Кампания ограничена списком тарифов Битрикс24, а тариф аккаунта платформа ещё не прочитала. Купон цел, попытка не засчитывается в лимит — повторите вызов позже |
| 409 | `COUPON_SEAT_PAUSED` | Место приостановлено из-за нехватки средств. Подарок причину паузы не снимает — сначала пополнить баланс |
| 409 | `COUPON_SEAT_CANCELLATION_SCHEDULED` | На месте назначена отмена, а погашение пришло без `action: resume-and-apply`. Когда тариф подарка не ниже действующего, это действие приходит в `options` проверки — отправьте его, чтобы снять отмену и применить подарок. Тариф подарка ниже действующего — действия не будет вовсе, отказ терминальный |
| 409 | `COUPON_TIER_DOWNGRADE_BLOCKED` | Купон даёт тариф ниже действующего, и пользователь не выбрал `force` |
| 409 | `COUPON_SEAT_ALREADY_ON_TIER` | Этот тариф на месте уже стоит, а погашение пришло без `action: extend`. Продление кампанией не разрешено либо срок покупки больше месяца — тогда проверка возвращает решение `refuse`, а не `extend` |
| 409 | `COUPON_SEAT_PAID_TERM_ACTIVE` | У места идёт оплаченный отрезок без автосписания. Подарок перезаписал бы его, поэтому отказ терминальный — направляйте пользователя в поддержку |
| 409 | `COUPON_SEAT_ALREADY_PAID_LONGER` | Место оплачено дальше, чем даёт купон, и пользователь не выбрал `force` |
| 409 | `COUPON_SELF_PORTAL` | За порталом стоит сотрудник платформы с правом выдавать купоны — погашение на таком портале закрыто |
| 409 | `COUPON_SEAT_IS_PAID` | На месте живое списание, и пользователь не выбрал `force` |
| 409 | `COUPON_ACTION_NOT_AVAILABLE` | Присланное действие устарело: состояние места изменилось между проверкой и погашением. Купон цел и не потрачен |
| 409 | `CONCURRENT_REDEMPTION` | За то же место конкурирует другая операция |
| 429 | `RATE_LIMITED` | Суммарный лимит платформы — 10 запросов в минуту. Действующее для вашего ключа значение приходит в заголовке `x-ratelimit-limit` — оно ниже суммарного, поскольку лимит делится между репликами |

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

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

**Общий экран успеха на `accessGranted: false` — самый дорогой класс обращений в поддержку.** Операция для пользователя выглядит удачной, а запустить Cowork/Code он не может, и это читается как поломка. Нужен отдельный экран — «Тариф активирован, заявка на доступ отправлена администратору», — а не общий «Готово».

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

**`CONCURRENT_REDEMPTION` повторяют, остальные отказы купона — никогда.** Купон исправен, правила пройдены, подарок не израсходован: к моменту ответа купон уже вернулся в оборот. Делайте до трёх повторов с задержкой 300–800 мс и случайным разбросом — без разброса десятки клиентов повторят синхронно и воспроизведут ту же конкуренцию. Пользователю на это время показывают ожидание, а не ошибку. Автоматический повтор любого другого отказа купона выжигает счётчик неудач за секунды. Отдельно от них стоит `429 RATE_LIMITED` — это не отказ купона, счётчик неудач он не трогает, и повтор после паузы уместен.

**Действие `force` сжигает остаток оплаченного срока без возврата.** Сколько именно суток сгорит, приходит в `losesDays` [проверки купона](/docs/cowork/coupon-preview) и на клиенте не пересчитывается. Подпись кнопки обязана называть это число до нажатия — после погашения вернуть срок нечем.

**Купон одноразов на пользователя, но не обязательно на всех.** Часть купонов рассчитана на несколько активаций разными людьми. Сколько людей держит один и тот же купон, наружу не сообщается, и в тексте для пользователя это не упоминают.

**Счётчик неудачных попыток общий с проверкой купона** — пара, на которую он заведён, и условие обнуления описаны в [проверке купона](/docs/cowork/coupon-preview).

**Суммарный лимит платформы у погашения — 10 запросов в минуту, у проверки — 20 запросов в минуту.** Действующие для вашего ключа значения приходят в заголовке `x-ratelimit-limit`. Они ниже суммарных, поскольку лимиты делятся между репликами.

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

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

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