REST API reference

Export

Export
Download Markdown Use with AI

GET /v1/export hands you your whole help center, a page at a time: every category on the first page, then every article with its full content, in id order. Use it for backups and for the first sync of an integration, then stay current with updated_since and webhooks.

Export your content

GET/v1/export

Export the help center page by page: every category on the first page, then every article with its full content.

Parameter

Type

Description

cursor

integer

Default 0, the first page. Then send the cursor.next of the previous page.

limit

integer

Articles per page. Default 100, from 1 to 200.

Any API key can export, a Read only key included. An OAuth token needs content.read.

The response

Field

Type

Description

snapshot.generated_at

string

When this page was read, in ISO 8601 with its offset.

snapshot.site_id

integer

The help center's id.

snapshot.helpcenter_id

string

The help center's stable identifier, the uuid of GET /v1/sites.

categories

array

Every category, in the shape of Categories, ordered by id. Only on the first page (cursor 0); later pages send [].

articles

array

The next articles by id, in the shape of Articles, with every language and full content.

cursor.has_more

boolean

true when more articles follow.

cursor.next

integer or null

The id of the last article on this page: send it as cursor for the next page. null on the last page.

Walk every page

Start with cursor=0, keep the categories from the first page, and follow cursor.next while cursor.has_more is true. This script saves everything to helpcenter-export.json and waits when the API asks it to slow down:

// Export every category and article to helpcenter-export.json.
const fs = require('fs');

const API = 'https://api.helpcenter.io/v1';
const headers = { apikey: process.env.HELPCENTER_API_KEY, Accept: 'application/json' };
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

async function getPage(cursor) {
  for (;;) {
    const res = await fetch(`${API}/export?cursor=${cursor}&limit=100`, { headers });
    if (res.status === 429) {
      await sleep(Number(res.headers.get('Retry-After') || 60) * 1000);
      continue;
    }
    if (!res.ok) throw new Error(`Export failed: HTTP ${res.status}`);
    return res.json();
  }
}

async function main() {
  const backup = { snapshot: null, categories: [], articles: [] };
  let cursor = 0;
  for (;;) {
    const page = await getPage(cursor);
    if (cursor === 0) {
      backup.snapshot = page.snapshot;
      backup.categories = page.categories;
    }
    backup.articles.push(...page.articles);
    if (!page.cursor.has_more) break;
    cursor = page.cursor.next;
  }
  fs.writeFileSync('helpcenter-export.json', JSON.stringify(backup, null, 2));
  const { categories, articles } = backup;
  console.log(`Saved ${categories.length} categories and ${articles.length} articles.`);
}

main().catch((err) => {
  console.error(err);
  process.exit(1);
});
# Export every category and article to helpcenter-export.json.
import json
import os
import time
import urllib.error
import urllib.request

API = "https://api.helpcenter.io/v1"
HEADERS = {"apikey": os.environ["HELPCENTER_API_KEY"], "Accept": "application/json"}


def get_page(cursor):
    url = f"{API}/export?cursor={cursor}&limit=100"
    while True:
        try:
            request = urllib.request.Request(url, headers=HEADERS)
            with urllib.request.urlopen(request) as response:
                return json.load(response)
        except urllib.error.HTTPError as error:
            if error.code != 429:
                raise
            time.sleep(int(error.headers.get("Retry-After", "60")))


backup = {"snapshot": None, "categories": [], "articles": []}
cursor = 0
while True:
    page = get_page(cursor)
    if cursor == 0:
        backup["snapshot"] = page["snapshot"]
        backup["categories"] = page["categories"]
    backup["articles"] += page["articles"]
    if not page["cursor"]["has_more"]:
        break
    cursor = page["cursor"]["next"]

with open("helpcenter-export.json", "w", encoding="utf-8") as f:
    json.dump(backup, f, ensure_ascii=False, indent=2)
categories, articles = len(backup["categories"]), len(backup["articles"])
print(f"Saved {categories} categories and {articles} articles.")
<?php
// Export every category and article to helpcenter-export.json.
$api = 'https://api.helpcenter.io/v1';
$headers = ['apikey: ' . getenv('HELPCENTER_API_KEY'), 'Accept: application/json'];

function getPage(string $api, array $headers, int $cursor): array
{
    while (true) {
        $retryAfter = 60;
        $ch = curl_init("$api/export?cursor=$cursor&limit=100");
        curl_setopt_array($ch, [
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_HTTPHEADER => $headers,
            CURLOPT_HEADERFUNCTION => function ($ch, $line) use (&$retryAfter) {
                if (stripos($line, 'Retry-After:') === 0) {
                    $retryAfter = (int) trim(substr($line, 12));
                }
                return strlen($line);
            },
        ]);
        $body = curl_exec($ch);
        $status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
        if ($status === 429) {
            sleep($retryAfter);
            continue;
        }
        if ($status !== 200) {
            throw new RuntimeException("Export failed: HTTP $status");
        }
        return json_decode($body, true);
    }
}

$backup = ['snapshot' => null, 'categories' => [], 'articles' => []];
$cursor = 0;
while (true) {
    $page = getPage($api, $headers, $cursor);
    if ($cursor === 0) {
        $backup['snapshot'] = $page['snapshot'];
        $backup['categories'] = $page['categories'];
    }
    array_push($backup['articles'], ...$page['articles']);
    if (!$page['cursor']['has_more']) {
        break;
    }
    $cursor = $page['cursor']['next'];
}

$flags = JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE;
$json = json_encode($backup, $flags);
file_put_contents('helpcenter-export.json', $json);
$categories = count($backup['categories']);
$articles = count($backup['articles']);
echo "Saved $categories categories and $articles articles.\n";

It prints how much it saved, for example Saved 4 categories and 11 articles. For a scheduled backup that keeps dated copies, see Back up your help center.

What the export includes

  • Included: every category, whatever its privacy, and every article that is not in Trash: drafts, private and link-only articles, and articles in categories readers can't see.

  • Article content is the same as the Articles API returns: HTML in every language, with internal notes, the highlights of team notes and AI drafts waiting for review removed.

  • Not included: articles in Trash, image files (only their addresses in the HTML), versions, comments, team notes, interface texts, your design and your settings. To keep copies of images, download them from the addresses in the HTML, or list them with the Images API.

An export is not frozen at one moment: each page reads your content as it is when you ask for it. The id cursor never skips or repeats an article, but an article created during the export appears if its id comes after the cursor, and a change to an article you already exported is not in your copy. Run exports when nobody is making large changes, or catch up afterwards with GET /v1/articles, sending the snapshot.generated_at of your first page as updated_since. URL-encode it: an unencoded + in its offset makes the date invalid.

Rate limit

Export has a limit of its own: 30 requests a minute from one IP address. That count is shared with bulk article imports and image uploads from the same address, so a busy import slows an export down, and the other way round. Every export request also counts against your credential's 300 requests a minute.

Over the limit you get 429 Too Many Requests with {"message": "Too Many Attempts."} and a Retry-After header with the seconds to wait, as the script above handles. At the default page size of 100 articles, 30 requests a minute cover 3,000 articles. See Rate limits for the other limits.

Errors

Status

When

401

The credential is missing or not valid: {"status": "unauthorized"}.

422

cursor is not a whole number from 0, or limit is not from 1 to 200. The body is {"status": "validation_error", "message": "The request could not be accepted.", "errors": {...}}, with or without Accept: application/json.

429

Over the rate limit. Wait Retry-After seconds.

Was this article helpful?