Doc-as-code with the KB API

Author articles in a Git repo, sync them to Atender on every commit. This is the same pattern Atender uses for its own help center. The v1 API gives you idempotent upserts when paired with a small client-side push script.

May 12, 20266 min read

Doc-as-code with the KB API

Doc-as-code is the pattern of treating help-center articles like application code — authored as files in a version-controlled repository, reviewed in pull requests, and synced into the live KB by an automated pipeline. Atender is built to support this pattern via /api/v1/kb/* and a tenant API key.

It is the pattern this very help center uses. The articles you’re reading live as markdown files in a Git-tracked vault, and a small Python script reads them and upserts to the KB whenever the source files change.

Why doc-as-code

Some help centers fit the in-app editor perfectly — small team, low volume, content authored by the same people who deliver support. For those teams, the in-app editor is exactly right.

The case for doc-as-code is stronger when:

  • Multiple authors need to review each other’s changes. A pull request is a much better review surface than “tell me when you’re done and I’ll look at the article in the editor.”
  • The source of truth lives elsewhere. If your engineering team writes specs in Markdown and your support team wants those specs translated into customer-facing articles, syncing from one repo to the KB closes the loop automatically.
  • You want a version history beyond Atender. Git keeps every revision, with author attribution, in your own repo. The in-app editor doesn’t keep an external history.
  • You’re migrating from another help center. A bulk import via the API is dramatically faster than recreating articles in the UI.
  • You want CI to validate articles. Spell checkers, link checkers, frontmatter linters — all easy to run on Markdown files in CI, hard to bolt onto a hosted editor.

If none of those apply, stay in the in-app editor. Doc-as-code is overhead that only pays off when one of those needs is real.

The architecture

A doc-as-code setup has three parts:

  • Source of truth — Git repo (or any version-controlled folder) — Markdown files with frontmatter for metadata
  • Push script — The same repo or your CI — Reads the files, calls the API to upsert
  • Live KB — Atender — The rendered, searchable, AI-retrievable surface

The push script is the bridge. It walks the repo, calls /api/v1/kb/categories to ensure each category exists, then calls /api/v1/kb/articles to upsert each article. It runs on every commit (in CI) or on demand (locally).

Idempotency, the practical version

Idempotency in doc-as-code means “running the script twice with the same inputs produces the same outputs.” The v1 API doesn’t enforce idempotency on its own — POST /api/v1/kb/articles always creates a new article and appends -1, -2 to the slug if it collides. The script supplies idempotency by:

  1. Listing existing articles by slug before pushing.
  2. If a slug matches an existing article, PATCH it instead of POST.
  3. If the slug doesn’t match anything, POST to create.

This pattern is robust against partial runs — a script that crashes halfway through can re-run, find the articles it already created, and update them on the second pass instead of creating duplicates.

See What happens when I push the same article twice? for the exact behavior.

What the script needs

  • Authentication — A tenant API key with knowledge:read (for listing) and knowledge:write (for create/update). Generate in Settings → API Keys.
  • Frontmatter parser — Any Markdown frontmatter library. The script reads title, slug, summary, category, etc. from the frontmatter and sends them to the API.
  • Categories upserted before articles — Articles fail to create without an existing category. The script must ensure categories first, collect IDs, then push articles.
  • Slug-based lookup — List articles, build a slug → ID map, decide create vs. update per article.
  • Markdown-to-HTML conversion — The API accepts Markdown in content; the renderer handles the HTML conversion server-side. No client-side conversion required.

The Atender vault uses a 600-line Python script that does all of this in one pass. It’s a useful reference but the same shape is easy to build in any language.

Frontmatter as metadata contract

The piece that makes doc-as-code workable is the frontmatter contract. Every article carries metadata at the top:

---
title: "How to update your payment method"
slug: "update-payment-method"
category: "Billing"
type: "how-to"
summary: "Open Account, go to Billing, click Change."
keywords: [payment, credit card, billing]
ux_path: "Account → Billing → Payment methods"
roles: [admin]
status: "published"
---

The push script reads this and translates it into the API’s payload shape. The script becomes a thin transformation layer between the human-friendly frontmatter and the API’s JSON.

Where translation and embeddings fit

When you push an article via the API:

  • Embeddings rebuild on a background scheduler that scans for missing or stale embeddings. Allow a minute or two for the article to appear in retrieval after pushing.
  • Translations are queued explicitly via the in-app Sync Translations button. They don’t auto-trigger on every API push. If your pipeline includes translation, the push script should call POST /api/kb/translations/sync after the bulk push (or you click it in the editor afterward).

This is a design difference from the in-app editor, which embeds on every save. The API trades that automation for the simpler “save now, embed in the background” model that’s easier to reason about in bulk operations.

What you can’t do from the API

  • Role tagging isn’t yet exposed on the v1 surface. The push script can read roles: from frontmatter but can’t apply them; do this in the in-app editor after a push.
  • Section management isn’t on v1. Sections are configured in the editor.
  • Knowledge Base settings (branding, layout, theme) aren’t on the API.
  • Handbook procedures have their own internal API that requires Supabase session auth, not API keys.

These are bounded — the doc-as-code pattern is for articles. Sections and branding are tenant-level layout decisions you set once.

A real example: this very help center

The Atender help center is doc-as-code. The pattern in production:

  1. Articles live in Atender/KB Articles/ in an Obsidian vault on a maintainer’s machine.
  2. A Python script reads each .md file, parses the frontmatter, and walks every category.
  3. The script calls /api/v1/kb/* with a production API key stored in 1Password.
  4. Categories upsert by name; articles upsert by slug.
  5. After the bulk push, the maintainer clicks Sync Translations in the editor to refresh non-English versions.

The whole loop — edit, save, push — takes under a minute for a single article and a few minutes for a whole batch. The KB stays current without anyone “remembering to update the help center.”

See also

Tags

AdvancedConcept

See Atender in action

Book a personalized demo and see how AI-powered customer service with expert humans can transform your support operation.