JSON API (/api/v1)
All API requests authenticate with an API key issued at /admin/api-keys.
Send the key in either:
Authorization: Bearer sytwm_sk_…(preferred)X-API-Key: sytwm_sk_…
A submit-scoped key (sytwm_sk_…) may POST feedback and telemetry.
A read-scoped key (sytwm_rk_…) may GET feedback, telemetry, and post
definitions.
Error envelope
Every error response (4xx / 5xx) carries this shape:
{
"error": {
"code": "validation_failed",
"message": "Submitted answer did not match the step's input kind.",
"details": [{"field": "answers.tried", "code": "invalid_choice", "message": "Expected 'yes' or 'no'."}],
"request_id": "01J..."
}
}
Stable codes:
validation_failed(400/422)unauthorized(401)forbidden(403)not_found(404)conflict(409)rate_limited(429) — carriesextra.retry_after_sinternal_error(500/503)post_not_published— post does not exist or is unpublishedapi_key_revoked— the supplied key is disabled
Endpoints
GET /api/v1/posts
List every published post. Both scopes pass.
[
{"slug": "default", "title": "Quick feedback", "version": 1, "definition": {...}}
]
GET /api/v1/posts/{slug}
Get a single published post definition. Both scopes pass.
POST /api/v1/feedback
Scope: submit.
Request:
{
"post_slug": "default",
"answers": [
{"step_id": "tried", "answer": true},
{"step_id": "what_tried", "answer": "the install script"}
],
"is_public": true,
"public_text": "Worked on the second try!",
"submitter_label": "anon",
"client_dedup_id": "uuid-or-hash-of-the-form-submit"
}
Response (201):
{ "id": "1f3b…", "is_public": true, "replayed": false }
If client_dedup_id matches a prior submission from the same key, the
existing entry is returned with replayed: true — supports safe retries.
GET /api/v1/feedback?limit=50&cursor=...
Scope: read. Paginated list, newest first.
{
"items": [
{
"id": "1f3b…",
"post_slug": "default",
"is_public": true,
"is_hidden": false,
"public_text": "...",
"submitter_label": "anon",
"submitted_at": "2026-05-29T12:34:56Z",
"answers": [{"step_id": "tried", "step_label": "Did you try it?", "answer": true}]
}
],
"next_cursor": "uuid-of-last-item-or-null"
}
POST /api/v1/telemetry
Scope: submit. Records a single telemetry event.
{
"event_type": "page_hit",
"post_slug": "default",
"step_id": "tried",
"session_id": "client-session",
"path": "/feedback/default",
"referrer": "https://news.example/post/123",
"payload": { "experiment": "homepage-A" }
}
Response (201): { "id": 42 }
GET /api/v1/telemetry?cursor=...&limit=100
Scope: read. Returns the most recent events with a next_cursor.
Rate limits
- Public post POSTs and admin login: 30/min/IP.
POST /api/v1/*: 120/min/IP.GET /api/v1/*: 600/min/IP.
Over-limit responses are HTTP 429 with Retry-After and the error envelope
above.