Most days your help center runs on the editor. You write an article, you hit publish, the new piece is live on your subdomain. That covers the daily flow. The cases I want to talk about in this article are the ones that don't fit the editor: pulling a list of every published article into a backup script, mirroring release notes from your engineering tracker the moment they ship, importing a few hundred FAQs from a CSV export, or building a custom search experience that talks to your content from your own product.
Those are jobs for the API. HelpCenter.io exposes a small, focused REST API that lets your code do the things your authors would otherwise do by hand. Read articles. Read categories. Create new articles. Update existing ones. Upload the images those articles point at, and clean them up afterwards. It is deliberately small, and most teams never need more than a handful of those calls.
This article is the reference. It walks through how to authenticate, what the shared conventions look like, and every endpoint in v1 with a realistic request and response example.
Quick start
You need three things: a HelpCenter.io account, an API key, and somewhere to send a request from.
Open your dashboard, go to Settings, scroll to the API Keys section. Pick a descriptive name (something like 'Production import script' or 'Release notes sync' is better than 'key1'), choose the permission level, and click Generate Key. You can create as many keys as you want.
There are two permission levels. Read Only keys can list articles and categories. That covers backups, exports, custom search, content audits, anything where you are pulling data out. Read & Write keys can do everything Read Only can do, plus create and update articles and categories. Use Read Only by default. Issue a Read & Write key only for the specific job that needs it, and rotate it when that job is done.
Keys are tied to a single site. If you run more than one help center under your account (an English instance and a French one, or one site per product), each site has its own keys.
Once you have a token, send it as the apikey header on every request:
curl https://api.helpcenter.io/v1/articles \
-H "apikey: YOUR_API_TOKEN_HERE"Two notes before you build anything serious against this. First, your token is a secret. Treat it like a password. Don't commit it to git, don't paste it in a screenshot, don't email it. If a key leaks, open Settings and delete it. The dashboard lets you revoke any key with one click and existing requests using that token stop working immediately. Second, when you build, build against a staging help center first if you have one. The PATCH and POST endpoints modify real content. There is no soft-delete and no undo button.
Conventions
A few things hold across every endpoint, so you don't have to relearn them per call.
Base URL. Every public API request goes to https://api.helpcenter.io/v1. The v1 is the version prefix. When we ship a v2, the v1 endpoints will keep working on their existing paths. We will not silently change response shapes on you inside a version.
Authentication. One header, apikey: YOUR_TOKEN. We don't use Authorization: Bearer ... and we don't use X-API-Key. Just apikey. Requests without the header, or with an unknown token, get a 401.
Content type. Send Content-Type: application/json on every POST and PATCH that carries a JSON body. The one exception is the image endpoints, which take multipart/form-data (see Images below). We don't accept form-encoded bodies anywhere else.
Response envelope. Successful responses are JSON with a status field set to "success". List endpoints add a meta block at the bottom with pagination info. Single-resource endpoints return the resource under a key named after the resource (article, category).
{
"status": "success",
"articles": [ /* ... */ ],
"meta": {
"page": 1,
"per_page": 100,
"total_pages": 3,
"items_count": 247
}
}Pagination. List endpoints accept ?page= and ?limit= query params. limit defaults to 100 and caps at 100. page defaults to 1. The meta block tells you page, per_page, total_pages, and the absolute items_count, so you can build a loop that stops when page >= total_pages.
Rate limit. The public API allows up to 60 requests per minute per token. That is enough for almost every batch job. If you exceed it, you get a 429 Too Many Requests response and should wait a minute before retrying. If you legitimately need a higher ceiling (a one-time bulk import of thousands of articles, for example), reply to support with what you are building and we will lift the limit for your token.
Multi-language fields. HelpCenter.io is multilingual. Fields like title, slug, content on articles and name, description on categories are stored per language. In API responses they come back as objects keyed by language code (en, de, es, fr, etc.). When you write to those fields, you send the same shape: a JSON object whose keys are the language codes you want to set.
{
"title": {
"en": "Getting started",
"de": "Erste Schritte"
}
}You don't have to send every language. If a key isn't present in the request, the existing translation is preserved. The languages you can use are the ones enabled on your site (your site's default language is always allowed, and any extra languages you have turned on in Settings, Languages).
Error envelope. Errors come back with a non-2xx status code and a JSON body. The exact shape depends on the error class (see the Errors and troubleshooting section at the end of this article), but you can rely on every error being valid JSON with a meaningful HTTP status.
Articles
Articles are the units of content in your help center. Each article belongs to one or more categories, has translations per language, a publish state, and a visibility level. The API gives you four ways to work with them: list, create, update, and updating individual fields again with PATCH.
List articles
Returns a paginated list of articles on your site. You can filter by category, search by keyword, sort by views, and switch the response language.
/v1/articlesList the articles on your site. Supports pagination, full-text search, category filter, sort, and per-language responses.
Create an article
Creates a new article on your site. Needs a Read & Write token. The new article is owned by the user the token was generated for, and lives under your site.
A few specifics worth knowing before you call this. title is the only required field, and it has to be a translation object (at minimum your site's default language). If you don't send a slug, one is generated from the title for each language you provided. Setting published: true publishes immediately. Setting published: false (or leaving it off) creates the article as a draft.
If you set visibility: "link_only" the API generates a private share token for you and includes it in the response, so you can build the shareable URL yourself.
/v1/articlesCreate a new article. Needs Read & Write scope on your token.
A short note on categories vs category_id. The original model has a single category_id. Articles can also be assigned to multiple categories at once, via the categories array (a list of category IDs). If you send categories, the article gets that set of categories and category_id becomes a placeholder value (-1). For most setups, picking one category_id is what you want. Use categories only if you genuinely need an article to appear under several headings.
Update an article
Updates fields on an existing article. Needs a Read & Write token. Send only the fields you want to change. Anything you don't send is left alone, including translations you don't include in the request.
/v1/articles/{articleId}Update fields on an existing article. Partial updates are supported. Needs Read & Write scope.
Two quirks worth pre-empting. First, if you send a content field and the article already had content_json cached (from the editor), the cache is dropped so the next edit in the dashboard picks up your new content cleanly. You don't need to do anything special. Second, published: false flips the article back to draft, so it goes off the public site. If you only want to make a quick correction, leave published out of the request.
Images
Article content is HTML, and HTML points at images by URL. The API will not take a base64 blob inside a content field, and it will not take a multipart article. So an illustrated article is always two steps: upload the file first, get a permanent URL back, then put that URL in the HTML you send.
That split is deliberate and it buys you two things. Article payloads stay small, plain JSON that you can log, diff, and store next to the source content they came from. And an image has exactly one URL, so the same screenshot can appear in ten articles without ten copies sitting in storage.
In practice the loop is:
POST the file to /v1/images. You get back an ID, a URL, a checksum, and ready-to-paste HTML and Markdown snippets.
Build your article content with that URL in an img tag, or drop the snippet straight in.
POST or PATCH the article the way you normally would.
Everything else in this section exists to make step 1 survivable when it is running a few hundred times unattended.
Upload an image
Multipart, not JSON. This is the one place in the API where that is true. Allowed types are JPEG, PNG, GIF and WebP, up to 10 MB each by default.
The type is read from the file's own bytes rather than from its name or the Content-Type you send, so renaming payload.svg to photo.jpg does not get it past the door. SVG is not on the list at all, and that is on purpose. It is XML that can carry script and pull in external references, and these files are served from a shared public asset host, so accepting one would be a stored cross-site-scripting hole on a trusted origin. It stays out until we sanitise on ingest.
/v1/imagesUpload one image and get back a permanent URL to embed in article content. Needs Read & Write scope.
Uploads are idempotent by content. We store the sha256 of the bytes. Send a file this site already holds and you get the existing image back with a 200 and reused: true, instead of a second copy under a second random name. The filename is not part of that comparison, only the bytes. A first upload answers 201 with reused: false.
That one behaviour is what makes an import safe to re-run. If a migration dies halfway, replay the whole batch: the files that already landed resolve to the URLs you already have, and only the missing ones are stored. Pass dedupe=false when you genuinely want a distinct second copy.
alt_text is optional, and it is not kept on the image. Alt text describes a picture in the place it is used, and the same screenshot can mean different things in two articles, so we only use what you send to build the snippets in that one response. Read the image back later and alt_text comes out empty. The real alt text belongs in the article HTML.
Upload several at once
Same rules, one request. Send files[] instead of file, with optional alt_texts[] matched by position. The cap is 20 files per request, but the 30 MB limit on the whole request body usually bites first, so batch by total bytes rather than by count.
/v1/images/bulkUpload up to 20 images in one request, with a per-file outcome. Always answers 207.
This one always answers 207 Multi-Status, even when every file worked. A batch where some files land and others do not is the normal case, and folding that into a single 200 or 500 would just make you re-derive what actually happened. Each entry in results carries its index, an outcome of created, reused or error, and either the image or an error object. Read the entries, not the status code.
Worth knowing: per-file validation runs before anything is stored, so a batch containing one disallowed file is rejected whole with a 422 and nothing is written. Only storage failures produce per-item errors inside the 207.
List and read what you uploaded
A Read Only token is enough for both of these. They exist for reconciliation: after a run that timed out or crashed, you want to know what actually landed before you decide what to retry.
/v1/imagesList the images this site uploaded through the API, newest first.
/v1/images/{imageId}Read one image record. An id from another help center returns 404, so an id is never a probe.
The checksum filter is the precise version of that question. Hash your local file, ask for that checksum, and you get either the image (it landed, here is its URL) or an empty list (it did not). That beats paging through everything looking for a filename, especially since the stored name is not what you uploaded it as.
These endpoints only ever show images uploaded through this API. Files you added from the editor's attachments panel are a separate inventory with separate endpoints, and the two never mix. An image uploaded here does not appear as a download chip in the editor, which is exactly what you want for something embedded in an article body.
Delete an image
The cleanup half of the same story, and the one that can hurt, so it is guarded by default.
/v1/images/{imageId}Delete an image and its stored file. Refuses by default while an article still embeds it. Needs Read & Write scope.
A delete refuses while an article still embeds the image. Nothing else in the system tracks which articles point at a given URL, so an unguarded delete is precisely how a published article quietly loses its screenshots. When the image is still in use you get a 409 and a referenced_by list of the articles to fix first.
The scan is scoped to your own help center, so another site hotlinking your URL cannot block your cleanup. If you are tidying up after a failed import and you know the reference does not matter, force=true skips the check. It will leave broken images behind if you are wrong, so keep it for that case.
A successful delete removes the record and the stored file together. There is no undo and no soft delete, the same as everywhere else in this API.
Rate limits on the image endpoints
The two upload endpoints carry their own limits, counted separately from the general one: 60 single uploads a minute, and 20 bulk requests a minute. They are stricter because these calls accept multi-megabyte bodies and write to storage every time. Reading and deleting are not separately limited. If you cross a limit you get a 429, so wait and retry rather than hammering.
Categories
Categories are the buckets your articles live under. The API gives you two operations: list them, and update an existing one. There is no public endpoint for creating categories today. We held that one back because creating categories programmatically tends to produce taxonomy sprawl, which is the single most common cause of help centers becoming hard to navigate. Most teams are better off creating categories deliberately in the dashboard and then bulk-importing articles into them via the API.
If you have a genuine use case for creating categories from code (a single-purpose migration script, for example), let us know and we will add the endpoint.
List categories
/v1/categoriesList the categories on your site. Includes both root-level categories and nested children (use the 'parent' field to walk the tree).
Update a category
/v1/categories/{categoryId}Update a category's name, description, parent, or icon. Partial updates are supported. Needs Read & Write scope.
Common workflows
The endpoints above are the building blocks. Here are three things teams actually do with them.
Mirror your release notes into a "What's new" category
The pattern: your engineering team writes release notes in your CMS, your task tracker, or a Markdown folder in your repo. Each time you ship, a small script (a GitHub Action, a webhook from your CMS, or a nightly cron) takes the new entries and posts them as articles in HelpCenter.io under a single category.
Set up a Read & Write token, scope it mentally to 'release notes only', and call POST /v1/articles once per release with category_id set to your "What's new" category and published: true. If you want each entry available in multiple languages, send a title and content map with the languages you have translations for. Anything not translated stays untranslated and the article only appears on the languages where it has content.
For idempotency, store the article ID returned in the 201 response next to the release-notes entry in your source system. The next time the same entry needs updating (a typo fix, an added screenshot), call PATCH /v1/articles/{articleId} instead of creating a duplicate.
Back up everything weekly
The pattern: a scheduled job pulls every article and category from the API and stores the JSON in S3, or in a snapshot table, or in a versioned file in a private GitHub repo. The point is having an export that's independent of the live system.
Use a Read Only token. The flow:
Call GET /v1/categories?limit=100&page=1. If meta.total_pages > 1, loop until page >= total_pages.
Call GET /v1/articles?limit=100&page=1. Loop the same way.
Save the resulting arrays as one JSON file (or one per resource type) with a date stamp.
Most help centers fit comfortably under a few thousand articles, which is a handful of API calls. The whole job typically runs in under a minute and well inside the rate limit.
Import from a CSV or Markdown folder
The pattern: you have content somewhere else (a CSV export from a competitor's tool, a folder of Markdown files in your repo, a Notion export) and you want it inside HelpCenter.io.
The two steps that trip people up: language mapping and category mapping. Decide upfront which language each source row belongs to (most CSV exports don't store this explicitly) and which HelpCenter.io category it should land in. Once you have that, every row becomes one POST /v1/articles call. Send the language code as the key in your title and content objects, set category_id to the matching category, and decide whether to publish on import or stage as drafts.
Two practical tips. First, import as drafts on the first pass so you can review the formatting before publishing. Then iterate by calling PATCH /v1/articles/{articleId} with published: true once each article is ready. Second, do a small batch (5 to 10 articles) end-to-end before you start the full run. The 60 requests per minute limit is generous for a one-shot migration of a few hundred articles, but if you have thousands, sleep briefly between batches or reply on support and we can lift the cap for the duration of your import.
Import illustrated content end to end
The pattern: a folder of articles with image files sitting next to them, and you want the whole thing inside HelpCenter.io with the pictures intact.
Do the images first, as their own complete pass, before you create a single article. Walk the folder, upload every image, and keep a map of local path to returned URL. Then walk the articles, rewrite each local image reference using that map, and POST the article.
Two reasons to keep the passes apart. An article that references an image you have not uploaded yet is a broken article on a live site. And one failure in the middle of a mixed loop leaves you with articles half-written and images half-uploaded, with no clean place to restart. Split, the image pass is safe to run as many times as you like (identical bytes resolve to the URL you already have) and the article pass starts from a complete map.
Import as drafts on the first run so you can look at the formatting and the images in the dashboard before anything is public, then PATCH published: true once each article checks out. Store the article ID from each 201 next to the source file, so a second run updates rather than duplicates.
When the migration is done, GET /v1/images is your inventory. Anything you uploaded by mistake can be deleted, and anything still in use will refuse to be, which is a useful safety net at exactly the moment you are least sure what is where.
Errors and troubleshooting
The error shapes are simple, but they vary by endpoint. Here is a quick map.
401 Unauthorized comes back when the apikey header is missing, or the token doesn't match a key in our database. Check that the header name is exactly 'apikey' (lowercase, one word) and that you are sending the full token (64 characters, no whitespace). If you recently deleted the key in the dashboard, the token stops working immediately.
403 Forbidden comes back when you try a write operation with a Read Only token. The response message tells you which action was blocked (creating articles, updating articles, updating categories). Generate a Read & Write token in Settings if you need write access.
400 Bad Request is a validation error. The shape varies. GET /v1/articles returns a wrapped envelope with status: 'validation_error' and an 'errors' object. POST and PATCH endpoints return the bare Laravel validation shape: { field: [message] } with no status wrapper. This is a known inconsistency we will harmonise in v2. For now, code defensively and treat any non-2xx as a failure regardless of body shape. An unsupported language code (one that isn't enabled on your site) returns a status: 'error' message.
404 Not Found on PATCH /v1/articles/{id} and PATCH /v1/categories/{id} means no resource with that ID exists on the site the token is associated with. If you are sure the ID is right, double-check that the token is for the correct site (each site has its own keys).
409 Conflict comes back from DELETE /v1/images/{imageId} when article content still embeds the image. The body carries a referenced_by array so you know which articles to fix. Retry with ?force=true only when you are sure the reference does not matter.
422 Unprocessable Entity is the validation shape the image endpoints use, where the older JSON endpoints return 400. The body is { status: 'validation_error', message, errors: { file: [...] } }. You get it for a missing file, a type that is not on the allowlist, a file over the size limit, or a bulk batch over the file cap.
429 Too Many Requests means you have crossed the 60 requests per minute limit on this token. Wait at least one minute and retry. If you regularly need more headroom, reply on support with your use case.
5xx Server Error is on us. If you see one, capture the request ID from the response headers if there is one, plus a copy of the request body that produced it, and email support. We treat these as bugs and they get attention quickly.
What's next
That is the v1 surface covered here: list and create articles, update articles, list and update categories, and upload, list, read and delete the images those articles use. Small on purpose. Most help center automation jobs fit comfortably inside it.
One more piece has its own guide. If you need to edit an article that is already live and hold the change until you are ready to release it, rather than publishing the moment you PATCH, read Staging article changes over the API.
That said, the API will grow as people tell us what they need next. If you hit something the API can't do, that is the most useful thing you can tell us. Reply on the support form with the workflow you are trying to build and the gap you ran into. Our docs and our API both grow when our customers point at what is missing. What are you building?