API

Redaction, wired into your product

The same face, license plate, and screen redaction that powers the Scanon.ai web app, available as a REST API - for individual developers or full business accounts.

API REFERENCE

What the API does

The Scanon API detects and redacts personally identifiable information - faces, license plates, and screens - in images and video. Send it a file, get back a redacted one; the original is never retained. It's the same detection engine that powers the Scanon.ai web app, available programmatically.

The API is identical for individual and business accounts - the only differences are your rate-limit tier and credit balance, both tied to your account's plan.

Authentication

Every request needs an API key

Create a key from your Business Dashboard or Account page, then send it as a bearer token on every request:

Authorization: Bearer scanon_your_api_key

Keys that are missing, malformed, or revoked get a 401.

Code examples

Pick a language once - every sample on this page switches with it.

Images - synchronous

POST /v1/images returns the redacted image directly

Param
file
Type
multipart file
Default
required
Notes
Max 10MB. Must be an image content type.
Param
detect_faces
Type
form boolean
Default
true
Notes
-
Param
detect_plates
Type
form boolean
Default
false
Notes
-
Param
detect_tattoos
Type
form boolean
Default
false
Notes
Blurs screens/tattoo-class regions.
Param
encryption_mode
Type
form string
Default
"normal"
Notes
"normal" or "cryptographic". Anything else returns 400.

On 200, the response body is the redacted image as raw image/png bytes - not JSON. The X-Image-Credits-Remaining header reports your balance after the call. On any other status, the body is JSON - see Errors below.

encryption_mode="cryptographic" AES-scrambles each detected region with a fresh, never-stored key before blurring on top - the original pixels are unrecoverable, not just visually hidden. It costs 2 credits instead of 1; an unrecognized value is rejected with 400 rather than silently falling back to normal blur.

# curl's -o writes the response body to disk no matter the status code.
# Write to a temp file first and only keep it if the status was 200 - on
# error the body is JSON (e.g. {"detail": "Insufficient credits"}), not a PNG.
STATUS=$(curl -s -o /tmp/scanon_response -w "%{http_code}" \
  -X POST https://api.scanon.ai/v1/images \
  -H "Authorization: Bearer scanon_your_api_key" \
  -F "file=@photo.jpg" \
  -F "detect_faces=true" \
  -F "detect_plates=false" \
  -F "detect_tattoos=false" \
  -F "encryption_mode=normal")
# Change encryption_mode to "cryptographic" for AES-scrambled (irreversible)
# redaction - costs 2 image credits instead of 1.

if [ "$STATUS" -eq 200 ]; then
  mv /tmp/scanon_response redacted.png
  echo "Saved redacted.png"
else
  echo "Request failed with status $STATUS:"
  cat /tmp/scanon_response
  exit 1
fi

Videos - asynchronous

Submit, poll, then fetch the result

1

POST /v1/videos

Accepts file (multipart, required) and encryption_mode (form string, "normal" or "cryptographic", default "normal"). Returns {"job_id": "...", "status": "queued"} immediately.

# Requires jq (https://jqlang.org) for JSON parsing.
API_KEY="scanon_your_api_key"

JOB=$(curl -s -X POST https://api.scanon.ai/v1/videos \
  -H "Authorization: Bearer $API_KEY" \
  -F "file=@video.mp4" \
  -F "encryption_mode=normal")
JOB_ID=$(echo "$JOB" | jq -r '.job_id')
echo "Submitted job $JOB_ID"
2

GET /v1/videos/{job_id}

Returns status (queued processing done/failed/cancelled), progress (0–100 or null), and error (set when status is failed). Poll this until you reach a terminal status.

# Poll every 4s with a sensible max wait - never a tight loop
POLL_INTERVAL=4
MAX_WAIT=300
WAITED=0
STATUS="queued"

while [ "$STATUS" != "done" ] && [ "$STATUS" != "failed" ] && [ "$STATUS" != "cancelled" ]; do
  sleep "$POLL_INTERVAL"
  WAITED=$((WAITED + POLL_INTERVAL))
  if [ "$WAITED" -ge "$MAX_WAIT" ]; then
    echo "Timed out waiting for job $JOB_ID"
    exit 1
  fi

  POLL=$(curl -s "https://api.scanon.ai/v1/videos/$JOB_ID" -H "Authorization: Bearer $API_KEY")
  STATUS=$(echo "$POLL" | jq -r '.status')
  echo "status=$STATUS progress=$(echo "$POLL" | jq -r '.progress')"
