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:
- Listing existing articles by slug before pushing.
- If a slug matches an existing article,
PATCHit instead ofPOST. - If the slug doesn’t match anything,
POSTto 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) andknowledge: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 are marked for rebuild by the API push, then processed by a background scheduler. They typically update on the next scheduler cycle, but allow up to about 10 minutes 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/syncafter 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 keeps saves fast and bulk pushes simple: the article is stored immediately, and retrieval catches up asynchronously in the background.
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:
- Articles live in
Atender/KB Articles/in an Obsidian vault on a maintainer’s machine. - A Python script reads each
.mdfile, parses the frontmatter, and walks every category. - The script calls
/api/v1/kb/*with a production API key stored in 1Password. - Categories upsert by name; articles upsert by slug.
- 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.”