REST API reference

Design API

Export
Download Markdown Use with AI

Change a help center's design from code: its theme, pages and components, navigation, custom CSS and scripts. You edit the same draft that the template editor opens, a person reviews it there with a live preview, and visitors see nothing until you publish.

Start by reading where the design stands:

curl https://api.helpcenter.io/v1/template \
  -H "apikey: $HELPCENTER_API_KEY" \
  -H "Accept: application/json"

The response names the draft and the live version, gives the link to review the draft, and outlines the draft (trimmed here to the colors and the synced regions):

{
  "status": "success",
  "template": {
    "engine": "v2",
    "site": {
      "id": 391,
      "name": "Acme Help",
      "subdomain": "acme"
    },
    "draft": {
      "id": 223,
      "etag": "\"223-1790778695000-9b93f595\"",
      "updated_at": "2026-09-30T14:31:35+00:00",
      "preset_key": "compass",
      "has_unpublished_changes": false
    },
    "published": {
      "id": 222,
      "status": "published",
      "live": true,
      "message": "Initial template",
      "preset_key": "compass",
      "published_at": "2026-09-30T14:31:28+00:00"
    },
    "urls": {
      "live_site": "https://acme.helpcenter.io",
      "customizer": "https://helpcenter.io/app/sites/acme/customizer-v2",
      "draft_preview": "https://acme.helpcenter.dev"
    }
  },
  "outline": {
    "theme": {
      "colors": {
        "primary": "#24644f",
        "accent": "#edf6f1",
        "fg": "#1c2926",
        "bg": "#ffffff",
        "muted": "#626c68",
        "surface": "#f6f8f6",
        "border": "#e2e8e4"
      }
    },
    "syncedRegions": [
      "footer"
    ],
    "syncedButDiverged": null
  }
}

How it works

  1. Read. GET /v1/template returns an outline of the draft and its etag. Read any part in full with GET /v1/template/draft, and look up what you can change with GET /v1/template/schema.

  2. Start from a theme, if you like. POST /v1/template/themes/{themeKey}/apply copies a theme onto the draft, whole or only some parts of it, such as its colors and fonts.

  3. Edit. PATCH /v1/template/draft applies a batch of operations to the draft, all or nothing.

  4. Review. Send the person responsible for the design to template.urls.customizer. The template editor opens on the same draft, with a live preview, and they can adjust it there.

  5. Publish. POST /v1/template/publish puts the draft live. It's the only call that changes what visitors see.

  • One draft per help center, shared with the template editor. Your edits appear in the editor, and edits made in the editor appear in your next read, so a person can pick up where your code stopped, and the other way round (see Get started with the template editor).

  • Visitors keep seeing the live design until you publish. urls.draft_preview shows the draft as a website too, but only to signed-in members of the help center's team.

  • You can always go back. Discard the draft to return to the live design, or load an earlier version into the draft and review it before it goes live again.

Help centers that use the template editor

The design API works with help centers designed in the template editor. On a help center that hasn't switched to the template editor yet, every endpoint answers 409 Conflict with the code template_migration_required, and customizer_url in the body links to its template editor. The request changes nothing.

Switching is the help center owner's decision, and it happens in the dashboard: the template editor turns the current design into a draft to review, and publishing that draft completes the switch. From then on, the design API works.

Get access

Access to the design is granted separately from access to your content, and checked twice on every request. The design API works on every plan, with an API key or an OAuth access token.

  • An API key must be created with design access. When you create the key, tick Allow access to the help center design under Design (see API keys). A Read only key then holds design.read and can read the design. A Read & write key also holds design.write and can change and publish it. A key created without the option can't reach the design, whatever its scope, and you can't add the option to an existing key: create a new one. Keys with design access carry a Design tag in the list of keys.

  • OAuth apps get design.read and design.write only when they ask for these scopes and the person connecting the app approves them. content.read and content.write don't include them (see Scopes).

  • The person behind the credential must be able to edit the design. Every request checks that the team member who created the key, or who approved the app, can still edit this help center's design in the dashboard: an Owner, Admin or Editor. Translators and Viewers can't. Without that access, the API answers 403 Forbidden with the code design_forbidden, whatever the credential holds. Replace the key with one created by a current Owner or Admin, or connect the app again as someone who can edit the design.

