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"
      }'
  • slug defaults to a slugified name and must be unique in your workspace
    (409 slug_taken otherwise).
  • visibility is public, authenticated or restricted.
  • defaultLocale is always added to enabledLocales, so you cannot end up with
    a space that cannot hold its own default language.
  • PATCH accepts name, description, defaultLocale, enabledLocales,
    visibility, status (active | archived) and settings.

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/safety and service/safety can co-exist.
  • path and depth are derived from the parent — the list is flat and ordered
    by depth then sortOrder, so you can rebuild the nesting client-side.
  • name is stored as a per-locale translation. It goes to the space's default
    locale unless you pass locale.
  • Re-parenting recomputes path/depth for 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:

  1. freezes title + body as version N (byte-identical content is deduped —
    republishing the same text does not mint a new version),
  2. mints or updates the canonical URL slug (an old slug becomes a 301),
  3. reindexes the article for micro-site search and AI answers,
  4. refreshes the reusable-content edges (@[include] usage),
  5. flips the article to published, and marks other locales outdated when 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.

Was this helpful?