Skip to main content

Blog API

Overview

The Blog API manages the content of the core blog module: posts, categories, tags, comments, and reactions. It exposes 14 endpoints across two very different access levels.

Eleven endpoints are private — they require an admin access token exactly like every other admin endpoint (see Authentication).

Three endpoints are public. They accept unauthenticated writes from anonymous storefront visitors, because that is what a comment box and a reaction button need. Read the Public endpoints section before exposing your store — these are the only write surfaces in EverShop that anyone on the internet can call.

Every {id} path parameter is the entity's uuid, not its integer primary key.

MethodEndpointAccessPurpose
POST/api/blog/postsPrivateCreate a post
PATCH/api/blog/posts/{id}PrivateUpdate a post
DELETE/api/blog/posts/{id}PrivateDelete a post
POST/api/blog/categoriesPrivateCreate a category
PATCH/api/blog/categories/{id}PrivateUpdate a category
DELETE/api/blog/categories/{id}PrivateDelete a category
POST/api/blog/tagsPrivateCreate a tag
PATCH/api/blog/tags/{id}PrivateUpdate a tag
DELETE/api/blog/tags/{id}PrivateDelete a tag
PATCH/api/blog/comments/{id}PrivateModerate a comment
DELETE/api/blog/comments/{id}PrivateDelete a comment
POST/api/blog/commentsPublicSubmit a comment
POST/api/blog/comments/{id}/likePublicToggle a comment like
POST/api/blog/posts/{id}/reactPublicToggle a post reaction

There are no GET endpoints. Blog content is read through GraphQL — see the Blog module guide for the blogPost, blogPosts, blogCategories, blogTags, and blogComments queries.

Endpoints

Create A Blog Post

Creates a blog post. Only name is required; everything else has a database default.

description is a block array (the Editor.js document shape), not an HTML string — each entry is a row object containing columns, and each column carries data.blocks. reading_time is computed from it inside the transaction and stored on the post, so you never send it.

url_key must match ^[a-z0-9]+(?:-[a-z0-9]+)*$ — lowercase letters, digits, and single hyphens. Omit it and a database trigger slugifies name for you. Blog slugs get no numeric disambiguation suffix, so two posts with the same name collide on the url_key unique constraint; set an explicit url_key in that case.

tags is an array of blog tag ids, replacing the post's tag pivot. category_id and author_id accept an integer, a numeric string, or null — an empty string is coerced to null.

metafields is folded into the post's meta_data JSONB column and is shaped { namespace: { key: value } }. It is validated against the blog_post metafield definitions. Omit the key entirely to leave meta_data untouched.

Setting status to 1 stamps published_at with the current time if it is not already set. There is no publishing scheduler.

Request Schema (application/json)
Field NameField TypeRequired
statusstring or integer ('0', '1', 0, 1)No
namestringYes
url_keystring (^[a-z0-9]+(?:-[a-z0-9]+)*$)No
short_descriptionstring or nullNo
descriptionarray of objectNo
category_idinteger or string or nullNo
author_idinteger or string or nullNo
thumbnailstring or nullNo
tagsarray of integer,stringNo
published_atstring or nullNo
meta_titlestring or nullNo
meta_descriptionstring or nullNo
metafieldsobjectNo
POST/api/blog/posts
curl
-H "Accept: application/json"
-H "Authorization: Bearer <admin JWT token>"
--data-raw '<JSON DATA>'
https://<your domain>/api/blog/posts
Sample Of Response
{
"data": {
"blog_post_id": 12,
"uuid": "9f2b1c44-6d81-4a20-9d0e-2f7c8a1b3e55",
"status": 1,
"category_id": 3,
"author_id": 1,
"thumbnail": "/assets/catalog/blog/summer-guide.jpg",
"reaction_counts": {},
"comment_count": 0,
"reading_time": 4,
"published_at": "2026-02-18T09:12:44.108Z",
"meta_data": {
"custom": {
"featured": true
}
},
"created_at": "2026-02-18 09:12:44",
"updated_at": "2026-02-18 09:12:44",
"blog_post_description_id": 12,
"blog_post_description_blog_post_id": 12,
"name": "A Complete Guide To Summer Fabrics",
"short_description": "Linen, cotton, and everything between.",
"url_key": "a-complete-guide-to-summer-fabrics",
"meta_title": "Summer Fabrics Guide",
"meta_description": "How to choose fabrics that breathe.",
"links": [
{
"rel": "blogPostGrid",
"href": "/admin/blog/posts",
"action": "GET",
"types": [
"text/xml"
]
},
{
"rel": "edit",
"href": "/admin/blog/posts/edit/9f2b1c44-6d81-4a20-9d0e-2f7c8a1b3e55",
"action": "GET",
"types": [
"text/xml"
]
}
]
}
}

