API Endpoints
Meerkat supplies a read-only JSON API at /api/meerkat/*. The prefix is {statamic.api.route}/meerkat.
#Access
#Public read routes
These endpoints do not require authentication:
GET threads/{threadId}: thread details and current public countsGET threads/{threadId}/comments: full thread, published only by defaultGET threads/{threadId}/roots: paginated root commentsGET threads/{threadId}/children/{commentId}: paginated replies to a specific commentGET threads/{threadId}/stats: current public counts, or stored moderation counts when requested by an authorized userGET entries/{entryId}/{thread,comments,roots,stats}: the same reads using an entry ID
#Authenticated moderation routes
These endpoints require authentication and the view comments permission:
GET comments/recent: recent comments across all threadsGET comments/{commentId}/history: full moderation audit trailGET comments/{commentId}/revisions: complete content revision history. This endpoint requires Statamic Pro andmeerkat.revisions.enabled(off by default); it returns 404 otherwise.GET authors/{identifier}/comments: one author's full historyGET search: comment-body substring search
Requests authorized with view comments receive moderation fields and author details from every endpoint. Other requests receive only public fields.
Collection and site permissions also apply. A moderator who can view only blog entries sees only blog comments.
#Response structure
Authorized responses include moderation data, author email and ID, and request metadata. Public responses omit these fields. Public author.gravatar URLs contain an MD5 hash of the commenter's email address.
Single-item responses are JSON objects:
1{
2 "thread": { ... },
3 "metrics": { ... }
4}
Paginated responses use Laravel's standard format:
1{
2 "data": [{ ... }, { ... }],
3 "links": { "first": "...", "last": "...", "prev": null, "next": "..." },
4 "meta": { "current_page": 1, "per_page": 15, "total": 47, ... }
5}
Comment listings inside a thread response are a flat array:
1{
2 "thread_id": "blog/welcome",
3 "comments": [{ ... }, { ... }, ...]
4}
#Threads
#GET threads/{threadId}
Returns thread details and public counts.
{threadId} accepts a thread ID or an entry ID. Both find the same thread.
Query params:
include_moderation(requiresview comments): include pending and spam counts.
Response:
1{
2 "thread": {
3 "thread_id": "blog/welcome",
4 "cached_title": "Welcome to the blog",
5 ...
6 },
7 "metrics": {
8 "total_comments": 47,
9 "published_comments": 42,
10 "participants": 18,
11 ...
12 }
13}
#GET threads/{threadId}/comments
Returns the complete thread as a flat list, including nested replies. By default, removed comments (is_removed = true) and their replies are excluded.
Full-thread responses are capped by config('meerkat.api.max_full_thread_comments') (500 by default). Larger responses return 413; use the paginated /roots and /children routes instead. Set the limit to 0 to disable it.
Query params:
include_unpublished(requiresview comments): include pending, spam, and rejected comments.include_tombstones(requiresview comments): include removed comments as placeholders.include_tombstone_replies(requiresview comments): include descendants of removed comments. Use withinclude_tombstones.
Response:
1{
2 "thread_id": "blog/welcome",
3 "comments": [
4 { "id": 1, "author": {...}, "comment_text": "...", ... },
5 { "id": 2, "parent_id": 1, ... },
6 ...
7 ]
8}
Each comment includes id, thread_id, parent_id, depth, replies_count, comment_text, comment_html, anchor, author.name, author.gravatar, and created_at. Use parent_id to group replies under their parents. See Fetch a thread's comments.
#GET threads/{threadId}/roots
Paginated root comments (top-level only).
Query params:
per_page(integer, defaultconfig('meerkat.api.per_page'))page(integer, default 1)include_unpublished(requiresview comments)include_tombstones(requiresview comments): include removed root comments.include_tombstone_replies(requiresview comments): use withinclude_tombstones; load descendants through/children.
Response: a paginated { data, links, meta } object containing top-level comments. Load direct replies through /children/{commentId}. See Paginate roots for a long thread.
#GET threads/{threadId}/children/{commentId}
Paginated direct replies to a specific comment.
Query params:
per_page(integer)page(integer)include_unpublished(requiresview comments)include_tombstones/include_tombstone_replies(requiresview comments): use the same rules as the full thread endpoint.
Response: a paginated { data, links, meta } object.
#GET threads/{threadId}/stats
Recalculates counts using comments visible to everyone.
Query params:
include_moderation(requiresview comments): include pending and spam counts.
Response: the thread's metrics. See Thread stats for the full field list.
#Entry ID routes
Each thread read endpoint also has an entry-ID route:
GET entries/{entryId}/threadGET entries/{entryId}/commentsGET entries/{entryId}/rootsGET entries/{entryId}/stats
The behavior is identical. An unknown {entryId} returns 404.
#Moderation routes
#GET comments/recent
Returns recent comments across all threads.
Query params:
limit(integer, default 25, capped byapi.max_per_page)
Response: a paginated list of comments with moderation fields.
#GET comments/{commentId}/history
Full moderation audit trail for a single comment.
Response:
1{
2 "audits": [
3 {
4 "id": 12,
5 "comment_id": 47,
6 "actor_id": "moderator-1",
7 "action": "marked_spam",
8 "details": { ... },
9 "created_at": "..."
10 },
11 ...
12 ]
13}
#GET comments/{commentId}/revisions
Full content-revision history for a single comment, ordered newest-first.
Revisions track only content changes. The /history endpoint tracks moderation actions.
Comment revisions require the Statamic Pro edition. Without Pro, Meerkat does not capture revisions, and this endpoint returns 404.
Response:
1{
2 "revisions": [
3 {
4 "id": 8,
5 "comment_id": 47,
6 "revision_number": 3,
7 "comment_text": "Latest state",
8 "comment_data": { ... },
9 "edited_by": "moderator-1",
10 "edit_reason": null,
11 "edited_at": "2026-05-13T17:00:00.000Z",
12 "created_at": "...",
13 "updated_at": "..."
14 },
15 { "revision_number": 2, ... },
16 { "revision_number": 1, ... }
17 ]
18}
See Comment Revisions.
#GET authors/{identifier}/comments
One author's full history. Identifier can be a user ID or an email address.
Query params:
limit(integer, default 50, capped byapi.max_per_page)
Response: a paginated list of comments.
#GET search
Searches comment_text. Case sensitivity depends on the database configuration.
Query params:
q(string, required, min 2 chars)limit(integer, default 25, max 100)
Response: a paginated list of matching comments.
#JavaScript examples
#Fetch a thread's comments
1const threadId = 'my-blog-post';
2
3const response = await fetch(`/api/meerkat/threads/${encodeURIComponent(threadId)}/comments`, {
4 headers: { 'Accept': 'application/json' },
5});
6
7const { comments } = await response.json();
8
9const roots = comments.filter(c => c.parent_id === null);
10const childrenOf = (id) => comments.filter(c => c.parent_id === id);
#Paginate roots for a long thread
For large threads, /roots returns one page of top-level comments at a time:
1async function loadRootPage(threadId, page = 1, perPage = 20) {
2 const url = new URL(`/api/meerkat/threads/${encodeURIComponent(threadId)}/roots`, location.origin);
3 url.searchParams.set('page', page);
4 url.searchParams.set('per_page', perPage);
5
6 const response = await fetch(url, { headers: { 'Accept': 'application/json' } });
7 return response.json();
8}
To paginate replies under a single comment separately:
1const response = await fetch(`/api/meerkat/threads/${tid}/children/${commentId}?per_page=10`, {
2 headers: { 'Accept': 'application/json' },
3});
Values above meerkat.api.max_per_page are reduced to the configured maximum of 100 by default.
#Error responses
The API uses standard HTTP status codes:
| Status | Meaning |
|---|---|
200 |
Success. |
401 |
Authentication required. |
403 |
Authenticated but lacks the required permission. |
404 |
Resource not found, invalid entry or comment ID, or the API is disabled. |
413 |
The full-thread endpoint exceeded api.max_full_thread_comments. Use the paginated root and child routes. |
422 |
Validation failure. For example, the search q is too short. |
429 |
Too many requests. Configure limits through meerkat.api.middleware. |
Validation failures use the standard Laravel error structure:
1{
2 "message": "The given data was invalid.",
3 "errors": {
4 "q": ["The q must be at least 2 characters."]
5 }
6}
#Disabling the API
Set meerkat.api.enabled to false to make every API route return 404. The Control Panel and submission form continue to operate. See Configuration: Web API.
#Change access rules
See API middleware to change authentication, permissions, or request limits.