Docs authoring
Documentation has three levels: a space (one manual or help centre) holds
collections (chapters, nestable) which hold articles. An article is
language-agnostic; its titles and bodies live per locale, and each publish
freezes an immutable version.
All paths below are relative to https://api-v1.zingasuite.com/papi/v1.
Spaces
| Method | Path | Purpose |
|---|---|---|
GET |
/solve/spaces |
List spaces |
POST |
/solve/spaces |
Create a space |
GET |
/solve/spaces/{id} |
Read a space |
PATCH |
/solve/spaces/{id} |
Update a space |
DELETE |
/solve/spaces/{id} |
Soft-delete a space |
curl -X POST https://api-v1.zingasuite.com/papi/v1/solve/spaces \
-H "Authorization: Bearer zk_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"name": "Installer Handbook",
"slug": "installer-handbook",
"defaultLocale": "en",
"enabledLocales": ["en", "hi"],
"visibility": "public"
}'
slugdefaults to a slugifiednameand must be unique in your workspace
(409 slug_takenotherwise).visibilityispublic,authenticatedorrestricted.defaultLocaleis always added toenabledLocales, so you cannot end up with
a space that cannot hold its own default language.PATCHacceptsname,description,defaultLocale,enabledLocales,
visibility,status(active|archived) andsettings.
The response carries siteSlug — the public locator for the space at
https://docs.zingasuite.com/<siteSlug>/.
Chapters (collections)
| Method | Path | Purpose |
|---|---|---|
GET |
/solve/collections?spaceId=<uuid> |
The space's chapter tree |
POST |
/solve/collections |
Create a chapter |
GET |
/solve/collections/{id} |
Read a chapter |
PATCH |
/solve/collections/{id} |
Rename / re-slug / re-parent / reorder |
DELETE |
/solve/collections/{id} |
Soft-delete a chapter |
curl -X POST https://api-v1.zingasuite.com/papi/v1/solve/collections \
-H "Authorization: Bearer zk_live_xxx" \
-H "Content-Type: application/json" \
-d '{"spaceId": "<space-id>", "name": "Commissioning", "sortOrder": 2}'
- Slugs are sibling-scoped: unique among children of the same parent, so
installation/safetyandservice/safetycan co-exist. pathanddepthare derived from the parent — the list is flat and ordered
by depth thensortOrder, so you can rebuild the nesting client-side.nameis stored as a per-locale translation. It goes to the space's default
locale unless you passlocale.- Re-parenting recomputes
path/depthfor that row; move a subtree one
level at a time.
Articles
| Method | Path | Purpose |
|---|---|---|
GET |
/solve/articles?spaceId=<uuid> |
List articles in a space |
POST |
/solve/articles |
Create an article shell |
GET |
/solve/articles/{id} |
Read an article + its per-locale heads |
PATCH |
/solve/articles/{id} |
Update slug / chapter / visibility / status |
DELETE |
/solve/articles/{id} |
Soft-delete an article |
List filters: collectionId, status (draft | published | archived), q
(slug substring), plus limit / offset.
curl -X POST https://api-v1.zingasuite.com/papi/v1/solve/articles \
-H "Authorization: Bearer zk_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"spaceId": "<space-id>",
"collectionId": "<chapter-id>",
"title": "Commissioning checklist",
"slug": "commissioning-checklist"
}'
A new article is a draft shell with an empty head for its default locale —
creating it publishes nothing. Article slugs are unique per space.
PATCH accepts slug, collectionId, visibility, status, sortOrder and
isReusable. Status and slug changes reindex the article for search and the AI
assistant, so unpublishing (status: "archived") removes it from both.
Set isReusable: true to make the article a master section that other
articles can embed with @[include](<article-id>); the include resolves to the
master's current published body at render time.
Content: read, draft, publish
Bodies are per locale. There is one draft slot per (article, locale) and a chain
of immutable published versions.
| Method | Path | Purpose |
|---|---|---|
GET |
/solve/articles/{id}/content?locale=en |
The working copy |
PUT |
/solve/articles/{id}/content |
Save the draft |
POST |
/solve/articles/{id}/publish |
Publish a new version |
GET returns the draft if there is one, else the published version, with
hasUnpublishedChanges telling you they differ:
{
"articleId": "…",
"locale": "en",
"title": "Commissioning checklist",
"body": "## Before you start\n…",
"format": "mdx",
"toc": [],
"status": "published",
"version": 4,
"publishedAt": "2026-08-14T09:12:00Z",
"hasUnpublishedChanges": true
}
Save a draft (nothing is public yet — the same two-step the editor uses):
curl -X PUT https://api-v1.zingasuite.com/papi/v1/solve/articles/<id>/content \
-H "Authorization: Bearer zk_live_xxx" \
-H "Content-Type: application/json" \
-d '{"locale": "en", "title": "Commissioning checklist", "body": "## Before you start\n…"}'
Then publish:
curl -X POST https://api-v1.zingasuite.com/papi/v1/solve/articles/<id>/publish \
-H "Authorization: Bearer zk_live_xxx" \
-H "Content-Type: application/json" \
-d '{"locale": "en", "title": "Commissioning checklist", "body": "## Before you start\n…"}'
Publishing is one atomic step that:
- freezes
title+bodyas version N (byte-identical content is deduped —
republishing the same text does not mint a new version), - mints or updates the canonical URL slug (an old slug becomes a
301), - reindexes the article for micro-site search and AI answers,
- refreshes the reusable-content edges (
@[include]usage), - flips the article to
published, and marks other localesoutdatedwhen the
default locale moves ahead.
locale must be one of the space's enabledLocales, otherwise you get
400 locale_not_enabled.
toc is an optional list of heading anchors; omit it and the renderer derives
the page contents from the body.
A minimal publishing pipeline
import requests
API = "https://api-v1.zingasuite.com/papi/v1"
H = {"Authorization": "Bearer zk_live_xxx"}
space = requests.post(f"{API}/solve/spaces", headers=H,
json={"name": "Installer Handbook"}).json()
chapter = requests.post(f"{API}/solve/collections", headers=H,
json={"spaceId": space["id"], "name": "Installation"}).json()
for order, (slug, title, path) in enumerate(FILES): # your own source tree
article = requests.post(f"{API}/solve/articles", headers=H, json={
"spaceId": space["id"], "collectionId": chapter["id"],
"slug": slug, "title": title, "sortOrder": order,
}).json()
requests.post(f"{API}/solve/articles/{article['id']}/publish", headers=H,
json={"title": title, "body": open(path).read()})
Re-running it is safe: creating an existing slug returns 409 slug_taken, so
look the article up (GET /solve/articles?spaceId=…&q=<slug>) and publish into
it instead of creating a duplicate.