A minimal description payload looks like this:

{
"name": "A Complete Guide To Summer Fabrics",
"status": 1,
"description": [
{
"id": "row-1",
"size": 12,
"columns": [
{
"id": "col-1",
"size": 12,
"data": {
"blocks": [
{
"type": "header",
"data": { "text": "Why fabric weight matters" }
},
{
"type": "paragraph",
"data": { "text": "Lighter weaves move more air." }
}
]
}
}
]
}
]
}

Update A Blog Post

Updates an existing post. Identical to the create payload except that nothing is required — send only the fields you want to change.

reading_time is recomputed only when description is present in the payload. Supplying tags replaces the whole tag set; omitting it leaves the existing tags alone. Changing url_key re-points the post's url_rewrite row, so the old friendly URL stops resolving.

Request Schema (application/json)
Field NameField TypeRequired
statusstring or integer ('0', '1', 0, 1)No
namestringNo
url_keystring (^[a-z0-9]+(?:-[a-z0-9]+)*$)No
short_descriptionstring or nullNo
descriptionarray of objectNo
category_idinteger or string or nullNo
author_idinteger or string or nullNo
thumbnailstring or nullNo
tagsarray of integer,stringNo
published_atstring or nullNo
meta_titlestring or nullNo
meta_descriptionstring or nullNo
metafieldsobjectNo
PATCH/api/blog/posts/9f2b1c44-6d81-4a20-9d0e-2f7c8a1b3e55
curl
-H "Accept: application/json"
-H "Authorization: Bearer <admin JWT token>"
--data-raw '<JSON DATA>'
https://<your domain>/api/blog/posts/9f2b1c44-6d81-4a20-9d0e-2f7c8a1b3e55
Sample Of Response
{
"data": {
"blog_post_id": 12,
"uuid": "9f2b1c44-6d81-4a20-9d0e-2f7c8a1b3e55",
"status": 1,
"category_id": 3,
"author_id": 1,
"thumbnail": "/assets/catalog/blog/summer-guide.jpg",
"reaction_counts": {
"like": 8,
"clap": 2
},
"comment_count": 5,
"reading_time": 6,
"published_at": "2026-02-18T09:12:44.108Z",
"meta_data": {},
"created_at": "2026-02-18 09:12:44",
"updated_at": "2026-02-19 14:03:02",
"name": "A Complete Guide To Summer Fabrics (2026)",
"short_description": "Linen, cotton, and everything between.",
"url_key": "a-complete-guide-to-summer-fabrics",
"meta_title": "Summer Fabrics Guide",
"meta_description": "How to choose fabrics that breathe.",
"links": [
{
"rel": "blogPostGrid",
"href": "/admin/blog/posts",
"action": "GET",
"types": [
"text/xml"
]
},
{
"rel": "edit",
"href": "/admin/blog/posts/edit/9f2b1c44-6d81-4a20-9d0e-2f7c8a1b3e55",
"action": "GET",
"types": [
"text/xml"
]
}
]
}
}

Delete A Blog Post

Permanently removes a post. The description row, tag pivot rows, and all comments cascade via foreign keys, and the post's url_rewrite row is removed by the blog_post_deleted subscriber. The response body is the post as it was before deletion.

