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.
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
{
"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.
// 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.
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.
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
<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>