{"openapi":"3.1.0","info":{"title":"AI Content Authenticity Detector","description":"Server-side API for the mobile content-authenticity detector app. See `docs/plans/` for the implementation roadmap.","version":"0.1.0"},"paths":{"/healthz":{"get":{"tags":["System"],"summary":"Liveness check","operationId":"healthz_healthz_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":{"type":"string"},"type":"object","title":"Response Healthz Healthz Get"}}}}}}},"/v1/submissions":{"post":{"tags":["Submissions"],"summary":"Create a submission","description":"Submit **exactly one** of: a file upload (multipart `file`), a text body (`text` form field), or a URL (`url` form field). Returns a submission id you can poll via `GET /v1/submissions/{id}`.\n\nThe response is `202 Accepted` because detection runs asynchronously on a worker. The initial status is always `received`; poll the detail endpoint to observe transitions.","operationId":"create_submission_v1_submissions_post","requestBody":{"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_create_submission_v1_submissions_post"}}}},"responses":{"202":{"description":"Submission created and queued for processing.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateSubmissionResponse"},"example":{"id":"0780de70-1c32-43e4-b61e-a9f6e18fce80","status":"received","created_at":"2026-04-23T20:58:48.687858Z"}}}},"400":{"description":"Input validation failed (missing/multiple inputs, bad URL, bad cursor).","content":{"application/json":{"example":{"error":{"code":"missing_input","message":"one of `file`, `text`, or `url` is required","details":{}}},"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"413":{"description":"Upload exceeds size limits.","content":{"application/json":{"example":{"error":{"code":"upload_too_large","message":"upload exceeds 524288000 bytes","details":{}}},"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["Submissions"],"summary":"List submissions (newest first)","description":"Cursor-paginated list of submissions ordered by `created_at DESC, id DESC`. Pass the `next_cursor` returned in the previous page as the `cursor` query param to fetch the next page.","operationId":"list_submissions_v1_submissions_get","parameters":[{"name":"cursor","in":"query","required":false,"schema":{"type":"string","description":"Opaque cursor from a previous page's `next_cursor`. Leave empty to fetch the first page.","title":"Cursor"},"description":"Opaque cursor from a previous page's `next_cursor`. Leave empty to fetch the first page."},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"description":"Maximum items per page (1-100).","default":20,"title":"Limit"},"description":"Maximum items per page (1-100)."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SubmissionList"}}}},"400":{"description":"Input validation failed (missing/multiple inputs, bad URL, bad cursor).","content":{"application/json":{"example":{"error":{"code":"missing_input","message":"one of `file`, `text`, or `url` is required","details":{}}},"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/submissions/{submission_id}":{"get":{"tags":["Submissions"],"summary":"Get a submission by id","description":"Returns the current status and (once the pipeline lands the result) the detection verdict. Poll this endpoint to observe the state machine transitions. `thumbnail_url` is always `null` until step 09 lands thumbnail generation.","operationId":"get_submission_v1_submissions__submission_id__get","parameters":[{"name":"submission_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Submission Id"}}],"responses":{"200":{"description":"Submission detail.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SubmissionDetail"},"example":{"id":"0780de70-1c32-43e4-b61e-a9f6e18fce80","created_at":"2026-04-23T20:58:48.687858Z","status":"failed","kind":"text","failure_stage":"ingesting","error_code":"pipeline_not_implemented"}}}},"404":{"description":"Unknown submission id.","content":{"application/json":{"example":{"error":{"code":"submission_not_found","message":"submission 11111111-2222-3333-4444-555555555555 not found","details":{}}},"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["Submissions"],"summary":"Hard-delete a submission","description":"Removes the submission row and any residual blobs/thumbnails. The append-only `submission_status_events` trail is retained for audit. Returns `204 No Content` on success, `404` if the id is unknown.","operationId":"delete_submission_v1_submissions__submission_id__delete","parameters":[{"name":"submission_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Submission Id"}}],"responses":{"204":{"description":"Deleted."},"404":{"description":"Unknown submission id.","content":{"application/json":{"example":{"error":{"code":"submission_not_found","message":"submission 11111111-2222-3333-4444-555555555555 not found","details":{}}},"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/submissions/{submission_id}/events":{"get":{"tags":["Submissions"],"summary":"List a submission's status-transition timeline","description":"Returns the append-only `submission_status_events` rows for a submission, oldest first. Each event records the from/to statuses and a JSONB `payload` with stage-specific details (extractor name, exception type, traceback for failures, etc.). Useful for debugging stuck or failed pipelines.","operationId":"get_submission_events_v1_submissions__submission_id__events_get","parameters":[{"name":"submission_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Submission Id"}}],"responses":{"200":{"description":"Timeline.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SubmissionEvents"}}}},"404":{"description":"Unknown submission id.","content":{"application/json":{"example":{"error":{"code":"submission_not_found","message":"submission 11111111-2222-3333-4444-555555555555 not found","details":{}}},"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}}},"components":{"schemas":{"Body_create_submission_v1_submissions_post":{"properties":{"file":{"type":"string","contentMediaType":"application/octet-stream","title":"File","description":"Binary upload (image / video / audio / generic file). Max size: `MAX_UPLOAD_BYTES` env setting (default 500 MiB)."},"text":{"type":"string","title":"Text","description":"Raw text body for text-authenticity detection. Max length: `MAX_TEXT_CHARS` (default 50 000).","examples":["Lorem ipsum dolor sit amet, consectetur adipiscing elit."]},"url":{"type":"string","title":"Url","description":"Public `http://` or `https://` URL. Social-platform URLs (YouTube, TikTok, etc.) get scraped via yt-dlp in step 08; direct content URLs get fetched. Max length: `MAX_URL_LENGTH` (default 2 048).","examples":["https://www.youtube.com/watch?v=dQw4w9WgXcQ"]}},"type":"object","title":"Body_create_submission_v1_submissions_post"},"CreateSubmissionResponse":{"properties":{"id":{"type":"string","format":"uuid","title":"Id"},"status":{"type":"string","enum":["received","ingesting","routing","scraping","downloading","thumbnail_pending","detecting","completed","failed"],"title":"Status"},"created_at":{"type":"string","format":"date-time","title":"Created At"}},"type":"object","required":["id","status","created_at"],"title":"CreateSubmissionResponse"},"ErrorBody":{"properties":{"code":{"type":"string","title":"Code"},"message":{"type":"string","title":"Message"},"details":{"additionalProperties":true,"type":"object","title":"Details"}},"type":"object","required":["code","message"],"title":"ErrorBody"},"ErrorEnvelope":{"properties":{"error":{"$ref":"#/components/schemas/ErrorBody"}},"type":"object","required":["error"],"title":"ErrorEnvelope"},"HTTPValidationError":{"properties":{"detail":{"items":{"$ref":"#/components/schemas/ValidationError"},"type":"array","title":"Detail"}},"type":"object","title":"HTTPValidationError"},"SubmissionDetail":{"properties":{"id":{"type":"string","format":"uuid","title":"Id"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"status":{"type":"string","enum":["received","ingesting","routing","scraping","downloading","thumbnail_pending","detecting","completed","failed"],"title":"Status"},"kind":{"type":"string","enum":["file","text","url"],"title":"Kind"},"source_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Source Url"},"source_filename":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Source Filename"},"source_mime":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Source Mime"},"thumbnail_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Thumbnail Url","description":"Populated once step 09 generates thumbnails; null until then."},"result":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Result"},"failure_stage":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Failure Stage"},"error_code":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Error Code"}},"type":"object","required":["id","created_at","status","kind"],"title":"SubmissionDetail","description":"Full projection returned by GET /v1/submissions/{id}."},"SubmissionEvents":{"properties":{"submission_id":{"type":"string","format":"uuid","title":"Submission Id"},"events":{"items":{"$ref":"#/components/schemas/SubmissionStatusEventOut"},"type":"array","title":"Events"}},"type":"object","required":["submission_id","events"],"title":"SubmissionEvents"},"SubmissionList":{"properties":{"items":{"items":{"$ref":"#/components/schemas/SubmissionListItem"},"type":"array","title":"Items"},"next_cursor":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Next Cursor"}},"type":"object","required":["items"],"title":"SubmissionList"},"SubmissionListItem":{"properties":{"id":{"type":"string","format":"uuid","title":"Id"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"status":{"type":"string","enum":["received","ingesting","routing","scraping","downloading","thumbnail_pending","detecting","completed","failed"],"title":"Status"},"kind":{"type":"string","enum":["file","text","url"],"title":"Kind"},"thumbnail_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Thumbnail Url"},"verdict_summary":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Verdict Summary","description":"Short human-readable verdict line; populated by step 09."}},"type":"object","required":["id","created_at","status","kind"],"title":"SubmissionListItem","description":"Minimal projection for GET /v1/submissions (list view)."},"SubmissionStatusEventOut":{"properties":{"id":{"type":"string","format":"uuid","title":"Id"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"from_status":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"From Status"},"to_status":{"type":"string","title":"To Status"},"payload":{"additionalProperties":true,"type":"object","title":"Payload"}},"type":"object","required":["id","created_at","from_status","to_status"],"title":"SubmissionStatusEventOut","description":"One row from `submission_status_events` — the timeline of a submission."},"ValidationError":{"properties":{"loc":{"items":{"anyOf":[{"type":"string"},{"type":"integer"}]},"type":"array","title":"Location"},"msg":{"type":"string","title":"Message"},"type":{"type":"string","title":"Error Type"},"input":{"title":"Input"},"ctx":{"type":"object","title":"Context"}},"type":"object","required":["loc","msg","type"],"title":"ValidationError"}}},"tags":[{"name":"Submissions","description":"Create + inspect AI-detection submissions.\n\nA submission is a single piece of content (file, text, or URL) that moves through this state machine:\n\n`received → ingesting → (routing | scraping | downloading) → thumbnail_pending → detecting → completed`\n\nor terminates at `failed` with a `failure_stage` + `error_code`. Step 07 (current) ships a deliberate stub: every submission lands at `failed { pipeline_not_implemented }` until step 08 wires in the real pipeline."},{"name":"System","description":"Health and diagnostics."}]}