# Hour boosting

> Author boost plans, attach accounts, watch the live console, and start or stop hours from code.


Everything the **Boost** cluster does, over HTTP: author a plan (which games, for how long, on what schedule), attach accounts to it, start it, and watch the console that shows what is playing right now.

Two scopes cover the domain. `boost.read` for the listings and the live console, `boost.write` for everything that changes a plan or moves an account.

> [!IMPORTANT]
> Starting a boost holds a real Steam login for hours against a capped budget, so every endpoint that starts one requires an `Idempotency-Key` header: `quick-boost`, `POST /api/v1/boost/plans/start`, a plan's `start` and `toggle`, and an assignment's `start` and `retry`. Nothing that stops, pauses, detaches or edits needs one. See [Bulk operations](/docs/api/en/concepts/bulk-operations).

## Your plan gates placement, not authoring

Hour boosting is the one entitlement enforced while work runs rather than when it is created. That has consequences worth knowing before you build against it.

Creating plans, editing them, attaching accounts and deleting them stay available whatever your subscription includes. Only the calls that **start** boosting refuse: starting or resuming a plan, resetting its progress, quick boosting, and starting or retrying a single account. Those answer `403` with code `plan_limit_reached` and the upgrade path attached.

Stopping is never refused. Pausing a plan, stopping an account and `stop-all` stay available even when your plan no longer covers boosting, because a lapsed subscription must never leave you holding accounts you cannot stop. Nothing is deleted or rewritten by a lock either: your plans, schedules and earned progress survive it, and the queue drains again the moment your plan covers boosting.

Concurrency (`max_boost_concurrent`, reported as `plan.max_boost_concurrent` on [`GET /api/v1/me`](/docs/api/en/concepts/overview)) is not a refusal. It caps how many accounts play **at once**, and the queue is how that cap is respected: start as many as you like and the control plane places them as slots free up. The live console reports `totals.capUsed` and `totals.capLimit` so you can show the ceiling instead of discovering it.

## List plans

```endpoint
method: GET
path: /api/v1/boost/plans
description: Your boost plans, with account counts and progress.
auth: bearer
```

Filter with `status` (repeatable: `draft`, `running`, `paused`, `completed`, `archived`) and `search` (matches the plan name). Sort with `sort` (`created_at`, `updated_at`, `name`, `status`, `started_at`) and `direction`.

```bash tab=curl
curl "https://dashboard.steamlabs.dev/api/v1/boost/plans?status=running" \
  -H "Authorization: Bearer $STEAMLABS_API_KEY"
```

```json
{
    "data": [
        {
            "id": "019fb42e-9a61-70d2-818a-f6a56593f3a5",
            "name": "Nightly grind",
            "status": "running",
            "game_mode": "fixed",
            "app_ids": [730],
            "games": [
                { "app_id": 730, "name": "Counter-Strike 2", "image": "https://cdn.cloudflare.steamstatic.com/steam/apps/730/header.jpg" }
            ],
            "games_per_account": 1,
            "rotate_every_minutes": null,
            "custom_game_name": null,
            "persona_state": "online",
            "target_mode": "fixed",
            "target_minutes": 6000,
            "target_min_minutes": null,
            "target_max_minutes": null,
            "max_concurrent": null,
            "stagger_seconds": 5,
            "schedule_enabled": false,
            "schedule": {},
            "schedule_summary": { "always": true, "never": false, "hours": 168 },
            "repeat_on_complete": false,
            "accounts_count": 120,
            "boosting_count": 48,
            "progress_percent": 21.4,
            "started_at": "2026-07-29T20:00:00+00:00",
            "completed_at": null,
            "created_at": "2026-07-01T09:15:00+00:00",
            "updated_at": "2026-07-30T14:02:11+00:00"
        }
    ],
    "meta": { "page": 1, "per_page": 50, "total": 1, "last_page": 1 }
}
```

Time is always **minutes**, never hours: the panel edits hours because a person is reading it, the API speaks the unit the planner counts in.

```endpoint
method: GET
path: /api/v1/boost/plans/{id}
description: One plan, in the same shape a list row has.
auth: bearer
```

## Create and edit a plan

