v1.0 sandbox
GM

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.

Internal sandbox. Centrox engineering uses this to exercise the /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 C# Python JavaScript Java
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();
Sandbox dev token
Auto-minted by the gateway in non-production environments. Code samples above embed this token at page load.
loading...
issuer: - expires: -

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.

POST /api/v1/forensic/analyze
Request Body
ParameterTypeDescription
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.
Example Request
JSON cURL C# Python JavaScript Java
{
  "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();
Response
200 Success
400 Invalid
401 Unauthorized
422 Rejected
429 Rate Limit
503 Unavailable
{
  "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."
  }
}
POSTs the example JSON above with a sandbox dev token. For real images, use the playground.

Health Check

Liveness + readiness probe. Anonymous. Used by orchestrators and the playground.

GET /api/v1/health
Example Response
{
  "success": true,
  "status": "healthy",
  "components": {
    "gateway": "healthy",
    "forensic_engine": "healthy"
  },
  "uptimeSeconds": 1287
}
Calls 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.

0.00 — 0.24
0.25 — 0.44
0.45 — 0.69
0.70 — 1.00
CLEAN No fraud signals detected
Score 0.00 to 0.24. All stages returned low or zero scores. No hard overrides triggered. Document can be auto-approved in most workflows.
LOW_RISK Minor anomalies, likely genuine
Score 0.25 to 0.44. One or two stages flagged minor anomalies. Common with aged documents, poor image quality, or unusual printing. Consider manual spot-check for high-value transactions.
SUSPICIOUS Manual review recommended
Score 0.45 to 0.69. Multiple stages flagged significant anomalies. May include field-level tampering, missing security features, or inconsistent fonts. Always route to a human reviewer.
TAMPERED Strong fraud signals
Score 0.70 or above, or a hard override triggered. Critical fraud indicators such as MRZ check-digit failure, editing software in EXIF, extreme ELA tampering, or missing required security features. Reject and flag for investigation.

Document Types

Four document types are supported in the current release.

greek_id_old
Pre-2005 Greek national ID. No MRZ. Visual checks: stamp, watermark, laminate.
greek_id_new
Post-2005 Greek national ID. MRZ present with check-digit validation.
passport
ICAO passport. MRZ + chip-indicator visual checks.
proof_of_address
Utility bill or bank statement. No MRZ, lighter visual checks.

Hard Overrides

Rules that escalate the verdict to TAMPERED regardless of stage scores.

RuleTrigger
mrz_check_digit_failureMRZ check-digit validation failed for the document number.
mrz_ir_prefixIR-prefix detected in the MRZ document number (high-risk template).
editing_software_detectedEXIF metadata indicates the image was processed in editing software.
ela_extreme_tamperingELA ratio exceeds 4x background threshold across multiple regions.
old_id_missing_stampRequired Hellenic Republic stamp not detected on the front side.
old_id_fake_watermarkWatermark 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.

StageWhat it checks
elaError Level Analysis. Flags regions whose JPEG compression history differs from the background.
metadataEXIF inspection. Reports editing-software signatures and EXIF presence.
font_consistencyDetects character-level anomalies between adjacent fields (kerning, stroke width, baseline).
ai_generationClassifier predicts whether the image was generated by a diffusion or GAN model.
visual_forensicsVLM checks for type-specific security features (stamps, watermarks, holograms, chip indicators).
mrz_validationICAO MRZ parser. Check-digit validation, field cross-match against OCR. Returns not_applicable for non-MRZ document types.
classificationPredicts the document template and compares against the declared document_type.