Request Schema (application/json)
No request body required
DELETE/api/blog/posts/9f2b1c44-6d81-4a20-9d0e-2f7c8a1b3e55
curl
-H "Accept: application/json"
-H "Authorization: Bearer <admin JWT token>"
https://<your domain>/api/blog/posts/9f2b1c44-6d81-4a20-9d0e-2f7c8a1b3e55
Sample Of Response
{
"data": {
"blog_post_id": 12,
"uuid": "9f2b1c44-6d81-4a20-9d0e-2f7c8a1b3e55",
"status": 1,
"category_id": 3,
"author_id": 1,
"thumbnail": "/assets/catalog/blog/summer-guide.jpg",
"reaction_counts": {
"like": 8
},
"comment_count": 5,
"reading_time": 6,
"published_at": "2026-02-18T09:12:44.108Z",
"meta_data": {},
"created_at": "2026-02-18 09:12:44",
"updated_at": "2026-02-19 14:03:02"
}
}

Create A Blog Category

Creates a blog category. name is required.

comment_policy decides what happens to comments submitted on posts in this category and accepts exactly open, moderated, or closed. It defaults to moderated at the database level, which stores new comments as pending until an admin approves them. open auto-approves; closed makes the public comment endpoint respond 403.

position orders categories in listings. metafields is folded into the category's meta_data column against the blog_category definitions.

Request Schema (application/json)
Field NameField TypeRequired
statusstring or integer ('0', '1', 0, 1)No
namestringYes
url_keystring (^[a-z0-9]+(?:-[a-z0-9]+)*$)No
comment_policystring (open, moderated, closed)No
short_descriptionstring or nullNo
positioninteger or string or nullNo
meta_titlestring or nullNo
meta_descriptionstring or nullNo
metafieldsobjectNo
POST/api/blog/categories
curl
-H "Accept: application/json"
-H "Authorization: Bearer <admin JWT token>"
--data-raw '<JSON DATA>'
https://<your domain>/api/blog/categories
Sample Of Response
{
"data": {
"blog_category_id": 3,
"uuid": "1a7d5e90-3c62-4f18-8b44-05e9d7c21a6b",
"status": 1,
"comment_policy": "moderated",
"position": 10,
"meta_data": {},
"created_at": "2026-02-10 11:40:07",
"updated_at": "2026-02-10 11:40:07",
"blog_category_description_id": 3,
"blog_category_description_blog_category_id": 3,
"name": "Style Guides",
"short_description": "How to wear it.",
"url_key": "style-guides",
"meta_title": "Style Guides",
"meta_description": "Seasonal styling advice.",
"links": [
{
"rel": "blogCategoryGrid",
"href": "/admin/blog/categories",
"action": "GET",
"types": [
"text/xml"
]
},
{
"rel": "edit",
"href": "/admin/blog/categories/edit/1a7d5e90-3c62-4f18-8b44-05e9d7c21a6b",
"action": "GET",
"types": [
"text/xml"
]
}
]
}
}

Update A Blog Category

Updates a category. No field is required. Changing comment_policy affects comments submitted from that point on; comments already stored keep their status until moderated.

Request Schema (application/json)
Field NameField TypeRequired
statusstring or integer ('0', '1', 0, 1)No
namestringNo
url_keystring (^[a-z0-9]+(?:-[a-z0-9]+)*$)No
comment_policystring (open, moderated, closed)No
short_descriptionstring or nullNo
positioninteger or string or nullNo
meta_titlestring or nullNo
meta_descriptionstring or nullNo
metafieldsobjectNo
PATCH/api/blog/categories/1a7d5e90-3c62-4f18-8b44-05e9d7c21a6b
curl
-H "Accept: application/json"
-H "Authorization: Bearer <admin JWT token>"
--data-raw '<JSON DATA>'
https://<your domain>/api/blog/categories/1a7d5e90-3c62-4f18-8b44-05e9d7c21a6b
Sample Of Response
{
"data": {
"blog_category_id": 3,
"uuid": "1a7d5e90-3c62-4f18-8b44-05e9d7c21a6b",
"status": 1,
"comment_policy": "open",
"position": 10,
"meta_data": {},
"created_at": "2026-02-10 11:40:07",
"updated_at": "2026-02-20 08:22:31",
"name": "Style Guides",
"short_description": "How to wear it.",
"url_key": "style-guides",
"meta_title": "Style Guides",
"meta_description": "Seasonal styling advice.",
"links": [
{
"rel": "blogCategoryGrid",
"href": "/admin/blog/categories",
"action": "GET",
"types": [
"text/xml"
]
},
{
"rel": "edit",
"href": "/admin/blog/categories/edit/1a7d5e90-3c62-4f18-8b44-05e9d7c21a6b",
"action": "GET",
"types": [
"text/xml"
]
}
]
}
}