A credential without the scope gets 403 with the code insufficient_scope and a message that names the scope, such as This action requires the design.write scope.

design.write reaches the help center's custom CSS and scripts, which run in every visitor's browser once the design is published. Treat a key with design access like an admin password: keep it on a server, give it only to tools you trust, and delete it when the work is done.

Endpoints

Method and path

What it does

Scope

GET /v1/template

Read where the design stands, and an outline of the draft

design.read

GET /v1/template/draft

Read the draft document, whole or one part

design.read

GET /v1/template/schema

Read the customization guide

design.read

GET /v1/template/themes

List the themes

design.read

GET /v1/template/themes/{themeKey}

Read one theme, and what applying it would change

design.read

POST /v1/template/themes/{themeKey}/apply

Apply a theme to the draft

design.write

PATCH /v1/template/draft

Edit the draft

design.write

POST /v1/template/draft/discard

Discard the draft's changes

design.write

POST /v1/template/publish

Publish the draft

design.write

GET /v1/template/versions

List the published versions

design.read

POST /v1/template/versions/{versionId}/restore

Load an earlier version into the draft

design.write

Every request counts toward your credential's 300 requests a minute, and the five writes also toward its 120 writes a minute (see Rate limits).

The template object

GET /v1/template and every write return template, which says where the design stands:

Field

Type

Description

engine

string

Always v2.

site

object

The help center: id, name (its current name, which becomes the draft's brand.name when the draft is published) and subdomain.

draft.id

integer

The draft. Every publish starts a new draft, with a new id.

draft.etag

string

The draft's current version. Send it with a write to change only the draft you read (see Avoid overwriting someone else's changes, below). The quotes inside it are part of the value.

draft.updated_at

string

When the draft last changed, ISO 8601 with an offset.

draft.preset_key

string

The key of the theme the draft came from, or of the theme last applied to it. Applying only some sections of a theme sets it too, and discarding the draft leaves it as it was.

draft.has_unpublished_changes

boolean

true when the draft differs from the live design.

published

object or null

The live version: id, status, live, message, preset_key and published_at, as described under List the published versions, below.

urls.live_site

string

The live help center, on its custom domain when it has one.

urls.customizer

string

The template editor, open on this draft. Send people here to review your changes and to publish them.

urls.draft_preview

string

The draft as a website. It opens only for signed-in members of the help center's team.

The design document

A help center's design is one document. GET /v1/template/draft returns it, and the customization guide describes every part of it.

Part

What it holds

brand

The help center's name, tagline, logo, dark-mode logo, favicon and main website. When the draft is published, brand.name becomes the help center's name everywhere it appears, including the widget, contact form emails and the API.

theme

Colors for light mode (colors) and dark mode (darkColors), fonts, corner radius and density.

meta

The SEO title and description, social sharing texts and image, text direction and the search engine settings.

pages

Six pages: home, category, article, search, contact and notFound. Each has a layout, its own settings, and six regions: header, hero, sidebar, main, footer and utility. A region holds an ordered list of components, and a Hero or Container holds components of its own.

syncedRegions

The regions every page shares (see Synced regions whose pages differ, below).

globals

navigation, the menu that every Navigation component draws, and scripts, the scripts and snippets injected into the pages.

customCssItems

Labelled blocks of custom CSS. customCss is the stylesheet compiled from the enabled blocks, rebuilt on every change.

schemaVersion, preset

Kept by HelpCenter.io. You can't set them.

A component is {id, type, config, visibleOn, children}. Its id is how operations find it, and how custom CSS targets it, with [data-hc-instance-id="<id>"]. To see how pages, regions and shared headers look in the template editor, see Pages, regions and shared headers explained.

Text you set is in the help center's default language. Translations of design texts are made in the dashboard (see Translate texts in your design). The API reads them but can't edit them. When you move or copy a component, its translations go with it, and when you remove it, they go too.

Read the design

GET/v1/template

Read where the design stands: the draft, the live version, the review link, and an outline of the draft.

