API
All responses are JSON. Errors share one shape:
{ "error": "invalid_post", "message": "post is invalid" }GET /api/comments
GET /api/comments?post=hello-world{
"post": "hello-world",
"updatedAt": "2026-10-30T05:00:00.000Z",
"comments": [
{ "id": "...", "parentId": null, "name": "Alice", "content": "Nice.", "createdAt": "2026-10-30T04:12:00.000Z" }
]
}- Missing file returns
200with an empty array - Email fields are never present
Cache-Control: public, s-maxage=60, stale-while-revalidate=600- CORS only for origins in
ALLOWED_ORIGINS
Errors: 400 invalid_post, 403 origin_not_allowed, 502 git_unavailable, 503 service_unavailable
POST /api/submissions
The body must contain action.
challenge
{ "action": "challenge", "post": "hello-world", "name": "Alice", "content": "Nice.", "parentId": null }Returns { "ok": true, "siteKey": "...", "turnstileAction": "submit", "cData": "..." }.
submit
Challenge fields plus turnstileToken, optional email, and the honeypot field website which must be empty.
With moderation: { "ok": true, "pending": true } Without moderation: { "ok": true, "pending": false, "id": "..." }
Errors: 400 turnstile_failed, 400 invalid_submission, 400 spam_rejected, 403 origin_not_allowed, 429 rate_limited, 502 mail_send_failed, 502 git_write_failed, 503 mail_not_configured
preview
{ "action": "preview", "token": "<envelope>" }Verifies only. Returns post, name, content, parentId, createdAt, hasEmail, exp.
Errors: 400 token_invalid, 400 token_expired
approve
{ "action": "approve", "token": "<envelope>" }Returns { "ok": true, "id": "...", "created": true }. created: false means an idempotent hit.
Errors: 400 token_invalid, 400 token_expired, 403 origin_not_allowed, 409 conflict, 502 git_write_failed
GET /api/health
No auth, no secrets.
{
"status": "ok",
"git": { "repo": "owner/repo", "branch": "main", "path": "comments", "readable": true, "writable": true },
"sameRepo": false,
"sameRepoWarning": null,
"moderation": true,
"moderationWarning": null,
"mail": { "provider": "resend", "configured": true },
"issues": [],
"timestamp": "2026-10-30T05:00:00.000Z"
}Incomplete configuration returns 503 with status: "degraded".