
## Cowork subscription state

`GET /v1/cowork/state`

Returns a full snapshot of the Cowork subscription state: the tier, the subscription state, quota usage across three windows in percent, a recommendation to wait or move to a higher tier, and the tier catalog for a comparison table.

For a desktop key, the response also contains `modelAnnouncement`: an active [new model announcement](./model-announcement.md) or `null`.

## Examples

### curl — personal key

```bash
curl https://vibecode.bitrix24.com/v1/cowork/state \
  -H "X-Api-Key: YOUR_API_KEY"
```

### curl — OAuth application

```bash
curl https://vibecode.bitrix24.com/v1/cowork/state \
  -H "X-Api-Key: YOUR_APP_KEY" \
  -H "Authorization: Bearer USER_SESSION_TOKEN"
```

### JavaScript — personal key

```javascript
const res = await fetch('https://vibecode.bitrix24.com/v1/cowork/state', {
  headers: { 'X-Api-Key': 'YOUR_API_KEY' },
})

if (!res.ok) {
  const { error } = await res.json()
  console.error(error.code, error.message)
} else {
  const state = await res.json()
  const tight = state.windows[state.bottleneck]
  console.log(`Window ${state.bottleneck}: ${tight.pctUsed}%`, tight.exhausted ? 'exhausted' : 'ok')
}
```

### JavaScript — OAuth application

```javascript
const res = await fetch('https://vibecode.bitrix24.com/v1/cowork/state', {
  headers: {
    'X-Api-Key': 'YOUR_APP_KEY',
    'Authorization': 'Bearer USER_SESSION_TOKEN',
  },
})

const state = await res.json()
```

## Response fields