The outline holds what you need to decide on an edit, without the full document:

  • brand, theme and meta, in full.

  • pages: for each page, settings with its layout and the settings that differ from the defaults, and regions with each region's components. A component shows its id, type, the settings that differ from its defaults, and its children. Values longer than 120 characters are cut short and large lists are summarized: read them whole with section=component.

  • sharedRegions: the synced regions whose pages are the same, listed once instead of on every page.

  • syncedButDiverged: null, or an object whose regions lists the synced regions whose pages differ, with a note on how to choose. Those regions then appear under each page.

  • styledByCustomCss on a region: the labels of the custom CSS blocks that restyle it. A theme's CSS can lay a region out its own way, and whatever you add there is laid out the same way.

  • navigation, scripts and customCss: lists of ids and labels, without the bodies. presetStartedFrom is the theme the document came from.

If the help center has no draft yet, for example because no one has opened its design since the help center was created, reading starts a new one from the live design.

Read the draft document

GET/v1/template/draft

Read the draft document, whole or one part of it.

Every answer has section, the draft's etag and the part you asked for in data. The whole document of a theme can be large (the Compass theme's is about 85 KB, most of it custom CSS), so read the part you need.

section

Other parameters

data

document (the default)

None

The whole document.

brand, theme, meta

None

That part of the document.

navigation

None

The menu items.

scripts

id, optional

Every script, or the one with that id.

custom_css

id, optional

Every custom CSS block, or the one with that id.

page

page, required

One page: its settings and its six regions.

region

page and region, required

One region: instances, its components, and config, its styling.

component

id, required. page, optional

page, region and component: the component with all its settings and children. Add page when the id is on several pages.

translations

None

The translated design texts, by language code. An empty list, [], when there are none.

An unknown component id answers 404 Not Found with component_not_found, and an unknown script or CSS block id 404 with not_found.

Read the customization guide

GET/v1/template/schema

Read the customization guide: every operation, component, setting and rule the draft accepts, one topic at a time.

The guide is built from the components and settings this API accepts, so it always matches what PATCH /v1/template/draft checks. Read it before you change a component's settings.

topic

What it holds

overview (the default)

The model of pages, regions and components, every operation with an example, an index of the components (what each is for, and on which pages and in which regions it can go), this help center's languages, FAQ sections and uploaded fonts, and the limits.

components

Every setting of the component types you list in types, comma-separated, up to 8, such as types=Hero,SearchBar: its type, allowed values or range, default and description, and what the component can hold. Without types, the index of components.

settings

Every path the set operation accepts, with the values it takes, and the region styling update_region takes.

theme

The color tokens, font formats, radius and density, and the CSS variables they become.

css

How custom CSS is scoped, the hooks for components and regions, dark mode, and what is removed.

scripts

Script positions, kinds, page filters and safety.

navigation

The format of menu items.

List themes

GET/v1/template/themes

List the themes you can apply to the draft.

Each theme has a key and name, a tagline, summary and best_for, its light and dark palettes, fonts and density, a preview_image and, for some, an industry. current is true for the theme in the draft's preset_key. The blank theme is an empty canvas: default colors, and no components except an ArticleMetadata on the article page and a ContactForm on the contact page.

Read a theme

GET/v1/template/themes/{themeKey}

Read one theme, and which sections of the draft applying it would change.

One theme adds its description, screenshots of the home, category, article and search pages and of dark mode and a phone, and anatomy, what each page is built from. changed_sections lists the sections of your draft that applying the theme would change, and sections every section you can apply.

Apply a theme

POST/v1/template/themes/{themeKey}/apply

Copy a theme onto the draft: every section, or only the ones you name.

Field

Type

Description

sections

array of strings

The parts of the theme to take. Default: every section, brand and meta included (see Name your sections, below). Any of brand, theme.colors, theme.darkColors, theme.fonts, theme.radius, theme.density, pages.home, pages.category, pages.article, pages.search, pages.contact, pages.notFound, globals.navigation, globals.scripts, meta and customCss.

etag

string

