Upload the images your articles show and get back a permanent URL to put in article HTML. You can send the file, give the https address of an image for HelpCenter.io to import, or create a single-use upload URL for a process that has the file but not your API key.
An article's content is HTML, so an illustrated article takes two steps: upload the image, then use its URL in an <img> tag when you create or update the article. Every upload response includes a ready-made HTML and Markdown snippet. To skip the first step, write the article with rehost_images and HelpCenter.io copies the images it links to (see Articles).
Endpoints
Method and path | What it does | Scope |
|---|---|---|
| Upload one image, as a file or from an https address |
|
| Upload up to 20 images in one request |
|
| Create a single-use upload URL |
|
| Upload one image to an upload URL, with no API key | None: the URL is the credential |
| List images, newest first |
|
| Get one image |
|
| Delete an image and its file |
|
A Read only key has content.read; a Read & write key also has content.write. OAuth access tokens work too, with the same scopes (see Scopes).
The image object
Field | Type | Description |
|---|---|---|
| integer | The image's id. |
| string | The image's permanent, absolute URL. Use it in article HTML. |
| string | For an upload, the name of the file you sent. For an import, the last part of the address's path, with the extension of the detected type. |
| string or null |
|
| integer | Size in bytes. |
| string or null | The SHA-256 of the file, as 64 hex characters. |
| string | The |
| string | An |
| string | A Markdown image with the URL and the alt text. |
| string | When the image was stored, as |
This is the answer to a first upload. The image object sits under image, next to reused:
{
"status": "success",
"reused": false,
"image": {
"id": 3,
"url": "https://helpcenter-io.s3.amazonaws.com/uploads/acme/76LPXpORQpiIyW5Bf4Wvo7HtHsyBOjgGGH7NNID3.png",
"filename": "create-board.png",
"mime_type": "image/png",
"size": 52409,
"checksum": "85a038dfadb5e2c6e1f801b31a2ea21b49b9187053571c5df999af581e429e2a",
"alt_text": "The Create a board article",
"snippets": {
"html": "<img src=\"https://helpcenter-io.s3.amazonaws.com/uploads/acme/76LPXpORQpiIyW5Bf4Wvo7HtHsyBOjgGGH7NNID3.png\" alt=\"The Create a board article\">",
"markdown": ""
},
"created_at": "2026-09-30 08:17:20"
}
}
HelpCenter.io stores the file exactly as you send it: it does not resize, compress or convert images. Images stay in place when you delete an article that uses them.
Upload an image
Send the image as multipart/form-data under file. A new image answers 201 Created; bytes this help center already has answer 200 OK with the existing image.
/v1/imagesUpload one image file and get back its permanent URL.
Field | Type | Description |
|---|---|---|
| file | Required unless you send |
| string | Required unless you send |
| string | Optional, up to 255 characters. Used only to build |
| boolean | Defaults to on. Send |
Which files are accepted
JPEG, PNG, GIF and WebP, up to 10 MB (10,485,760 bytes) each.
The type is read from the file's content, not from its name or the
Content-Typeyou send. A text file renamed to.pngis refused.SVG is not accepted, in any form.
The same image twice
Uploads are matched by content. When this help center already has an image with the same bytes, you get that image back with 200 OK and "reused": true, including its original filename, url and created_at. The file name you send plays no part, and an import and an upload of the same bytes resolve to the same image. That makes a failed import safe to run again: the files that already landed come back with the URLs you already have.
To store another copy anyway, send dedupe as 0. In a multipart body a boolean must be 1 or 0: the strings true and false are refused with 422 Unprocessable Entity and The dedupe field must be true or false. In a JSON body, true and false work.
Import from a URL
Instead of the file, send source_url: the https address of an image. HelpCenter.io downloads it and stores it like an upload, duplicate check included. This request can be JSON, so it suits clients that cannot send files, such as AI agents.
/v1/imagesImport an image from an https address instead of sending the file.
The download follows these rules:
The address, and every redirect, must be https. Up to 4 redirects are followed.
Addresses that resolve to private or reserved networks are refused.
A download, redirects included, has 15 seconds and 10 MB.
The type is read from the downloaded bytes, so the file must be a JPEG, PNG, GIF or WebP image whatever the address ends with.
When the address is refused or the download fails, you get 422 with The image at source_url could not be imported. and the reason under errors.source_url, for example The image could not be imported: the image returned HTTP 404. The reasons you are most likely to meet:
Reason | What to check |
|---|---|
| The host name in the address. |
| The image must be on a public address. |
| The address answered with that status. Any status other than 2xx fails. |
| The server took longer than 15 seconds. |
| The file is over 10 MB. |
| The address serves another type, such as SVG or an HTML page. |
| A redirect went to an http address. |
Send either file or source_url, never both.
Errors
Status | Message | When |
|---|---|---|
|
| There is no file and no address. |
|
| The file is not one of the accepted types. |
|
| The file is over 10 MB. |
|
| The address is not https. |
|
| You sent both. |
|
| The import failed; the reason is in |
|
| The key is Read only, or the token lacks |
|
| Storage failed. Nothing is left behind, so retry. |
Validation errors use the {"status": "validation_error", "message": "The upload could not be accepted.", "errors": {…}} shape, with each message under the field it concerns.
Upload several images
Send up to 20 images in one request: files as files[], addresses to import as source_urls[], or both. The answer is always 207 Multi-Status once the request is valid, with one result per image.
/v1/images/bulkUpload up to 20 image files in one request, with a result for each.
Import several addresses with JSON:
/v1/images/bulkImport up to 20 images from https addresses in one request.
Items are matched by position:
alt_texts[0]belongs to item 0. To mix files and addresses in one multipart request, give every item its own index, for examplefiles[0]andsource_urls[1]. One index cannot carry both.The limit of 20 counts files and addresses together.
Every file is checked before anything is stored. One refused file fails the whole request with
422, and nothing is written; the error names the item, such asfiles.1.Addresses are downloaded one at a time, so a refused or failed address is an
errorresult inside the207, with codeimport_failed. All the downloads in one request share 45 seconds.A request body can be at most 32 MB, so batch large files by total size: 20 files of 10 MB do not fit in one request.
Read each entry in results, not the status code. The top-level status is "success" even when every item failed:
Field | Description |
|---|---|
| The item's position in the request. |
|
|
| The image object, for |
| On an error, which file or address it was. |
| On an error, |
|
|
More than 20 items answers 422 with Too many images in one request. The maximum is 20 per batch, files and source URLs together. (or the files-only or URLs-only form of the same message).
Create an upload URL
Use an upload URL when the process that has the file should not hold your API key: an AI agent, a browser, a build job. Your server creates the URL with the key, and passes it on. Whoever has the URL can upload one image to this help center, as the credential that created it, without a key of their own.
/v1/images/uploadsCreate a single-use URL that accepts one image upload without an API key.
An upload URL works for 15 minutes (
expires_at) and accepts a single image of up tomax_bytesbytes (10 MB).It is on
api.helpcenter.io, and it is signed: use it exactly as returned.Anyone who has the URL can use it until it is spent or expires, so hand it only to the process that needs it.
Upload to an upload URL
POST the image as multipart/form-data to the exact upload_url, query string included. Send no API key. The fields and the answer are the same as for POST /v1/images, except that file is required (source_url is not read here):
curl -sS -X POST "$UPLOAD_URL" \
-H "Accept: application/json" \
-F "file=@help-center-home.png" \
-F "alt_text=The help center home page"
Quote the URL: it contains &. The response is 201 Created (or 200 OK with "reused": true) with the image, exactly as for POST /v1/images.
/v1/images/uploads/{intent}Upload one image to an upload URL, with no API key: the URL is the credential.
When the URL is refused, the body is {"status": "error", "code": "…", "message": "…"}:
Status | Code | When |
|---|---|---|
|
| The URL was changed, or it is not one HelpCenter.io issued. Create a new one. |
|
| The 15 minutes are over. Create a new one. |
|
| An image was already uploaded to it. Each URL takes one upload. |
|
| The key that created it can no longer upload images to this help center, for example because the key was deleted. |
A refused file (422) or a storage failure (500) does not spend the URL: correct the file and upload again to the same URL.
List images
List the images uploaded through the API, newest first. The list includes images uploaded with any of the endpoints above, and the copies HelpCenter.io makes when an article is written with rehost_images. Images added in the article editor are not included.
/v1/imagesList the images uploaded through the API, newest first.
Parameter | Type | Description |
|---|---|---|
| integer | Images per page, 1 to 100. Defaults to 50. |
| integer | Page number, starting at 1. Defaults to 1. |
| string | Only images whose SHA-256 is exactly this. |
To check whether a file is already uploaded, hash it and ask for its checksum. You get the matching images, more than one if copies were stored with dedupe off, or an empty list:
CHECKSUM=$(shasum -a 256 create-board.png | cut -d ' ' -f 1)
curl -sS "https://api.helpcenter.io/v1/images?checksum=$CHECKSUM" \
-H "apikey: $HELPCENTER_API_KEY" \
-H "Accept: application/json"
Send Accept: application/json. Without it, an invalid limit, page or checksum is answered with a redirect instead of the 422 error. The meta block counts every matching image: page, per_page, total_pages and items_count (see Pagination).
Get an image
/v1/images/{imageId}Get one image.
An id that belongs to another help center answers 404 with Image not found., the same as an id that does not exist.
Delete an image
Delete an image's record and its file. There is no undo: pages that still show the URL show a broken image.
/v1/images/{imageId}Delete an image and its file, unless article content still uses it.
Before deleting, HelpCenter.io checks the current content of this help center's articles, drafts included, for the image's URL. When articles use it, the delete is refused with 409 Conflict and referenced_by lists up to 20 of them, each with its id and title in every language. Update those articles first, or add ?force=true to delete anyway.
The check covers only that current content. Articles in Trash, staged changes and earlier versions are not checked, so an image they use can be deleted without a 409, and shows as broken if you restore or publish that content later.
Rate limits
Every request made with your key or token counts toward its limits: 300 requests a minute, and for deletes also the 120 writes a minute. Uploads have extra limits per IP address:
POST /v1/imagesandPOST /v1/images/uploads: 60 a minute.POST /v1/images/bulk: 20 a minute.Uploads to an upload URL: 60 a minute, counted on their own.
The first two share one counter per IP address with POST /v1/articles/bulk and GET /v1/export, and each endpoint compares that shared count with its own limit. So 20 single uploads in a minute make the next bulk request from the same address answer 429 Too Many Requests. Wait the number of seconds in the Retry-After header, then retry. See Rate limits.
Errors
Status | Body | When |
|---|---|---|
|
| The key or token is missing or unknown. |
|
| A Read only key, or a token without |
|
| An upload URL was refused. See Upload to an upload URL above. |
|
| No image with that id in this help center. |
|
| A delete of an image that articles still use. |
|
| A refused file, address or field. |
|
| A rate limit. Wait for |
|
| Storage failed. Retry. |
For the shapes every endpoint shares, see Errors.