Posts API
The posts API has two faces: a public read-only surface that any frontend can hit without auth, and an admin write surface for creating, updating, and publishing content.
Public Endpoints
GET /api/v1/posts
List published blog posts, paginated.
Query parameters:
| Param | Default | Description |
|---|---|---|
page |
1 |
Page number |
per_page |
12 |
Items per page (max 100) |
category |
— | Filter by category slug |
q |
— | Search query (min 2 chars) |
Example:
curl "https://your-site.com/api/v1/posts?category=cms&per_page=5"
Response shape:
{
"success": true,
"message": "Posts retrieved successfully",
"data": [
{
"id": 267,
"title": "UnfoldCMS Now Has a Real REST API",
"slug": "unfoldcms-public-api-v1-launch",
"excerpt": "The new /api/v1/* namespace ships in Core...",
"content_type": "post",
"featured_image": {
"thumbnail": "https://...thumbnail.webp",
"medium": "https://...medium.webp",
"large": "https://...large.webp",
"original": "https://...webp"
},
"author": {
"name": "Hamed Pakdaman",
"avatar": null
},
"categories": [
{ "id": 5, "name": "Headless CMS", "slug": "headless-cms" }
],
"reading_time": 7,
"posted_at": "2026-05-26T15:00:00+00:00",
"updated_at": "2026-05-26T15:00:00+00:00"
}
],
"pagination": {
"current_page": 1,
"per_page": 5,
"total": 42,
"last_page": 9,
"from": 1,
"to": 5
}
}
GET /api/v1/posts/{slug}
Fetch a single published post with the full body and SEO fields.
Example:
curl https://your-site.com/api/v1/posts/unfoldcms-public-api-v1-launch
Response shape: same as list item, plus body (HTML), seo_title, meta_desc.
Returns 404 for unpublished posts, future-scheduled posts, and non-existent slugs.
GET /api/v1/posts/{slug}/related
Returns 3–5 posts sharing categories with the given post, ordered by recency.
curl https://your-site.com/api/v1/posts/unfoldcms-public-api-v1-launch/related
Response: array of post objects (same shape as list items). No pagination.
Admin Endpoints
Requires bearer token with admin ability.
GET /api/v1/admin/posts
List ALL posts (including drafts and scheduled).
Extra query params:
| Param | Description |
|---|---|
drafts_only |
true to filter to drafts only |
POST /api/v1/admin/posts
Create a new post.
Request body:
{
"title": "My new post",
"slug": "my-new-post",
"body": "<p>Post HTML content</p>",
"short_description": "Brief summary",
"content_type": "post",
"is_published": false,
"posted_at": "2026-06-01T09:00:00Z",
"seo_title": "SEO Title (max 60 chars)",
"meta_desc": "Meta description (max 155 chars)",
"category_ids": [5, 3]
}
Required: title.
Defaults: content_type → post, body → empty string.
Returns 201 + the created post object.
GET /api/v1/admin/posts/{id}
Single post by numeric ID (not slug). Includes drafts.
PATCH /api/v1/admin/posts/{id}
Update one or more fields. Send only the fields you want to change.
curl -X PATCH https://your-site.com/api/v1/admin/posts/267 \
-H "Authorization: Bearer ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"title": "Updated title"}'
DELETE /api/v1/admin/posts/{id}
Permanently deletes the post (no soft delete by default).
POST /api/v1/admin/posts/{id}/publish
Sets is_published=true and fires the post.published webhook event. If posted_at is null, it's set to now.
curl -X POST https://your-site.com/api/v1/admin/posts/267/publish \
-H "Authorization: Bearer ADMIN_TOKEN"
POST /api/v1/admin/posts/{id}/unpublish
Sets is_published=false. The post stays in the database but isn't returned by public endpoints.
Common Errors
| Status | When |
|---|---|
| 401 | No Authorization header or token revoked |
| 403 | Token lacks admin ability (for admin endpoints) |
| 404 | Post doesn't exist, or unpublished (on public endpoints) |
| 422 | Validation failed — missing title, invalid date, duplicate slug, etc. |
| 429 | Rate limit exceeded |