Optional. The draft's etag from your last read or write.

  • Each section you apply replaces that part of the draft with the theme's. Applying a page replaces its components, with new ids, so read the draft again before you edit them. Applying customCss replaces your custom CSS blocks. warnings says when either happened.

  • To borrow a theme's look without its layout, apply only theme.colors, theme.darkColors, theme.fonts, theme.radius and theme.density.

  • Name your sections. Without sections, every section is applied, brand and meta included: the draft gets the theme's placeholder name and tagline instead of yours, such as Help Center, loses your logo, dark-mode logo and favicon, and takes the theme's SEO title and description, with the no-index setting off. Once published, that placeholder name becomes your help center's name. The template editor leaves brand out unless someone ticks it.

  • The draft's preset_key becomes the theme's key, even when you apply only some sections. Nothing changes for visitors until you publish.

Edit the draft

PATCH/v1/template/draft

Edit the draft with a batch of up to 50 operations, applied in order and all or nothing.

Field

Type

Description

operations

array

Required. 1 to 50 operations, each an object with an op and that operation's fields.

etag

string

Optional. The draft's etag from your last read or write.

  • In order. Each operation sees what the ones before it did. Give a new container your own id, and you can add components into it in the same batch.

  • All or nothing. The first operation that can't be applied fails the whole batch with 422 Unprocessable Entity and template_invalid, and nothing is saved, not even the operations before it.

  • The template editor's rules. A component goes only on the pages and in the regions it's made for, a component that's allowed once per region stays unique, a container takes only the component types it accepts, and a setting takes only the values the template editor offers.

  • What you get back. results has one entry per operation, in order: operation (its position in the batch, from 0), op, and what it did, such as the id of a new component, CSS block or script, or the paths a set changed. template carries the new etag.

  • Warnings. warnings points out what is allowed but probably not what you want, such as a script that every visitor will run, a Hero with an image background and no image, a Logo that shows an image when brand.logoAssetId is empty, or a batch that left the draft as it was.

Operations

Operation

Fields

What it does

set

path, value

Changes document settings: brand.*, theme.colors.*, theme.darkColors.*, theme.fonts.*, theme.radius.*, theme.density, meta.* and pages.<page>.<setting>. An object sets several at once: {"op": "set", "path": "theme.colors", "value": {"primary": "#1d4ed8", "accent": "#dbeafe"}}. null empties a setting that can be empty. The guide's settings topic lists every path.

add_component

page, region, type, config, index, parent_id, id

Adds a component. Settings you leave out in config get the component's defaults. index is its position from 0; leave it out to add it at the end. parent_id puts it inside a Hero or Container, whose page and region it takes. id is optional: 1 to 64 letters, digits, - or _, starting with a letter or digit, and not used elsewhere in the draft. A new Hero comes with a SearchBar inside it.

update_component

id, config, page

Changes some of a component's settings. null puts a setting back to its default. Add page when the id is on several pages of a region that isn't synced.

remove_component

id, page

Removes a component and everything inside it.

move_component

id, region, parent_id, index, page

Moves a component on its page: to another region, into a container with parent_id, or out to the top level with "parent_id": null. With only id and index, it reorders the component among its neighbors, inside a container too.

update_region

page, region, config

Styles a region: background (a hex color or a token such as var(--hc-color-primary)), padding (compact, standard or tall), borderBottom (true or false) and align (left or center). null resets one.

sync_region

region, enabled, source_page

true makes every page share the region and copies the source_page's version (default home) over the others. false lets pages differ. Works on header, hero, sidebar, footer and utility: main always belongs to its page.

copy_region

region, from_page, to_pages

Copies a region that isn't synced from one page to others, once. Later edits don't travel.

add_css

css, label, enabled, index, id

Adds a labelled custom CSS block. The stylesheet is rebuilt from the enabled blocks, in order.

update_css

id, css, label, enabled, index

Changes a block, or moves it with index. "enabled": false keeps the block without applying it.

remove_css

id

Deletes a block.

add_script

position, and one of src, inlineBody or htmlBody. Optional: label, pages, enabled, defer, async, integrity, index, id

Injects a script at headStart, headEnd, bodyStart or bodyEnd: an external https URL in src, JavaScript without <script> tags in inlineBody, or raw HTML such as a vendor snippet or a verification <meta> tag in htmlBody. pages limits it to some pages; null runs it on every page.

