face-intel / docs /API_REFERENCE.md
Marwan
Restructure + add reverse face search (PimEyes-style)
f5eeb1c
|
Raw
History Blame Contribute Delete
33.6 kB

Face Intel β€” API Reference

This document is the complete reference for the Face Intel REST API. Every endpoint is documented with method, path, request schema, response schema, status codes, and a curl example.

Interactive docs: Once the server is running, open http://localhost:8000/docs for Swagger UI or http://localhost:8000/redoc for ReDoc.


Table of Contents

  1. Conventions
  2. Authentication & Consent
  3. Rate Limiting
  4. Request Size Limits
  5. Error Response Format
  6. Health Endpoints
  7. Stats Endpoints
  8. Provider Endpoints
  9. Cache Endpoints
  10. Job Endpoints
  11. Face Endpoints
  12. Search Endpoints
  13. Analysis Endpoints
  14. Export Endpoints
  15. Core Schemas

1. Conventions

  • Base URL: http://<host>:<port> (default http://localhost:8000).
  • Content type: application/json for all request and response bodies, except GET /export/{job_id} which returns application/json as a downloadable attachment.
  • Trailing slash: All collection endpoints accept both /resource and /resource/. The two forms are equivalent.
  • IDs: Job IDs are 12-character hex strings (first 12 chars of a UUID4). Provider names are snake_case strings.
  • Timestamps: ISO 8601 with timezone (2026-07-10T14:30:00.123+00:00).
  • Response headers: Every response includes X-Request-ID (12-char hex) and X-Response-Time-ms (e.g. 42.18).

2. Authentication & Consent

Authentication

There is no authentication. The API is open by default. Put it behind a reverse proxy with auth (e.g. OAuth2 Proxy, AWS API Gateway authorizer, Cloudflare Access) before exposing it to the internet. See docs/DEPLOYMENT.md for recommended patterns.

Consent header

Face Intel processes biometric data. The platform supports an optional consent header that downstream consumers can use to demonstrate that the end user consented to face analysis. The header is not currently enforced in code, but the configuration knobs exist so a downstream gateway or future middleware layer can require it.

Setting Default Purpose
FI_REQUIRE_CONSENT_HEADER true Whether to require the header (enforced by your gateway).
FI_CONSENT_HEADER_NAME X-Consent-Statement Header name to look for.

Example request:

curl -X POST http://localhost:8000/faces/detect \
  -H "Content-Type: application/json" \
  -H "X-Consent-Statement: User consented to face analysis at 2026-07-10T14:30Z" \
  -d '{"image_url": "https://example.com/photo.jpg"}'

3. Rate Limiting

The RateLimitMiddleware enforces a per-IP sliding-window limit.

