| # 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://<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`](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. | |
| | `<ExceptionClassName>` | 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.<kind>.completed`, `jobs.<kind>.failed`, `jobs.<kind>.timeout`, `retries.<provider>`, `failures.<provider>`. | |
| | `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_<name> = 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=<job_id>.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. |
|
|