← SafeReel · OpenAPI reference (Swagger) · Accuracy report

SafeReel API — v1 Reference

Base URL: https://api.safereel.ai (production). During evaluation you may be given a sandbox URL instead — use exactly what we send you. Interactive OpenAPI docs are also available at /docs (Swagger UI) and /openapi.json on a running instance.

Browser warning: never call this API from browser JavaScript on a public page — it exposes your API key. The playground and dashboard are the only sanctioned browser flows. CORS is disabled for browser origins by default.

All errors share one shape:

{"error": {"code": "invalid_key", "message": "unknown or disabled API key", "retry_after": null}}

Error codes: invalid_request (400) · invalid_key (401) · quota_exceeded (402) · turnstile_failed (403) · not_found (404) · payload_too_large (413) · unsupported_media (415) · rate_limited / demo_rate_limited / demo_at_capacity (429) · processing_failed / submit_failed / email_failed (500) · service_starting / inference_unavailable (503).

On 503 inference_unavailable (capacity momentarily full): retry with exponential backoff starting at 1s (max ~30s); it clears as soon as a worker is free. On 429 rate_limited: wait retry_after seconds.

Pricing (reference): $0.020/video-minute PAYG (20-minute billing cap per video) and $0.50/1k images past the free tier; plans $19–$399/mo. Charged usage is only ever real inference — cache hits and failed jobs are free.


Authentication

Every endpoint except GET /v1/status and POST /v1/demo/check requires a bearer key:

Authorization: Bearer sr_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

Get a key

The fastest path is the dashboard: sign in at /dash (magic link, no password) and create a key under API keys — it's shown exactly once. There's also a zero-friction form on the homepage: enter your email, click the link we send, and your key is issued on the spot.

Programmatic signup exists for integrations (POST /v1/signupGET /v1/verify), but it is not part of the stable public contract — prefer the dashboard unless you specifically need it.

Free tier: 1,000 images/month, 25 videos AND 100 video-minutes/month (max 10 min per video), 1 req/s. Paid tiers raise all of these.

Tiers and limits

Free Pro
Images (/v1/check) 1 req/s, 500/day, 1k/month 10 req/s, 50k/day, 1M/month
Videos (/v1/videos) 25/month AND 100 video-minutes/month, max 10 min per video 100k/month

Images: POST /v1/check

Classify one image. Synchronous; median ~150–250ms on cache miss, 0ms on hit.

curl -X POST $BASE/v1/check \
    -H "Authorization: Bearer $KEY" \
    -H "Content-Type: image/jpeg" \
    --data-binary @frame.jpg
{
  "id": "chk_27a68debe90e1c57",
  "verdict": "clean",            // "clean" | "nsfw"
  "confidence": 1.0,
  "model": "safereel-v1",
  "cached": false,
  "processing_ms": 141
}

Errors: 400 invalid_request · 401 invalid_key · 402 quota_exceeded · 413 payload_too_large · 415 unsupported_media (wrong type or undecodable) · 429 rate_limited · 500 processing_failed · 503 service_starting / 503 inference_unavailable (capacity momentarily full — retry shortly).

Python:

import httpx

BASE, KEY = "https://api.safereel.ai", "sr_live_…"

with open("frame.jpg", "rb") as f:
    resp = httpx.post(
        f"{BASE}/v1/check",
        headers={"Authorization": f"Bearer {KEY}", "Content-Type": "image/jpeg"},
        content=f.read(),
        timeout=30,
    )
resp.raise_for_status()
print(resp.json()["verdict"])

Anonymous demo: POST /v1/demo/check

The public, no-signup image demo that powers the playground. Same response shape as /v1/check.


Videos: POST /v1/videosGET /v1/videos/{id}

Classify a video by source URL — you never upload bytes. Give us any public http(s) URL (your CDN, S3, a direct file link). We analyze the full duration of the file and return a single verdict.

Submit

curl -X POST $BASE/v1/videos \
    -H "Authorization: Bearer $KEY" \
    -H "Content-Type: application/json" \
    -d '{"url": "https://cdn.example.com/movie.mp4",
         "webhook_url": "https://you.example.com/safereel-hook"}'
{
  "id": "vid_52271e017ebc44e6",
  "status": "queued",
  "webhook_secret": "7bba1544c9ed359ac8195cb0eeaa9e6f"
}

Poll

curl $BASE/v1/videos/vid_52271e017ebc44e6 -H "Authorization: Bearer $KEY"
{
  "id": "vid_52271e017ebc44e6",
  "status": "done",        // queued | processing | done | failed
  "verdict": "nsfw",       // null until done
  "frames": 93,            // frames sampled
  "flagged": 93,           // frames flagged
  "duration_s": 30.011,
  "error": null
}

Job ids are owner-only — another key gets 404 not_found, same as unknown ids.

Webhook

If you passed webhook_url, we POST the same body as the poll response on completion. Delivery: up to 4 attempts (immediate, then after 2s, 10s, 30s), 10s timeout per attempt; anything other than a 2xx response is a failure. Verify the signature with your webhook_secret (signing covers the exact raw body — check it before parsing):

import hashlib, hmac

expected = hmac.new(webhook_secret.encode(), raw_body, hashlib.sha256).hexdigest()
assert hmac.compare_digest(f"sha256={expected}", request.headers["X-SafeReel-Signature"])

The signature is over the exact request body — compute it on the raw bytes, not a re-serialized JSON object.


Status: GET /v1/status

Public, no auth. Powers the status page; poll no more than every 30s.

{"status": "ok", "load": "normal", "version": "0.1.0", "uptime_s": 3600}

load is one of normal / elevated / unavailable / starting.


Content handling