Setting Default Behavior
FI_RATE_LIMIT_PER_MINUTE 30 Max requests per client IP per 60s window.
  • /health/* endpoints are excluded from rate limiting (so load balancer probes don't burn the quota).
  • The limiter is in-memory per process β€” for multi-process deployments, replace it with a Redis-backed limiter (see api/middleware.py docstring).
  • When exceeded, the API returns 429 Too Many Requests with the standard error envelope (see Β§5).

4. Request Size Limits

Setting Default Behavior
FI_MAX_REQUEST_BODY_BYTES 26214400 (25 MB) Hard cap on Content-Length. Returns 413 Payload Too Large.
FI_MAX_IMAGE_BYTES 20971520 (20 MB) Decoded image size limit, enforced by InputValidator. Returns 200 with success=false and error_type=ValidationError.

The size limit applies to the entire HTTP body, so a base64-encoded image of ~18 MB will pass both checks (base64 expands by ~33%).


5. Error Response Format

All errors use a uniform envelope:

{
  "success": false,
  "error": "Human-readable error message",
  "error_type": "ExceptionClassName",
  "details": null,
  "request_id": "abc123def456"
}

HTTP status codes

Code When
200 OK Successful request, including business-level failures (e.g. validation error in a service). Check success in the JSON body.
404 Not Found Resource (job, provider, result) not found.
413 Payload Too Large Request body exceeded FI_MAX_REQUEST_BODY_BYTES.
422 Unprocessable Entity Pydantic validation error on the request body (FastAPI default).
429 Too Many Requests Rate limit exceeded.
500 Internal Server Error Unhandled exception β€” GlobalExceptionMiddleware catches and converts.

Service-level errors (HTTP 200 with success=false)

Some endpoints return HTTP 200 with a body whose success field is false. This is intentional: the request was processed, but the business result is a failure (e.g. invalid image input, unknown provider). The error_type field tells you what kind of failure:

error_type Meaning
ValidationError Input validation failed (missing image, bad base64, unrecognized format).
NotConfigured Provider was invoked but is_available() returned False.
TimeoutError Orchestrator or job-level timeout exceeded.
<ExceptionClassName> Anything else β€” see error for the message.

6. Health Endpoints

Tag: health. Defined in api/routes/health.py.

GET /health

Purpose: Liveness probe. Returns immediately.

Response:

{ "status": "ok" }

Curl:

curl http://localhost:8000/health

GET /health/live

Purpose: Liveness probe (Kubernetes /livenessProbe).

Response:

{ "status": "alive" }

GET /health/ready

Purpose: Readiness probe (Kubernetes /readinessProbe). Currently minimal β€” returns ready unconditionally. A production deployment should extend this to check DB + cache connectivity.

Response:

{ "status": "ready" }

GET /health/providers

Purpose: Per-provider health snapshot including circuit breaker state. Useful for dashboards.

Response: A SystemHealthSnapshot (see models/health.py):

{
  "status": "healthy",
  "version": "1.0.0",
  "uptime_seconds": 142.7,
  "providers": [
    {
      "name": "haar",
      "healthy": true,
      "consecutive_failures": 0,
      "last_success": null,
      "last_failure": null,
      "last_check": null,
      "avg_latency_ms": 6.2,
      "circuit_open": false,
      "circuit_opened_at": null,
      "metadata": {}
    }
  ],
  "cache": {
    "cache.hits": 18,
    "cache.misses": 24,
    "jobs.detection.completed": 42
  },
  "metrics": { "providers": 7 }
}
Field Type Meaning
status string healthy if all providers' circuits are closed, else degraded.
version string App version from FI_APP_VERSION.
uptime_seconds float Seconds since process start.
providers array One ProviderHealthSnapshot per provider that has been invoked at least once.
cache object Snapshot of the global counter registry (includes cache hits/misses).
metrics object Aggregate counts (currently {"providers": N}).

Curl:

curl http://localhost:8000/health/providers | jq .

7. Stats Endpoints

Tag: stats. Defined in api/routes/stats.py.

GET /stats

Purpose: Full metrics snapshot for dashboards.

Response: The output of MetricsCollector.snapshot():

{
  "providers": [
    {
      "name": "haar",
      "invocations": 42,
      "successes": 41,
      "failures": 1,
      "retries": 0,
      "avg_latency_ms": 6.2,
      "p95_latency_ms": 12.4,
      "success_rate": 0.976
    }
  ],
  "timings": {
    "job.detection": {
      "count": 42,
      "avg_ms": 14.3,
      "p50_ms": 12.0,
      "p95_ms": 28.1
    },
    "provider.haar": { "count": 42, "avg_ms": 6.2, "p50_ms": 5.1, "p95_ms": 12.4 }
  },
  "counters": {
    "cache.hits": 18,
    "cache.misses": 24,
    "jobs.detection.completed": 42,
    "retries.dnn": 0,
    "failures.dnn": 0
  },
  "health": [
    {
      "name": "haar",
      "consecutive_failures": 0,
      "last_success": "2026-07-10T14:30:00.123+00:00",
      "last_failure": null,
      "avg_latency_ms": 6.2,
      "circuit_open": false
    }
  ]
}
Section Description
providers Per-provider invocations, successes, failures, retries, avg/p95 latency, success_rate.
timings Per-operation duration histograms (job-level + provider-level).
counters Global integer counters: cache.hits, cache.misses, jobs.<kind>.completed, jobs.<kind>.failed, jobs.<kind>.timeout, retries.<provider>, failures.<provider>.
health Per-provider circuit-breaker state.

Curl:

curl http://localhost:8000/stats | jq .

8. Provider Endpoints

Tag: providers. Defined in api/routes/providers.py.

GET /providers

Purpose: List every provider known to the manifest, with current status and any manifest discovery errors.

Response:

{
  "providers": [
    {
      "name": "haar",
      "capability": "detection",
      "status": "healthy",
      "available": true,
      "description": "OpenCV Haar Cascade β€” fast, frontal faces only",
      "version": "",
      "timeout_seconds": 0.0,
      "retry_max_attempts": 0,
      "metadata": {}
    },
    {
      "name": "retinaface",
      "capability": "detection",
      "status": "disabled",
      "available": false,
      "description": "RetinaFace β€” SOTA, requires insightface",
      "version": "",
      "timeout_seconds": 0.0,
      "retry_max_attempts": 0,
      "metadata": {}
    },
    {
      "name": "insightface",
      "capability": "recognition",
      "status": "not_configured",
      "available": false,
      "description": "deepinsight/insightface β€” ArcFace 512-d",
      "version": "",
      "timeout_seconds": 0.0,
      "retry_max_attempts": 0,
      "metadata": {}
    }
  ],
  "errors": {
    "insightface": "missing dependency: No module named 'insightface'"
  }
}

Status values (from ProviderStatus enum):

status Meaning
healthy Enabled, instantiated, is_available() == True.
degraded Reserved for future use.
unhealthy Reserved for future use.
disabled enable_<name> = False in settings.
not_configured Enabled but optional dependency missing OR is_available() == False.

Curl:

curl http://localhost:8000/providers | jq '.providers[] | {name, status, available}'

GET /providers/{name}

Purpose: Get a single provider's info.

Path parameters:

Name Type Description
name string Provider name (e.g. haar, dnn, serpapi).

Response (200): Same shape as one entry in the GET /providers array.

Response (404):

{ "detail": "Provider 'nonexistent' not found" }

Curl:

curl http://localhost:8000/providers/haar | jq .

9. Cache Endpoints

Tag: cache. Defined in api/routes/cache.py.

GET /cache

Purpose: Cache statistics β€” entries, hit ratio, evictions.

Response:

{
  "entries": 42,
  "max_entries": 1000,
  "ttl_seconds": 3600,
  "hits": 18,
  "misses": 24,
  "hit_ratio": 0.4286,
  "evictions": 0
}

Curl:

curl http://localhost:8000/cache | jq .

DELETE /cache

Purpose: Clear the entire cache. Returns the number of entries removed.

Response:

{ "cleared": 42 }

Curl:

curl -X DELETE http://localhost:8000/cache

10. Job Endpoints

Tag: jobs. Defined in api/routes/jobs.py.

The /jobs endpoints are the most general way to invoke the platform. The convenience endpoints under /faces/*, /search/*, and /analysis/* are thin wrappers that construct a JobRequest and delegate to the same JobService.

POST /jobs

Purpose: Create and synchronously run a job of any kind. The response returns once the job completes (or times out).

Request body β€” JobRequest:

Field Type Required Description
kind enum yes One of detection, recognition, search, image_analysis, metadata, forensics, full_pipeline.
image_url string one of HTTP(S) URL of the image. Localhost/private IPs rejected.
image_base64 string one of Base64-encoded image (with or without data:image/...;base64, prefix).
providers string[] no Whitelist of provider names. Empty/missing = use all enabled providers for the kind.
options object no Free-form options. Currently used for scrape_url in search jobs.

At least one of image_url / image_base64 must be present.

Example request:

{
  "kind": "detection",
  "image_url": "https://example.com/photo.jpg",
  "providers": ["haar", "dnn"]
}

Response (200) β€” single-kind job:

{
  "job_id": "abc123def456",
  "status": "completed",
  "result": {
    "success": true,
    "report": { /* UnifiedFaceReport β€” see Β§15 */ },
    "elapsed_ms": 142.7
  },
  "elapsed_ms": 142.7
}