| Field | Type | Description |
|------|-----|----------|
| `portal` | object | The Bitrix24 account that the tier and quota in the response belong to. A key is bound to one Bitrix24 account for good, and a person who belongs to several Bitrix24 accounts has a separate subscription in each. Present in every successful response |
| `portal.id` | string | ID of the Bitrix24 account on the Vibecode platform. Matches `portalId` in the [key self-description](/docs/keys-auth/me) |
| `portal.domain` | string or null | Domain of the Bitrix24 account. `null` only when the platform no longer has a record of that account |
| `portal.isPartner` | boolean | Partner (NFR) Bitrix24 account. Always present for free and paid seats. `false` if the account is missing or deleted. |
| `subscription.tier` | string | Current tier: `FREE`, `PRO`, `MAX`, `ULTRA` |
| `subscription.state` | string | Subscription state: `ACTIVE`, `PAUSED`, `CANCELLED`, `PARKED`. Only `ACTIVE` grants access to Cowork, so an unknown value also means no access |
| `subscription.currentPeriodStart` | string | Start of the billing period (ISO 8601) |
| `subscription.currentPeriodEnd` | string | End of the billing period (ISO 8601) |
| `subscription.nextChargeAt` | string or null | Date of the next charge, `null` for the `FREE` tier. For a seat in the `PARKED` state it is the date the paid term ends, and nothing is charged on that date |
| `subscription.cancelAtPeriodEnd` | boolean | `true` if cancellation is scheduled for the end of the period |
| `subscription.pendingTier` | string or null | A tier below the current one that the seat will move to at the end of the paid term: `FREE`, `PRO` or `MAX`. The transition date is `subscription.paidThroughAt`. A move to a higher tier applies immediately and never appears in this field. `null` when no downgrade is scheduled, and also for a free seat, for a subscription not in the `ACTIVE` state and for a subscription with `cancelAtPeriodEnd: true` |
| `subscription.termMonths` | number | How many months the seat is paid for in advance: `1`, `3`, `6` or `12`. `1` for a monthly subscription |
| `subscription.paidThroughAt` | string | The date the seat is paid through (ISO 8601). Different from `currentPeriodEnd`: the billing period is the quota window and rolls over every 30 days regardless of the term. `paidThroughAt` is when the move to `pendingTier` happens, and access after a cancellation also runs through that date. **The field is only meaningful while `nextChargeAt` is not `null`.** On a free seat it is written once when the seat is created and never moves again, so it drifts into the past — there is nothing to pay for there and nothing to renew |
| `windows.fiveHour` | object | Sliding 5-hour window |
| `windows.fiveHour.pctUsed` | number | Usage in percent, integer 0–100 |
| `windows.fiveHour.resetAt` | string | Window reset time (ISO 8601) |
| `windows.fiveHour.exhausted` | boolean | `true` if the window quota is exhausted |
| `windows.week` | object | Sliding weekly window with the same fields as `fiveHour` |
| `windows.month` | object | Monthly window that matches the billing period, with the same fields as `fiveHour` |
| `completion.phase` | string | Five-hour window phase: `normal`, `landing` (the server asks an ongoing turn to finish), or `exhausted`. New tasks are admitted normally during `landing`. This is subscription state, not a marker for a particular model response |
| `completion.resetAt` | string | Five-hour window reset time (ISO 8601) |
| `bottleneck` | string | The busiest window: `fiveHour`, `week`, or `month` |
| `recommendation.reason` | string | Reason for the recommendation: `none`, `approaching` (≥75% and a higher tier exists), `window_exhausted` (the 5-hour or weekly window is exhausted, but the monthly one is not yet), `fully_exhausted` (the monthly window is exhausted) |
| `recommendation.triggerWindow` | string or null | The window that triggered the recommendation, `null` when `reason: none` |
| `recommendation.wait` | object or null | When the restriction lifts: `{ window, resetAt }`, `null` if no window is exhausted |
| `recommendation.upgrade.available` | boolean | Whether moving to a higher tier is available |
| `recommendation.upgrade.nextTier` | string or null | The recommended next tier, `null` on the `ULTRA` tier |
| `tiers` | array | All four tiers for the comparison table |
| `tiers[].tier` | string | Tier name: `FREE`, `PRO`, `MAX`, `ULTRA` |
| `tiers[].multiplier` | string or null | Tier multiplier: `×1`, `×5`, `×20`, `null` for `FREE` |
| `tiers[].feeVibes` | number | Catalog price of the tier in Vibe credits per month. What will actually be debited for a move to this tier comes from the [tier change preview](/docs/cowork/subscription-preview) |
| `tiers[].current` | boolean | `true` if the tier is active for the key owner |
| `tiers[].isNext` | boolean | `true` if the tier matches `recommendation.upgrade.nextTier` |
| `serverTime` | string | Server time at the moment of the response (ISO 8601) |
| `boostPct` | integer | Temporary limit increase granted by platform support, in percent (100 = double limits). 0 when none is active. Every `pctUsed` share already accounts for it |
| `boostExpiresAt` | string or null | When the temporary increase ends. `null` when none is active |
| `offPeak` | object | Off-peak hours: whether the discount applies now, when the next one starts, and the grid of multipliers by hour of the week. The contents of the block — [Off-peak hours in the Cowork subscription](/docs/cowork/off-peak) |
| `touSavedPct` | number or null | The share of the subscription's MONTHLY ALLOWANCE that off-peak hours gave back over the current billing period, in percent, capped at 100. The denominator is the same as for the monthly share `windows.month.pctUsed` — this is NOT a share of usage. Details: [Off-peak hours](/docs/cowork/off-peak) |
| `activation` | object | How this account is activated for work with Bitrix24: the access model of its region, the link to the plan terms, and whether the Marketplace trial can still be offered |
| `activation.model` | string | Access model of the account region: `subscription` — the Marketplace path applies, trial included; `tariff` — access comes through a commercial Bitrix24 plan, which is how every international account works |
| `activation.tariffInfoUrl` | string | Page with Bitrix24 plan terms. Present only when `model` is `tariff` and the account is not self-hosted |
| `activation.marketTrial.available` | boolean | Whether the trial can be offered right now |
| `activation.marketTrial.unavailableReason` | string or null | Why it should not be offered; `null` when `available: true`. Values: `trial_already_activated`, `subscription_active`, `demo_used`, `region_not_supported`, `not_cloud`, `portal_state`, `not_supported`, `not_required_for_data` |
| `activation.marketTrial.status` | string | Log of our own attempts: `not_attempted` — never tried, `activated` — the trial was switched on through us, `failed` — an attempt was made and has not succeeded yet (an activation still in flight reads the same way) |
| `activation.marketTrial.endsAt` | string or null | When the trial ends, as reported by Bitrix24 (ISO 8601) |
| `activation.marketTrial.activatedAt` | string or null | When we switched the trial on (ISO 8601) |
| `relief` | object | Quota relief: when platform support last reset the usage counters and when it last granted a temporary limit increase. Both fields inside the block are always present |
| `relief.resetGrantedAt` | string or null | When the usage counters were reset, to the hour (ISO 8601). `null` when no reset was granted, and also when the stamp is more than 7 days old |
| `relief.boostGrantedAt` | string or null | When a temporary limit increase was granted, to the hour (ISO 8601). `null` when none was granted, when the stamp is more than 7 days old, and when the increase has already ended. An empty value does not mean there is no increase — read `boostPct` |
| `demo` | object or null | Demo access of the seat: an object while it is in effect, otherwise `null`. The tier in `subscription.tier` stays `FREE`, and the `pctUsed` shares in `windows` are already computed against the demo limits. Show the plan as "Demo" with the end date and a link to the terms of use |
| `demoOffer` | object or null | Available offer before activation; `null` when unavailable. Reading grants nothing |
| `demoOffer.tier` | string | Offered tier: `PRO`, `MAX`, `ULTRA`; send it on acceptance |
| `demoOffer.durationDays` | integer | Offered duration in days; send it on acceptance |
| `demoOffer.rulesUrl` | string | Offer rules; show before consent |
| `demoOffer.shouldPrompt` | boolean | `true` allows the prompt; `false` after decline, while manual acceptance of an available offer is permitted |
| `demo.until` | string | When the demo access ends (ISO 8601) |
| `demo.rulesUrl` | string | Link to the Cowork terms of use page |
| `infraState` | object | Whether infrastructure is stopped over an unpaid balance. The block is shared with the [self-description](/docs/keys-auth/me), and the strings inside it are machine-readable |
| `infraState.frozen` | boolean | Whether servers, deploys and storage are stopped over an unpaid balance. This call itself is not paid for from the balance, so on a frozen account it goes through and returns `true`. Until the refusal is narrowed for your Bitrix24 account, the call answers `402 ACCOUNT_FROZEN` for an account in debt, and the stopped state is read from the [self-description](/docs/keys-auth/me). What the refusal covers — [Error codes](/docs/errors) |
| `infraState.reason` | string or null | The stop reason code: `DEBT`, or `null` when nothing is stopped |
| `infraState.topupUrl` | string or null | The top-up link for your Vibecode account, or `null` when there is nothing to top up |
| `limitReset` | object | Rights to reset limits under a platform promotion. The key may be absent — see below |
| `limitReset.rights` | array | The user's live rights. Rights that can be spent on the current seat come first, each group ordered by expiry — the one expiring soonest first, then by `id`. An empty array means no rights |
| `limitReset.rights[].id` | string | Right ID — the `rightId` for resetting limits in the Cowork app |
| `limitReset.rights[].expiresAt` | string | When the right expires — the end of its promotion (ISO 8601). Show it as a local date and time |
| `limitReset.rights[].seatAlreadyReset` | boolean | `true` — the current seat was already reset under this right's promotion; for example, a colleague reset the limits before handing the seat over. The right is still valid but cannot be spent on this seat |
| `limitReset.lastUsedAt` | string or null | When the user last reset limits with a right that still counts: its promotion is running, or under the no-stacking rule it stood in for the right of a running promotion. Otherwise `null` |
| `limitReset.accountFrozen` | boolean | `true` — the billing account is frozen for debt, and a reset request would now return `402 ACCOUNT_FROZEN` |

