# Change sets

_Category: REST API reference_

A change set is a release: it groups staged edits to live articles, articles that are not published yet and hidden categories, and publishes them in one action, now or at a time you choose. Use it to put your documentation live together with the feature it describes.

**API keys only.** The change-set endpoints accept an `apikey` header and nothing else. An OAuth access token is refused with `401` and `{"status": "error", "message": "Unknown API key."}`. Change sets are part of the Catalyst plan, in early preview, like [staged changes](https://developers.helpcenter.io/content/staged-changes-api).

## Endpoints

| Method and path | What it does | Key |
| --- | --- | --- |
| `GET /v1/change-sets` | List change sets | Any |
| `POST /v1/change-sets` | Create a change set | Read & write |
| `GET /v1/change-sets/{changeSetId}` | Get a change set with its items and readiness | Any |
| `PATCH /v1/change-sets/{changeSetId}` | Rename a change set or change its description | Read & write |
| `DELETE /v1/change-sets/{changeSetId}` | Delete a change set | Read & write |
| `POST /v1/change-sets/{changeSetId}/items` | Add articles and categories | Read & write |
| `DELETE /v1/change-sets/{changeSetId}/items/{itemId}` | Remove an item | Read & write |
| `POST /v1/change-sets/{changeSetId}/publish` | Publish now, or schedule | Read & write |
| `DELETE /v1/change-sets/{changeSetId}/schedule` | Cancel a schedule | Read & write |
| `GET /v1/change-sets/{changeSetId}/status` | Follow a publish run | Any |

A Read only key on a write answers `403` with `insufficient_scope`. Every change-set request, reads and status polls included, counts toward the 120 writes a minute of your key (see [Rate limits](https://developers.helpcenter.io/content/rate-limits)).

## How a release works

1. Prepare the work: stage edits to live articles (see [Staged changes and versions](https://developers.helpcenter.io/content/staged-changes-api)), write new articles as drafts, and keep a new category hidden.
2. Create a change set and add those articles and categories. Each becomes one or more items.
3. Check its `readiness`: blockers stop a publish, warnings never do.
4. Publish it, or schedule it. The answer is `202 Accepted`: the run happens after the response.
5. Poll the status until `done` is `true`, then check what applied.

A change set only points at work that exists on its own. Removing an item or deleting a change set never deletes or changes an article or category. Nothing in a change set is visible to readers until it publishes.

### Items

| Type | Made from | What publishing does |
| --- | --- | --- |
| `staged_version` | A published article with staged changes: one item for each staged language. | Publishes that language's staged changes, like `POST /v1/articles/{articleId}/staged/publish` without `force`. |
| `article_publish` | An article that is not published: a draft, or an article that was unpublished. | Publishes the article, and any staged changes it has. |
| `category_visibility` | A category. | Sets the category's visibility to the value you chose when you added it. |

### Statuses

| Status | Meaning |
| --- | --- |
| `open` | Not published yet. You can change it and publish it. A scheduled change set is `open` with `is_scheduled` set to `true`. |
| `publishing` | A run is in progress. Its items cannot change, and it cannot be deleted. |
| `published` | Every item applied. |
| `partially_published` | Some items applied and some failed. Fix the failed items and publish again. |
| `failed` | No item applied. Fix the items and publish again. |

There is no `draft` or `scheduled` status. `open`, `partially_published` and `failed` change sets can still be edited and published.

### Limits

- A change set holds up to 500 items.
- A help center can have up to 25 change sets that are `open`, `partially_published` or `failed`. Published change sets do not count.
- A name can be up to 120 characters, a description up to 2,000 and a release note up to 500.

## The change set object

| Field | Type | Description |
| --- | --- | --- |
| `id` | integer | The change set's id. |
| `name` | string | Its name. |
| `description` | string or null | Its description. |
| `status` | string | `open`, `publishing`, `published`, `partially_published` or `failed`. |
| `is_scheduled` | boolean | `true` while an `open` change set waits for its scheduled time. |
| `scheduled_for` | string or null | The scheduled time, ISO 8601. |
| `published_at` | string or null | When the first item was published. It stays the same when you publish again. |
| `release_note` | string or null | The note you sent with the publish. |
| `items_count` | integer | How many items it holds. |
| `processed_items`, `total_items` | integer | Progress of the last run. Both are `0` until the first publish. |
| `created_at`, `updated_at` | string | ISO 8601, with an offset. |
| `readiness` | object | Whether it can publish. Not in the list endpoint. |
| `items` | array | The items, in the order they were added. Not in the list endpoint. |

Each item has `id`, `type`, `article_id`, `category_id`, `locale` (for `staged_version` items), `variant` (always `default`), `payload` (for a category item, `from` and `to` visibility), `applied` and `error` (the reason the item failed in the last run).

`readiness` has `counts` (items by type), `locales` (the languages the release touches), `articles` (how many articles), `blockers`, `warnings` and `publishable`, which is `true` when there is at least one item and no blocker. Here is a change set with five items, three of them shown:

```
{
  "id": 3,
  "name": "Release 3.4",
  "description": "Stage gates for boards",
  "status": "open",
  "is_scheduled": false,
  "scheduled_for": null,
  "published_at": null,
  "release_note": null,
  "items_count": 5,
  "processed_items": 0,
  "total_items": 0,
  "created_at": "2026-09-30T08:21:23+00:00",
  "updated_at": "2026-09-30T08:21:23+00:00",
  "readiness": {
    "counts": {
      "staged_version": 3,
      "article_publish": 1,
      "category_visibility": 1
    },
    "locales": [
      "de",
      "en"
    ],
    "articles": 3,
    "blockers": [],
    "warnings": [
      {
        "article_id": 598,
        "article_title": "Move cards between stages",
        "reason": "locale_gap",
        "missing_locales": [
          "de"
        ],
        "message": "This article has no de changes, but the release includes de."
      }
    ],
    "publishable": true
  },
  "items": [
    {
      "id": 2,
      "type": "staged_version",
      "article_id": 597,
      "category_id": null,
      "locale": "de",
      "variant": "default",
      "payload": null,
      "applied": false,
      "error": null
    },
    {
      "id": 5,
      "type": "article_publish",
      "article_id": 600,
      "category_id": null,
      "locale": null,
      "variant": "default",
      "payload": null,
      "applied": false,
      "error": null
    },
    {
      "id": 6,
      "type": "category_visibility",
      "article_id": null,
      "category_id": 307,
      "locale": null,
      "variant": "default",
      "payload": {
        "to": "public",
        "from": "unlisted"
      },
      "applied": false,
      "error": null
    }
  ]
}
```

### Blockers and warnings

Each blocker and warning has a `reason`, a `message` and the ids that locate it (`item_id`, `article_id`, `article_title`, `locale` where they apply).

| Reason | Kind | Message |
| --- | --- | --- |
| `conflict` | Blocker | `The live article changed after these edits were staged.` |
| `article_gone` | Blocker | `The article was deleted.` or `The article was deleted. Remove it from this change set to publish the rest.` |
| `category_gone` | Blocker | `The category was deleted. Remove it from this change set to publish the rest.` |
| `locale_gap` | Warning | `This article has no de changes, but the release includes de.` The release touches a language this article has no changes for. |
| `staged_not_added` | Warning | `This article has DE changes staged that are not in this change set.` Add the article again to include them. |
| `already_applied` | Warning | A category item whose category already has the target visibility. |

## List change sets

List change sets, most recently updated first. Each has the fields above, without `readiness` and `items`.

GET`/v1/change-sets`

List change sets, most recently updated first.

- `status` must match exactly. An unknown value, such as `draft`, returns an empty list rather than an error.
- Paging uses `page` and `per_page` (default 25, up to 100), and `meta` is `current_page`, `last_page` and `total`, unlike most list endpoints (see [Pagination](https://developers.helpcenter.io/content/pagination)).

## Create a change set

A change set starts empty and `open`. Name it after what it ships with, so you can find it later.

POST`/v1/change-sets`

Create an empty change set.

- Leaving out `name` answers `422` with `{"status": "error", "message": "A change set needs a name."}`. An empty name answers `422` with `validation_error`.
- A help center without change sets answers `402 Payment Required` with `reason` `CHANGE_SETS_UNAVAILABLE`.
- A 26th open change set answers `409 Conflict` with `TOO_MANY_OPEN_CHANGE_SETS`: `You already have 25 open change sets. Publish or delete one before starting another.`

## Get a change set

GET`/v1/change-sets/{changeSetId}`

Get a change set with its items and readiness.

A change set that does not exist, belongs to another help center or was deleted answers `404` with `Change set not found.`

## Rename a change set

Change `name`, `description` or both. This works in every status, including after publishing. `null` clears the description. An empty `name` answers `422`.

PATCH`/v1/change-sets/{changeSetId}`

Rename a change set or change its description.

## Delete a change set

Delete the change set itself. Staged changes stay staged, unpublished articles stay unpublished, and categories keep their visibility. A change set that is publishing answers `409` with `CHANGE_SET_NOT_OPEN`.

DELETE`/v1/change-sets/{changeSetId}`

Delete a change set without changing its articles or categories.

## Add items

Send article ids, category ids or both. Each id is handled on its own, so one article in an unexpected state does not stop the others.

POST`/v1/change-sets/{changeSetId}/items`

Add articles and categories to a change set.

| Field | Type | Description |
| --- | --- | --- |
| `article_ids` | array of integers | Up to 500 article ids. |
| `category_ids` | array of integers | Up to 500 category ids. |
| `category_visibility` | string | The visibility the categories get when the change set publishes: `public` (the default), `unlisted`, `team_private` or `private`. In the dashboard these are **Everybody**, **Everybody with a link (hidden from navigation)**, **Team members only** and **Me and selected team members**. It applies to every category in the request. |

- A published article adds one `staged_version` item for each language it has staged. An article that is not published adds one `article_publish` item.
- A published article with nothing staged is not added, and neither is an id that does not exist, belongs to another help center or is in Trash. When anything is not added, the answer is `207 Multi-Status` with `"status": "partial"` and a `rejected` list:

```
{
  "status": "partial",
  "added": 0,
  "rejected": [
    {
      "article_id": 599,
      "reason": "Article 599 is live and has no staged changes, so there is nothing to release."
    },
    {
      "article_id": 999999,
      "reason": "not_found"
    },
    {
      "category_id": 999999,
      "reason": "not_found"
    }
  ],
  "change_set": "…"
}
```

- `added` counts items, so an article with two staged languages adds 2. The response always includes the whole `change_set`, with its readiness.
- Adding an article that is already in the change set updates its items to the current staged copies and counts them in `added` again. Do this after you restage an article, or when a `staged_not_added` warning names it.
- Past 500 items, the request stops with `409` and `CHANGE_SET_FULL`. Items added before that point stay added, and the answer does not list them: read the change set to see where it stopped.
- A change set that is not open answers `409` with `CHANGE_SET_NOT_OPEN`.

This is the only way to change a category's visibility over the API. A typical launch: your team builds the new section in a category shared with **Team members only**, you add that category here with `category_visibility` set to `public`, and the release shows it to everybody.

## Remove an item

Take one item out of the change set. The staged changes, article or category it pointed at stay as they are.

DELETE`/v1/change-sets/{changeSetId}/items/{itemId}`

Take one item out of a change set, leaving the work it points at as it is.

An item id that is not in this change set answers `404` with `Change set not found.` A change set that is not open answers `409` with `CHANGE_SET_NOT_OPEN`.

## Publish or schedule

Send the request with no `scheduled_for` to publish now, or with `scheduled_for` to schedule. Both answer `202 Accepted`, and `status` says which: `queued` comes with a `status_url` to poll; `scheduled` means nothing publishes until the time you set.

POST`/v1/change-sets/{changeSetId}/publish`

Publish a change set now or schedule it, with a 202 answer because the run happens later.

| Field | Type | Description |
| --- | --- | --- |
| `scheduled_for` | string | Optional. When to publish, in the future. ISO 8601 with an offset, such as `2026-10-06T07:00:00Z`. A time without an offset is read as UTC. |
| `release_note` | string | Optional. Up to 500 characters, kept on the change set. |

- **The plan is checked first.** Without change sets, the answer is `402` with `CHANGE_SETS_UNAVAILABLE`.
- **Then readiness.** With a blocker, or with no items, the answer is `422` with `Some items need attention before this change set can publish.` or `There is nothing in this change set to publish.`, and the `readiness` object.
- **A change set that is publishing or published** answers `409` with `CHANGE_SET_NOT_OPEN`.
- **Scheduling.** Scheduled change sets publish at the first minute at or after `scheduled_for`. At that time, HelpCenter.io checks the plan again: if the help center no longer has change sets, the schedule is cleared and the change set stays open. Readiness is not checked again, so an item that conflicts by then fails on its own. Sending publish again replaces the schedule, or publishes at once when you leave out `scheduled_for`.
- **Schedule only `open` change sets.** A `partially_published` or `failed` change set accepts `scheduled_for` and answers `202` with `"status": "scheduled"`, but `is_scheduled` stays `false` and it does not publish at that time. Publish those now instead.
- `scheduled_for` in the past answers `422` with `A change set cannot be scheduled for a time that has passed.`

## Cancel a schedule

Clear `scheduled_for`. The change set stays `open`, with its items.

DELETE`/v1/change-sets/{changeSetId}/schedule`

Cancel a scheduled publish and keep the change set open.

## Follow a publish run

Poll the status until `done` is `true`. Branch on `done`, not on the status string: it is `true` whenever nothing more will happen without you, including after a run stops early.

GET`/v1/change-sets/{changeSetId}/status`

Follow a publish run: poll it until done is true.

| Field | Description |
| --- | --- |
| `change_set_status` | The change set's status. |
| `done` | `true` when the change set is not publishing and not scheduled. A change set that was never published is also `done`; a scheduled one is not, until its run finishes. |
| `processed_items`, `total_items` | Progress of the current or last run. |
| `published_at` | When the first item was published. |
| `items` | Each item's `id`, `type`, `article_id`, `locale`, `variant`, `applied` and `error`. There is no `category_id` here: get the change set to see which category an item is. |

Poll every few seconds: status reads count toward the 120 writes a minute. In a shell, with `jq`:

```
until [ "$(curl -sS "https://api.helpcenter.io/v1/change-sets/$CHANGE_SET_ID/status" \
  -H "apikey: $HELPCENTER_API_KEY" -H "Accept: application/json" | jq .done)" = true ]; do
  sleep 5
done
```

When a run ends `partially_published`, the failed items carry an `error`:

```
{
  "status": "success",
  "change_set_status": "partially_published",
  "done": true,
  "processed_items": 2,
  "total_items": 2,
  "published_at": "2026-09-30T08:21:52+00:00",
  "items": [
    {
      "id": 8,
      "type": "staged_version",
      "article_id": 599,
      "locale": "en",
      "variant": "default",
      "applied": false,
      "error": "The live article changed after version 27 (en) was staged."
    },
    {
      "id": 9,
      "type": "staged_version",
      "article_id": 730,
      "locale": "en",
      "variant": "default",
      "applied": true,
      "error": null
    }
  ]
}
```

### What a run does

- Items apply one at a time, each in its own transaction, in the order they were added. A run is not all or nothing: when an item fails, the items before it stay published and the run goes on. After 5 failures in a row, the run stops.
- Publishing again resumes: items that already applied are skipped. To finish a `partially_published` change set, fix what failed (for a conflict, discard and restage the article, then add it again), and publish again.
- A staged item whose changes were already published or discarded on their own is skipped and counts as applied. So is a category that already has the target visibility.
- Published items send the usual webhooks: `article.updated` for staged changes, `article.published` for articles that go live, `category.updated` for categories.

## Errors

Change-set refusals put their code in `reason`, not `code`: `{"status": "error", "reason": "CHANGE_SET_NOT_OPEN", "message": "…"}`.

| Status | Reason or message | When |
| --- | --- | --- |
| `401` | `Unknown API key.` | An OAuth access token, or a key whose help center was deleted. |
| `401` | `{"status": "unauthorized"}` | No credential, or an unknown key. |
| `402` | `CHANGE_SETS_UNAVAILABLE` | Creating, publishing or scheduling on a help center without change sets. |
| `403` | `insufficient_scope` | A Read only key on a write. |
| `404` | `Change set not found.` | No such change set or item. |
| `409` | `CHANGE_SET_NOT_OPEN` | Adding or removing items, publishing or scheduling when the change set is publishing or published; deleting it while it is publishing. |
| `409` | `CHANGE_SET_FULL` | More than 500 items. |
| `409` | `TOO_MANY_OPEN_CHANGE_SETS` | A 26th open change set. |
| `422` | A message (with `readiness` on a publish), or `validation_error` | Blockers, nothing to publish, no name, or a time in the past. |

For the complete release flow as a program, see [Publish release notes with change sets](https://developers.helpcenter.io/content/publish-release-notes-with-change-sets). For the dashboard side, see [Release many changes together with change sets](https://self.helpcenter.io/content/change-sets).

## Related

- [Staged changes and versions](https://developers.helpcenter.io/content/staged-changes-api)
- [Publish release notes with change sets](https://developers.helpcenter.io/content/publish-release-notes-with-change-sets)
- [Articles](https://developers.helpcenter.io/content/articles-api)
