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/docsfor Swagger UI orhttp://localhost:8000/redocfor ReDoc.
Table of Contents
- Conventions
- Authentication & Consent
- Rate Limiting
- Request Size Limits
- Error Response Format
- Health Endpoints
- Stats Endpoints
- Provider Endpoints
- Cache Endpoints
- Job Endpoints
- Face Endpoints
- Search Endpoints
- Analysis Endpoints
- Export Endpoints
- Core Schemas
1. Conventions
- Base URL:
http://<host>:<port>(defaulthttp://localhost:8000). - Content type:
application/jsonfor all request and response bodies, exceptGET /export/{job_id}which returnsapplication/jsonas a downloadable attachment. - Trailing slash: All collection endpoints accept both
/resourceand/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) andX-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.pydocstring). - When exceeded, the API returns
429 Too Many Requestswith 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
docs/ARCHITECTURE.mdβ execution flow, layers.docs/CONFIGURATION.mdβ everyFI_*env var.docs/PROVIDERS.mdβ what each provider returns innormalizedandraw.docs/DEPLOYMENT.mdβ running behind a reverse proxy, health probes for load balancers.docs/TROUBLESHOOTING.mdβ decoding error responses and circuit-breaker states.