Push articles via API
This walks through the minimal flow for pushing a Knowledge Base article from outside Atender — useful for doc-as-code pipelines, migrations, or any scripted content sync.
1. Generate an API key
- Go to Settings → API Keys.
- Click New API key.
- Pick scopes:
knowledge:read— for listing and reading.knowledge:write— for creating, updating, deleting. - Name the key descriptively (“doc-as-code”, “migration”).
- Save and copy the key. It’s shown once.
For testing, use a sa_test_* key. For production, sa_live_*. Keep both out of source control — store them in your CI’s secret store or a local .env ignored by Git.
2. Set the base URL
Use your environment’s domain:
- Production —
https://prod.atender.dev/api/v1/kb - Staging —
https://staging.atender.dev/api/v1/kb - Development —
https://dev.atender.dev/api/v1/kb
Custom domains for the public help center don’t apply to the API — the API stays on Atender’s domain even if the help center is at help.example.com.
3. Make sure the category exists
Articles need a category. Create it first if it doesn’t already exist.
curl -X POST "$BASE_URL/categories" \
-H "Authorization: Bearer $ATENDER_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"title": "Billing",
"slug": "billing",
"description": "Invoices, payments, and refunds",
"sortOrder": 1
}'
A successful create returns the category with its server-generated ID. If the slug already exists for your tenant, the call returns the existing category — categories are upserted by slug.
Save the id from the response — you’ll need it for the article.
4. Push an article
curl -X POST "$BASE_URL/articles" \
-H "Authorization: Bearer $ATENDER_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"title": "How to update your payment method",
"slug": "update-payment-method",
"summary": "Open Account, go to Billing, click Change.",
"content": "## Update payment method\n\nOpen your account settings...",
"categoryId": "cat_abc123",
"status": "published",
"difficulty": "beginner",
"estimatedMinutes": 2,
"keywords": ["payment", "credit card", "billing"],
"customMetadata": {
"uxPath": "Account → Billing → Payment methods"
}
}'
Notes:
status: "published"publishes immediately. Use"draft"to upload-but-not-show. Use"needs-review"to flag the article in the in-app editor’s review queue.slugis what makes the article uniquely identifiable. Re-POSTing the same slug updates the existing article — the API treats this as upsert.contentis markdown. The KB renderer converts it server-side. Tables and nested lists render correctly on the public site (with the public renderer’s HTML allow-list).customMetadatais an open jsonb bucket. Use it for downstream consumers — the Agent Stack readsuxPath, you can addvideoUrl,sopChecklist, anything.
5. Update an existing article
To update an article you’ve already pushed, PATCH it by ID:
curl -X PATCH "$BASE_URL/articles/article_xyz" \
-H "Authorization: Bearer $ATENDER_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"summary": "New summary",
"content": "Updated body…"
}'
PATCH is partial — fields you don’t include are unchanged. Editing triggers an embedding refresh; if you have languages enabled, translation jobs queue for the changed content.
If your pipeline upserts by slug, you don’t need PATCH at all — re-POST the article and the slug match handles it.
6. Mark an article reviewed
Calling mark-reviewed updates lastReviewedAt without round-tripping the article body. Useful for dashboards that surface stale articles and let reviewers approve them in bulk.
curl -X POST "$BASE_URL/articles/article_xyz/mark-reviewed" \
-H "Authorization: Bearer $ATENDER_API_KEY"
The response is the full updated article.
7. Search
To verify retrieval:
curl -X POST "$BASE_URL/search" \
-H "Authorization: Bearer $ATENDER_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"query": "update payment method",
"limit": 5
}'
The response is a list of articles ranked by the same hybrid retrieval pipeline customer-facing search uses.
A minimal Python upsert
import os, requests, frontmatter
BASE = "https://prod.atender.dev/api/v1/kb"
HEADERS = {"Authorization": f"Bearer {os.environ['ATENDER_API_KEY']}"}
def ensure_category(title, slug):
r = requests.post(f"{BASE}/categories", headers=HEADERS,
json={"title": title, "slug": slug})
r.raise_for_status()
return r.json()["id"]
def upsert_article(path, category_id):
post = frontmatter.load(path)
payload = {
"title": post["title"],
"slug": post["slug"],
"summary": post.get("summary", ""),
"content": post.content,
"categoryId": category_id,
"status": post.get("status", "draft"),
"keywords": post.get("keywords", []),
"customMetadata": {"uxPath": post.get("ux_path")},
}
r = requests.post(f"{BASE}/articles", headers=HEADERS, json=payload)
r.raise_for_status()
return r.json()
That’s the whole shape — read frontmatter + body, ensure the category, upsert the article. Run it on every commit and your help center stays in sync with your repo.
Common gotchas
- The category must exist before the article. Create categories first; collect IDs; then push articles. The article create call fails with 400 if the category doesn’t exist.
- Slugs are tenant-unique. Two articles with the same slug in your tenant collide — the second updates the first.
- HTML in content gets sanitized. The public KB renderer enforces an allow-list. Tables and nested lists render correctly today; arbitrary
<script>,<iframe>, and inline event handlers are stripped. - Embeddings take a moment. A freshly pushed article appears in retrieval within a minute. If your test query doesn’t return it immediately, wait and try again.
- Roles aren’t yet on v1. If your articles need role tagging, apply them in the in-app editor after the push, or use the internal
/api/kb/*surface (Supabase auth required) for that step.