A category is a node in the tree that organizes articles inside a knowledge base. Categories nest through parent_id; each carries a name, a slug, an icon, and a sort order. An article points to a category through its category_id.
Anatomy
{
"object": "article_category",
"id": "5d694e14-5440-4f5d-9e8a-7c3b2a1d0f9e",
"knowledge_base_id": "2c1b4a9f-7e8d-4a3c-9b1f-6e5d4c3b2a1f",
"parent_id": null,
"name": "Returns",
"slug": "returns",
"description": "Policies and processes for product returns.",
"icon": "package",
"sort_order": 0,
"created_at": "2026-05-22T13:50:00+00:00"
}
Where categories live
Categories are scoped to an org and live inside a knowledge base — knowledge_base_id is required when you create one. The tree is built from parent_id — null means “root of the tree.” There is no enforced depth limit, but most teams keep it to two or three levels for usability.
Creating a category
POST /article-categories requires name, slug, and knowledge_base_id. The slug must be unique within the org. Pass parent_id to nest under another category.
Both knowledge_base_id and parent_id must reference resources that belong to your organization. A foreign or nonexistent id is rejected with 422 — "A referenced resource does not exist." sort_order, when supplied, must be an integer; a non-integer value returns invalid_payload.
curl https://api.awardee.dev/v1/article-categories \
-X POST \
-H "Authorization: Bearer aw_live_4f8a3c7e2d1b9a5c6f8e3d2c1b4a9f7e" \
-H "Content-Type: application/json" \
-d '{
"name": "Returns",
"slug": "returns",
"description": "Policies and processes for product returns.",
"icon": "package",
"knowledge_base_id": "2c1b4a9f-7e8d-4a3c-9b1f-6e5d4c3b2a1f"
}'
Listing
GET /article-categories returns a flat, paginated list. Use the query parameters to scope:
| Param | Effect |
|---|
knowledge_base_id | Return categories in this KB only |
parent_id | Return direct children of this category |
root_only | Return only categories with parent_id === null |
page, page_size | Standard pagination |
The API does not return a tree — assemble it client-side from the flat list (see the object reference).
Deleting
DELETE /article-categories/{id} has two modes.
Default: refuse if non-empty
If the category has child categories or contains articles, the call returns 409 category_not_empty with counts of what’s in the way. Empty categories delete cleanly with a 204.
{
"error": "category_not_empty",
"message": "Category has children or articles. Pass ?cascade=true to delete recursively.",
"child_category_count": 2,
"article_count": 14,
"request_id": "req_8fK2x9aLp0qR"
}
The counts are scoped to direct contents — a child category that itself has children only contributes 1 to child_category_count. Use them to decide whether a cascade is safe.
Cascade: delete everything
Pass ?cascade=true to delete the category, every descendant category, and every article inside them in one call. The response is a summary:
{
"object": "article_category.delete_result",
"cascade": true,
"deleted_category_count": 3,
"deleted_article_count": 14
}
Cascade is irreversible and destroys articles. The article.deleted webhook fires per article. There is no soft-delete fallback — for a recoverable removal, set article status to archived instead.
# Default: 409 if non-empty
curl https://api.awardee.dev/v1/article-categories/5d694e14-5440-4f5d-9e8a-7c3b2a1d0f9e \
-X DELETE \
-H "Authorization: Bearer aw_live_4f8a3c7e2d1b9a5c6f8e3d2c1b4a9f7e"
# Cascade: delete everything inside
curl "https://api.awardee.dev/v1/article-categories/5d694e14-5440-4f5d-9e8a-7c3b2a1d0f9e?cascade=true" \
-X DELETE \
-H "Authorization: Bearer aw_live_4f8a3c7e2d1b9a5c6f8e3d2c1b4a9f7e"
Webhook events
| Event | Fires on |
|---|
article_category.created | New category written |
article_category.updated | Any field changes |
article_category.deleted | Category removed (one event per deleted category, including cascade descendants) |