
## Redeem a coupon

`POST /v1/cowork/coupon/redeem`

Redeems a Cowork/Code coupon and grants the user a complimentary tier for the term set by the coupon at the platform's expense, with no charge to the Bitrix24 account.

The user and the account come from the key binding; there are no path parameters. The operation is irreversible: a redemption cannot be undone.

## Request fields (body)

| Field | Type | Required | Description |
|------|-----|:-----:|----------|
| `code` | string | yes | Coupon in the form `PREFIX-BODY-CHECKSUM`, from 1 to 64 characters. Case, whitespace and dash variants are normalized to a canonical form on the server |
| `action` | string | no | Action selected by the user: `apply`, `extend`, `force` or `resume-and-apply`. Take the value from `decision` returned by the [coupon check](/docs/cowork/coupon-preview): for `kind` `apply` or `extend`, use that value; for `kind: choose`, use the `kind` of the selected item in `options`. If the field is omitted, it defaults to `apply`, preserving the behavior from before the choice was introduced |

## Examples

This endpoint supports only one authorization method: a Cowork/Code desktop key. Personal keys and application keys receive `403 INSUFFICIENT_SCOPE` or `403 COWORK_DESKTOP_KEY_REQUIRED`.

### curl — Cowork/Code desktop key

```bash
curl -X POST https://vibecode.bitrix24.com/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 desktop key

```javascript
const res = await fetch('https://vibecode.bitrix24.com/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') {
    // The only error code to retry automatically.
  }
  console.error(error.code)
} else {
  const { data } = await res.json()
  if (data.accessGranted) {
    const months = data.grantedTermMonths
    console.log(`${data.grantedTier} for ${months} ${months === 1 ? 'month' : 'months'}`)
  } else {
    // The tier is granted, but there is no access yet — an access request has been sent to the account administrator.
    console.log(`${data.grantedTier} activated, waiting for the administrator's decision`)
  }
}
```

## Response fields

| Field | Type | Description |
|------|-----|----------|
| `success` | boolean | `true` on a successful redemption |
| `data.grantedTier` | string | The granted tier: `PRO`, `MAX` or `ULTRA` |
| `data.grantedTermMonths` | number | Number of months for which the tier is granted |
| `data.accessGranted` | boolean | `false` means the tier is granted but the Bitrix24 account administrator has not yet granted access to Cowork/Code |

## Response example

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

The tier is granted, but Cowork/Code access has not yet been enabled for the Bitrix24 account:

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

## Error response example

409 — this tier is already assigned to the user's seat:

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

## Errors

Coupon-specific refusals return status `409`, with `error.code` identifying the reason. The `message` text is the same for all such refusals; do not parse it — branch on the code.

| HTTP | Code | Description |
|------|-----|----------|
| 400 | `INVALID_CODE` | The body has no string field `code`; the field is empty or longer than 64 characters; or the body contains a field the endpoint does not accept |
| 400 | `INVALID_ACTION` | The `action` field is present, but its value is not one of `apply`, `extend`, `force`, `resume-and-apply` |
| 400 | `INVALID_JSON_BODY` | A body was sent, but it does not parse as JSON |
| 401 | `MISSING_API_KEY` | The `X-Api-Key` header is missing |
| 401 | `INVALID_API_KEY` | The authorization key is invalid |
| 403 | `INSUFFICIENT_SCOPE` | The key lacks the `vibe:cowork` scope |
| 403 | `COWORK_DESKTOP_KEY_REQUIRED` | The key belongs to another class: personal key, application key, agent seat key or project deploy key |
| 403 | `COUPON_FEATURE_DISABLED` | Coupons are not enabled on the account |
| 403 | `KEY_NOT_BOUND_TO_USER` | The key is not bound to an account user |
| 403 | `WRITE_BLOCKED_READONLY_KEY` | The key was issued as read-only |
| 409 | `COUPON_INVALID` | The coupon does not exist, is revoked, already redeemed or expired, the campaign has ended, its overall redemption cap is exhausted, or the per-user or per-account limit is exhausted. Show a single message — "This code cannot be used" |
| 409 | `COUPON_TOO_MANY_ATTEMPTS` | Ten failures within an hour. Further attempts are blocked for thirty minutes |
| 409 | `COUPON_ALREADY_REDEEMED_BY_YOU` | This user has already activated this coupon |
| 409 | `COUPON_NOT_ASSIGNED_TO_YOU` | The coupon was issued to a different email address |
| 409 | `COUPON_PORTAL_ACCESS_GATED` | Cowork/Code access is disabled for the account |
| 409 | `COUPON_TARIFF_NOT_ELIGIBLE` | The campaign is limited to a list of Bitrix24 plans, and the account plan is not on it. The refusal is terminal: a repeated call returns the same result |
| 409 | `COUPON_TARIFF_UNKNOWN` | The campaign is limited to a list of Bitrix24 plans, and the platform has not read the account plan yet. The coupon is intact and the attempt is not counted against the limit — repeat the call later |
| 409 | `COUPON_SEAT_PAUSED` | The seat is paused because of insufficient funds. The gift does not resolve the cause of the pause — top up the balance first |
| 409 | `COUPON_SEAT_CANCELLATION_SCHEDULED` | The seat is scheduled for cancellation, and the redemption request omitted `action: resume-and-apply`. When the gifted tier is not below the current tier, the check includes this action in `options` — send it to resume the seat and apply the gift. When the gifted tier is lower, the action is unavailable and the refusal is terminal |
| 409 | `COUPON_TIER_DOWNGRADE_BLOCKED` | The coupon grants a tier below the current tier, and the user did not select `force` |
| 409 | `COUPON_SEAT_ALREADY_ON_TIER` | This tier is already assigned to the seat, and the redemption request omitted `action: extend`. If the campaign does not allow extending the term or the purchased term is longer than one month, the check returns the decision `refuse` rather than `extend` |
| 409 | `COUPON_SEAT_PAID_TERM_ACTIVE` | The seat has an active paid term with no recurring charge. The gift would overwrite that term, so the refusal is terminal — direct the user to support |
| 409 | `COUPON_SEAT_ALREADY_PAID_LONGER` | The seat's paid term extends beyond the term offered by the coupon, and the user did not select `force` |
| 409 | `COUPON_SELF_PORTAL` | The portal is backed by a platform employee holding the coupon-issuing right — redemption on such a portal is closed |
| 409 | `COUPON_SEAT_IS_PAID` | The seat has an active recurring charge, and the user did not select `force` |
| 409 | `COUPON_ACTION_NOT_AVAILABLE` | The submitted action is stale: the seat state changed between the check and redemption. The coupon remains valid and unspent |
| 409 | `CONCURRENT_REDEMPTION` | Another operation is competing for the same seat |
| 429 | `RATE_LIMITED` | The platform-wide limit is 10 requests per minute. The effective value for your key is returned in the `x-ratelimit-limit` header. It is lower than the platform-wide limit because that limit is divided across replicas |

See [Errors](/docs/errors) for the full list of common API errors.

## Known specifics

**Using a generic success screen for `accessGranted: false` creates the costliest category of support requests.** The operation looks successful to the user, yet they cannot launch Cowork/Code, which looks like a malfunction. This state needs a dedicated screen — "The tier is activated, and an access request has been sent to the administrator" — rather than a generic "Done".

**The submitted action expresses intent, not permission.** The seat may have changed between the check and redemption: the user may have paid for a tier in another tab, the administrator may have downgraded the seat, or a scheduled cancellation may have taken effect. The decision is recomputed from the current state. If the selected action is no longer available, the endpoint returns `COUPON_ACTION_NOT_AVAILABLE` — repeat the check and show the actions it currently returns.

**Retry `CONCURRENT_REDEMPTION`; never retry any other coupon refusal.** The coupon remains valid, the rules were satisfied, and the gift was not consumed: by the time the response arrives, the coupon is available again. Retry up to three times with a 300–800 ms delay and random jitter — without jitter, dozens of clients retry in lockstep and reproduce the same contention. Show the user a waiting state during that time, not an error. Automatically retrying any other coupon refusal exhausts the failure counter in seconds. `429 RATE_LIMITED` is separate: it is not a coupon refusal, does not affect the failure counter, and may be retried after a delay.

**The `force` action forfeits the remainder of the paid term with no refund.** The [coupon check](/docs/cowork/coupon-preview) returns the exact number of forfeited days in `losesDays`; do not recompute it on the client. The button label must include that number before the user clicks it — after redemption, the term cannot be restored.

**A coupon is single-use per user, but not necessarily overall.** Some coupons allow several activations by different people. The number of users who share a coupon is not disclosed and must not be mentioned in user-facing text.

**The failed-attempt counter is shared with the coupon check.** The [coupon check](/docs/cowork/coupon-preview) describes the pair used as the counter key and its reset condition.

**The platform-wide limit is 10 requests per minute for redemption and 20 requests per minute for the check.** The effective values for your key are returned in the `x-ratelimit-limit` response header. They are lower than the platform-wide limits because those limits are divided across replicas.

**Coupon issuance is intentionally unavailable to this key.** The desktop key is stored on the user's machine, so granting it issuance rights would make the campaign cap ineffective. Programmatic issuance requires a different key class and is performed by the partner's server, not the application.

## See also

- [Check a coupon](/docs/cowork/coupon-preview)
- [Cowork/Code subscription state](/docs/cowork/state)
- [Cowork/Code subscription summary](/docs/cowork/me)
- [Tier change preview](/docs/cowork/subscription-preview)
- [Cowork/Code](/docs/cowork)
- [Errors](/docs/errors)
