Skip to content

API Contracts: Tag Management (005)

Date: 2026-05-29 Type: Brownfield — documents existing REST API contracts

Base URL

All endpoints proxied through Next.js at /api/proxy → backend at http://backend:8000

Tag Group Endpoints

GET /tag-groups

List tag groups for a topic, optionally including similar groups.

Auth: Public

Query Params:

ParamTypeRequiredDescription
topic_idUUIDYesFilter by topic
include_similarboolNoInclude groups with cosine similarity >= 0.90

Response: Array of tag group objects, each containing id, name, display_name, description, color_hex, sort_order, tags[], and optionally similar_groups[]. Includes a virtual "Ungrouped" group (id=null) containing ungrouped tags.


POST /tag-groups

Create a new tag group.

Auth: Admin

Body:

FieldTypeRequiredDescription
namestringYesAuto-normalized to slug format
display_namestringYesAuto-normalized to title case
descriptionstringNo
color_hexstringNo7-char hex color
topic_idUUIDYes

Response: Created tag group object with auto-generated embedding.

Errors: 409 if name conflicts with existing group in same topic.


GET /tag-groups/

Get a single tag group with its tags.

Auth: Public

Response: Tag group object with tags[] array.


PUT /tag-groups/

Update a tag group.

Auth: Admin

Body:

FieldTypeRequiredDescription
namestringNoAuto-normalized to slug
display_namestringNoAuto-normalized to title case
descriptionstringNo
color_hexstringNo

Errors: 409 if name conflicts with another group in same topic.


DELETE /tag-groups/

Delete a tag group. Tags become ungrouped (not deleted).

Auth: Admin

Response: 204 No Content


POST /tag-groups/merge

Merge two tag groups.

Auth: Admin

Body:

FieldTypeRequiredDescription
source_group_idUUIDYesGroup to merge from (deleted)
target_group_idUUIDNoGroup to merge into (kept). Omit to create new.
result_namestringNoName for new group (if target_group_id omitted). Auto-slugified.
result_display_namestringNoDisplay name for new group. Auto-title-cased.
result_colorstringNoColor for new group.

Behavior: Tags with the same name in both groups are deduplicated (article_tags transferred to surviving tag, duplicate deleted).

Response: Merged group object.


POST /tag-groups/reorder

Batch update sort_order for tag groups.

Auth: Admin

Body:

FieldTypeRequiredDescription
groupsArray<{id: UUID, sort_order: int}>Yes

Response: 200 OK


Tag Endpoints

PUT /tags/

Rename a tag or move it to a different group.

Auth: Admin

Body:

FieldTypeRequiredDescription
namestringNoNew name (embedding regenerated)
tag_group_idUUID or nullNoNew group (null = ungrouped)

Response: Updated tag object.


DELETE /tags/

Delete a tag and all its article_tags associations.

Auth: Admin

Response: 204 No Content


POST /tags/batch-move

Move multiple tags to a different group.

Auth: Admin

Body:

FieldTypeRequiredDescription
tag_idsArray<UUID>YesTags to move
tag_group_idUUID or nullYesTarget group (null = ungrouped)

Response: 200 OK


Normalization Suggestion Endpoints

GET /tag-normalization-suggestions

List pending normalization suggestions.

Auth: Admin

Response: Array of suggestion objects with id, new_tag (name, group), existing_tag (name, group), similarity_score, article_id.


POST /tag-normalization-suggestions/{id}/approve

Approve (merge) a suggestion. Re-points article_tags from new_tag to existing_tag, deletes new_tag.

Auth: Admin

Body:

FieldTypeRequiredDescription
resolved_byUUIDYesAdmin user ID

Response: 200 OK


POST /tag-normalization-suggestions/{id}/reject

Reject a suggestion. Both tags remain.

Auth: Admin

Body:

FieldTypeRequiredDescription
resolved_byUUIDYesAdmin user ID

Response: 200 OK