Forensic Analysis API
Submit identity documents for multi-layer forensic analysis. The API runs seven independent stages (ELA, metadata, font consistency, AI generation, visual forensics, MRZ validation, classification), cross-validates the signals, and returns a structured risk assessment with automated fraud escalation via hard overrides.
/api/v1/forensic/analyze pipeline end-to-end against the staging engine. Responses follow the envelope { request_id, result, error } where result and error are mutually exclusive. Not a client integration surface; legacy ingestion remains the production path.Bearer Token Authentication
Include your JWT in the Authorization header on every request. Tokens are short-lived (60 minutes by default).
curl https://{domain}/api/v1/forensic/analyze \
-H "Authorization: Bearer {jwt_token}" \
-H "Content-Type: application/json"var client = new HttpClient();
client.DefaultRequestHeaders.Add("Authorization", $"Bearer {token}");
client.DefaultRequestHeaders.Add("Content-Type", "application/json");import requests
headers = {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json"
}
response = requests.post(url, headers=headers, json=payload)const response = await fetch(url, {
method: "POST",
headers: {
"Authorization": `Bearer ${token}`,
"Content-Type": "application/json"
},
body: JSON.stringify(payload)
});HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(url))
.header("Authorization", "Bearer " + token)
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(json))
.build();loading...
Error Handling
Errors return the envelope with result: null and a populated error object carrying a stable code, the HTTP status, a human message, and the offending field when applicable. Status codes follow the canonical mapping shown under Analyze Document.
Rate Limits
A fixed-window rate limit is applied per authenticated subject. Exceeding the window returns 429 RATE_LIMIT_EXCEEDED with a Retry-After header. Defaults are 60 requests per minute in sandbox.
Analyze Document
Submit a document for forensic analysis. Returns verdict, risk score, per-stage results, and any hard overrides that were triggered.
| Parameter | Type | Description |
|---|---|---|
| document_typestringREQUIRED | string | Type of document being analyzed.greek_id_oldgreek_id_newpassportproof_of_address |
| imagesarrayREQUIRED | array | One or two document images. At least one entry must have side: "front"; duplicates are rejected. |
| images[].sidestringREQUIRED | string | Document side.frontback |
| images[].base64stringREQUIRED | string | Base64-encoded image bytes. Maximum 8 MB after decoding. |
| images[].mime_typestringREQUIRED | string | Image format.image/jpegimage/png |
| ocr_responseobjectoptional | object | Raw Vision-API output from your OCR pipeline. Forwarded as-is to the forensic engine for field cross-validation. |
{
"document_type": "greek_id_old",
"images": [
{ "side": "front", "base64": "/9j/4AAQSkZJRg...", "mime_type": "image/jpeg" },
{ "side": "back", "base64": "/9j/4AAQSkZJRg...", "mime_type": "image/jpeg" }
],
"ocr_response": {}
}curl -X POST https://{domain}/api/v1/forensic/analyze \
-H "Authorization: Bearer {jwt_token}" \
-H "Content-Type: application/json" \
-d '{
"document_type": "greek_id_old",
"images": [{
"side": "front",
"base64": "/9j/4AAQSkZJRg...",
"mime_type": "image/jpeg"
}],
"ocr_response": {}
}'var request = new
{
document_type = "greek_id_old",
images = new[]
{
new
{
side = "front",
base64 = Convert.ToBase64String(imageBytes),
mime_type = "image/jpeg"
}
},
ocr_response = new { }
};
var json = JsonSerializer.Serialize(request);
var content = new StringContent(json, Encoding.UTF8, "application/json");
var response = await client.PostAsync("/api/v1/forensic/analyze", content);import base64, requests
with open("front.jpg", "rb") as f:
img_b64 = base64.b64encode(f.read()).decode()
payload = {
"document_type": "greek_id_old",
"images": [{
"side": "front",
"base64": img_b64,
"mime_type": "image/jpeg"
}],
"ocr_response": {}
}
resp = requests.post(url, json=payload, headers=headers)const payload = {
document_type: "greek_id_old",
images: [{
side: "front",
base64: btoa(imageData),
mime_type: "image/jpeg"
}],
ocr_response: {}
};
const res = await fetch("/api/v1/forensic/analyze", {
method: "POST",
headers: {
"Authorization": `Bearer ${token}`,
"Content-Type": "application/json"
},
body: JSON.stringify(payload)
});HttpClient client = HttpClient.newHttpClient();
String json = """
{
"document_type": "greek_id_old",
"images": [{
"side": "front",
"base64": "%s",
"mime_type": "image/jpeg"
}],
"ocr_response": {}
}
""".formatted(base64Image);
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(url))
.header("Authorization", "Bearer " + token)
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(json))
.build();{
"request_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"result": {
"verdict": "SUSPICIOUS",
"risk_score": 0.52,
"document_type": "greek_id_old",
"processing_time_ms": 1843,
"stages": {
"ela": { "status": "completed", "score": 0.65, "suspicious_regions": [] },
"metadata": { "status": "completed", "score": 0.0, "editing_software_detected": false, "exif_present": true },
"font_consistency": { "status": "completed", "score": 0.35, "anomalous_fields": [] },
"ai_generation": { "status": "completed", "score": 0.08, "ai_generated": false },
"visual_forensics": { "status": "completed", "score": 0.40, "checks": { "stamp_present": true, "watermark_genuine": true, "laminate_intact": true } },
"mrz_validation": { "status": "not_applicable", "reason": "old_greek_id_no_mrz" },
"classification": { "status": "completed", "score": 0.0, "detected_type": "greek_id_old", "matches_declared": true }
},
"hard_overrides": []
},
"error": null
}{
"request_id": "b12cd34e-...",
"result": null,
"error": {
"code": "INVALID_REQUEST",
"status": 400,
"message": "Missing required field: images",
"field": "images"
}
}{
"request_id": "",
"result": null,
"error": {
"code": "UNAUTHORIZED",
"status": 401,
"message": "Authentication failed."
}
}{
"request_id": "h90ij12k-...",
"result": null,
"error": {
"code": "UNSUPPORTED_DOCUMENT_TYPE",
"status": 422,
"message": "Document type 'drivers_license' is not supported",
"field": "document_type"
}
}{
"request_id": "j34kl56m-...",
"result": null,
"error": {
"code": "RATE_LIMIT_EXCEEDED",
"status": 429,
"message": "Too many requests. Retry after 30 seconds."
}
}{
"request_id": "l78mn90o-...",
"result": null,
"error": {
"code": "SERVICE_UNAVAILABLE",
"status": 503,
"message": "Forensic engine is temporarily unavailable."
}
}Health Check
Liveness + readiness probe. Anonymous. Used by orchestrators and the playground.
{
"success": true,
"status": "healthy",
"components": {
"gateway": "healthy",
"forensic_engine": "healthy"
},
"uptimeSeconds": 1287
}GET /api/v1/health against this gateway and shows the live envelope below.
Verdicts & Scoring
Every analysis returns a verdict based on the combined risk score from all stages. Hard overrides can force escalation regardless of score.
Document Types
Four document types are supported in the current release.
Hard Overrides
Rules that escalate the verdict to TAMPERED regardless of stage scores.
| Rule | Trigger |
|---|---|
| mrz_check_digit_failure | MRZ check-digit validation failed for the document number. |
| mrz_ir_prefix | IR-prefix detected in the MRZ document number (high-risk template). |
| editing_software_detected | EXIF metadata indicates the image was processed in editing software. |
| ela_extreme_tampering | ELA ratio exceeds 4x background threshold across multiple regions. |
| old_id_missing_stamp | Required Hellenic Republic stamp not detected on the front side. |
| old_id_fake_watermark | Watermark appears printed on the surface rather than embedded. |
Forensic Stages
Seven independent stages composed into the final verdict. Each emits a status, score, and stage-specific details.
| Stage | What it checks |
|---|---|
| ela | Error Level Analysis. Flags regions whose JPEG compression history differs from the background. |
| metadata | EXIF inspection. Reports editing-software signatures and EXIF presence. |
| font_consistency | Detects character-level anomalies between adjacent fields (kerning, stroke width, baseline). |
| ai_generation | Classifier predicts whether the image was generated by a diffusion or GAN model. |
| visual_forensics | VLM checks for type-specific security features (stamps, watermarks, holograms, chip indicators). |
| mrz_validation | ICAO MRZ parser. Check-digit validation, field cross-match against OCR. Returns not_applicable for non-MRZ document types. |
| classification | Predicts the document template and compares against the declared document_type. |