API reference
Send an image or an identity document from your backend. You get back pass, review or flag, with the evidence behind it. One credit per check.
First request
Create a key in the dashboard (sign-up gives 100 free checks, no card). Then send one image:
curl https://truesecurix.com/v1/image/authenticity \
-H "X-API-Key: $TRUESECURIX_API_KEY" \
-H "Idempotency-Key: loan-48211-doc-3" \
-F "image=@shop_front.jpg" \
-F "reference=loan-48211-doc-3"
Act on the decision field: pass review flag. Everything else in the response is the evidence for it.
Authentication
Send your API key in the X-API-Key header. Keep it on your server and never put it in a browser, a mobile app or public code. A missing key returns 401; an unknown or revoked key returns 403.
- An account can hold several keys. Revoke one in the dashboard and the others keep working.
- Each key can carry an allowlist of source IP addresses or CIDR ranges (for example
203.0.113.5or10.0.0.0/8). A request from any other address returns403. Leave it empty for no restriction. - Account, billing, team and key settings need a dashboard sign-in. An API key cannot reach them.
Test keys
A key that starts with tsx_test_ returns a simulated result. It is free, stores nothing and sends no webhooks. The response carries "mode": "test"; a live key (tsx_live_) returns "mode": "live". Test keys count towards your rate limit. Create one in the dashboard by ticking "Test key".
Choose the simulated answer with the reference field (upper or lower case):
| Endpoint | reference | Simulated result |
|---|---|---|
| /v1/image/authenticity | real | Real, decision pass. Also the answer for any other value. |
| /v1/image/authenticity | ai_generated | AI Generated, decision flag. |
| /v1/image/authenticity | ai_edited | AI Edited, decision flag. |
| /v1/image/authenticity | digitally_edited | Digitally Edited, decision flag. |
| /v1/image/authenticity | review | Answered Real with review_recommended: true, decision review, suspected AI Edited at 0.62. |
| /v1/verify | flag | decision flag, risk score 95. |
| /v1/verify | review | decision review, risk score 55. |
| /v1/verify | pass | decision pass, risk score 4. Also the answer for any other value. |
Simulated image results have "model_version": "sandbox", a single indicator with code sandbox, and empty regions, provenance and image. When your integration handles every outcome, swap in a live key. Nothing else changes.
Image authenticity
Tells you whether an image is real, AI generated, AI edited or digitally edited. Send any image a customer submits, such as a shop photo, a claim photo or an invoice. One image per call, one credit per completed check. The image is not stored. We keep only its SHA-256 and the result.
Request
| Part | Name | Required | Notes |
|---|---|---|---|
| Form field | image | Yes | JPEG, PNG or WebP. At least 128 pixels on the shorter side, at most 40 megapixels and 15 MB. HEIC is not accepted: convert it to JPEG first. |
| Form field | reference | No | Your application, claim, order, case or document id. Trimmed, cut to 120 characters, stored with the result and echoed back. With a test key it selects the simulated answer. |
| Header | X-API-Key | Yes | Your live or test key. |
| Header | Idempotency-Key | No | Makes a retry safe. See Idempotency and charging. |
Response
A 200 with this body. This example is an image held back for review:
{
"id": "6f1c2e0a9b7d4c3e8a5f1b2d3c4e5f60",
"object": "image_authenticity",
"api_version": "2026-10-16",
"mode": "live",
"reference": "loan-48211-doc-3",
"classification": "real",
"label": "Real",
"confidence": 0.31,
"review_recommended": true,
"decision": "review",
"suspected": { "classification": "ai_edited", "label": "AI Edited", "score": 0.62 },
"scores": { "real": 0.31, "ai_generated": 0.04, "ai_edited": 0.62, "digitally_edited": 0.03 },
"indicators": [
{ "code": "below_evidence_bar", "supports": "ai_edited", "strength": "weak",
"detail": "Some signs point to AI Edited (score 0.62), but not enough to accuse a genuine customer. Returned as Real and marked for review." }
],
"regions": [
{ "x": 0.62, "y": 0.71, "width": 0.30, "height": 0.22, "score": 0.71, "supports": "ai_edited" }
],
"provenance": {
"c2pa_present": false, "c2pa_valid": null, "c2pa_claim_generator": null,
"digital_source_type": null, "software": null, "camera_make": null, "camera_model": null
},
"image": { "width": 1200, "height": 1600, "format": "jpeg", "sha256": "9b2f..." },
"model_version": "ia-2026.10.21",
"processing_ms": 7600,
"credits_remaining": 1999
}
| Field | Meaning |
|---|---|
| id | The check's id. It appears in your history and in webhook events. |
| object | Always image_authenticity. |
| api_version | The response contract version. See Versioning. |
| mode | live (analysed and billed) or test (test key, simulated). |
| reference | Your reference as stored, or null. |
| classification | The model's answer: real, ai_generated, ai_edited or digitally_edited. |
| label | The same answer for people: Real, AI Generated, AI Edited, Digitally Edited. |
| confidence | Calibrated probability of classification, 0 to 1. |
| review_recommended | true when the evidence is not strong enough to act on without a person. |
| decision | The outcome to act on: pass, review or flag. See the outcome rule. |
| suspected | Only for a review: the manipulated class the evidence leans toward, as {classification, label, score}. null for a pass or a flag. |
| scores | Calibrated score for each of the four classes. They sum to about 1. |
| indicators | The evidence: items for the returned class first, strongest first. Each has code (stable, safe to build rules on), supports (the class it points to, or null for context only), strength (strong, moderate, weak or neutral) and detail (plain English for a reviewer). |
| regions | Located edited areas. x, y, width and height are fractions of the image width and height from the top-left corner, as the image is displayed (camera rotation applied). Each has a score and the class it supports. Often empty. |
| provenance | What the file declares about itself: content credentials (C2PA) present and valid, the claim generator, the IPTC digital source type, the editing software and the camera make and model. Each is null when absent. A declared camera never clears an image on its own. |
| image | The analysed image: width, height, format and sha256. |
| model_version | The model that produced the answer. Store it with your decision. |
| processing_ms | Time the server spent on this check. |
| credits_remaining | Your balance after this check. |
The outcome rule
When the evidence points to a manipulation but is not strong enough to act on, the image goes to review with the class it leans toward. decision comes from two fields:
| review_recommended | classification | decision | What to do |
|---|---|---|---|
| true | any | review | A person decides. suspected says which manipulation to look for, and regions where. |
| false | real | pass | Treat the image as genuine. |
| false | any other class | flag | Manipulated, with confidence. Hold the file and investigate. |
A review can carry classification: "real" while a manipulated class scores higher, as in the example above. Branch on decision, not on classification, or a review will read as a pass.
Indicator codes
| Code | What it reports |
|---|---|
| generator_fingerprint | Generative-model traces across areas of the image, at a level genuine camera photos do not reach. |
| localized_generative_region | One area carries generative traces that the rest of the image does not. |
| tamper_localized | An area whose camera noise does not match the rest of the image. |
| noise_inconsistency | Part of the image has a different sensor-noise pattern from the rest, typical of pasted or retouched content. |
| compression_inconsistency | A region responds to re-compression differently from the rest, which suggests a different save history. |
| jpeg_grid_misaligned | The JPEG block grid is shifted from where a single camera save would put it, as after cropping and re-saving or pasting. |
| duplicated_region | A region appears twice in the same image, the signature of clone stamping. |
| combined_pattern | The whole-image analysis chose the class with no single localized check behind it. Its strength follows the calibrated confidence. |
| below_evidence_bar | The image leaned toward a manipulated class but not strongly enough to say so. Returned as Real with review_recommended: true. |
| no_manipulation_found | None of the checks found generation or editing. |
| c2pa_digital_source_type | Signed content credentials (C2PA) declare how the image was made. |
| c2pa_generative_issuer | Content credentials were issued by a generative tool. |
| c2pa_invalid | Content credentials are present but fail validation, which happens when a signed image is altered afterwards. |
| iptc_digital_source_type | IPTC metadata declares a digital source type, such as trained algorithmic media. |
| generator_parameters | Generation settings (prompt, sampler) embedded in the file. |
| generative_software_tag | The file names a generative tool as its creator. |
| editing_software | The file was last saved by image-editing software. Common, and not proof of tampering on its own. |
| camera_metadata | Camera metadata with a capture time is present. Supporting evidence only, since metadata can be copied. |
| metadata_absent | The file has no metadata, common for images sent through messaging apps. Not counted as evidence. |
New codes may be added; existing codes are not renamed. Test keys return the code sandbox.
Identity checks
Checks a selfie for deepfakes, validates Aadhaar, PAN and passport numbers (Verhoeff, PAN format, MRZ check digits), matches the selfie to the ID and reconciles bank statement balances. Send at least one file. One credit per completed check.
| Field | Type | Notes |
|---|---|---|
| selfie | file | A selfie image. Checked for deepfake and face synthesis, and for presentation attacks (a photo or screen held to the camera). |
| document | file | An ID image (Aadhaar, PAN, passport) or a bank statement PDF. ID images get checksum validation and a tamper check; statements get balance reconciliation. |
| video | file | Optional short selfie clip, checked for frame-to-frame deepfake signals. |
| reference | text | Optional case id, up to 120 characters, echoed back. With a test key it selects the simulated answer. |
| device_signals | text (JSON) | Optional. The signals collected by the capture SDK, used to detect virtual cameras and emulators. |
Each file is limited to 15 MB and 40 megapixels. Headers are the same as for image authenticity: X-API-Key, and optionally Idempotency-Key.
curl https://truesecurix.com/v1/verify \
-H "X-API-Key: $TRUESECURIX_API_KEY" \
-F "selfie=@selfie.jpg" \
-F "document=@aadhaar.jpg" \
-F "reference=user_12345"
{
"verification_id": "3f2a9c1d...",
"mode": "live",
"reference": "user_12345",
"risk_score": 4.8,
"decision": "pass",
"selfie": { "synthetic_score": 0.05, "verdict": "likely_authentic", "is_deepfake": false },
"liveness": null,
"video": null,
"injection": null,
"face_match": { "match_probability": 0.93, "is_match": true, "verdict": "face_match" },
"document": {
"tamper_score": 0.07, "verdict": "analyzed", "is_tampered": false,
"deterministic": { "doc_type": "aadhaar", "ocr_ok": true,
"checks": [ { "field": "verhoeff", "valid": true, "reason": "Aadhaar checksum valid" } ] }
},
"evidence": [ { "check": "Document verhoeff", "finding": "Aadhaar checksum valid", "weight": 1.0 } ],
"recommendation": "Low fraud risk: media is consistent with authentic capture.",
"consensus_families": 0,
"signal_breakdown": [
{ "name": "CLIPDetector", "family": "deepfake", "raw_score": 0.05, "calibrated_score": 0.05,
"confidence": 0.72, "is_fitted": true, "informative": true, "verdict": "likely_authentic" }
],
"credits_remaining": 982,
"processing_ms": 975
}
| Field | Meaning |
|---|---|
| verification_id | The check's id in your history. |
| mode, reference | As for image authenticity. |
| risk_score | 0 to 100. Higher means more likely fraud. |
| decision | pass, review or flag. A failed checksum or a statement that does not reconcile can flag on its own. Model signals flag only when independent signal families agree. Anything less goes to review. |
| selfie | synthetic_score, verdict, is_deepfake, or null without a selfie. |
| liveness | Presentation-attack result: pad_score (higher is more likely an attack), verdict, is_live, or null. |
| video | temporal_score and verdict when a video was sent, else null. |
| injection | score, verdict, virtual_camera, reasons from device_signals, or null. |
| face_match | match_probability (0 to 1), is_match, verdict, when a selfie and an ID image were both sent and a face was found in each. A clear mismatch routes to review. |
| document | tamper_score, verdict (analyzed or structural_fail), is_tampered, and deterministic: doc_type, ocr_ok and checks, each with field, valid (true, false or null) and reason. null without a document. |
| evidence | The findings behind the decision: check, finding, weight. |
| recommendation | A plain-language summary for a reviewer. |
| consensus_families | How many independent signal families agreed on fraud. |
| signal_breakdown | Every signal with its raw and calibrated score and whether it was informative. Only calibrated, informative signals can drive the decision. |
| credits_remaining, processing_ms | Your balance after the check, and the server time spent. |
Idempotency and charging
A timeout does not tell you whether a check ran. Send an Idempotency-Key header (your own value, up to 128 characters) and reuse it when you retry.
- A repeat of a completed request returns the original result and is not charged again, even if your balance has since reached zero.
- A repeat while the first request is still running returns
409. Wait and retry with the same key. - A request that failed releases its key, so a retry with the same key runs the check.
- Keys are scoped to the endpoint and your account, and kept for 48 hours. A good key is the image's SHA-256 or your document id.
Only a 200 from a live key is charged, one credit per check. A check that fails or is refused costs nothing. Test keys are never charged. Credits never expire.
Errors
Errors share one JSON body. Each response has an X-Request-ID header; quote it to support. You can send your own X-Request-ID (up to 64 characters) instead.
{
"error": { "code": "bad_request", "message": "Unsupported image format (GIF). Send JPEG, PNG or WebP." },
"request_id": "b7e4c0d2a9f14e6b",
"detail": "Unsupported image format (GIF). Send JPEG, PNG or WebP."
}
| Status | error.code | Meaning | Charged | Retry? |
|---|---|---|---|---|
| 400 | bad_request | No file sent, the file is not a readable image, the format is not JPEG, PNG or WebP, or the image is smaller than 128 pixels on the shorter side. | No | No, fix the file |
| 401 | unauthorized | The X-API-Key header is missing. | No | No |
| 402 | payment_required | No credits left. Top up in the dashboard. | No | After top-up |
| 403 | forbidden | The key is unknown or revoked, the account is disabled, or the request came from an IP address outside the key's allowlist. | No | No |
| 409 | conflict | A request with the same Idempotency-Key is still running. | No | Yes, same key |
| 413 | payload_too_large | The file is over 15 MB, or the image is over 40 megapixels. | No | No, resize |
| 422 | validation_error | A required field is missing or malformed (for example no image part). error.details lists each field and why. | No | No, fix the request |
| 429 | rate_limited | Your account's per-minute limit is used up. | No | Yes, back off |
| 503 | engine_unavailable | The analysis service is temporarily unavailable or at capacity. | No | Yes, back off |
| 502, 504 | engine_bad_response, engine_timeout | Identity checks only: the analysis service gave an unexpected answer or took too long. | No | Yes, back off |
| 500 | internal_error | An error on our side. Write to us with the request id. | No | Yes, with the same key |
Retry with exponential backoff and jitter, starting at a few seconds, and keep the same Idempotency-Key. The Python SDK does this for you.
Rate limits
Limits are per account and per minute, shared by all your keys (test keys included) and both endpoints. Going over returns 429.
| Plan | Requests a minute |
|---|---|
| Free and pay as you go | 120 |
| Starter | 240 |
| Scale | 600 |
| Enterprise | 1,200, more on request |
Each image takes several seconds to analyse, so more parallel requests will not always finish a large batch sooner. Enterprise capacity is agreed per contract.
Webhooks
Get a signed POST to your server when a check completes. An account owner or admin can add up to five endpoints in the dashboard and choose events for each. Each endpoint gets a signing secret (whsec_...), shown once and rotatable at any time.
| Event | Sent when |
|---|---|
| check.completed | Every completed live check, whatever the outcome. |
| check.flagged | A live check came back flag. |
| check.review | A live check came back review. |
| credits.low | Your balance reached 20 or 5 credits. |
| webhook.test | You pressed "Send test" in the dashboard. Sent to that endpoint whatever events it is subscribed to. |
A flagged check sends both check.completed and check.flagged, each as its own event with its own id. Test keys send no events. The body is JSON:
{
"id": "evt_5c1e9a0b7f3d2e4a6b8c0d1e",
"type": "check.review",
"created": 1760500000,
"data": {
"id": "6f1c2e0a9b7d4c3e8a5f1b2d3c4e5f60", "object": "image_authenticity", "reference": "loan-48211-doc-3",
"decision": "review", "classification": "real", "label": "Real", "confidence": 0.31,
"review_recommended": true,
"suspected": { "classification": "ai_edited", "label": "AI Edited", "score": 0.62 },
"model_version": "ia-2026.10.21"
}
}
created is in Unix seconds. data is a summary, never the submitted files. For an identity check it carries object: "verification", reference, decision and risk_score; match it to your case by reference. For credits.low it is {"credits_remaining": 20}.
Headers
| Header | Value |
|---|---|
| TrueSecurix-Signature | t=<unix seconds>,v1=<hex HMAC-SHA256(secret, "<t>.<raw body>")> |
| TrueSecurix-Event | The event type. |
| TrueSecurix-Delivery | The delivery id. It stays the same on every retry of this event to this endpoint. |
| Content-Type | application/json |
Verify the signature
Compute the HMAC over the raw request bytes before any JSON parsing. Compare in constant time, and reject a timestamp more than five minutes from your clock to stop replays.
# Python (standard library only)
import hashlib, hmac, time
def verify_signature(secret: str, header: str, body: bytes, tolerance: int = 300) -> bool:
try:
parts = dict(p.split("=", 1) for p in header.split(","))
t, sig = int(parts["t"]), parts["v1"]
except (KeyError, ValueError):
return False
if abs(time.time() - t) > tolerance:
return False
expected = hmac.new(secret.encode(), f"{t}.".encode() + body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, sig)
# Flask: body = request.get_data(); header = request.headers["TrueSecurix-Signature"]
// Node.js (built-in crypto)
const crypto = require("crypto");
function verifySignature(secret, header, rawBody, toleranceSec = 300) {
const parts = Object.fromEntries(String(header).split(",").map((p) => {
const i = p.indexOf("=");
return [p.slice(0, i), p.slice(i + 1)];
}));
const t = Number(parts.t);
if (!Number.isInteger(t) || !parts.v1) return false;
if (Math.abs(Date.now() / 1000 - t) > toleranceSec) return false;
const expected = crypto.createHmac("sha256", secret).update(`${t}.`).update(rawBody).digest();
const given = Buffer.from(parts.v1, "hex");
return given.length === expected.length && crypto.timingSafeEqual(given, expected);
}
// Express: keep the raw bytes for this route
app.post("/truesecurix/webhook", express.raw({ type: "application/json" }), (req, res) => {
if (!verifySignature(process.env.TRUESECURIX_WEBHOOK_SECRET, req.get("TrueSecurix-Signature"), req.body)) {
return res.sendStatus(400);
}
const event = JSON.parse(req.body);
res.sendStatus(200); // answer fast, then process the event
});
Delivery
- Answer with any
2xxwithin 15 seconds. The response body is not read, so keep it short. Redirects are not followed. - Any other answer, or no answer, is retried after 30 seconds, 2 minutes, 10 minutes, 30 minutes, 1 hour, 3 hours, 6 hours and 12 hours: about a day in all. Then the delivery is marked failed.
- Delivery is at least once. Use the event
idto ignore a repeat. - Endpoints must be
httpsaddresses on port 443 or 8443 that resolve to public IP addresses. Private and local addresses are refused. - The dashboard shows each endpoint's last 50 deliveries with their status code and error. Delivery history is kept for 30 days.
Python SDK
A single file, truesecurix.py. It needs only httpx (pip install httpx).
import os
from truesecurix import TrueSecurix, TrueSecurixError
with TrueSecurix(os.environ["TRUESECURIX_API_KEY"]) as client:
try:
check = client.image_authenticity("shop_front.jpg", reference="loan-48211-doc-3",
idempotency_key="loan-48211-doc-3")
except TrueSecurixError as err:
print(err.status_code, err.code, err.request_id)
raise
if check["decision"] == "review":
print("Send to a reviewer, look for", check["suspected"]["label"])
kyc = client.verify(selfie="selfie.jpg", document="aadhaar.jpg", reference="user_123")
print(kyc["decision"], kyc["risk_score"])
TrueSecurix(api_key, base_url="https://truesecurix.com", timeout=60.0, max_retries=2)image_authenticity(image, reference=None, idempotency_key=None)takes a file path and returns the response as a dict.verify(selfie=None, document=None, reference=None, idempotency_key=None)takes file paths. Video anddevice_signalsare sent with a direct HTTP call.429,502,503,504and network errors are retried with jittered backoff of several seconds. Pass anidempotency_keyso a retry is never charged twice.- A final failure raises
TrueSecurixErrorwithstatus_code,codeandrequest_id.
Batch script
batch_image_authenticity.py sends every image in a folder (and its subfolders) to /v1/image/authenticity and writes one CSV. Keep it next to truesecurix.py.
export TRUESECURIX_API_KEY=tsx_live_your_key
python batch_image_authenticity.py ./images results.csv
python batch_image_authenticity.py ./images results.csv --workers 4 --labels labels.csv
- Safe to stop and re-run. Images already in the CSV are skipped, and each request uses the image's SHA-256 as its
Idempotency-Key, so no image is charged twice. - Files that are not JPEG, PNG or WebP (iPhone HEIC, for example) are listed as not sent. Images the API cannot analyse are reported separately and are not charged.
- Images refused because the server was busy are retried before the run ends. If any still failed, the script exits with status 1; run the same command again.
- The CSV has the file,
decision, class, confidence, review flag,suspectedclass and score, the four class scores, the top indicators, regions, check id, model version and SHA-256. The file's relative path is sent as thereference. --labelstakes a CSV offile,label(real, ai_generated, ai_edited, digitally_edited) and prints accuracy per class on your own labelled images, plus the confusion matrix, manipulations caught, genuine images wrongly flagged and the share sent to review.--workerssets parallel requests (default 4). Raising it rarely makes a run faster.--base-urlorTRUESECURIX_BASE_URLchanges the server.
Capture SDK
Injection attacks feed a virtual camera (such as OBS), an emulator or a screen into the camera. Catching them needs signals from the device. The capture SDK runs in your customer's browser or app, takes the selfie and collects those signals. Your backend then sends the selfie and device_signals to /v1/verify, so the API key never leaves your server.
<script src="https://truesecurix.com/sdk/truesecurix-capture.js"></script>
const cap = new TrueSecurixCapture();
await cap.start(document.querySelector("video")); // opens the camera
const selfie = await cap.captureFrame(); // Blob
const signals = await cap.collectSignals(); // device and injection signals
cap.stop();
// Send { selfie, device_signals: JSON.stringify(signals) } to YOUR backend,
// which posts them to /v1/verify with your X-API-Key.
- Web: truesecurix-capture.js, with a working sample at the capture sample page.
- React Native, Expo and Capacitor: truesecurix-capture-mobile.js.
Postman collection
Import the TrueSecurix Postman collection. It covers image authenticity and identity checks. Set the api_key variable to a test key first, then pick a file for the image or selfie field.
Versioning
api_version(image authenticity responses, currently2026-10-16) names the response contract. Within it, fields are added but never renamed or removed, and indicator codes are added but never renamed. Ignore fields you do not know.model_version(currentlyia-2026.10.21) names the model that produced the answer. It changes when the model is retrained. Store it with each decision.- Identity check responses carry no
api_version. Read them by field name and ignore fields you do not know.
Status and health
| Endpoint | Auth | Returns |
|---|---|---|
| GET /v1/status | None | Each part of the service now, and measured uptime per day for 90 days. Shown on the status page. |
| GET /health | None | {"status": "healthy"} while the API process is up. |
| GET /ready | None | {"ready": true} when the analysis service is ready for checks. |
| GET /v1/demo/quota, POST /v1/demo/verify | None | The public website demo. Limited to 5 checks a day per visitor, not stored. Not for integration: use your own key. |
Questions about an integration? Write to contact@truesecurix.com with the request id.