Delete A Blog Category

Permanently removes a category. Posts in the category are not deleted — blog_post.category_id is ON DELETE SET NULL, so those posts become uncategorised and fall back to the default moderated comment policy.

Request Schema (application/json)
No request body required
DELETE/api/blog/categories/1a7d5e90-3c62-4f18-8b44-05e9d7c21a6b
curl
-H "Accept: application/json"
-H "Authorization: Bearer <admin JWT token>"
https://<your domain>/api/blog/categories/1a7d5e90-3c62-4f18-8b44-05e9d7c21a6b
Sample Of Response
{
"data": {
"blog_category_id": 3,
"uuid": "1a7d5e90-3c62-4f18-8b44-05e9d7c21a6b",
"status": 1,
"comment_policy": "open",
"position": 10,
"meta_data": {},
"created_at": "2026-02-10 11:40:07",
"updated_at": "2026-02-20 08:22:31"
}
}

Create A Blog Tag

Creates a tag. name is required. Tags are a single flat table — there is no description split, no status, and no metafields key.

Request Schema (application/json)
Field NameField TypeRequired
namestringYes
url_keystring (^[a-z0-9]+(?:-[a-z0-9]+)*$)No
meta_titlestring or nullNo
meta_descriptionstring or nullNo
POST/api/blog/tags
curl
-H "Accept: application/json"
-H "Authorization: Bearer <admin JWT token>"
--data-raw '<JSON DATA>'
https://<your domain>/api/blog/tags
Sample Of Response
{
"data": {
"blog_tag_id": 7,
"uuid": "c53f8a10-9b47-4de2-a1f6-77b0d3e4c982",
"name": "Linen",
"url_key": "linen",
"meta_title": "Linen",
"meta_description": "Posts about linen.",
"created_at": "2026-02-11 16:05:19",
"links": [
{
"rel": "blogTagGrid",
"href": "/admin/blog/tags",
"action": "GET",
"types": [
"text/xml"
]
},
{
"rel": "edit",
"href": "/admin/blog/tags/edit/c53f8a10-9b47-4de2-a1f6-77b0d3e4c982",
"action": "GET",
"types": [
"text/xml"
]
}
]
}
}

Update A Blog Tag

Updates a tag. No field is required.

Request Schema (application/json)
Field NameField TypeRequired
namestringNo
url_keystring (^[a-z0-9]+(?:-[a-z0-9]+)*$)No
meta_titlestring or nullNo
meta_descriptionstring or nullNo
PATCH/api/blog/tags/c53f8a10-9b47-4de2-a1f6-77b0d3e4c982
curl
-H "Accept: application/json"
-H "Authorization: Bearer <admin JWT token>"
--data-raw '<JSON DATA>'
https://<your domain>/api/blog/tags/c53f8a10-9b47-4de2-a1f6-77b0d3e4c982
Sample Of Response
{
"data": {
"blog_tag_id": 7,
"uuid": "c53f8a10-9b47-4de2-a1f6-77b0d3e4c982",
"name": "Linen & Cotton",
"url_key": "linen-and-cotton",
"meta_title": "Linen",
"meta_description": "Posts about linen.",
"created_at": "2026-02-11 16:05:19",
"links": [
{
"rel": "blogTagGrid",
"href": "/admin/blog/tags",
"action": "GET",
"types": [
"text/xml"
]
},
{
"rel": "edit",
"href": "/admin/blog/tags/edit/c53f8a10-9b47-4de2-a1f6-77b0d3e4c982",
"action": "GET",
"types": [
"text/xml"
]
}
]
}
}

