# 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](#1-conventions) 2. [Authentication & Consent](#2-authentication--consent) 3. [Rate Limiting](#3-rate-limiting) 4. [Request Size Limits](#4-request-size-limits) 5. [Error Response Format](#5-error-response-format) 6. [Health Endpoints](#6-health-endpoints) 7. [Stats Endpoints](#7-stats-endpoints) 8. [Provider Endpoints](#8-provider-endpoints) 9. [Cache Endpoints](#9-cache-endpoints) 10. [Job Endpoints](#10-job-endpoints) 11. [Face Endpoints](#11-face-endpoints) 12. [Search Endpoints](#12-search-endpoints) 13. [Analysis Endpoints](#13-analysis-endpoints) 14. [Export Endpoints](#14-export-endpoints) 15. [Core Schemas](#15-core-schemas) --- ## 1. Conventions - **Base URL:** `http://:` (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`](DEPLOYMENT.md#reverse-proxy) 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: ```bash 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`](../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: ```json { "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. | | `` | Anything else — see `error` for the message. | --- ## 6. Health Endpoints Tag: `health`. Defined in [`api/routes/health.py`](../api/routes/health.py). ### `GET /health` **Purpose:** Liveness probe. Returns immediately. **Response:** ```json { "status": "ok" } ``` **Curl:** ```bash curl http://localhost:8000/health ``` --- ### `GET /health/live` **Purpose:** Liveness probe (Kubernetes `/livenessProbe`). **Response:** ```json { "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:** ```json { "status": "ready" } ``` --- ### `GET /health/providers` **Purpose:** Per-provider health snapshot including circuit breaker state. Useful for dashboards. **Response:** A `SystemHealthSnapshot` (see [`models/health.py`](../models/health.py)): ```json { "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:** ```bash curl http://localhost:8000/health/providers | jq . ``` --- ## 7. Stats Endpoints Tag: `stats`. Defined in [`api/routes/stats.py`](../api/routes/stats.py). ### `GET /stats` **Purpose:** Full metrics snapshot for dashboards. **Response:** The output of `MetricsCollector.snapshot()`: ```json { "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..completed`, `jobs..failed`, `jobs..timeout`, `retries.`, `failures.`. | | `health` | Per-provider circuit-breaker state. | **Curl:** ```bash curl http://localhost:8000/stats | jq . ``` --- ## 8. Provider Endpoints Tag: `providers`. Defined in [`api/routes/providers.py`](../api/routes/providers.py). ### `GET /providers` **Purpose:** List every provider known to the manifest, with current status and any manifest discovery errors. **Response:** ```json { "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_ = False` in settings. | | `not_configured` | Enabled but optional dependency missing OR `is_available() == False`. | **Curl:** ```bash 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):** ```json { "detail": "Provider 'nonexistent' not found" } ``` **Curl:** ```bash curl http://localhost:8000/providers/haar | jq . ``` --- ## 9. Cache Endpoints Tag: `cache`. Defined in [`api/routes/cache.py`](../api/routes/cache.py). ### `GET /cache` **Purpose:** Cache statistics — entries, hit ratio, evictions. **Response:** ```json { "entries": 42, "max_entries": 1000, "ttl_seconds": 3600, "hits": 18, "misses": 24, "hit_ratio": 0.4286, "evictions": 0 } ``` **Curl:** ```bash curl http://localhost:8000/cache | jq . ``` --- ### `DELETE /cache` **Purpose:** Clear the entire cache. Returns the number of entries removed. **Response:** ```json { "cleared": 42 } ``` **Curl:** ```bash curl -X DELETE http://localhost:8000/cache ``` --- ## 10. Job Endpoints Tag: `jobs`. Defined in [`api/routes/jobs.py`](../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:** ```json { "kind": "detection", "image_url": "https://example.com/photo.jpg", "providers": ["haar", "dnn"] } ``` **Response (200) — single-kind job:** ```json { "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:** ```json { "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:** ```json { "job_id": "abc123def456", "status": "timeout", "error": "Job timeout after 300.0s" } ``` **Response (200) — failure:** ```json { "job_id": "abc123def456", "status": "failed", "error": "No image input provided.", "error_type": "ValidationError" } ``` **Curl:** ```bash 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):** ```json { "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:** ```bash 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):** ```json { "detail": "Job not found" } ``` **Curl:** ```bash 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):** ```json { "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):** ```json { "detail": "Result not found" } ``` **Curl:** ```bash curl http://localhost:8000/jobs/abc123def456/result | jq . ``` --- ## 11. Face Endpoints Tag: `faces`. Defined in [`api/routes/faces.py`](../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):** ```json { "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:** ```json { "success": false, "error": "No image input provided.", "error_type": "ValidationError" } ``` **Curl — base64 input:** ```bash # 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:** ```bash 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. ```json { "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):** ```json { "persons": [ { "name": "alice", "num_embeddings": 3 }, { "name": "bob", "num_embeddings": 1 } ] } ``` **Curl:** ```bash 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):** ```json { "name": "alice", "removed": true } ``` If the person doesn't exist, `removed` is `false`. **Curl:** ```bash curl -X DELETE http://localhost:8000/faces/gallery/alice ``` --- ## 12. Search Endpoints Tag: `search`. Defined in [`api/routes/search.py`](../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):** ```json { "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:** ```bash 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`: ```bash 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`](../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):** ```json { "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):** ```json { "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):** ```json { "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:** ```bash 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`](../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=.json` - Body: pretty-printed JSON combining the job record + result: ```json { "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):** ```json { "detail": "Job not found" } ``` **Curl:** ```bash curl -OJ http://localhost:8000/export/abc123def456 # Saves as abc123def456.json ``` --- ## 15. Core Schemas These pydantic models are returned across multiple endpoints. Source: [`models/`](../models/). ### `UnifiedFaceReport` The central output of every job. Defined in [`models/reports.py`](../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`](ARCHITECTURE.md) — execution flow, layers. - [`docs/CONFIGURATION.md`](CONFIGURATION.md) — every `FI_*` env var. - [`docs/PROVIDERS.md`](PROVIDERS.md) — what each provider returns in `normalized` and `raw`. - [`docs/DEPLOYMENT.md`](DEPLOYMENT.md) — running behind a reverse proxy, health probes for load balancers. - [`docs/TROUBLESHOOTING.md`](TROUBLESHOOTING.md) — decoding error responses and circuit-breaker states.