Response (200) β€” full_pipeline job:

{
  "job_id": "abc123def456",
  "status": "completed",
  "result": {
    "success": true,
    "detection":     { "success": true, "report": { /* UnifiedFaceReport */ }, "elapsed_ms": 12.4 },
    "recognition":   { "success": true, "report": { /* UnifiedFaceReport */ }, "elapsed_ms": 35.1 },
    "image_analysis":{ "success": true, "report": { /* UnifiedFaceReport */ }, "elapsed_ms": 8.2 },
    "metadata":      { "success": true, "report": { /* UnifiedFaceReport */ }, "elapsed_ms": 3.1 },
    "forensics":     { "success": true, "report": { /* UnifiedFaceReport */ }, "elapsed_ms": 6.7 },
    "search":        { "success": true, "report": { /* UnifiedFaceReport */ }, "elapsed_ms": 87.3 }
  },
  "elapsed_ms": 142.7
}

Each sub-result is either a service result dict or, on exception, {"success": false, "error": "...", "error_type": "..."}.

Response (200) β€” timeout:

{
  "job_id": "abc123def456",
  "status": "timeout",
  "error": "Job timeout after 300.0s"
}

Response (200) β€” failure:

{
  "job_id": "abc123def456",
  "status": "failed",
  "error": "No image input provided.",
  "error_type": "ValidationError"
}

