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.

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_typepost, 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