Delete A Blog Tag

Permanently removes a tag. Its blog_post_tag pivot rows cascade, so the tag simply disappears from every post that carried it.

Request Schema (application/json)
No request body required
DELETE/api/blog/tags/c53f8a10-9b47-4de2-a1f6-77b0d3e4c982
curl
-H "Accept: application/json"
-H "Authorization: Bearer <admin JWT token>"
https://<your domain>/api/blog/tags/c53f8a10-9b47-4de2-a1f6-77b0d3e4c982
Sample Of Response
{
"data": {
"blog_tag_id": 7,
"uuid": "c53f8a10-9b47-4de2-a1f6-77b0d3e4c982",
"name": "Linen & Cotton",
"url_key": "linen-and-cotton",
"meta_title": "Linen",
"meta_description": "Posts about linen.",
"created_at": "2026-02-11 16:05:19"
}
}

Moderate A Blog Comment

Flips a comment's moderation status. status is required and must be one of pending, approved, or spam. The post's comment_count is recomputed from the approved comments in the same transaction.

Only approved comments are returned by the storefront comments GraphQL field. spam comments are retained in the database, never displayed.

Request Schema (application/json)
Field NameField TypeRequired
statusstring (pending, approved, spam)Yes
PATCH/api/blog/comments/4e19b7c3-2a58-41d6-b0f9-6c8e5d2a7143
curl
-H "Accept: application/json"
-H "Authorization: Bearer <admin JWT token>"
--data-raw '<JSON DATA>'
https://<your domain>/api/blog/comments/4e19b7c3-2a58-41d6-b0f9-6c8e5d2a7143
Sample Of Response
{
"data": {
"uuid": "4e19b7c3-2a58-41d6-b0f9-6c8e5d2a7143",
"status": "approved"
}
}

Delete A Blog Comment

Permanently removes a comment. Replies cascade through the self-referencing parent_id foreign key, the comment's like rows in blog_reaction are purged, and the post's comment_count is recomputed.

Request Schema (application/json)
No request body required
DELETE/api/blog/comments/4e19b7c3-2a58-41d6-b0f9-6c8e5d2a7143
curl
-H "Accept: application/json"
-H "Authorization: Bearer <admin JWT token>"
https://<your domain>/api/blog/comments/4e19b7c3-2a58-41d6-b0f9-6c8e5d2a7143
Sample Of Response
{
"data": {
"uuid": "4e19b7c3-2a58-41d6-b0f9-6c8e5d2a7143"
}
}

Public endpoints

Unauthenticated write surface

The three endpoints below carry "access": "public" in their route.json. They accept writes from any unauthenticated caller — no admin token, no customer session. This is intentional: anonymous visitors must be able to comment and react. It also means they are the blog module's spam and abuse surface.

The module ships these defences out of the box:

  • Moderation by default. A category's comment_policy defaults to moderated, so submitted comments land as pending and never reach the storefront until an admin approves them. A post with no category also falls back to moderated.
  • Strict sanitisation. name and comment are run through sanitizeHtml with no allowed tags or attributes, whitespace-collapsed, and truncated to 120 and 5000 characters respectively.
  • A honeypot field. The optional website field is hidden in the storefront form. Any submission that fills it is stored as spam.
  • A link heuristic. A comment containing more than three http(s):// occurrences is stored as spam.
  • Cookie-scoped reactions. Reactions are de-duplicated by a signed, httpOnly blog_visitor cookie, and the unique constraint on blog_reaction caps a visitor at one reaction per post and one like per comment.

None of this is rate limiting. Put your usual rate limiter or WAF rule in front of these three routes before going live.

Submit A Blog Comment

