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 counts
  • GET threads/{threadId}/comments: full thread, published only by default
  • GET threads/{threadId}/roots: paginated root comments
  • GET threads/{threadId}/children/{commentId}: paginated replies to a specific comment
  • GET threads/{threadId}/stats: current public counts, or stored moderation counts when requested by an authorized user
  • GET 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 threads
  • GET comments/{commentId}/history: full moderation audit trail
  • GET comments/{commentId}/revisions: complete content revision history. This endpoint requires Statamic Pro and meerkat.revisions.enabled (off by default); it returns 404 otherwise.
  • GET authors/{identifier}/comments: one author's full history
  • GET 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 (requires view 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 (requires view comments): include pending, spam, and rejected comments.
  • include_tombstones (requires view comments): include removed comments as placeholders.
  • include_tombstone_replies (requires view comments): include descendants of removed comments. Use with include_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, default config('meerkat.api.per_page'))
  • page (integer, default 1)
  • include_unpublished (requires view comments)
  • include_tombstones (requires view comments): include removed root comments.
  • include_tombstone_replies (requires view comments): use with include_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 (requires view comments)
  • include_tombstones / include_tombstone_replies (requires view 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 (requires view 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}/thread
  • GET entries/{entryId}/comments
  • GET entries/{entryId}/roots
  • GET 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 by api.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 by api.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.

Was this page helpful?