The `boostPct`, `boostExpiresAt`, `offPeak`, `touSavedPct`, `relief`, `demo`, and `infraState` keys arrive in every successful response. `null` inside the `relief` block carries a meaning of its own — it is not an absence of data.

The `limitReset` key may be absent entirely — check for its presence: the block arrives when limit reset with a promotion right is enabled for the account. No key means no capability.

## Response example

```json
{
  "portal": {
    "id": "d41f0b93-6c85-42e7-9a13-58bd0e7c2f46",
    "domain": "mycompany.bitrix24.com",
    "isPartner": false
  },
  "subscription": {
    "tier": "FREE",
    "state": "ACTIVE",
    "currentPeriodStart": "2026-06-01T00:00:00.000Z",
    "currentPeriodEnd": "2026-07-01T00:00:00.000Z",
    "nextChargeAt": null,
    "cancelAtPeriodEnd": false,
    "pendingTier": null,
    "termMonths": 1,
    "paidThroughAt": "2026-05-15T00:00:00.000Z"
  },
  "windows": {
    "fiveHour": { "pctUsed": 40, "resetAt": "2026-06-09T17:30:00.000Z", "exhausted": false },
    "week":     { "pctUsed": 24, "resetAt": "2026-06-12T09:00:00.000Z", "exhausted": false },
    "month":    { "pctUsed": 20, "resetAt": "2026-07-01T00:00:00.000Z", "exhausted": false }
  },
  "completion": { "phase": "normal", "resetAt": "2026-06-09T17:30:00.000Z" },
  "bottleneck": "fiveHour",
  "recommendation": {
    "reason": "none",
    "triggerWindow": null,
    "wait": null,
    "upgrade": { "available": true, "nextTier": "PRO" }
  },
  "tiers": [
    { "tier": "FREE",  "multiplier": null,  "feeVibes": 0,   "current": true,  "isNext": false },
    { "tier": "PRO",   "multiplier": "×1",  "feeVibes": 20,  "current": false, "isNext": true  },
    { "tier": "MAX",   "multiplier": "×5",  "feeVibes": 100, "current": false, "isNext": false },
    { "tier": "ULTRA", "multiplier": "×20", "feeVibes": 200, "current": false, "isNext": false }
  ],
  "serverTime": "2026-06-09T14:05:00.000Z",
  "boostPct": 0,
  "boostExpiresAt": null,
  "offPeak": {
    "enabled": true,
    "visible": "now",
    "timezone": "UTC",
    "currentMultiplier": 0.5,
    "currentWindowEndsInHours": 6,
    "nextAtOrBelow": { "inHours": 1, "multiplier": 0.5 },
    "perModel": false,
    "modelName": null,
    "grid": [
      [0.75, 0.87, 0.87, 0.87, 0.87, 0.87, 0.87, 0.87, 0.75, 0.75, 0.75, 0.75, 0.62, 0.5, 0.75, 0.87, 1, 1, 0.87, 0.87, 0.87, 0.75, 0.75, 0.75],
      [0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.62, 0.62, 0.62, 0.62, 0.75, 0.75, 0.87, 0.87, 0.75, 0.75, 0.75, 0.75, 0.62, 0.5, 0.5],
      [0.5, 0.5, 0.62, 0.62, 0.75, 0.62, 0.62, 0.62, 0.75, 0.75, 0.87, 1, 1, 0.87, 0.75, 0.87, 0.87, 0.75, 0.75, 0.62, 0.62, 0.62, 0.62, 0.62],
      [0.75, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.62, 0.62, 0.62, 0.75, 0.75, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0.87],
      [1, 1, 1, 1, 1, 0.87, 0.87, 0.87, 0.87, 1, 1, 1, 1, 1, 1, 0.87, 0.75, 0.75, 0.75, 0.87, 1, 1, 1, 1],
      [1, 1, 1, 1, 1, 0.87, 0.87, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0.87, 0.87, 0.75, 0.87],
      [0.75, 0.62, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.62, 0.75, 0.75, 0.62, 0.62, 0.75, 0.75, 0.87, 0.87, 1, 1, 1, 1, 0.87]
    ],
    "nowCell": { "dow": 1, "hour": 3 }
  },
  "touSavedPct": 12.4,
  "activation": {
    "model": "tariff",
    "tariffInfoUrl": "https://www.bitrix24.com/prices/",
    "marketTrial": {
      "available": false,
      "unavailableReason": "region_not_supported",
      "status": "not_attempted",
      "endsAt": null,
      "activatedAt": null
    }
  },
  "relief": {
    "resetGrantedAt": "2026-06-09T12:00:00.000Z",
    "boostGrantedAt": null
  },
  "demo": null,
  "demoOffer": null,
  "infraState": {
    "frozen": false,
    "reason": null,
    "topupUrl": null
  }
}
```

