{"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","android","web","chrome-extension","other"],"example":"macos"},"product":{"type":"string","enum":["screenkite","pilotcut","milelog","wedglow","frenchmate","twilar","trinity-camera","photoanime","gtmeasy","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"},"editorVideoReplaySessionID":{"type":"string","minLength":8,"maxLength":128,"pattern":"^[a-zA-Z0-9_-]+$","example":"01HZXVIDEOREPLAY"},"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}},"identity":{"type":"object","properties":{"sentry":{"type":"object","properties":{"userId":{"type":"string","minLength":1,"maxLength":256},"email":{"type":"string","maxLength":320,"format":"email"}},"additionalProperties":false},"analytics":{"type":"object","properties":{"installId":{"type":"string","minLength":1,"maxLength":256},"posthogDistinctId":{"type":"string","minLength":1,"maxLength":256},"statsigUserId":{"type":"string","minLength":1,"maxLength":256},"growthUserId":{"type":"string","minLength":1,"maxLength":256}},"additionalProperties":false},"auth":{"type":"object","properties":{"userId":{"type":"string","minLength":1,"maxLength":256},"email":{"type":"string","maxLength":320,"format":"email"}},"additionalProperties":false}},"additionalProperties":false,"description":"Optional cross-system identity hints. All nested fields are optional.","example":{"analytics":{"installId":"install-uuid"},"sentry":{"userId":"install-uuid"}}}},"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."}}}]},"WindowVideoReplayMetadataRequest":{"type":"object","properties":{"session":{"type":"object","properties":{"sessionId":{"type":"string","minLength":8,"maxLength":128},"product":{"type":"string","minLength":1,"maxLength":64},"platform":{"type":"string","minLength":1,"maxLength":32,"default":"macos"},"codec":{"type":"string","enum":["h264","hevc"]},"appVersion":{"type":"string","minLength":1,"maxLength":200},"appBuild":{"type":"string","minLength":1,"maxLength":200},"osVersion":{"type":"string","minLength":1,"maxLength":200},"sdkName":{"type":"string","minLength":1,"maxLength":200},"sdkVersion":{"type":"string","minLength":1,"maxLength":200},"installHash":{"type":"string","minLength":1,"maxLength":128},"projectHash":{"type":"string","minLength":1,"maxLength":128},"deviceModel":{"type":"string","minLength":1,"maxLength":200},"cpuArchitecture":{"type":"string","minLength":1,"maxLength":200},"localeIdentifier":{"type":"string","minLength":1,"maxLength":200},"localeRegion":{"type":"string","minLength":1,"maxLength":200},"timeZoneIdentifier":{"type":"string","minLength":1,"maxLength":200},"screenCount":{"type":"integer","nullable":true,"minimum":0,"maximum":32},"mainScreenScale":{"type":"string","minLength":1,"maxLength":200},"memoryGb":{"type":"integer","nullable":true,"minimum":0,"maximum":2048},"clientDedupeKey":{"type":"string","minLength":1,"maxLength":128},"sentryEventId":{"type":"string","minLength":1,"maxLength":128},"identity":{"type":"object","properties":{"sentry":{"type":"object","properties":{"userId":{"type":"string","minLength":1,"maxLength":256},"email":{"type":"string","maxLength":320,"format":"email"}},"additionalProperties":false},"analytics":{"type":"object","properties":{"installId":{"type":"string","minLength":1,"maxLength":256},"posthogDistinctId":{"type":"string","minLength":1,"maxLength":256},"statsigUserId":{"type":"string","minLength":1,"maxLength":256},"growthUserId":{"type":"string","minLength":1,"maxLength":256}},"additionalProperties":false},"auth":{"type":"object","properties":{"userId":{"type":"string","minLength":1,"maxLength":256},"email":{"type":"string","maxLength":320,"format":"email"}},"additionalProperties":false}},"additionalProperties":false}},"required":["sessionId","product"]},"chunk":{"type":"object","properties":{"sessionId":{"type":"string","minLength":8,"maxLength":128},"chunkIndex":{"type":"integer","minimum":0,"maximum":100000},"contentType":{"type":"string","enum":["video/mp4","video/quicktime"]},"codec":{"type":"string","enum":["h264","hevc"]},"width":{"type":"integer","minimum":2,"maximum":3840},"height":{"type":"integer","minimum":2,"maximum":2160},"fps":{"type":"integer","minimum":1,"maximum":30},"duration":{"type":"number","minimum":0,"maximum":120},"videoBytes":{"type":"integer","minimum":1,"maximum":26214400},"bodySha256":{"type":"string","pattern":"^[a-f0-9]{64}$"},"events":{"type":"array","items":{"type":"object","properties":{"t":{"type":"number","minimum":0,"maximum":86400},"type":{"type":"string","enum":["click","keyboard","lifecycle"]},"action":{"type":"string","maxLength":128},"keyCode":{"type":"integer","minimum":0,"maximum":512},"key":{"type":"string","maxLength":32},"modifiers":{"type":"array","items":{"type":"string","maxLength":32},"maxItems":8},"isRepeat":{"type":"boolean"},"button":{"type":"integer","minimum":0,"maximum":8},"clickCount":{"type":"integer","minimum":0,"maximum":16},"x":{"type":"number","minimum":0,"maximum":1},"y":{"type":"number","minimum":0,"maximum":1},"playhead":{"type":"number","minimum":0,"maximum":86400},"subtype":{"type":"string","maxLength":64}},"required":["t","type"]},"maxItems":10000,"default":[]}},"required":["sessionId","chunkIndex","contentType","videoBytes","bodySha256"]}},"required":["session","chunk"]}},"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"}}}}}}},"/api/api/v1/window-video-replay/chunks/metadata":{"post":{"tags":["window-video-replay"],"summary":"Upload a window video replay MP4 chunk","description":"Dedicated replay upload endpoint. Accepts a low-FPS H.264 or H.265 video chunk as `video/mp4` or `video/quicktime`; timestamped click/keyboard markers are supplied as compact JSON sidecar metadata.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WindowVideoReplayMetadataRequest"}}}},"responses":{"200":{"description":"Replay chunk stored.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"chunkID":{"type":"string"},"deduped":{"type":"boolean"},"uploadURL":{"type":"string"}},"required":["ok","chunkID","deduped","uploadURL"]}}}},"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"}}}},"409":{"description":"Chunk index conflict.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"413":{"description":"Replay metadata is too large.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Durable write failed; safe to retry.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/api/v1/window-video-replay/chunks/{chunkId}/video":{"put":{"tags":["window-video-replay"],"summary":"Upload a window video replay video body after metadata creation","parameters":[{"schema":{"type":"string","minLength":1},"required":true,"name":"chunkId","in":"path"}],"requestBody":{"required":true,"content":{"video/mp4":{"schema":{"type":"string","format":"binary"}},"video/quicktime":{"schema":{"type":"string","format":"binary"}}}},"responses":{"200":{"description":"Replay video stored.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"id":{"type":"string"},"deduped":{"type":"boolean"}},"required":["ok","id","deduped"]}}}},"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"}}}},"409":{"description":"Chunk index conflict.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"411":{"description":"Missing Content-Length.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"413":{"description":"Replay video body is too large.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Durable write failed; safe to retry.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}}}