
## Off-peak hours in the Cowork/Code subscription

During certain hours of the week the subscription quota is consumed more slowly — the same request takes a smaller share of the monthly limit. The subscription responses show whether the discount applies right now and when it starts.

The discount applies on its own, there is nothing to switch on. The `offPeak` block reports what has already been taken into account when the quota was consumed, so an app can offer to move a bulk job into a cheap hour. Off-peak hours are not in effect on this platform yet, so today the block does not arrive here at all.

## 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' },
})

const state = await res.json()
const off = state.offPeak

if (!off || !off.enabled) {
  console.log('Off-peak hours are not in effect for this subscription')
} else if (off.visible === 'now') {
  const discount = Math.round((1 - off.currentMultiplier) * 100)
  console.log(`Quota is consumed ${discount}% more slowly for another ${off.currentWindowEndsInHours} h`)
} else if (off.visible === 'next' && off.nextAtOrBelow) {
  console.log(`An off-peak hour starts in ${off.nextAtOrBelow.inHours} h`)
}
```

### 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

The block arrives in two responses and differs in composition.

| Response | Block composition |
|-------|--------------|
| [`GET /v1/cowork/state`](/docs/cowork/state) | The whole block — together with the `grid` and the `touSavedPct` field |
| [`GET /v1/cowork/me`](/docs/cowork/me) | The block without the grid and without `touSavedPct` |

Both keys are optional. The capability is switched on account by account, and until it is on, the `offPeak` and `touSavedPct` keys are absent from the response body entirely.

| Field | Type | Description |
|------|-----|---------|
| `offPeak` | object | Off-peak hours block |
| `offPeak.enabled` | boolean | Whether off-peak hours are in effect for this subscription. When `false`, the remaining fields are empty and every hour is at full price |
| `offPeak.visible` | string | What to show the user: `now` — the discount applies right now, `next` — a meaningful discount starts within the next few hours, `none` — nothing to show |
| `offPeak.timezone` | string or null | Schedule timezone — an identifier from the IANA time zone database, for example `UTC`. The day of week and hour are computed against it |
| `offPeak.currentMultiplier` | number | Quota-consumption multiplier for the current hour, greater than `0` and no more than `1`. A value of `0.5` means the quota is consumed twice as slowly, a value of `1` means no discount |
| `offPeak.currentWindowEndsInHours` | number or null | In how many hours consumption stops being this favourable. `null` when it never gets more expensive within a week ahead |
| `offPeak.nextAtOrBelow` | object or null | The nearest hour whose discount is deep enough to be worth showing. `null` when there is no such hour within a week ahead |
| `offPeak.nextAtOrBelow.inHours` | number | How many hours until that hour arrives |
| `offPeak.nextAtOrBelow.multiplier` | number | Quota-consumption multiplier in that hour |
| `offPeak.perModel` | boolean | `true` when the schedule was picked for a single model rather than shared by every model of the subscription |
| `offPeak.modelName` | string or null | Name of the model the schedule was picked for. `null` when the schedule is shared |
| `offPeak.grid` | array or null | Grid of multipliers `grid[day][hour]`. Seven rows of 24 values. Day `0` is Sunday, day `6` is Saturday. The hour is `0` to `23` in the `timezone` time zone. Arrives only in `GET /v1/cowork/state` |
| `offPeak.nowCell` | object or null | The grid cell that the current moment maps to. Computed on the server, so it matches `currentMultiplier` |
| `offPeak.nowCell.dow` | number | Day of week, from `0` (Sunday) to `6` (Saturday) |
| `offPeak.nowCell.hour` | number | Hour, from `0` to `23` |
| `touSavedPct` | number or null | What share of the subscription MONTHLY ALLOWANCE the off-peak hours gave back over the current billing period, in percent with one decimal place, capped at 100. Arrives only in `GET /v1/cowork/state` |

## Response example

A `GET /v1/cowork/state` response. It shows the off-peak block and the saved share, the remaining fields are in [Cowork/Code subscription state](/docs/cowork/state).

```json
{
  "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
}
```

The same moment in a `GET /v1/cowork/me` response — without the grid and without the saved share:

```json
{
  "offPeak": {
    "enabled": true,
    "visible": "now",
    "timezone": "UTC",
    "currentMultiplier": 0.5,
    "currentWindowEndsInHours": 6,
    "nextAtOrBelow": { "inHours": 1, "multiplier": 0.5 },
    "perModel": false,
    "modelName": null,
    "nowCell": { "dow": 1, "hour": 3 }
  }
}
```

## 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` | Invalid API key |
| 403 | `INSUFFICIENT_SCOPE` | The key lacks the `vibe:cowork` scope |
| 404 | `COWORK_NOT_ACTIVATED` | No Cowork/Code subscription found for this user and Bitrix24 account |
| 500 | `INVALID_TIER_CONFIGURATION` | The tier configuration on the platform is invalid |
| 503 | `COWORK_FEATURE_DISABLED` | Cowork/Code is disabled at the platform level |

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

