
## Off-peak hours in the Cowork 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 to an off-peak 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 its contents differ between them.

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

`offPeak` arrives in every successful response of both endpoints, and `touSavedPct` in every successful response of `GET /v1/cowork/state`.

| 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 time zone — 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 | How many hours until consumption stops being this favorable. `null` when it does not get more expensive within the next week |
| `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 the next week |
| `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 is tailored to a single model rather than shared by all models of the subscription |
| `offPeak.modelName` | string or null | Name of the model the schedule is tailored to. `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's 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 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` | 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 |
| 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).

## The hint in the exhausted-quota refusal

When a subscription quota window is exhausted, [`POST /v1/chat/completions`](/docs/ai/chat/completions) responds with `402` and the code `cowork_quota_exhausted`. The body of that refusal carries an optional `error.offPeakHint` field — how many hours until the lockout ends 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 lockout ends |
| `error.offPeakHint.inHours` | number | How many hours until the lockout ends. Rounded up, from 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 unblock moment falls into a discounted hour. Otherwise the `offPeakHint` key is absent from the body entirely — it never arrives as `null`. 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 platform does not look for the nearest off-peak window. If that hour carries no discount, no hint arrives either — even when an off-peak hour starts a little later.

## Known specifics

**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 as for the monthly quota bar, so the two shares 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 subscription state](/docs/cowork/state)
- [Cowork subscription summary](/docs/cowork/me)
- [Off-peak schedule](/docs/ai/consumption/off-peak)
- [Cowork](/docs/cowork)