Curl:

curl -X POST http://localhost:8000/jobs \
  -H "Content-Type: application/json" \
  -d '{
    "kind": "full_pipeline",
    "image_url": "https://example.com/photo.jpg"
  }' | jq .

GET /jobs

Purpose: List recent jobs, optionally filtered by status, newest first.

Query parameters:

Name Type Default Constraint Description
limit int 50 1–500 Max number of jobs to return.
status string (none) enum Filter by status: pending, queued, running, completed, failed, cancelled, timeout.

Response (200):

{
  "jobs": [
    {
      "id": "abc123def456",
      "kind": "detection",
      "status": "completed",
      "created_at": "2026-07-10T14:30:00.123+00:00",
      "started_at": "2026-07-10T14:30:00.124+00:00",
      "completed_at": "2026-07-10T14:30:00.265+00:00",
      "request": {
        "kind": "detection",
        "image_url": "https://example.com/photo.jpg",
        "image_base64": null,
        "providers": [],
        "options": {}
      },
      "image_hash": "9f86d081...",
      "error": null
    }
  ]
}

Curl:

curl 'http://localhost:8000/jobs?limit=10&status=completed' | jq .

GET /jobs/{job_id}

Purpose: Fetch a single job's metadata.

Path parameters:

Name Type Description
job_id string Job ID returned by POST /jobs.

Response (200): Same shape as one entry in GET /jobs array.

Response (404):

{ "detail": "Job not found" }

Curl:

curl http://localhost:8000/jobs/abc123def456 | jq .

GET /jobs/{job_id}/result

Purpose: Fetch the persisted result of a completed job. Useful for retrieving reports later without re-running the job.

Response (200):

{
  "job_id": "abc123def456",
  "status": "completed",
  "report": { /* UnifiedFaceReport */ },
  "error": null,
  "elapsed_ms": 142.7,
  "created_at": "2026-07-10T14:30:00.265+00:00"
}

Response (404):

{ "detail": "Result not found" }

Curl:

curl http://localhost:8000/jobs/abc123def456/result | jq .

11. Face Endpoints

Tag: faces. Defined in api/routes/faces.py.

POST /faces/detect

Purpose: Convenience wrapper β€” run a detection-only job. Equivalent to POST /jobs with {"kind": "detection", ...}.

Request body β€” FaceRequest:

Field Type Required Description
image_url string one of HTTP(S) URL of the image.
image_base64 string one of Base64-encoded image.
providers string[] no Provider whitelist (e.g. ["haar", "dnn"]).

Response (200):

{
  "success": true,
  "report": {
    "metadata": {
      "job_id": "abc123def456",
      "created_at": "2026-07-10T14:30:00.123+00:00",
      "image_hash": "9f86d081...",
      "total_elapsed_ms": 14.3,
      "providers_invoked": ["haar", "dnn"],
      "providers_succeeded": ["haar", "dnn"],
      "providers_failed": [],
      "limitations": []
    },
    "detections": [
      {
        "box": { "x": 50, "y": 50, "w": 100, "h": 100 },
        "confidence": {
          "overall": 0.92,
          "components": { "detector_agreement": 1.0, "image_quality": 0.85 },
          "explanation": "Both detectors agreed on this face.",
          "method": "weighted_average"
        },
        "landmarks": null,
        "detected_by": ["haar", "dnn"],
        "embedding": null,
        "embedding_provider": null
      }
    ],
    "matches": [],
    "scraped_images": [],
    "reverse_matches": [],
    "image_analyses": [],
    "metadata_extractions": [],
    "forensics": [],
    "evidence": [ /* one entry per provider invoked */ ],
    "conflicts": [],
    "overall_confidence": { "overall": 0.92, "components": {}, "explanation": "", "method": "weighted_average" }
  },
  "elapsed_ms": 14.3
}