## The hint in the exhausted-quota refusal

When a subscription quota window is exhausted, [`POST /v1/chat/completions`](/docs/ai/chat/completions) answers `402` with the code `cowork_quota_exhausted`. The body of that refusal carries an optional `error.offPeakHint` field — in how many hours the block is lifted and which consumption multiplier will be in effect at that moment. An app reads it to decide whether simply waiting is enough.

| Field | Type | Description |
|------|-----|---------|
| `error.offPeakHint` | object | Hint about the off-peak hour at the moment the block is lifted |
| `error.offPeakHint.inHours` | number | In how many hours the block is lifted. Rounded up — the same source as the `Retry-After` header |
| `error.offPeakHint.multiplier` | number | The quota-consumption multiplier that will be in effect at that moment |

```json
{
  "error": {
    "message": "Cowork/Code quota exhausted for the 5h window. Wait until 2026-08-10T15:00:00.000Z or upgrade your tier.",
    "type": "insufficient_quota",
    "code": "cowork_quota_exhausted",
    "window": "5h",
    "resetAt": "2026-08-10T15:00:00.000Z",
    "nextTier": "PRO",
    "offPeakHint": { "inHours": 3, "multiplier": 0.5 }
  }
}
```

The hint arrives only when the capability is enabled for the Bitrix24 account and the unblock moment falls into a discounted hour. Otherwise the `offPeakHint` key is absent from the body entirely — it never arrives as `null`, exactly like the other off-peak fields. Off-peak hours are not in effect on this platform yet, so today the hint does not arrive here at all.

The multiplier is computed exactly at the unblock moment; the nearest off-peak window is not searched for. If that hour carries no discount, no hint arrives either — even when a cheap hour starts a little later.

## Known specifics

**Check for the presence of the `offPeak` key, not for its value.** Until the capability is on for the account, the key is absent from the response body entirely — it does not arrive as `null`. A client that reads `state.offPeak.enabled` without checking `offPeak` itself will throw on such a response.

**Branch the interface on `visible`, not on a threshold of your own.** The threshold above which a discount is worth showing is decided on the platform side and is not exposed. Comparing `currentMultiplier` against a number baked into the client will diverge from the platform the next time that threshold is tuned.

**The multiplier is a consumption coefficient, not the discount size.** A value of `0.62` means the call takes 62% of the quota share it would take without a discount, that is, consumption is reduced by 38%. The discount size is computed as `1 - currentMultiplier`.

**`nextAtOrBelow` looks for a meaningful discount, not for any price drop.** That is how it differs from the `nextWindow` field in the [Off-peak schedule](/docs/ai/consumption/off-peak): there it is the nearest strictly cheaper hour, here it is the nearest hour whose discount is deep enough to show.

**The block stays silent while the subscription is not active.** A paused or cancelled subscription gets `enabled: false` — the schedule no longer applies to it.

**`touSavedPct` is measured against the monthly allowance, not against consumption.** The denominator is the same one the month quota bar is a share of, so the two read side by side: «N% used» and «off-peak hours gave back M%». The value is `null` only when nothing has been saved yet in the current period.

## See also

- [Cowork/Code subscription state](/docs/cowork/state)
- [Cowork/Code subscription summary](/docs/cowork/me)
- [Off-peak schedule](/docs/ai/consumption/off-peak)
- [Cowork/Code](/docs/cowork)