update_script

id, and any field of add_script

Changes a script. Sending a different body field switches it to that kind.

remove_script

id

Deletes a script.

set_navigation

items

Replaces the whole menu: up to 30 items, each {id, label, href, type, cssClass, children}, with one level of children, up to 20 per item. type is link (the default), category or external. Keep an item's id to keep its translations. An empty list clears the menu.

  • Every operation refuses fields it doesn't know, and the error names them.

  • A custom CSS block, and all blocks together, can hold up to 204,800 bytes. A block that would render as nothing is refused. @import, @charset, @namespace, expression(), behavior: and javascript: URLs are removed when the page renders, and a warning says so. See Custom CSS reference: selectors and examples.

  • A script body can hold up to 51,200 bytes.

When an operation fails

A batch that can't be applied answers 422, and nothing is saved:

{
  "status": "error",
  "code": "template_invalid",
  "message": "Nothing was saved. operation 1 (set), theme.colors.primary: must be a hex colour like \"#635bff\" (or \"#rrggbbaa\") (got \"blue\")",
  "errors": [
    {
      "operation": 1,
      "op": "set",
      "path": "theme.colors.primary",
      "message": "must be a hex colour like \"#635bff\" (or \"#rrggbbaa\") (got \"blue\")"
    }
  ]
}

errors lists what went wrong, and message sums it up in one paragraph. Each error names the operation (its position in the batch, from 0), the op, the path of the field or setting, and in its message what would have been accepted. One operation can report several errors, for example a set with two invalid colors. An unknown component, CSS block or script id inside a batch is a template_invalid error too, not a 404.

A request that isn't a batch at all, with no operations or more than 50 of them, answers 422 with "status": "validation_error" instead.

Synced regions whose pages differ

A synced region is one region that every page shares: edit it on any page, and the edit is copied to all six pages, under the same component ids. header and footer are often synced.

A synced region can still hold different content on different pages. Some themes, for example, give category and article pages a header with a search field that the home page doesn't have. Editing such a region would copy one page's version over all the others, so the API refuses the edit until you choose what should happen. The outline lists these regions in syncedButDiverged.regions, and the refusal says how to choose:

{
  "status": "error",
  "code": "template_invalid",
  "message": "Nothing was saved. operation 0 (update_component), region: the header region is synced (one header shared by every page), but its pages currently differ: category, article have a different header from the home page. Editing it would copy the home page's header over all of them. Choose first, in this batch or a new one: {\"op\": \"sync_region\", \"region\": \"header\", \"enabled\": false} to keep each page's header and edit them separately (then name the page on each edit), or {\"op\": \"sync_region\", \"region\": \"header\", \"enabled\": true, \"source_page\": \"<page>\"} to make every page use that page's header.",
  "errors": [
    {
      "operation": 0,
      "op": "update_component",
      "path": "region",
      "message": "the header region is synced (one header shared by every page), but its pages currently differ: category, article have a different header from the home page. Editing it would copy the home page's header over all of them. Choose first, in this batch or a new one: {\"op\": \"sync_region\", \"region\": \"header\", \"enabled\": false} to keep each page's header and edit them separately (then name the page on each edit), or {\"op\": \"sync_region\", \"region\": \"header\", \"enabled\": true, \"source_page\": \"<page>\"} to make every page use that page's header."
    }
  ]
}
  • Keep each page's version: {"op": "sync_region", "region": "header", "enabled": false}. Each page keeps its own copy, and the component ids don't change. Where the same id is on several pages, name the page in the edit: without it, the edit is refused and asks which page you mean.

  • Make every page use one page's version: {"op": "sync_region", "region": "header", "enabled": true, "source_page": "category"}. The other pages lose what their own version had, and a warning says so.

You can choose and edit in the same batch:

{
  "operations": [
    {
      "op": "sync_region",
      "region": "header",
      "enabled": false
    },
    {
      "op": "update_component",
      "id": "01M3SC61SPBDHVRP65G50W2F8J",
      "page": "home",
      "config": {
        "variant": "text-only"
      }
    }
  ]
}

Strings are stored as you send them

