Add anonymous-friendly comment threads to any page or content item, with moderation, rate limiting, and per-project settings.

Comments API

The SimplyComments API powers anonymous-friendly comment threads on any URL, slug, or arbitrary identifier. It supports moderation, rate limiting, blocked-word filtering, and per-project settings.

Concepts

Every comment belongs to a thread, identified by a free-form string called the thread_key. There is no separate "thread" resource — the first comment with a given thread_key implicitly creates the thread, and subsequent comments referencing the same thread_key join it. Typical thread_keys include the path of a blog post (/blog/my-post), a content item ID (content:abc-123), or a custom application identifier.

Authentication

Read endpoints (public listings of approved comments) and the public POST endpoint do not require an API key — instead, the request must include a project_id. Moderation endpoints (PATCH/DELETE) and settings management require a project-scoped API key in the x-api-key header.

Submit a Comment (Public)

POST /api/v1/comments

Submits a new comment on a thread. Defaults to status "pending" until moderated, unless the project has auto_approve enabled.

JavaScript
const res = await fetch('https://www.simplystack.dev/api/v1/comments', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    project_id: 'YOUR_PROJECT_ID',
    thread_key: '/blog/my-post-slug',
    author_name: 'Jane Doe',
    author_email: 'jane@example.com',
    body: 'Great post! Thanks for sharing.',
  }),
});
const { data } = await res.json();
console.log(data.status); // "pending" or "approved"

Request Body

  • project_id (string, required if no API key) — the project that owns the thread
  • thread_key (string, required) — free-form thread identifier, max 500 chars
  • parent_id (uuid, optional) — for replies; must belong to the same thread
  • author_name (string, required) — max 100 chars
  • author_email (string, optional) — required if project setting require_email is true
  • author_url (string, optional) — link shown next to the author name
  • body (string, required) — comment text, max 10,000 chars
  • metadata (object, optional) — arbitrary JSON stored alongside the comment

Response

JSON
{
  "data": {
    "id": "8f8a...",
    "thread_key": "/blog/my-post-slug",
    "parent_id": null,
    "author_name": "Jane Doe",
    "author_url": null,
    "body": "Great post! Thanks for sharing.",
    "status": "pending",
    "created_at": "2026-04-24T10:30:00Z",
    "project_id": "YOUR_PROJECT_ID"
  }
}

List Comments

GET /api/v1/comments

Without an API key, returns only approved comments. With a project API key, returns all statuses and accepts a status filter.

JavaScript
// Public read (only approved)
const res = await fetch(
  `https://www.simplystack.dev/api/v1/comments?` +
  `project_id=${projectId}&thread_key=${encodeURIComponent('/blog/my-post-slug')}`
);

// Moderator read (all statuses)
const modRes = await fetch(
  'https://www.simplystack.dev/api/v1/comments?status=pending',
  { headers: { 'x-api-key': 'YOUR_PROJECT_API_KEY' } }
);

Query Parameters

  • project_id (string) — required for unauthenticated requests
  • thread_key (string) — filter to a single thread
  • status (string) — pending | approved | spam | deleted (auth only)
  • limit (number) — max 200, default 50

Moderate a Comment

PATCH /api/v1/comments/:id

Updates the status of a comment. Requires a project API key matching the comment's project.

JavaScript
await fetch(`https://www.simplystack.dev/api/v1/comments/${id}`, {
  method: 'PATCH',
  headers: {
    'x-api-key': 'YOUR_PROJECT_API_KEY',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ status: 'approved' }),
});
  • status (string, required) — one of: pending, approved, spam, deleted

Delete a Comment

DELETE /api/v1/comments/:id

Hard-deletes a comment and all its replies (cascade). Requires a project API key.

Project Comment Settings

GET /api/v1/comments/settings

PUT /api/v1/comments/settings

Manage moderation behaviour for this project. All fields are optional on PUT — only provided fields are updated.

JavaScript
await fetch('https://www.simplystack.dev/api/v1/comments/settings', {
  method: 'PUT',
  headers: {
    'x-api-key': 'YOUR_PROJECT_API_KEY',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    auto_approve: false,
    require_email: true,
    rate_limit_per_hour: 10,
    allowed_origins: ['https://yoursite.com'],
    blocked_words: ['casino', 'viagra'],
  }),
});
  • enabled (boolean) — turn comments on or off entirely
  • auto_approve (boolean) — skip the moderation queue
  • require_email (boolean) — reject comments without author_email
  • rate_limit_per_hour (number 0-1000) — per-IP per-thread cap; 0 disables
  • allowed_origins (string[]) — when non-empty, only matching Origin headers may post
  • blocked_words (string[]) — case-insensitive substring match flags as spam on submit

Errors

  • 400 — validation error (missing fields, invalid email, body too long, or invalid parent_id)
  • 401 — missing or invalid API key (when required)
  • 403 — comments disabled or origin not allowed
  • 404 — project or comment not found
  • 429 — rate limit exceeded

Example: Embedding a Comment Form

HTML
<form id="comment-form">
  <input name="author_name" required />
  <input name="author_email" type="email" />
  <textarea name="body" required></textarea>
  <button>Post comment</button>
</form>
<script>
  document.getElementById('comment-form').addEventListener('submit', async (e) => {
    e.preventDefault();
    const fd = new FormData(e.target);
    const res = await fetch('https://www.simplystack.dev/api/v1/comments', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        project_id: 'YOUR_PROJECT_ID',
        thread_key: window.location.pathname,
        author_name: fd.get('author_name'),
        author_email: fd.get('author_email'),
        body: fd.get('body'),
      }),
    });
    if (res.ok) e.target.reset();
  });
</script>
Last updated: 4/28/2026