{"openapi":"3.0.0","info":{"version":"1.0.0","title":"Feedback Central API","description":"Backup feedback collection for ScreenKite/PilotCut clients. Durable even when Sentry is unavailable."},"components":{"schemas":{"Health":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"service":{"type":"string","example":"feedback-central"},"time":{"type":"number","example":1748600000000}},"required":["ok","service","time"]},"FeedbackSuccess":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"id":{"type":"string","example":"fb_01HZX..."},"deduped":{"type":"boolean","example":false},"notified":{"type":"boolean","description":"Whether a Discord notification was sent (false when throttled or deduped).","example":true},"attachments":{"type":"array","items":{"$ref":"#/components/schemas/FeedbackAttachmentResult"}},"skippedAttachments":{"type":"array","items":{"$ref":"#/components/schemas/SkippedAttachment"},"description":"Attachments dropped (invalid) but never fatal to the text write."}},"required":["ok","id","deduped","notified","attachments","skippedAttachments"]},"FeedbackAttachmentResult":{"type":"object","properties":{"id":{"type":"string","example":"att_01HZX..."},"status":{"type":"string","enum":["pending","stored","failed"],"example":"pending"}},"required":["id","status"]},"SkippedAttachment":{"type":"object","properties":{"filename":{"type":"string","example":"huge.psd"},"reason":{"type":"string","enum":["too_large","unsupported_type","too_many"],"example":"too_large"}},"required":["filename","reason"]},"Error":{"type":"object","properties":{"ok":{"type":"boolean","enum":[false]},"error":{"type":"string","example":"validation_error"},"error_description":{"type":"string","example":"message: Required"}},"required":["ok","error","error_description"]},"FeedbackRequest":{"type":"object","properties":{"message":{"type":"string","minLength":1,"maxLength":5000,"example":"The recorder froze when I plugged in a second monitor."},"name":{"type":"string","maxLength":200,"example":"Ada Lovelace"},"email":{"type":"string","maxLength":320,"format":"email","description":"A valid email address (an empty string is also accepted).","example":"ada@example.com"},"appVersion":{"type":"string","maxLength":50,"example":"3.4.1"},"appBuild":{"type":"string","maxLength":50,"example":"3410"},"osVersion":{"type":"string","maxLength":50,"example":"15.5.0"},"platform":{"type":"string","enum":["macos","windows","ios","web","chrome-extension","other"],"example":"macos"},"product":{"type":"string","enum":["screenkite","pilotcut","other"],"example":"screenkite"},"severity":{"type":"string","enum":["info","warning","critical"],"example":"warning"},"category":{"type":"string","enum":["bug","idea","praise","question","other"],"example":"bug"},"sentryEventId":{"type":"string","maxLength":64,"example":"a1b2c3d4e5f6"},"clientDedupeKey":{"type":"string","minLength":1,"maxLength":128,"description":"Idempotency key; resending with the same key returns the first id.","example":"device-7f3a-2026-05-30T12:00:00Z"},"screenshotCount":{"type":"integer","minimum":0,"maximum":10,"example":2},"diagnostics":{"type":"object","additionalProperties":{"nullable":true},"description":"Free-form JSON (<= 8KB serialized).","example":{"fps":60}}},"required":["message"]},"FeedbackMultipartRequest":{"allOf":[{"$ref":"#/components/schemas/FeedbackRequest"},{"type":"object","properties":{"attachments[]":{"type":"array","items":{"type":"string","format":"binary"},"description":"Up to 10 files, <= 5MB each."}}}]}},"parameters":{}},"paths":{"/api/api/v1/health":{"get":{"tags":["system"],"summary":"Health check","responses":{"200":{"description":"Service is up.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Health"}}}}}}},"/api/api/v1/feedback":{"post":{"tags":["feedback"],"summary":"Submit user feedback","description":"The single public ingestion endpoint. Accepts `application/json` (fields only) or `multipart/form-data` (fields + attachments). The feedback text is persisted durably before a 200 is returned; attachment upload and Discord notification are best-effort and never fail the request. Send `clientDedupeKey` to make retries idempotent. If the service is configured with an ingest token, send it as the `X-Feedback-Token` header.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FeedbackRequest"}},"multipart/form-data":{"schema":{"$ref":"#/components/schemas/FeedbackMultipartRequest"}}}},"responses":{"200":{"description":"Feedback stored.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FeedbackSuccess"}}}},"400":{"description":"Validation error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing or invalid ingest token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Durable write failed; safe to retry.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}}}