Unlike the rest of the API (see Requests and responses), the design endpoints don't trim strings and don't turn empty strings into null:

  • An empty string clears a text. {"op": "update_component", "id": "01M3SBMGRXRW19W78F2THFE327", "config": {"subheading": ""}} removes a Hero's subheading.

  • null puts a component setting back to its default. The same operation with "subheading": null brings back the Hero's default subheading, "Search articles, browse categories, or contact us."

  • Settings that can't be empty refuse it. brand.name and meta.title refuse an empty string, null and a string of only spaces.

  • Spaces and line breaks are kept in Markdown, CSS and scripts, so an indented code line in a RichText component stays indented.

Avoid overwriting someone else's changes

People can edit the draft in the template editor while your code works on it. Every write takes an optional etag to keep you from building on a draft you haven't seen:

  1. Read the etag: template.draft.etag from GET /v1/template or any write, or etag from GET /v1/template/draft. The quotes inside it are part of the value: send it back exactly, as a JSON string.

  2. Send it with your write. If the draft changed since you read it, because someone saved in the template editor, another integration wrote to it or someone published, nothing changes and the API answers 412 Precondition Failed with stale_draft and the current etag.

  3. Read the draft again, make your change on top of what is there now, and retry.

{
  "status": "error",
  "code": "stale_draft",
  "message": "The draft changed since you read it (someone edited it in the Template Editor, or another agent did), so nothing was changed. Read the template again and re-apply your change on top of what is there now.",
  "etag": "\"223-1790779485000-880c4754\""
}
  • Every write returns the draft's new etag in template.draft.etag, so a series of writes can pass it along without reading in between.

  • Without etag, a write applies to the draft as it is at that moment. Two writes never mix, but yours can land on changes you haven't seen.

  • Every publish starts a new draft, so an etag from before a publish always answers 412.

  • The template editor checks the same etag. If a person saves there after your write, the editor says This draft was changed elsewhere. and offers Reload, which keeps your changes, or Overwrite, which replaces them with theirs.

Discard the draft

POST/v1/template/draft/discard

Throw the draft's changes away: the draft goes back to the live design.

Discarding resets the draft to the live design, like Reset draft in the template editor. Every unpublished change is lost, including changes people made in the template editor, and it can't be undone. The draft keeps its id, and its preset_key stays as it was. The body is optional: send etag to discard only the draft you read.

Publish the draft

POST/v1/template/publish

Publish the draft: visitors see it at once, it's recorded as a version, and a new draft starts from it.

Field

Type

Description

message

string

Optional. A note of up to 300 characters that says what changed. It becomes the version's message, and your team sees it in the template editor's Version history.

etag

string

Optional, and recommended: the etag of the draft that was reviewed. If the draft changed after the review, nothing is published and you get 412.

  • The whole draft goes live, including changes people made in the template editor. The version that was live becomes an earlier version, and the draft becomes the new live version, keeping the draft's id. A new draft starts from it, with a new id and etag.

  • The brand name follows. brand.name becomes the help center's name everywhere, including the widget, contact form emails and the API.

  • Nothing to publish. A draft that is the same as the live design answers 409 Conflict with nothing_to_publish.

  • Publish after the review. Publishing changes what visitors see, so publish once the person responsible for the design has reviewed the draft and wants it live (see Publish your design).

List the published versions

GET/v1/template/versions

List the published versions of the design, newest first: the live one and up to 49 earlier ones.

The list is newest first and holds up to 50 versions: the live one and earlier ones.

Field

Type

Description

id

integer

The version. A draft keeps its id when it's published.

status

string

published for the live version, archived for earlier ones.

live

boolean

true for the version visitors see.

message

string or null

The note it was published with.

preset_key

string

The draft's preset_key when it was published.

published_at

string

When it was published, ISO 8601 with an offset.

published_by

string or null

The name of the team member who published it, or null when it isn't known. Only in this list.

Restore a version into the draft

POST/v1/template/versions/{versionId}/restore

Load an earlier version into the draft, for review. It goes live only when you publish.

  • It goes to the draft, not live. The version, with its translated texts, replaces what the draft holds. Visitors see it only after you publish, so a restore is reviewed like any other change. In the template editor, Restore publishes at once; over the API it doesn't.

  • Any version in the list works, the live one included. An id that isn't a version of this help center answers 404 with version_not_found.

  • The draft's changes are replaced. The body is optional: send etag to restore only onto the draft you read.