The example shows an account on the tariff model, which is how every international account works. Where the subscription model applies, `activation.model` is `subscription` and the `tariffInfoUrl` key is absent entirely. For such an account `marketTrial.available` can be `true`. It comes back `false` with reason `not_required_for_data` once the platform no longer treats the Marketplace path as a precondition for reading company data with this key — there is no need to offer the trial in that case. The account type makes no difference here: cloud and self-hosted answer the same way.

## Error response example

404 — subscription not activated:

```json
{
  "success": false,
  "error": {
    "code": "COWORK_NOT_ACTIVATED",
    "message": "No active Cowork/Code subscription for this user+portal"
  }
}
```

## Errors

| HTTP | Code | Description |
|------|-----|----------|
| 401 | `MISSING_API_KEY` | The `X-Api-Key` header was not passed |
| 401 | `INVALID_API_KEY` | The key is not recognized — no such key exists on the platform |
| 403 | `INSUFFICIENT_SCOPE` | The key lacks the `vibe:cowork` scope |
| 404 | `COWORK_NOT_ACTIVATED` | No Cowork subscription found for the user and portal |
| 429 | `RATE_LIMITED` | Polling faster than allowed |
| 500 | `INVALID_TIER_CONFIGURATION` | The tier configuration on the platform is invalid |
| 503 | `COWORK_FEATURE_DISABLED` | Cowork is disabled at the platform level |