Response (200) β€” validation failure:

{
  "success": false,
  "error": "No image input provided.",
  "error_type": "ValidationError"
}

Curl β€” base64 input:

# Encode a local image
B64=$(base64 -w0 photo.jpg)

curl -X POST http://localhost:8000/faces/detect \
  -H "Content-Type: application/json" \
  -d "{\"image_base64\": \"$B64\"}" | jq '.report.detections | length'

Curl β€” URL input + provider whitelist:

curl -X POST http://localhost:8000/faces/detect \
  -H "Content-Type: application/json" \
  -d '{
    "image_url": "https://example.com/photo.jpg",
    "providers": ["haar", "dnn"]
  }' | jq .

POST /faces/recognize

Purpose: Recognize faces against the known-faces gallery.

Request body: Same FaceRequest shape as /faces/detect.

Response (200): Same envelope; the report's matches array contains one FaceMatch per detected face that matched a known person.

{
  "success": true,
  "report": {
    "metadata": { /* ... */ },
    "detections": [ /* FaceDetection[] */ ],
    "matches": [
      {
        "query_face_index": 0,
        "best_match": "alice",
        "confidence": {
          "overall": 0.87,
          "components": { "distance_score": 0.87 },
          "explanation": "Closest match: alice (distance 0.32)",
          "method": "weighted_average"
        },
        "distances": { "face_recognition": 0.32 }
      }
    ],
    "evidence": [ /* ... */ ],
    "conflicts": []
  },
  "elapsed_ms": 35.1
}

GET /faces/gallery

Purpose: List known persons in the reference gallery.

Response (200):

{
  "persons": [
    { "name": "alice", "num_embeddings": 3 },
    { "name": "bob",   "num_embeddings": 1 }
  ]
}

Curl:

curl http://localhost:8000/faces/gallery | jq .

DELETE /faces/gallery/{name}

Purpose: Remove a person and all their embeddings from the gallery.

Path parameters:

Name Type Description
name string Person name (case-insensitive, spaces β†’ _).

Response (200):

{ "name": "alice", "removed": true }

If the person doesn't exist, removed is false.

Curl:

curl -X DELETE http://localhost:8000/faces/gallery/alice

12. Search Endpoints

Tag: search. Defined in api/routes/search.py.

POST /search/reverse

Purpose: Reverse image search β€” find other pages where this image appears. Invokes all REVERSE_SEARCH providers (Google Lens, SerpAPI, Yandex, TinEye depending on enable flags).

Request body β€” SearchRequest:

Field Type Required Description
image_url string one of HTTP(S) URL of the image to search for.
image_base64 string one of Base64-encoded image.
providers string[] no Provider whitelist.
scrape_url string no If set, scrapers also target this URL for additional images.

Response (200):

{
  "success": true,
  "report": {
    "metadata": { /* ... */ },
    "detections": [],
    "matches": [],
    "scraped_images": [],
    "reverse_matches": [
      {
        "image_url": "https://example.com/found.jpg",
        "source_page": "https://blog.example.com/post",
        "title": "Blog post containing the image",
        "snippet": "…",
        "thumbnail": "https://example.com/thumb.jpg",
        "provider": "serpapi"
      }
    ],
    "evidence": [ /* one entry per reverse-search provider invoked */ ],
    "conflicts": []
  },
  "elapsed_ms": 87.3
}

Curl:

curl -X POST http://localhost:8000/search/reverse \
  -H "Content-Type: application/json" \
  -d '{"image_url": "https://example.com/photo.jpg"}' \
  | jq '.report.reverse_matches | length'

POST /search/scrape

Purpose: Scrape images from a target URL. Same request/response shape as /search/reverse β€” both invoke the SearchService which runs SCRAPING + REVERSE_SEARCH providers concurrently.

To scrape a specific page, set scrape_url:

curl -X POST http://localhost:8000/search/scrape \
  -H "Content-Type: application/json" \
  -d '{"scrape_url": "https://example.com/gallery-page"}' \
  | jq '.report.scraped_images | length'

If no image_url/image_base64 is provided but scrape_url is set, the request still requires some image input (the validator currently requires one). Pass any image URL or set image_base64 to a 1Γ—1 pixel β€” the scrapers will use scrape_url for their work.