```endpoint
method: POST
path: /api/v1/boost/plans
description: Create a plan. Always a draft holding no accounts.
auth: bearer
```

A plan needs something to play: at least one entry in `app_ids`, or a `custom_game_name` (a free-text "non-Steam game" that occupies one of Steam's 32 slots). Everything else has a sensible default.

```bash tab=curl
curl -X POST "https://dashboard.steamlabs.dev/api/v1/boost/plans" \
  -H "Authorization: Bearer $STEAMLABS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
        "name": "Nightly grind",
        "app_ids": [730, 570],
        "game_mode": "random",
        "games_per_account": 1,
        "target_mode": "fixed",
        "target_minutes": 6000,
        "stagger_seconds": 10,
        "schedule_enabled": true,
        "schedule": { "mon": [20, 21, 22, 23], "tue": [20, 21, 22, 23] }
      }'
```

| Field | Notes |
| --- | --- |
| `game_mode` | `fixed` (every account plays the whole pool), `random` (each draws `games_per_account` picks), `rotate` (re-picked every `rotate_every_minutes`, minimum 5). |
| `target_mode` | `unlimited` (runs until stopped), `fixed` (`target_minutes`), `random` (each account draws between `target_min_minutes` and `target_max_minutes`). |
| `persona_state` | `online`, `away`, `snooze`, `invisible` or `offline`. Offline still accrues playtime, it just does not light up for friends. |
| `max_concurrent` | A cap for this plan alone. Never raises your account's ceiling: it is a way to run one plan gently. |
| `schedule` | Whole local hours per weekday, `{"mon": [0..23], ...}`, evaluated in **your** timezone. Switching `schedule_enabled` on with no hours selected is refused. |
| `repeat_on_complete` | On completion, reset every account's progress and run again instead of parking the plan. |

```endpoint
method: PATCH
path: /api/v1/boost/plans/{id}
description: Change a plan. Every field is optional.
auth: bearer
```

Only what you send is written, but the rules still see the plan as it will be **afterwards**: switching `target_mode` to `fixed` without a `target_minutes` is refused even though the body names only one of the two.

Editing a running plan is allowed, and the change is pushed down onto the accounts already attached to it. The response carries `accounts_resynced` with how many were reached. That matters: a worker plays what its **assignment** says, frozen at attach time, so without the resync a plan edited from CS2 to Dota would list as Dota while every account carried on playing CS2.

```endpoint
method: DELETE
path: /api/v1/boost/plans/{id}
description: Delete a plan and detach its accounts. Answers 204.
auth: bearer
```

The run history survives: sessions keep their rows with the plan reference nulled, so the hours those accounts earned never disappear from your totals.

## Start, pause and reset

```endpoint
method: POST
path: /api/v1/boost/plans/{id}/toggle
description: The panel's one button. Running pauses, paused resumes, completed resets and re-runs, draft starts.
auth: bearer
```

```endpoint
method: POST
path: /api/v1/boost/plans/{id}/start
description: Start or resume one plan by name rather than by toggling.
auth: bearer
```

```endpoint
method: POST
path: /api/v1/boost/plans/{id}/pause
description: Hold one plan. Its accounts sign out and keep every minute they earned.
auth: bearer
```

All three answer with the plan record, so `status` tells you what happened. `start` is convergent: a plan that is already running answers `200` unchanged. A plan with no games or no accounts answers `422` with code `plan_not_startable` and a `reason` of `no_games` or `no_accounts`. Re-running a **completed** plan means resetting its progress, which is what `toggle` does for you.

`toggle` and `start` require an `Idempotency-Key`. `pause` does not: it only ever takes work away.

## Bulk plan actions

```endpoint
method: POST
path: /api/v1/boost/plans/start
description: Start every startable plan in a list.
auth: bearer
```

```endpoint
method: POST
path: /api/v1/boost/plans/pause
description: Pause every running plan in a list.
auth: bearer
```

```endpoint
method: DELETE
path: /api/v1/boost/plans
description: Delete a list of plans.
auth: bearer
```

Each takes `plan_ids`, capped at 1,000 (`bulk_limit_exceeded` past that). `POST /api/v1/boost/plans/start` requires an `Idempotency-Key` header, because it starts real logins; the bulk pause and the bulk delete do not.

Plans that cannot start are skipped rather than failing the batch:

```json
{
    "plans_affected": 1,
    "plans_skipped": 2,
    "skipped": {
        "019fb42e-9a61-70d2-818a-f6a56593f3a5": "no_accounts",
        "019fb430-1c22-73a4-9f0e-2b7c5d1e8a44": "running"
    }
}
```

## A plan's accounts

```endpoint
method: GET
path: /api/v1/boost/plans/{id}/assignments
description: Who is on the plan, and how far along each one is.
auth: bearer
```

Filter with `state` (repeatable: `pending`, `starting`, `boosting`, `paused`, `completed`, `error`, `cooldown`, `stopped`) and `search` (matches username or persona name). Sort with `sort` (`created_at`, `state`, `minutes_boosted`, `boost_started_at`) and `direction`.

```json
{
    "data": [
        {
            "id": "019fb431-77aa-7c10-b2d1-9e0f4a6c2b31",
            "plan_id": "019fb42e-9a61-70d2-818a-f6a56593f3a5",
            "account_id": "019fb2c1-4f88-70aa-9d33-1e8b7c5a2d90",
            "account": { "id": "019fb2c1-4f88-70aa-9d33-1e8b7c5a2d90", "username": "slabs_bot_01", "persona_name": "Nova", "avatar_url": null },
            "state": "boosting",
            "is_live": true,
            "app_ids": [730],
            "games": [{ "app_id": 730, "name": "Counter-Strike 2", "image": "https://cdn.cloudflare.steamstatic.com/steam/apps/730/header.jpg" }],
            "custom_game_name": null,
            "target_minutes": 6000,
            "minutes_boosted": 1284,
            "progress_percent": 21.4,
            "remaining_minutes": 4716,
            "worker_id": "worker-7",
            "error_message": null,
            "error_count": 0,
            "cooldown_until": null,
            "assigned_at": "2026-07-29T20:00:04+00:00",
            "boost_started_at": "2026-07-29T20:00:31+00:00",
            "last_heartbeat_at": "2026-07-30T15:41:52+00:00",
            "last_rotated_at": null,
            "completed_at": null,
            "created_at": "2026-07-29T19:58:00+00:00",
            "updated_at": "2026-07-30T15:41:52+00:00"
        }
    ],
    "meta": { "page": 1, "per_page": 50, "total": 1, "last_page": 1 }
}
```

### Attach accounts

```endpoint
method: POST
path: /api/v1/boost/plans/{id}/assignments
description: Put accounts on the plan, by id list or by filter.
auth: bearer
```

Takes the standard [account selection](/docs/api/en/concepts/bulk-operations): `account_ids` for a list you name, or `filters` using exactly the keys [`GET /accounts`](/docs/api/en/endpoints/accounts) accepts. The useful move is one copied object: list with `?boost=not_on_plan`, then post the same filters here.

> [!IMPORTANT]
> A filter key this endpoint does not recognise is **refused**, not ignored, and the same is true of `quick-boost` below. Send one and you get a `422` naming it. The accounts vocabulary these take is not the shorter one [task selections](/docs/api/en/endpoints/tasks) use, so a `filters` object copied from `POST /api/v1/tasks` is refused here rather than silently attaching, or boosting, every account you own.

An account belongs to **at most one plan**. Accounts already spoken for are skipped, not refused, so a batch of five hundred where three are taken attaches 497:

```json
{ "accounts_attached": 497, "accounts_skipped": 3, "queued": false }
```

A filter selection is unbounded, so it is resolved on the queue and answers `202`. `task_id` is `null` because attaching creates no task to poll; watch the roster instead.

```json
{ "task_id": null, "accounts_affected": 12480, "queued": true }
```

### One account

```endpoint
method: POST
path: /api/v1/boost/assignments/{id}/start
description: Queue one account on an otherwise untouched plan.
auth: bearer
```

```endpoint
method: POST
path: /api/v1/boost/assignments/{id}/retry
description: Clear a failed account's error streak and backoff, and requeue it.
auth: bearer
```

```endpoint
method: POST
path: /api/v1/boost/assignments/{id}/stop
description: Stop one account. It stays attached, in `stopped`, until a person starts it again.
auth: bearer
```

```endpoint
method: DELETE
path: /api/v1/boost/assignments/{id}
description: Take one account off its plan. Answers 204.
auth: bearer
```

```endpoint
method: POST
path: /api/v1/boost/assignments/detach
description: Take many accounts off their plans, by `assignment_ids`.
auth: bearer
```

`start` and `retry` require an `Idempotency-Key` and refuse under a plan lock. `stop` and the two detaches do neither.

## The live console

```endpoint
method: GET
path: /api/v1/boost/live
description: The snapshot the Boost console polls: totals, plan chips and one card per boosting account.
auth: bearer
```

This endpoint returns the panel's own payload verbatim, which is the one place on this API where keys are camelCase and times are epoch milliseconds. That is deliberate: one payload with one meaning means a dashboard built on it sees exactly what the console shows, rather than a re-shaped copy that drifts.

```json
{
    "generatedAt": 1785425000000,
    "fleetReporting": true,
    "totals": {
        "boosting": 48,
        "queued": 12,
        "hoursToday": 214.5,
        "hoursTotal": 91320.2,
        "gamesInPlay": 3,
        "workersInUse": 6,
        "capUsed": 48,
        "capLimit": 100
    },
    "planChips": [{ "id": "019fb42e-9a61-70d2-818a-f6a56593f3a5", "name": "Nightly grind", "live": 48 }],
    "cards": [],
    "overflow": 0,
    "cardCap": 48,
    "empty": null
}
```

The card grid is capped at `cardCap` with the remainder reported as `overflow`, and worker identity is anonymized into "Worker N" labels. `workersInUse` is `null` when the fleet is silent, which means "we cannot see", not zero. When nothing is boosting, `empty.reason` says why (`no_plans`, `queued`, `schedule_closed`, `idle`, `paused`, `completed`, `draft`) and `empty.nextOpen` says when a closed schedule reopens.

```endpoint
method: GET
path: /api/v1/boost/sessions
description: The run history, newest first.
auth: bearer
```

Paginated and in the ordinary house shape (snake_case, ISO 8601). Filter with `plan_id`, `account_id` and `open`. A run that is still going has `open: true`, no `ended_at`, and a `seconds` figure that is the last number a worker reported rather than a final total.

```endpoint
method: GET
path: /api/v1/boost/games
description: What you can boost: the free-to-play catalog merged with the games your accounts own.
auth: bearer
```

Filter with `search` (name, tag or app id) and `owned`. `free` marks a title any account can register a licence for; `owned` marks one at least one of your accounts already has.

## Quick boost and stop everything

```endpoint
method: POST
path: /api/v1/boost/quick-boost
description: Boost a set of accounts on a set of games without authoring a plan first.
auth: bearer
```

A real (one-off) plan is created behind it and behaves like any other afterwards: rename it, edit it, give it a schedule. Takes `app_ids`, an optional `target_minutes` (omit it to run until stopped), and the standard account selection. Requires an `Idempotency-Key` header.

```bash tab=curl
curl -X POST "https://dashboard.steamlabs.dev/api/v1/boost/quick-boost" \
  -H "Authorization: Bearer $STEAMLABS_API_KEY" \
  -H "Idempotency-Key: 7f3a9c2e-1b44-4d8a-9f01-cc2e5a9b1234" \
  -H "Content-Type: application/json" \
  -d '{ "app_ids": [730], "target_minutes": 90, "filters": { "boost": "not_on_plan" } }'
```

An id list answers `201` with the plan and the counts. A filter selection answers `202` with `plan_id` and `queued: true`. If every account you named was already on a plan, nothing was created and you get `422` with code `accounts_already_boosting`.

```endpoint
method: POST
path: /api/v1/boost/stop-all
description: Stop everything you have boosting or queued, in one pass.
auth: bearer
```

Queued accounts are stopped too, not just live ones: leaving them pending would have the control plane sign them straight back in. Answers `{"accounts_affected": 48}`, needs no `Idempotency-Key`, and is never refused.