Creates a comment on a published post. Public — no authentication.

The post is addressed by post_uuid in the request body, not by a path parameter, and it must resolve to a post with status = 1. Optionally set parent_uuid to reply to an existing comment; the parent must belong to the same post and already be approved, otherwise the reply is silently attached at the root instead.

website is the honeypot — leave it out of real submissions.

The response reports the status the comment was assigned, so the storefront can show "your comment is awaiting moderation" versus rendering it immediately.

When the post's category has comment_policy: "closed", this endpoint responds 403 with { "error": { "status": 403, "message": "Comments are closed for this post" } }.

Request Schema (application/json)
Field NameField TypeRequired
post_uuidstringYes
parent_uuidstring or nullNo
namestringYes
emailstring (^[^@\s]+@[^@\s]+\.[^@\s]+$)Yes
commentstringYes
websitestring or nullNo
POST/api/blog/comments
curl
-H "Accept: application/json"
-H "Authorization: Bearer <admin JWT token>"
--data-raw '<JSON DATA>'
https://<your domain>/api/blog/comments
Sample Of Response
{
"data": {
"uuid": "4e19b7c3-2a58-41d6-b0f9-6c8e5d2a7143",
"status": "pending"
}
}

The stored comment's email is write-only. It is declared on the admin GraphQL schema only and is never exposed on the storefront.


Like A Blog Comment

Toggles the current visitor's like on an approved comment. Public — no authentication.

{id} is the comment uuid. There is no request body — like is the only reaction type a comment supports.

The visitor is identified by the signed, httpOnly, sameSite: 'lax' blog_visitor cookie. Because this is a write path, the cookie is issued on the response when the caller does not already have one, with a one-year lifetime. Calling the endpoint again with the same cookie removes the like. blog_comment.like_count is recomputed from blog_reaction inside the transaction.

Request Schema (application/json)
No request body required
POST/api/blog/comments/4e19b7c3-2a58-41d6-b0f9-6c8e5d2a7143/like
curl
-H "Accept: application/json"
-H "Authorization: Bearer <admin JWT token>"
--data-raw '<JSON DATA>'
https://<your domain>/api/blog/comments/4e19b7c3-2a58-41d6-b0f9-6c8e5d2a7143/like
Sample Of Response
{
"data": {
"likeCount": 13,
"liked": true
}
}

React To A Blog Post

Toggles the current visitor's reaction on a published post. Public — no authentication.

{id} is the post uuid. type is required and must be one of exactly four values:

EntityAllowed reaction types
Blog postlike, love, clap, insightful
Blog commentlike only (implicit — the like endpoint takes no body)

A visitor holds at most one reaction per post. Sending the same type twice removes the reaction and returns "reacted": null; sending a different type switches the existing reaction rather than adding a second one. The post's reaction_counts JSONB column is rebuilt from blog_reaction in the same transaction and returned as counts — it only contains keys with a non-zero count.

Like the comment-like endpoint, this is a write path, so the blog_visitor cookie is issued on the response when absent.

Request Schema (application/json)
Field NameField TypeRequired
typestring (like, love, clap, insightful)Yes
POST/api/blog/posts/9f2b1c44-6d81-4a20-9d0e-2f7c8a1b3e55/react
curl
-H "Accept: application/json"
-H "Authorization: Bearer <admin JWT token>"
--data-raw '<JSON DATA>'
https://<your domain>/api/blog/posts/9f2b1c44-6d81-4a20-9d0e-2f7c8a1b3e55/react
Sample Of Response
{
"data": {
"counts": {
"like": 9,
"clap": 2
},
"reacted": "like"
}
}

Get Blog Data with GraphQL

The Blog API is write-only. Posts, categories, tags, and comments are read through GraphQL — blogPost, blogPosts, currentBlogPost, blogCategory, blogCategories, currentBlogCategory, blogTag, blogTags, currentBlogTag, and the admin-only blogComments.

See the Blog module guide for the full schema and collection filters, and the GraphQL documentation for how to issue queries.