Help Articles

7 views Markdown

The five endpoints that write, read, update and remove help articles.

Overview

A help article lives in two layers. The structural data holds the category, the state, who sees it and the order. The content per language holds the title, slug, body, tags and the search engine fields.

A title is required in every live language. The slug is optional and comes from the title when left empty, and it has to be unique per language.

The update is named as a partial change and behaves as a full write. The translation set is saved again for every language. Read the current state before changing anything.

Reference

Listing the Articles

get/api/v1/admin/knowledgebase/articles
Knowledgebase/GetKnowledgebaseArticles admin

Returns the help articles with their read and vote counts.

Query 3
searchstringSearches the id, the title and the tags.
pageintWhich page.
limitintRecords per page. 100 at the most.
Response fields data[] — 10 + meta — 3
idintThe article id.
titlestringThe title. In the panel's current language.
routestringThe address slug.
categorystringThe category name.
statusstringPublished or a draft.
privateboolWhether only signed-in clients see it.
viewsintHow often it was read.
usefulintThe helpful votes.
uselessintThe unhelpful votes.
created_atstringWhen it was written.
totalintHow many articles there are. It comes back under meta.
pageintThe page you are on.
limitintThe page size.
Errors 1
insufficient_scope403The key lacks the required scope.
Request
curl 'https://panel.example.com/api/v1/admin/knowledgebase/articles?search=password' \
  -H "Authorization: Bearer $API_KEY"
const res = await fetch('https://panel.example.com/api/v1/admin/knowledgebase/articles', {
  headers: { Authorization: `Bearer ${apiKey}` },
});

const { data } = await res.json();
const poor = data.filter((a) => a.useless > a.useful);
$ch = curl_init('https://panel.example.com/api/v1/admin/knowledgebase/articles');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER     => ['Authorization: Bearer ' . $apiKey],
]);

$body = json_decode(curl_exec($ch), true);
curl_close($ch);
// The title comes in ONE language; comparing translations wants the detail endpoint.
$rows = Api::Knowledgebase()->GetKnowledgebaseArticles()['data'];
$weak = array_filter($rows, fn ($a) => $a['useless'] > $a['useful']);

Writing an Article

post/api/v1/admin/knowledgebase/articles
Knowledgebase/CreateKnowledgebaseArticle admin every language needed

Opens a new help article.

Body 6
translationsobjectreqThe content per language. A title is needed in every live language.
titlestringThe article title. It is needed in every live language.
routestringThe address slug. Left empty it comes from the title.
contentstringThe article body. It takes markup.
tagsstringThe tags.
seo_titlestringThe search engine title.
seo_keywordsstringThe search engine keywords.
seo_descriptionstringThe search engine description.
category_idintThe category it sits in. Zero means none.
statusstringPublished or a draft.
privateboolWhether only signed-in clients see it.
sidebarboolWhether the side column shows.
rankintWhere it sits in the listing.
Response fields 201 — data — 11
dataobjectThe article made. Same shape as the read endpoint, and the new id comes under meta as well.
Errors 4
title_required422The title is missing in one of the live languages.
route_in_use422The slug is taken by another article in that language.
vetoed422A hook refused the save.
insufficient_scope403The key lacks the required scope.
Request
curl -X POST 'https://panel.example.com/api/v1/admin/knowledgebase/articles' \
  -H "Authorization: Bearer $API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"category_id":3,"translations":{"en":{"title":"How to reset your password","content":"<p>Open the account page.</p>"}}}'
const res = await fetch('https://panel.example.com/api/v1/admin/knowledgebase/articles', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${apiKey}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    category_id: 3,
    status: 'published',
    translations: {
      en: { title: 'How to reset your password', route: 'reset-password', content: html },
    },
  }),
});

const { data, meta } = await res.json();
$ch = curl_init('https://panel.example.com/api/v1/admin/knowledgebase/articles');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST           => true,
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => json_encode([
        'category_id'  => 3,
        'translations' => ['en' => ['title' => $title, 'content' => $html]],
    ]),
]);

$body = json_decode(curl_exec($ch), true);
curl_close($ch);
// It wants a title in EVERY live language; a missing one turns the whole call down and nothing is saved.
$langs = array_column(Api::Reference()->GetLanguages()['data'], 'code');
foreach ($langs as $l) $t[$l] = ['title' => $titles[$l] ?? $titles['en']];

Api::Knowledgebase()->CreateKnowledgebaseArticle(['translations' => $t]);

Reading an Article

get/api/v1/admin/knowledgebase/articles/{id}
Knowledgebase/GetKnowledgebaseArticle admin

Returns an article with all of its languages.

Response fields data — 11
idintThe article id.
category_idintThe category it sits in.
statusstringPublished or a draft.
privateboolWhether only signed-in clients see it.
sidebarboolWhether the side column shows.
rankintWhere it sits in the listing.
viewsintHow often it was read.
usefulintThe helpful votes.
uselessintThe unhelpful votes.
created_atstringWhen it was written.
translationsobjectThe content per language.
titlestringThe article title. It is needed in every live language.
routestringThe address slug. Left empty it comes from the title.
contentstringThe article body. It takes markup.
tagsstringThe tags.
seo_titlestringThe search engine title.
seo_keywordsstringThe search engine keywords.
seo_descriptionstringThe search engine description.
Errors 2
not_found404No such article.
insufficient_scope403The key lacks the required scope.
Request
curl 'https://panel.example.com/api/v1/admin/knowledgebase/articles/12' \
  -H "Authorization: Bearer $API_KEY"
