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.
POST /api/v1/forensic/analyze is the engineering sandbox (Bearer token, accepts our schema directly). POST /api/v1/forensic/intelli/v1/analyze is the client integration surface (X-API-Key header, accepts the client's native payload). All responses follow the envelope { request_id, result, error } where result and error are mutually exclusive.Authentication
Two endpoints, two schemes. Pick the one matching the endpoint you call.
API Key (client integration, /intelli)
Send your shared API key in the X-API-Key header on every request to POST /api/v1/forensic/intelli/v1/analyze. Keys are provisioned out of band; contact your integration owner for rotation.
curl https://{domain}/api/v1/forensic/intelli/v1/analyze \
-H "X-API-Key: {api_key}" \
-H "Content-Type: application/json"var client = new HttpClient();
client.DefaultRequestHeaders.Add("X-API-Key", apiKey);
client.DefaultRequestHeaders.Add("Content-Type", "application/json");import requests
headers = {
"X-API-Key": api_key,
"Content-Type": "application/json"
}
response = requests.post(url, headers=headers, json=payload)const response = await fetch(url, {
method: "POST",
headers: {
"X-API-Key": apiKey,
"Content-Type": "application/json"
},
body: JSON.stringify(payload)
});HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(url))
.header("X-API-Key", apiKey)
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(json))
.build();Bearer Token (sandbox, /analyze)
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.
Intelli Analyze
Client integration endpoint. Accepts the client's native payload (one or more documents with pre-parsed OCR fields and inline base64 images), runs the full forensic pipeline, and returns the same { request_id, result, error } envelope as /analyze.
X-API-Key header. See Authentication for samples.| Parameter | Type | Required | Description |
|---|---|---|---|
RequestId | string | optional | Echoed back as request_id in the response. Also used as the idempotency key (repeated requests within 5 minutes return the cached result). Auto-generated if omitted. |
StartProcessDate | string | optional | ISO timestamp from the client pipeline. Forwarded as-is, no validation. |
VerificationStatus | string | optional | Client-side verification status. Forwarded as-is. |
Documents | array | REQUIRED | One ID front (DocumentType: 200) and optionally one ID back (DocumentType: 999). Other document types are rejected 422. |
Documents[].DocumentType | integer | REQUIRED | 200 = ID front, 999 = ID back. 24 (proof of address) is not yet supported. |
Documents[].FileContents | string[] | REQUIRED | Exactly one base64-encoded image per document. JPEG or PNG. Max 8MB decoded. |
Documents[].ImagesOriginalNames | string[] | optional | Original filenames from the client pipeline. |
Documents[].LastName / FirstName / IDNumber / Sex / BirthDate / NationalityCode / IssueDate / ExpirationDate / ... | string | optional | OCR fields from the client's Vision pipeline. Forwarded to the forensic engine for cross-validation against what we extract from the image. |
curl -X POST https://{domain}/api/v1/forensic/intelli/v1/analyze \
-H "X-API-Key: {api_key}" \
-H "Content-Type: application/json" \
-d '{
"RequestId": "gdiak0122",
"StartProcessDate": "2026-05-07T17:37:49.107",
"VerificationStatus": "True",
"Documents": [
{
"DocumentType": 200,
"FileContents": ["/9j/4AAQSkZJRg..."],
"ImagesOriginalNames": ["FrontSide.jpg"],
"LastName": "ΝΤΑΛΙΑΣ",
"FirstName": "ΓΕΩΡΓΙΟΣ",
"IDNumber": "Α00707710",
"BirthDate": "1993-01-18",
"NationalityCode": "GRC"
},
{
"DocumentType": 999,
"FileContents": ["/9j/4AAQSkZJRg..."],
"ImagesOriginalNames": ["BackSide.jpg"]
}
]
}'
{
"request_id": "gdiak0122",
"result": {
"verdict": "CLEAN",
"risk_score": 0.08,
"document_type": "greek_id_new",
"processing_time_ms": 1652,
"stages": { /* ... same shape as /analyze */ },
"hard_overrides": []
},
"error": null
}{
"request_id": "gdiak0122",
"result": null,
"error": {
"code": "INVALID_REQUEST",
"status": 400,
"message": "Missing required field: documents",
"field": "documents"
}
}{
"request_id": "",
"result": null,
"error": {
"code": "UNAUTHORIZED",
"status": 401,
"message": "API key is missing or invalid."
}
}{
"request_id": "gdiak0122",
"result": null,
"error": {
"code": "UNSUPPORTED_DOCUMENT_TYPE",
"status": 422,
"message": "Document type '24' is not supported. Proof of address analysis is not yet available. Supported types: 200, 999.",
"field": "documents[].document_type"
}
}{
"request_id": "gdiak0122",
"result": null,
"error": {
"code": "PAYLOAD_TOO_LARGE",
"status": 413,
"message": "Image exceeds maximum allowed size of 8MB",
"field": "documents[].file_contents[]"
}
}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
Two 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. |