Try it end to end

This program reads the design, changes the draft's colors with the etag it read, reads again and retries if someone changed the draft in between, and prints the link to review the result. It doesn't publish. Run it with a Read & write key created with design access in HELPCENTER_API_KEY:

// Change the theme colors in the design draft, then print the link to review it.
// Visitors see nothing until someone publishes. Node.js 18 or later, no dependencies.
const API = 'https://api.helpcenter.io/v1';
const COLORS = { primary: '#1d4ed8', accent: '#dbeafe' };

async function api(method, path, body) {
  const res = await fetch(API + path, {
    method,
    headers: {
      apikey: process.env.HELPCENTER_API_KEY,
      Accept: 'application/json',
      'Content-Type': 'application/json',
    },
    body: body === undefined ? undefined : JSON.stringify(body),
  });
  return { status: res.status, data: await res.json() };
}

async function main() {
  for (let attempt = 1; attempt <= 3; attempt++) {
    // 1. Read where the design stands: the outline, and the draft's etag.
    const read = await api('GET', '/template');
    if (read.status !== 200) throw new Error(`GET /template: ${read.status} ${JSON.stringify(read.data)}`);
    console.log(`Primary color in the draft: ${read.data.outline.theme.colors.primary}`);

    // 2. Change the colors, but only if nobody changed the draft since you read it.
    const edit = await api('PATCH', '/template/draft', {
      etag: read.data.template.draft.etag,
      operations: [{ op: 'set', path: 'theme.colors', value: COLORS }],
    });
    if (edit.status === 412) {
      console.log('The draft changed since it was read. Reading it again.');
      continue;
    }
    if (edit.status !== 200) throw new Error(`PATCH /template/draft: ${edit.status} ${JSON.stringify(edit.data)}`);

    console.log(`Changed ${edit.data.results[0].paths.join(', ')}`);
    for (const warning of edit.data.warnings) console.log(`Warning: ${warning}`);
    // 3. A person reviews the draft in the template editor, then publishes it.
    console.log(`Review it in the template editor: ${edit.data.template.urls.customizer}`);
    return;
  }
  throw new Error('The draft kept changing. Try again later.');
}

main().catch((err) => {
  console.error(err.message);
  process.exit(1);
});
# Change the theme colors in the design draft, then print the link to review it.
# Visitors see nothing until someone publishes. Python 3.8 or later, standard library only.
import json
import os
import sys
import urllib.error
import urllib.request

API = "https://api.helpcenter.io/v1"
COLORS = {"primary": "#1d4ed8", "accent": "#dbeafe"}


def api(method, path, body=None):
    request = urllib.request.Request(
        API + path,
        method=method,
        data=None if body is None else json.dumps(body).encode(),
        headers={
            "apikey": os.environ["HELPCENTER_API_KEY"],
            "Accept": "application/json",
            "Content-Type": "application/json",
        },
    )
    try:
        with urllib.request.urlopen(request) as response:
            return response.status, json.load(response)
    except urllib.error.HTTPError as err:
        return err.code, json.loads(err.read() or b"{}")


for attempt in range(3):
    # 1. Read where the design stands: the outline, and the draft's etag.
    status, read = api("GET", "/template")
    if status != 200:
        sys.exit(f"GET /template: {status} {read}")
    print(f"Primary color in the draft: {read['outline']['theme']['colors']['primary']}")

    # 2. Change the colors, but only if nobody changed the draft since you read it.
    status, edit = api("PATCH", "/template/draft", {
        "etag": read["template"]["draft"]["etag"],
        "operations": [{"op": "set", "path": "theme.colors", "value": COLORS}],
    })
    if status == 412:
        print("The draft changed since it was read. Reading it again.")
        continue
    if status != 200:
        sys.exit(f"PATCH /template/draft: {status} {edit}")

    print("Changed " + ", ".join(edit["results"][0]["paths"]))
    for warning in edit["warnings"]:
        print(f"Warning: {warning}")
    # 3. A person reviews the draft in the template editor, then publishes it.
    print(f"Review it in the template editor: {edit['template']['urls']['customizer']}")
    break
