# Export

_Category: REST API reference_

`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](https://developers.helpcenter.io/content/categories-api), 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](https://developers.helpcenter.io/content/articles-api), 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](https://developers.helpcenter.io/content/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](https://developers.helpcenter.io/content/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](https://developers.helpcenter.io/content/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. |

## Related

- [Back up your help center](https://developers.helpcenter.io/content/back-up-your-help-center)
- [Pagination](https://developers.helpcenter.io/content/pagination)
- [Articles](https://developers.helpcenter.io/content/articles-api)