The full list of common API errors — [Errors](/docs/errors).

## Known specifics

**A successful response (200) is the state object itself, without a `success` wrapper.** Errors arrive in the envelope `{ success: false, error: { code, message } }`. Determine success by the HTTP status (`res.ok`).

**The window-exhaustion flag is the `exhausted` field, not `pctUsed === 100`.** The `pctUsed` value is rounded to an integer: 99.6% shows as `100`, though the window is not yet exhausted.

**Measure the time until reset from `serverTime`, not from the device clock** — clock drift between the client and the server skews the counter.

**Windows reset lazily on read** — if there have been no requests since the reset time, the reset is applied when you call the endpoint, so the snapshot is always current.

**Poll no more than once every 15–30 seconds.** The response is not cached (`Cache-Control: no-store`). The endpoint has a rate limit counted per account and key owner, so all of one person's devices share it; the recommended interval stays well inside it, and above it the endpoint answers `429 RATE_LIMITED`.

**`PARKED` means the seat has no active owner.** A paid seat moves to this state when its owner is no longer an active member of the Bitrix24 account while the paid term is still running. Nothing is charged for such a seat, and it keeps its tier and paid term. A Bitrix24 account administrator can hand it over to another employee. Within a day after the paid term ends, the seat moves to `PAUSED`.

**The `activation` block may be absent from the response entirely** — a platform that does not know about it yet responds this way. This is a normal state: if the block is missing, keep working as before. When the block is there, `available` and `unavailableReason` inside it are always present, so test for the BLOCK and read the VALUE of the field, never the other way round.

**`activation.marketTrial.available` is a forecast, not a guarantee.** `false` is final: do not show the trial step. `true` means you may offer it, but it does not promise success — the account state can change between the poll and the click, and the final decision stays with Bitrix24, so keep handling a refusal at activation time.

**The list of `unavailableReason` values may grow.** Read an unknown value as "do not offer the trial", with no special branch. The same goes for `status`.

**`activation.tariffInfoUrl` is a general terms page, not an address specific to your account.** The value is constant and may be absent from the response (a subscription-model region, a self-hosted account) — your UI must handle a missing link. Do not persist it and do not parse its host.

**An empty `relief.boostGrantedAt` does not mean there is no limit increase.** The stamp is there for a one-off notification and stays visible for 7 days, while an increase is granted for up to 30 days. An increase granted more than seven days ago reports an empty stamp and is still in effect. Only `boostPct` tells you whether an increase is in effect.

**The value of the `relief` block matches what the [subscription summary](/docs/cowork/me) returns.** Store the stamp you have already reported to the user once per application, not separately per endpoint, or the same relief will be shown twice. Notify only when a **non-empty** value differs from the one you stored: the server deliberately turns a stamp into `null` (the visibility window closed, the increase expired or was revoked, the seat was handed over), so a plain "it changed — show it" rule would fire a false notice on every such transition.

**The `limitReset` block holds the data for the limit reset button.** The Cowork app spends the right: the desktop key picks the first usable right (`seatAlreadyReset: false`); when there is none, do not show the button. The block value matches what the [subscription summary](/docs/cowork/me) returns.

## See also

- [Cowork subscription summary](/docs/cowork/me)
- [Off-peak hours in the Cowork subscription](/docs/cowork/off-peak)
- [Cowork](/docs/cowork)
- [Limits and optimization](/docs/optimization)
- [Errors](/docs/errors)

## Choose Demo before activation

When an offer is available, the Cowork app shows its `demoOffer` terms and sends the user's choice with the desktop key.