13. Analysis Endpoints

Tag: analysis. Defined in api/routes/analysis.py.

POST /analysis/image

Purpose: Run image-quality + image-properties analysis (brightness, contrast, sharpness, noise, dominant colors, dimensions).

Request body β€” AnalysisRequest:

Field Type Required Description
image_url string one of HTTP(S) URL.
image_base64 string one of Base64-encoded image.
providers string[] no Provider whitelist.

Response (200):

{
  "success": true,
  "report": {
    "metadata": { /* ... */ },
    "image_analyses": [
      {
        "provider": "image_quality",
        "quality_score": 0.72,
        "brightness": 132.4,
        "contrast": 58.3,
        "sharpness": 187.6,
        "noise_level": 4.2,
        "width": 1024,
        "height": 768,
        "channels": 3,
        "color_profile": null,
        "dominant_colors": [],
        "aspects": { "method": "variance_of_laplacian", "noise_method": "local_stddev_residual" },
        "confidence": { "overall": 0.9, "components": {}, "explanation": "", "method": "weighted_average" }
      },
      {
        "provider": "image_properties",
        "quality_score": null,
        "width": 1024, "height": 768, "channels": 3,
        "color_profile": "BGR",
        "dominant_colors": ["#a8c5e0", "#5a7a9c", "#2a3f5c", "#d8e2ed", "#1a2533"],
        "aspects": { "aspect_ratio": 1.333, "megapixels": 0.786 },
        "confidence": { "overall": 0.95, "components": {}, "explanation": "", "method": "weighted_average" }
      }
    ],
    "evidence": [ /* ... */ ],
    "conflicts": []
  },
  "elapsed_ms": 8.2
}

POST /analysis/metadata

Purpose: Extract EXIF / XMP / IPTC metadata from the original image bytes.

Request body: Same AnalysisRequest shape.

Response (200):

{
  "success": true,
  "report": {
    "metadata_extractions": [
      {
        "provider": "exif",
        "format": "JPEG",
        "exif": {
          "Make": "Canon",
          "Model": "EOS R5",
          "DateTimeOriginal": "2026:07:10 14:30:00",
          "Software": "Adobe Lightroom 7.0"
        },
        "xmp": {},
        "iptc": {},
        "gps": { "lat": 37.7749, "lon": -122.4194 },
        "camera_make": "Canon",
        "camera_model": "EOS R5",
        "software": "Adobe Lightroom 7.0",
        "capture_time": "2026:07:10 14:30:00",
        "confidence": { /* ... */ }
      }
    ],
    "evidence": [ /* ... */ ],
    "conflicts": []
  },
  "elapsed_ms": 3.1
}

POST /analysis/forensics

Purpose: Run forensics β€” integrity check, duplicate detection, manipulation analysis.

Request body: Same AnalysisRequest shape.

Response (200):

{
  "success": true,
  "report": {
    "forensics": [
      {
        "provider": "image_integrity",
        "integrity_score": 1.0,
        "is_duplicate": null,
        "duplicate_of": null,
        "similarity_score": null,
        "manipulation_indicators": [],
        "elA_score": null,
        "noise_inconsistency": null,
        "details": {
          "sha256": "9f86d081884c7d65...",
          "format": "JPEG",
          "size_bytes": 102400,
          "bytes_per_pixel": 0.13,
          "app_markers": 1
        },
        "confidence": { /* ... */ }
      },
      {
        "provider": "duplicate_detector",
        "integrity_score": null,
        "is_duplicate": false,
        "duplicate_of": null,
        "similarity_score": 1.0,
        "manipulation_indicators": [],
        "elA_score": null,
        "noise_inconsistency": null,
        "details": {
          "phash": "1010101010101010...",
          "dhash": "0101010101010101...",
          "sha256": "9f86d081884c7d65...",
          "registered_hashes": 1
        },
        "confidence": { /* ... */ }
      }
    ],
    "evidence": [ /* ... */ ],
    "conflicts": []
  },
  "elapsed_ms": 6.7
}

Curl:

curl -X POST http://localhost:8000/analysis/forensics \
  -H "Content-Type: application/json" \
  -d '{"image_url": "https://example.com/photo.jpg"}' \
  | jq '.report.forensics'

14. Export Endpoints

