Push articles via API

Generate an API key with knowledge:write, ensure the category exists with POST /api/v1/kb/categories, then POST or PATCH the article with /api/v1/kb/articles. Embeddings rebuild on the server.

May 10, 2026

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

  1. Go to Settings → API Keys.
  2. Click New API key.
  3. Pick scopes:
    knowledge:read — for listing and reading.
    knowledge:write — for creating, updating, deleting.
  4. Name the key descriptively (“doc-as-code”, “migration”).
  5. 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:

  • Productionhttps://prod.atender.dev/api/v1/kb
  • Staginghttps://staging.atender.dev/api/v1/kb
  • Developmenthttps://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.
  • slug is what makes the article uniquely identifiable. Re-POSTing the same slug updates the existing article — the API treats this as upsert.
  • content is 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).
  • customMetadata is an open jsonb bucket. Use it for downstream consumers — the Agent Stack reads uxPath, you can add videoUrl, 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.

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.

Tags

How ToAdvanced

See Atender in action

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