Posts
Post management endpoints. Posts are dynamic content entries such as blog posts, news articles, and similar time-based content. They support multiple languages, categories, tags, and a featured image.
Base path: /api/v1/posts
List Posts
Required permission: posts:read
Query parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
skip |
int | 0 |
Pagination offset |
limit |
int | 100 |
Pagination limit |
status |
string | — | Filter by status: draft, published, archived |
Response 200 OK
[
{
"id": "uuid",
"slug": "world-cup-final",
"status": "published",
"published_at": "2026-01-01T00:00:00Z",
"author_id": "uuid"
}
]
List Posts by Category
Required permission: posts:read
Query parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
skip |
int | 0 |
Pagination offset |
limit |
int | 100 |
Pagination limit |
Response 200 OK — list of PostSummary objects.
Errors
| Status | Description |
|---|---|
404 |
Category not found |
List Posts by Tag
Required permission: posts:read
Query parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
skip |
int | 0 |
Pagination offset |
limit |
int | 100 |
Pagination limit |
Response 200 OK — list of PostSummary objects.
Errors
| Status | Description |
|---|---|
404 |
Tag not found |
Create Post
Required permission: posts:create
Request body
{
"slug": "world-cup-final",
"status": "draft",
"featured_image_id": "uuid",
"author_id": "uuid",
"category_ids": ["uuid-1", "uuid-2"],
"tag_ids": ["uuid-1", "uuid-2"],
"translations": [
{
"language_id": "uuid",
"title": "World Cup Final",
"content": "<p>Full article content here...</p>",
"excerpt": "A short summary of the article.",
"meta_title": "World Cup Final 2026",
"meta_description": "Read about the thrilling World Cup Final."
}
]
}
| Field | Type | Required | Constraints |
|---|---|---|---|
slug |
string | ✅ | 2–255 characters, unique |
status |
string | ❌ | draft, published, archived. Default: draft |
featured_image_id |
UUID | ❌ | Must exist in media |
author_id |
UUID | ❌ | Defaults to current user |
category_ids |
array | ❌ | List of category UUIDs |
tag_ids |
array | ❌ | List of tag UUIDs |
translations |
array | ❌ | List of translations |
Response 201 Created — full PostResponse object.
Published at
If status is set to published on creation, published_at is automatically set to the current timestamp.
On subsequent publish actions, published_at is never overwritten — it always reflects the first publish date.
Errors
| Status | Description |
|---|---|
404 |
Featured image not found |
404 |
One or more categories not found |
404 |
One or more tags not found |
409 |
Post slug already exists |
Get Post
Required permission: posts:read
Response 200 OK
{
"id": "uuid",
"slug": "world-cup-final",
"status": "published",
"published_at": "2026-01-01T00:00:00Z",
"featured_image_id": "uuid",
"author_id": "uuid",
"created_by": "uuid",
"translations": [
{
"id": "uuid",
"post_id": "uuid",
"language_id": "uuid",
"title": "World Cup Final",
"content": "<p>Full article content here...</p>",
"excerpt": "A short summary of the article.",
"meta_title": "World Cup Final 2026",
"meta_description": "Read about the thrilling World Cup Final.",
"created_at": "2026-01-01T00:00:00Z",
"updated_at": null
}
],
"categories": [
{
"id": "uuid",
"slug": "sport",
"is_active": true,
"order": 0,
"parent_id": null
}
],
"tags": [
{
"id": "uuid",
"slug": "fifa",
"is_active": true
}
],
"featured_image": {
"id": "uuid",
"filename": "550e8400.jpg",
"original_filename": "stadium.jpg",
"mime_type": "image/jpeg",
"size": 204800,
"path": "uploads/media/550e8400.jpg",
"alt_text": "Stadium photo"
},
"created_at": "2026-01-01T00:00:00Z",
"updated_at": null
}
Errors
| Status | Description |
|---|---|
404 |
Post not found |
Update Post
Required permission: posts:update
Request body — all fields optional
{
"slug": "world-cup-final-2026",
"status": "published",
"featured_image_id": "uuid",
"author_id": "uuid",
"category_ids": ["uuid-1"],
"tag_ids": ["uuid-1", "uuid-2"]
}
Replacing relations
When category_ids or tag_ids are provided, they replace the existing list entirely.
To remove all categories, pass an empty array [].
Response 200 OK — updated PostResponse object.
Status transitions
draft → published → published_at set (only on first publish)
draft → archived
published → draft
published → archived
archived → draft
archived → published
Errors
| Status | Description |
|---|---|
404 |
Post not found |
404 |
Featured image not found |
404 |
One or more categories not found |
404 |
One or more tags not found |
409 |
Post slug already exists |
Delete Post
Soft-deletes a post.
Required permission: posts:delete
Response 204 No Content
Errors
| Status | Description |
|---|---|
404 |
Post not found |
Upsert Translation
Creates or updates a translation for a specific language. If a translation for the given language already exists, it will be updated.
Required permission: posts:update
Request body
{
"language_id": "uuid",
"title": "World Cup Final",
"content": "<p>Full article content here...</p>",
"excerpt": "A short summary of the article.",
"meta_title": "World Cup Final 2026",
"meta_description": "Read about the thrilling World Cup Final."
}
| Field | Type | Required | Description |
|---|---|---|---|
language_id |
UUID | ✅ | Must exist in DB |
title |
string | ✅ | Post title |
content |
string | ❌ | Full post content |
excerpt |
string | ❌ | Short summary |
meta_title |
string | ❌ | SEO title |
meta_description |
string | ❌ | SEO description |
Response 200 OK — updated PostResponse object.
Errors
| Status | Description |
|---|---|
404 |
Post not found |
404 |
Language not found |
Delete Translation
Removes a translation for a specific language from a post.
Required permission: posts:update
Response 200 OK — updated PostResponse object.
Errors
| Status | Description |
|---|---|
404 |
Post not found |
404 |
Translation not found |