Tag: export. Defined in api/routes/export.py.

GET /export/{job_id}

Purpose: Download a completed job's metadata + result as a pretty-printed JSON file attachment.

Path parameters:

Name Type Description
job_id string Job ID.

Response (200):

  • Content-Type: application/json
  • Content-Disposition: attachment; filename=<job_id>.json
  • Body: pretty-printed JSON combining the job record + result:
{
  "job": {
    "id": "abc123def456",
    "kind": "detection",
    "status": "completed",
    "created_at": "2026-07-10T14:30:00.123+00:00",
    "started_at": "2026-07-10T14:30:00.124+00:00",
    "completed_at": "2026-07-10T14:30:00.265+00:00",
    "request": { /* JobRequest */ },
    "image_hash": "9f86d081...",
    "error": null
  },
  "result": {
    "job_id": "abc123def456",
    "status": "completed",
    "report": { /* UnifiedFaceReport */ },
    "error": null,
    "elapsed_ms": 142.7,
    "created_at": "2026-07-10T14:30:00.265+00:00"
  }
}

Response (404):

{ "detail": "Job not found" }

Curl:

curl -OJ http://localhost:8000/export/abc123def456
# Saves as abc123def456.json

15. Core Schemas

These pydantic models are returned across multiple endpoints. Source: models/.

UnifiedFaceReport

The central output of every job. Defined in models/reports.py.

Field Type Description
metadata ReportMetadata Job ID, image hash, timing, providers invoked/succeeded/failed, limitations.
detections FaceDetection[] One per detected face, with cross-provider consensus.
matches FaceMatch[] One per recognized face (recognition jobs).
scraped_images object[] Images scraped from a target URL.
reverse_matches object[] Pages where the image was found.
image_analyses ImageAnalysisResult[] Quality, properties, visual features.
metadata_extractions MetadataResult[] EXIF, XMP, IPTC.
forensics ForensicsResult[] Integrity, duplicates, manipulation.
evidence Evidence[] One entry per provider invoked β€” preserves raw + normalized + error.
conflicts ConflictReport[] Cross-provider disagreements.
overall_confidence ConfidenceScore? Aggregated confidence for the whole report.

FaceDetection

Field Type Description
box {x, y, w, h} int Bounding box in image pixel coordinates.
confidence ConfidenceScore Decomposed sub-scores + explanation.
landmarks {name: [x,y]}? Facial landmarks if the detector provides them.
detected_by string[] List of provider names that detected this face (consensus).
embedding float[]? Face embedding vector (recognition jobs).
embedding_provider string? Name of the provider that produced the embedding.

ConfidenceScore

Field Type Description
overall float (0–1) Final weighted score.
components {name: float} Named sub-scores (e.g. detector_agreement, image_quality, distance_score).
explanation string Human-readable explanation.
method string Scoring method ("weighted_average").

Evidence

One per provider invocation. This is the evidence-first guarantee β€” nothing is discarded.

Field Type Description
provider string Provider name.
capability string "detection", "recognition", etc.
timestamp ISO 8601 When the provider finished.
raw any Verbatim provider response.
normalized object Cleaned fields used by the merger.
elapsed_ms float Provider-only latency.
success bool Whether the provider succeeded.
error string? Error message on failure.
error_type string? Exception class name.
metadata object Free-form (e.g. {"cache_hit": true}).
retry_count int Retries before this result.
limitations string[] Provider-reported limitations.

ConflictReport

Field Type Description
kind string E.g. "face_count_mismatch".
providers string[] Providers that disagreed.
description string Human-readable description.
severity string info | warning | error.

JobRequest

Field Type Description
kind enum detection | recognition | search | image_analysis | metadata | forensics | full_pipeline.
image_url string? HTTP(S) URL.
image_base64 string? Base64-encoded image.
providers string[] Provider whitelist (empty = all enabled).
options object Free-form (currently used for scrape_url).

Job (persisted)

Field Type Description
id string 12-char hex.
kind enum See JobKind.
status enum pending | queued | running | completed | failed | cancelled | timeout.
created_at ISO 8601 When the job was created.
started_at ISO 8601? When the job started running.
completed_at ISO 8601? When the job finished (any terminal state).
request JobRequest The original request.
image_hash string? SHA-256 of the preprocessed image.
error string? Error message on failure.

See Also