done

if [ "$STATUS" != "done" ]; then
  echo "Job did not complete: $(echo "$POLL" | jq -r '.error')"
  exit 1
fi
3

GET /v1/videos/{job_id}/result

Returns the redacted video as raw video/mp4 bytes - see Result lifecycle below before you call it.

# Single fetch only - a second call to /result returns 410
HTTP_STATUS=$(curl -s -o redacted.mp4 -w "%{http_code}" \
  "https://api.scanon.ai/v1/videos/$JOB_ID/result" \
  -H "Authorization: Bearer $API_KEY")

if [ "$HTTP_STATUS" -eq 200 ]; then
  echo "Saved redacted.mp4"
else
  echo "Fetch failed ($HTTP_STATUS) - result may have expired or already been retrieved"
  exit 1
fi

Note: detect_faces/detect_plates/detect_tattoos apply to images only. Video submissions currently redact every class your pipeline supports (faces, plates, screens) regardless of these flags - per-request filtering for video is planned but not yet wired up.

Rate limits

Per-API-key token buckets, tier-aware

Each key gets a token bucket per endpoint. Burst is how many calls you can fire back-to-back before you're throttled; the bucket then refills at a fixed rate up to that same burst ceiling. Your tier is your account's subscription plan (free / pro / team) - resolved automatically, no configuration needed.

Tier
free
Images - burst
20
Images - refill
+1 every 30s (~120/hr)
Videos - burst
2
Videos - refill
+1 every 15 min (~4/hr)
Tier
pro
Images - burst
100
Images - refill
+1 every 5s (~720/hr)
Videos - burst
10
Videos - refill
+1 every 3 min (~20/hr)
Tier
team
Images - burst
300
Images - refill
+1 every 2s (~1,800/hr)
Videos - burst
20
Videos - refill
+1 every 90s (~40/hr)

Video limits are intentionally capped near total processing capacity, so they won't move much even on higher tiers. Exceeding your limit returns 429 with a Retry-After header (seconds to wait) plus X-RateLimit-Limit and X-RateLimit-Remaining. A 429 is checked before credits are deducted, so a throttled request never costs you a credit.

Errors

Every non-2xx response is JSON

Error bodies always look like this - check the status code before treating the response body as a result:

{
  "detail": "Insufficient credits"
}
Status
400
Meaning
Bad request - not an image, or the file couldn't be decoded.
Example detail
"File must be an image"
Status
401
Meaning
API key missing, malformed, or revoked.
Example detail
"Invalid or revoked API key"
Status
402
Meaning
Not enough credits for this call.
Example detail
"Insufficient credits"
Status
404
Meaning
Job doesn't exist, or belongs to a different key.
Example detail
"Job not found"
Status
409
Meaning
You called /result before the job reached "done".
Example detail
"Job not ready (status: processing)"
Status
410
Meaning
Result already fetched once, or its TTL expired.
Example detail
"Result expired or already retrieved"
Status
429
Meaning
Rate limit reached for your tier - see Retry-After.
Example detail
"Rate limit reached for your free plan..."
Status
500
Meaning
Unexpected server error during redaction.
Example detail
"Redaction failed: ..."

Credits

Shared with the Scanon web app

  • API calls draw from the same credit balance as the web app - there's no separate API pool.
  • Images cost 1 credit, or 2 credits when encryption_mode="cryptographic".
  • Videos cost 1 credit, or 2 credits when encryption_mode="cryptographic".
  • Credits are deducted atomically before processing starts. If an image redaction fails unexpectedly (500), the credits charged for that call are automatically refunded - 2 for cryptographic mode, 1 otherwise.
  • A 402 (insufficient credits) or 429 (rate limited) never deducts a credit.

Result lifecycle (video)

Single fetch, then it's gone

  • Once a job reaches status: "done", the redacted video is held for 10 minutes.
  • GET /v1/videos/{job_id}/result deletes the cached file immediately after it streams the response - fetch it once and save it. A second call (or a call after the 10-minute window) returns 410.
  • Jobs that go more than ~2 minutes without a status poll are treated as abandoned and cancelled - keep polling every few seconds until you see a terminal status.

Need something beyond faces, plates, and screens?

We build custom detection pipelines tailored to your use case - contact us to discuss what you're trying to redact.

Contact us