{"openapi":"3.1.0","info":{"title":"SafeReel","description":"NSFW detection API for images and video. Docs: https://safereel.ai/docs · Status: /status.html","version":"1.0.0"},"servers":[{"url":"https://safereel.ai"}],"paths":{"/v1/check":{"post":{"tags":["images"],"summary":"Classify one image","description":"Synchronous NSFW verdict for one uploaded image (JPEG/PNG/WebP, max 10MB). Cached verdicts return instantly and are never billed. Rate-limit state is reported via RateLimit-* response headers.","operationId":"check_v1_check_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CheckResponse"}}}},"400":{"description":"Bad Request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"402":{"description":"Payment Required","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"413":{"description":"Request Entity Too Large","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"415":{"description":"Unsupported Media Type","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Too Many Requests","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Internal Server Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"503":{"description":"Service Unavailable","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"HTTPBearer":[]}]}},"/v1/videos":{"post":{"tags":["videos"],"summary":"Submit a video for scanning","description":"Queue an NSFW scan of a video by public http(s) URL — no upload, no file-size limit. Returns a job id to poll with GET /v1/videos/{id}, or set `webhook_url` for a signed callback. Send an `Idempotency-Key` header to make retries safe. Every video bills at most 20 minutes.","operationId":"submit_video_v1_videos_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VideoSubmitRequest"}}},"required":true},"responses":{"202":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/VideoSubmitResponse"}}}},"400":{"description":"Bad Request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"402":{"description":"Payment Required","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Not Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"Conflict","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Internal Server Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/v1/videos/{job_id}":{"get":{"tags":["videos"],"summary":"Poll a video job","description":"Current state of a video job: `queued` | `processing` | `done` | `failed`, with the verdict once done. Job ids are owner-only — other keys get 404, same as unknown ids.","operationId":"get_video_v1_videos__job_id__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"job_id","in":"path","required":true,"schema":{"type":"string","title":"Job Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/VideoStatusResponse"}}}},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}},"description":"Bad Request"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}},"description":"Unauthorized"},"402":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}},"description":"Payment Required"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}},"description":"Not Found"},"409":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}},"description":"Conflict"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}},"description":"Internal Server Error"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/status":{"get":{"tags":["status"],"summary":"Public service status","description":"Coarse service health and load for the status page. Public, no auth; poll no more than every 30s.","operationId":"status_v1_status_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusResponse"}}}}},"security":[]}},"/v1/demo/check":{"post":{"tags":["images"],"summary":"Anonymous demo image check","description":"Public, no-signup image demo (Turnstile-gated, 10 images/IP/day, 5MB max). Same response shape as POST /v1/check. Rate-limit state is reported via RateLimit-* response headers.","operationId":"demo_check_v1_demo_check_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CheckResponse"}}}},"400":{"description":"Bad Request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"413":{"description":"Request Entity Too Large","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"415":{"description":"Unsupported Media Type","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Too Many Requests","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Internal Server Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"503":{"description":"Service Unavailable","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[]}},"/health":{"get":{"summary":"Liveness probe","description":"Returns `ok` once the inference backend has loaded, `loading` while the model is still starting.","operationId":"health_health_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HealthResponse"}}}}},"security":[]}}},"components":{"schemas":{"CheckResponse":{"properties":{"id":{"type":"string","title":"Id"},"verdict":{"type":"string","enum":["nsfw","clean"],"title":"Verdict"},"confidence":{"type":"number","title":"Confidence"},"model":{"type":"string","title":"Model"},"cached":{"type":"boolean","title":"Cached"},"processing_ms":{"type":"integer","title":"Processing Ms"}},"type":"object","required":["id","verdict","confidence","model","cached","processing_ms"],"title":"CheckResponse","description":"200 response body of POST /v1/check."},"ErrorBody":{"properties":{"code":{"type":"string","title":"Code"},"message":{"type":"string","title":"Message"},"retry_after":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Retry After"}},"type":"object","required":["code","message"],"title":"ErrorBody"},"ErrorResponse":{"properties":{"error":{"$ref":"#/components/schemas/ErrorBody"}},"type":"object","required":["error"],"title":"ErrorResponse","description":"Error shape for all non-200 responses."},"HTTPValidationError":{"properties":{"detail":{"items":{"$ref":"#/components/schemas/ValidationError"},"type":"array","title":"Detail"}},"type":"object","title":"HTTPValidationError"},"HealthResponse":{"properties":{"status":{"type":"string","enum":["ok","loading"],"title":"Status"}},"type":"object","required":["status"],"title":"HealthResponse","description":"200 response body of GET /health (liveness probe)."},"StatusResponse":{"properties":{"status":{"type":"string","enum":["ok","starting"],"title":"Status"},"load":{"type":"string","enum":["normal","elevated","unavailable","starting"],"title":"Load"},"version":{"type":"string","title":"Version"},"uptime_s":{"type":"integer","title":"Uptime S"}},"type":"object","required":["status","load","version","uptime_s"],"title":"StatusResponse","description":"200 response body of GET /v1/status (public, no auth)."},"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"},"VideoStatusResponse":{"properties":{"id":{"type":"string","title":"Id"},"status":{"type":"string","title":"Status"},"verdict":{"anyOf":[{"type":"string","enum":["nsfw","clean"]},{"type":"null"}],"title":"Verdict"},"frames":{"type":"integer","title":"Frames"},"flagged":{"type":"integer","title":"Flagged"},"flagged_ts":{"anyOf":[{"items":{"type":"number"},"type":"array"},{"type":"null"}],"title":"Flagged Ts"},"duration_s":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Duration S"},"error":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Error"},"stage":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Stage"},"inference_online":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Inference Online"}},"type":"object","required":["id","status","frames","flagged"],"title":"VideoStatusResponse","description":"200 response body of GET /v1/videos/{id}."},"VideoSubmitRequest":{"properties":{"url":{"type":"string","title":"Url"},"webhook_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Webhook Url"}},"type":"object","required":["url"],"title":"VideoSubmitRequest","description":"Request body of POST /v1/videos."},"VideoSubmitResponse":{"properties":{"id":{"type":"string","title":"Id"},"status":{"type":"string","title":"Status"},"webhook_secret":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Webhook Secret"}},"type":"object","required":["id","status"],"title":"VideoSubmitResponse","description":"202 response body of POST /v1/videos. webhook_secret signs the webhook\nHMAC and is returned only at submit time (only when webhook_url was set);\nan Idempotency-Key replay returns the same submit response again."}},"securitySchemes":{"HTTPBearer":{"type":"http","scheme":"bearer"}}},"tags":[{"name":"images","description":"Image classification"},{"name":"videos","description":"Async video jobs"},{"name":"account","description":"Signup and keys"},{"name":"status","description":"Service status"}]}