Getting started

Rate limits

Export
Download Markdown Use with AI

The API limits how many requests you can make in a minute: per credential for every request, per IP address for a few heavy endpoints, and at the edge of HelpCenter.io's network. This page lists every limit, shows how to read your remaining budget from the response headers, and gives you a helper that waits exactly as long as the API asks.

Limits per credential

Every API key has its own budget. So does every OAuth app within a HelpCenter.io account: all the users and help centers of that account share it.

Limit

Requests a minute

Counts

All requests

300

Every request to /v1.

Writes

120

Every POST, PUT, PATCH and DELETE, except the bulk and image-upload endpoints in the next section. Also these reads: GET /v1/articles/{id}, GET /v1/articles/{id}/staged, GET /v1/articles/{id}/versions, and every GET under /v1/change-sets.

Analytics

6

GET /v1/analytics/summary, GET /v1/analytics/content and GET /v1/analytics/searches, together.

A request counts against every limit that applies to it, and the tightest one decides. A PATCH spends from both the 300 and the 120. Reading one article with GET /v1/articles/{id} counts as a write, so fetching articles one by one runs into the 120 long before the 300: list them with GET /v1/articles, which returns full articles, 100 at a time.

Requests without a valid key or token share a budget of 300 a minute per IP address. The limit is checked before the credential, so a burst of requests with a wrong key starts getting 429 instead of 401.

Limits per IP address

Five endpoints that move a lot of data have a limit per IP address as well:

Endpoint

Requests a minute

GET /v1/export

30

POST /v1/images

60

POST /v1/images/uploads

60

POST /v1/images/bulk

20

POST /v1/articles/bulk

20

These five share one counter per IP address, and each compares that count with its own limit. After 20 image uploads in a minute, the next POST /v1/articles/bulk from the same address gets 429 until the minute is over. The counter covers everyone behind that address, such as an office network or a shared build server, whichever key they use. Uploading a file to an upload URL from POST /v1/images/uploads has a separate limit of 60 a minute per IP address, and doesn't count against the per-credential limits.

These limits apply on top of the per-credential ones, and the bulk and image-upload endpoints don't count against the 120 writes.

The edge limit

Before a request reaches the API, HelpCenter.io's web servers allow each IP address about 2 requests a second, with bursts of up to 100, and 20 open connections at a time. Sustained, that is about 120 requests a minute from one address, fewer than the 300 a credential allows, so spread large jobs over time. A request refused here gets a 429 with an HTML page and no Retry-After header.

Read your remaining budget

Responses from the API carry two headers about the limit you are closest to on that endpoint:

Header

Meaning

X-RateLimit-Limit

The size of that limit: 300, 120, 6, or a per-IP limit.

X-RateLimit-Remaining

How many requests it still allows this minute.

Retry-After

On a 429: the seconds to wait before the limit lets you in again.

X-RateLimit-Reset

On a 429: when the limit starts over, as a Unix timestamp.

For example, GET /v1/articles reports the 300 limit, and GET /v1/articles/{id} reports the 120, because it counts as a write:

X-RateLimit-Limit: 120
X-RateLimit-Remaining: 119

Each limit counts in a window of one minute that starts with your first request, then starts over. Requests that never reach an endpoint, such as a wrong path or method, carry no rate-limit headers. Browser code can't read these headers.

When you hit a limit

The API answers 429 Too Many Requests. Here the 121st single-article read of the minute:

HTTP/1.1 429 Too Many Requests
Content-Type: application/json
Retry-After: 31
X-RateLimit-Limit: 120
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1790756655

{"message": "Too Many Attempts."}

That JSON body needs Accept: application/json; without it, the same 429 is an HTML page. The analytics limit answers with its own code and explanation, whatever you send:

{
  "status": "error",
  "code": "analytics_rate_limited",
  "message": "Analytics endpoints are limited to 6 requests per minute per credential. These figures are computed daily — re-reading the same range more often returns the same numbers, so wait rather than retrying in a loop."
}

Either way, wait for Retry-After seconds, then send the request again. Retrying sooner only gets another 429.

Retry with Retry-After

This helper sends a request, and when the answer is 429, waits for Retry-After seconds and tries again, up to five times. When the header is missing, as with the edge limit, it backs off for 1, 2, 4, 8 and then 16 seconds. It adds up to a second of random delay, so parallel workers don't retry at the same moment. Other statuses come back to you as they are.

// A request helper for the HelpCenter.io API that retries 429 responses,
// waiting as long as the Retry-After header asks.
// Node.js 18 or later, no dependencies. Run: node helpcenter-request.js
const API = 'https://api.helpcenter.io/v1';
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

async function helpcenterRequest(method, path, body, { maxRetries = 5 } = {}) {
  const headers = { apikey: process.env.HELPCENTER_API_KEY, Accept: 'application/json' };
  if (body !== undefined) headers['Content-Type'] = 'application/json';

  for (let attempt = 0; ; attempt++) {
    const res = await fetch(API + path, {
      method,
      headers,
      body: body === undefined ? undefined : JSON.stringify(body),
    });
    if (res.status !== 429 || attempt >= maxRetries) return res;

    // Retry-After is in seconds. Without it, back off: 1, 2, 4, 8, 16 seconds.
    // Up to a second of jitter keeps parallel workers from retrying in step.
    const retryAfter = Number(res.headers.get('retry-after'));
    const seconds = retryAfter > 0 ? retryAfter : 2 ** attempt;
    await sleep((seconds + Math.random()) * 1000);
  }
}

module.exports = { helpcenterRequest };