else:
    sys.exit("The draft kept changing. Try again later.")
<?php
// Change the theme colors in the design draft, then print the link to review it.
// Visitors see nothing until someone publishes. PHP 8 or later with the curl extension.

const API = 'https://api.helpcenter.io/v1';
const COLORS = ['primary' => '#1d4ed8', 'accent' => '#dbeafe'];

function api(string $method, string $path, ?array $body = null): array
{
    $ch = curl_init(API . $path);
    curl_setopt_array($ch, [
        CURLOPT_CUSTOMREQUEST => $method,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_HTTPHEADER => [
            'apikey: ' . getenv('HELPCENTER_API_KEY'),
            'Accept: application/json',
            'Content-Type: application/json',
        ],
    ]);
    if ($body !== null) {
        curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
    }
    $data = json_decode((string) curl_exec($ch), true);

    return [curl_getinfo($ch, CURLINFO_RESPONSE_CODE), $data];
}

for ($attempt = 1; $attempt <= 3; $attempt++) {
    // 1. Read where the design stands: the outline, and the draft's etag.
    [$status, $read] = api('GET', '/template');
    if ($status !== 200) {
        fwrite(STDERR, "GET /template: $status " . json_encode($read) . "\n");
        exit(1);
    }
    echo 'Primary color in the draft: ' . $read['outline']['theme']['colors']['primary'] . "\n";

    // 2. Change the colors, but only if nobody changed the draft since you read it.
    [$status, $edit] = api('PATCH', '/template/draft', [
        'etag' => $read['template']['draft']['etag'],
        'operations' => [['op' => 'set', 'path' => 'theme.colors', 'value' => COLORS]],
    ]);
    if ($status === 412) {
        echo "The draft changed since it was read. Reading it again.\n";
        continue;
    }
    if ($status !== 200) {
        fwrite(STDERR, "PATCH /template/draft: $status " . json_encode($edit) . "\n");
        exit(1);
    }

    echo 'Changed ' . implode(', ', $edit['results'][0]['paths']) . "\n";
    foreach ($edit['warnings'] as $warning) {
        echo "Warning: $warning\n";
    }
    // 3. A person reviews the draft in the template editor, then publishes it.
    echo 'Review it in the template editor: ' . $edit['template']['urls']['customizer'] . "\n";
    exit(0);
}

fwrite(STDERR, "The draft kept changing. Try again later.\n");
exit(1);

It prints:

Primary color in the draft: #635bff
Changed theme.colors.primary, theme.colors.accent
Review it in the template editor: https://helpcenter.io/app/sites/acme/customizer-v2

After the review, when the person asks for it, publish the draft:

curl -X POST https://api.helpcenter.io/v1/template/publish \
  -H "apikey: $HELPCENTER_API_KEY" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{"message": "New brand colors"}'

Errors

Status

Code

When

400

site_required

An OAuth token reaches several help centers and the request named none (see Choose which help center a request acts on).

401

None: {"status": "unauthorized"}

No key or token, or one the API doesn't accept.

403

insufficient_scope

The credential lacks design.read or design.write. Content scopes don't include them.

403

design_forbidden

The person behind the credential can't edit the design in the dashboard.

403

site_forbidden

An OAuth token named a help center it doesn't reach.

404

unknown_theme

No theme with that key. The message lists the keys.

404

component_not_found, not_found

Reading a component, script or custom CSS block that isn't in the draft.

404

version_not_found

No version with that id on this help center.

409

template_migration_required

The help center doesn't use the template editor yet.

409

nothing_to_publish

Publishing a draft that is the same as the live design.

412

stale_draft

The draft changed since the etag you sent. Nothing changed.

422

template_invalid

An operation couldn't be applied. Nothing was saved.

422

None: "status": "validation_error"

The request itself is invalid: an unknown section, topic or theme section, a missing page, region or id, no operations, or more than 50.

429

None

Too many requests. Wait for Retry-After seconds (see Rate limits).

For the shapes that every endpoint shares, see Errors.

Was this article helpful?