const res = await fetch(`https://panel.example.com/api/v1/admin/knowledgebase/articles/${id}`, {
  headers: { Authorization: `Bearer ${apiKey}` },
});

const { data } = await res.json();
const missing = langs.filter((l) => ! data.translations[l]?.content);
$ch = curl_init('https://panel.example.com/api/v1/admin/knowledgebase/articles/' . $id);
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER     => ['Authorization: Bearer ' . $apiKey],
]);

$body = json_decode(curl_exec($ch), true);
curl_close($ch);
// Read here BEFORE updating: an update rewrites the WHOLE translation set.
$a = Api::Knowledgebase()->GetKnowledgebaseArticle(['id' => $id])['data'];
$a['translations']['en']['content'] = $newHtml;

Updating an Article

patch/api/v1/admin/knowledgebase/articles/{id}
Knowledgebase/UpdateKnowledgebaseArticle admin a full write

Rewrites the article in full.

Body 6
translationsobjectreqThe content per language. The set is rewritten and a language you leave out is lost.
titlestringThe article title. It is needed in every live language.
routestringThe address slug. Left empty it comes from the title.
contentstringThe article body. It takes markup.
tagsstringThe tags.
seo_titlestringThe search engine title.
seo_keywordsstringThe search engine keywords.
seo_descriptionstringThe search engine description.
category_idintThe category it sits in. Zero means none.
statusstringPublished or a draft.
privateboolWhether only signed-in clients see it.
sidebarboolWhether the side column shows.
rankintWhere it sits in the listing.
Response fields data — 11
dataobjectThe article as it now stands. Same shape as the read endpoint.
Errors 5
not_found404No such article.
title_required422The title is missing in one of the live languages.
route_in_use422The slug is taken by another article in that language.
vetoed422A hook refused the save.
insufficient_scope403The key lacks the required scope.
Request
curl -X PATCH 'https://panel.example.com/api/v1/admin/knowledgebase/articles/12' \
  -H "Authorization: Bearer $API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"status":"draft","translations":{"en":{"title":"How to reset your password","content":"<p>Updated steps.</p>"}}}'
const cur = await fetch(`https://panel.example.com/api/v1/admin/knowledgebase/articles/${id}`, {
  headers: { Authorization: `Bearer ${apiKey}` },
}).then((r) => r.json());

cur.data.translations.en.content = html;

const res = await fetch(`https://panel.example.com/api/v1/admin/knowledgebase/articles/${id}`, {
  method: 'PATCH',
  headers: {
    Authorization: `Bearer ${apiKey}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify(cur.data),
});
$ch = curl_init('https://panel.example.com/api/v1/admin/knowledgebase/articles/' . $id);
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_CUSTOMREQUEST  => 'PATCH',
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => json_encode($article),
]);

$body = json_decode(curl_exec($ch), true);
curl_close($ch);
// It is named PATCH and behaves as a FULL WRITE: the translations are saved again for every language.
$a = Api::Knowledgebase()->GetKnowledgebaseArticle(['id' => $id])['data'];
$a['translations']['en']['content'] = $newHtml;

Api::Knowledgebase()->UpdateKnowledgebaseArticle($a + ['id' => $id]);

Removing an Article

delete/api/v1/admin/knowledgebase/articles/{id}
Knowledgebase/DeleteKnowledgebaseArticle admin

Removes an article, every translation of it and its header image.

Response fields data — 2
deletedboolWhether the delete ran.
idintThe id of the article removed.
Errors 3
not_found404No such article.
vetoed422A hook refused the delete.
insufficient_scope403The key lacks the required scope.
Request
curl -X DELETE 'https://panel.example.com/api/v1/admin/knowledgebase/articles/12' \
  -H "Authorization: Bearer $API_KEY"
const res = await fetch(`https://panel.example.com/api/v1/admin/knowledgebase/articles/${id}`, {
  method: 'DELETE',
  headers: { Authorization: `Bearer ${apiKey}` },
});

const body = await res.json();
$ch = curl_init('https://panel.example.com/api/v1/admin/knowledgebase/articles/' . $id);
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_CUSTOMREQUEST  => 'DELETE',
    CURLOPT_HTTPHEADER     => ['Authorization: Bearer ' . $apiKey],
]);

$body = json_decode(curl_exec($ch), true);
curl_close($ch);
// The slug goes as well: old links to that address land on a not-found page.
Api::Knowledgebase()->UpdateKnowledgebaseArticle(['id' => $id, 'status' => 'draft'] + $article);
// moving it to a draft rather than removing keeps the address

Pitfalls

The update rewrites the translation set

The update body is the whole article. Sending one language alone leaves the others unsaved and they go. Take the full set from the read endpoint first, change it there, and send all of it back.

A missing language stops the save outright

A title is needed in every live language, and one missing gives 422 while no language is saved. Articles that exist carry no title in a language added later, so the first edit meets this error. Take the language list from the reference endpoint and build the body from it.

The slug is unique per language

Using one slug on two articles gives route_in_use. The check is per language, so a slug taken in English can be free in another. During a bulk import the clash appears in one language alone and stops the whole save.

A private article is member-only rather than hidden

The private field limits an article to signed-in clients and it still shows in the panel and in search. Move the state to a draft when you mean to take content out of sight entirely.

Removing breaks the old links

Removing an article takes the slug with it and outside links to that address land on a not-found page. Moving one that search engines know to a draft is often better. The content goes out of sight and the address stays.

Was this helpful?

Thanks for your feedback!

Still Need Help?

Our support team is here around the clock for anything you can't find above.