if (require.main === module) {
  (async () => {
    const res = await helpcenterRequest('GET', '/articles?limit=1');
    const limit = res.headers.get('x-ratelimit-limit');
    const remaining = res.headers.get('x-ratelimit-remaining');
    console.log(`${res.status}: ${remaining} of ${limit} requests left this minute.`);
    if (!res.ok) process.exit(1);
  })().catch((err) => {
    console.error(err.message);
    process.exit(1);
  });
}
# A request helper for the HelpCenter.io API that retries 429 responses,
# waiting as long as the Retry-After header asks.
# Python 3.8 or later, standard library only. Run: python3 helpcenter_request.py
import json
import os
import random
import time
import urllib.error
import urllib.request

API = "https://api.helpcenter.io/v1"


def helpcenter_request(method, path, body=None, max_retries=5):
    """Return (status, headers, body): body is parsed JSON, or text if it isn't JSON."""
    headers = {"apikey": os.environ["HELPCENTER_API_KEY"], "Accept": "application/json"}
    data = None
    if body is not None:
        data = json.dumps(body).encode("utf-8")
        headers["Content-Type"] = "application/json"

    for attempt in range(max_retries + 1):
        request = urllib.request.Request(
            API + path, data=data, method=method, headers=headers
        )
        try:
            with urllib.request.urlopen(request) as response:
                return response.status, response.headers, _parse(response.read())
        except urllib.error.HTTPError as err:
            if err.code != 429 or attempt == max_retries:
                return err.code, err.headers, _parse(err.read())
            # Retry-After is in seconds. Without it, back off: 1, 2, 4, 8, 16 seconds.
            # Up to a second of jitter keeps parallel workers from retrying in step.
            retry_after = err.headers.get("Retry-After", "")
            seconds = int(retry_after) if retry_after.isdigit() else 0
            time.sleep((seconds if seconds > 0 else 2 ** attempt) + random.random())


def _parse(raw):
    text = raw.decode("utf-8", "replace")
    try:
        return json.loads(text)
    except ValueError:
        return text


if __name__ == "__main__":
    status, headers, body = helpcenter_request("GET", "/articles?limit=1")
    limit = headers.get("X-RateLimit-Limit")
    remaining = headers.get("X-RateLimit-Remaining")
    print(f"{status}: {remaining} of {limit} requests left this minute.")
    raise SystemExit(0 if status < 400 else 1)
<?php
// A request helper for the HelpCenter.io API that retries 429 responses,
// waiting as long as the Retry-After header asks.
// PHP 8 or later with the curl extension. Run: php helpcenter-request.php

/**
 * Returns [status, headers with lowercase names, body].
 */
function helpcenter_request(
    string $method,
    string $path,
    ?array $body = null,
    int $maxRetries = 5
): array {
    $headers = ['apikey: ' . getenv('HELPCENTER_API_KEY'), 'Accept: application/json'];
    if ($body !== null) {
        $headers[] = 'Content-Type: application/json';
    }

    for ($attempt = 0; ; $attempt++) {
        $responseHeaders = [];
        $ch = curl_init('https://api.helpcenter.io/v1' . $path);
        curl_setopt_array($ch, [
            CURLOPT_CUSTOMREQUEST => $method,
            CURLOPT_HTTPHEADER => $headers,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_HEADERFUNCTION => function ($ch, $line) use (&$responseHeaders) {
                $parts = explode(':', $line, 2);
                if (count($parts) === 2) {
                    $responseHeaders[strtolower(trim($parts[0]))] = trim($parts[1]);
                }
                return strlen($line);
            },
        ]);
        if ($body !== null) {
            curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
        }
        $raw = (string) curl_exec($ch);
        $status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);

        if ($status !== 429 || $attempt >= $maxRetries) {
            return [$status, $responseHeaders, json_decode($raw, true) ?? $raw];
        }

        // Retry-After is in seconds. Without it, back off: 1, 2, 4, 8, 16 seconds.
        // Up to a second of jitter keeps parallel workers from retrying in step.
        $retryAfter = (int) ($responseHeaders['retry-after'] ?? 0);
        $seconds = $retryAfter > 0 ? $retryAfter : 2 ** $attempt;
        usleep((int) (($seconds + mt_rand(0, 1000) / 1000) * 1000000));
    }
}

if (realpath($_SERVER['SCRIPT_FILENAME']) === __FILE__) {
    [$status, $headers] = helpcenter_request('GET', '/articles?limit=1');
    $limit = $headers['x-ratelimit-limit'] ?? '?';
    $remaining = $headers['x-ratelimit-remaining'] ?? '?';
    echo "$status: $remaining of $limit requests left this minute.\n";
    exit($status < 400 ? 0 : 1);
}

Run it with HELPCENTER_API_KEY set, and it lists one article and reports your budget:

200: 299 of 300 requests left this minute.

Use the helper for every call your integration makes. It doesn't retry 503 Service Unavailable: when GET /v1/analytics/* answers 503 with analytics_unavailable, its Retry-After is 300 seconds, so try again later rather than in the same job.

Stay under the limits

  • Read lists, not items. GET /v1/articles returns full articles, 100 per request, and counts against the 300. Single-article reads count against the 120.

  • Write in bulk. POST /v1/articles/bulk takes up to 50 articles in one request, and POST /v1/images/bulk up to 20 images.

  • Sync changes, not everything. Use updated_since on GET /v1/articles, or let webhooks tell you what changed instead of polling.

  • Store analytics. Most analytics figures change once a day, so store what you read instead of asking again.

  • Run heavy jobs one at a time. Exports, bulk imports and image uploads from one address share the per-IP counter.

  • Give each integration its own key. Every key has its own budget, so one busy integration doesn't slow the others down.

The limits are the same on every plan. HelpCenter.io can raise the three per-credential limits for a help center that needs more: contact support with what you are building. The per-IP and edge limits can't be raised.

Was this article helpful?