TrueSecurix

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.

Base URLhttps://truesecurix.com
AuthenticationX-API-Key: tsx_live_...
Request formatmultipart/form-data

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.5 or 10.0.0.0/8). A request from any other address returns 403. 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):

EndpointreferenceSimulated result
/v1/image/authenticityrealReal, decision pass. Also the answer for any other value.
/v1/image/authenticityai_generatedAI Generated, decision flag.
/v1/image/authenticityai_editedAI Edited, decision flag.
/v1/image/authenticitydigitally_editedDigitally Edited, decision flag.
/v1/image/authenticityreviewAnswered Real with review_recommended: true, decision review, suspected AI Edited at 0.62.
/v1/verifyflagdecision flag, risk score 95.
/v1/verifyreviewdecision review, risk score 55.
/v1/verifypassdecision 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

POST /v1/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

PartNameRequiredNotes
Form fieldimageYesJPEG, 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 fieldreferenceNoYour 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.
HeaderX-API-KeyYesYour live or test key.
HeaderIdempotency-KeyNoMakes 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
}
FieldMeaning
idThe check's id. It appears in your history and in webhook events.
objectAlways image_authenticity.
api_versionThe response contract version. See Versioning.
modelive (analysed and billed) or test (test key, simulated).
referenceYour reference as stored, or null.
classificationThe model's answer: real, ai_generated, ai_edited or digitally_edited.
labelThe same answer for people: Real, AI Generated, AI Edited, Digitally Edited.
confidenceCalibrated probability of classification, 0 to 1.
review_recommendedtrue when the evidence is not strong enough to act on without a person.
decisionThe outcome to act on: pass, review or flag. See the outcome rule.
suspectedOnly for a review: the manipulated class the evidence leans toward, as {classification, label, score}. null for a pass or a flag.
scoresCalibrated score for each of the four classes. They sum to about 1.
indicatorsThe 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).
regionsLocated 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.
provenanceWhat 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.
imageThe analysed image: width, height, format and sha256.
model_versionThe model that produced the answer. Store it with your decision.
processing_msTime the server spent on this check.
credits_remainingYour 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_recommendedclassificationdecisionWhat to do
trueanyreviewA person decides. suspected says which manipulation to look for, and regions where.
falserealpassTreat the image as genuine.
falseany other classflagManipulated, 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

CodeWhat it reports
generator_fingerprintGenerative-model traces across areas of the image, at a level genuine camera photos do not reach.
localized_generative_regionOne area carries generative traces that the rest of the image does not.
tamper_localizedAn area whose camera noise does not match the rest of the image.
noise_inconsistencyPart of the image has a different sensor-noise pattern from the rest, typical of pasted or retouched content.
compression_inconsistencyA region responds to re-compression differently from the rest, which suggests a different save history.
jpeg_grid_misalignedThe JPEG block grid is shifted from where a single camera save would put it, as after cropping and re-saving or pasting.
duplicated_regionA region appears twice in the same image, the signature of clone stamping.
combined_patternThe whole-image analysis chose the class with no single localized check behind it. Its strength follows the calibrated confidence.
below_evidence_barThe image leaned toward a manipulated class but not strongly enough to say so. Returned as Real with review_recommended: true.
no_manipulation_foundNone of the checks found generation or editing.
c2pa_digital_source_typeSigned content credentials (C2PA) declare how the image was made.
c2pa_generative_issuerContent credentials were issued by a generative tool.
c2pa_invalidContent credentials are present but fail validation, which happens when a signed image is altered afterwards.
iptc_digital_source_typeIPTC metadata declares a digital source type, such as trained algorithmic media.
generator_parametersGeneration settings (prompt, sampler) embedded in the file.
generative_software_tagThe file names a generative tool as its creator.
editing_softwareThe file was last saved by image-editing software. Common, and not proof of tampering on its own.
camera_metadataCamera metadata with a capture time is present. Supporting evidence only, since metadata can be copied.
metadata_absentThe 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

POST /v1/verify

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.

FieldTypeNotes
selfiefileA selfie image. Checked for deepfake and face synthesis, and for presentation attacks (a photo or screen held to the camera).
documentfileAn ID image (Aadhaar, PAN, passport) or a bank statement PDF. ID images get checksum validation and a tamper check; statements get balance reconciliation.
videofileOptional short selfie clip, checked for frame-to-frame deepfake signals.
referencetextOptional case id, up to 120 characters, echoed back. With a test key it selects the simulated answer.
device_signalstext (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
}
FieldMeaning
verification_idThe check's id in your history.
mode, referenceAs for image authenticity.
risk_score0 to 100. Higher means more likely fraud.
decisionpass, 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.
selfiesynthetic_score, verdict, is_deepfake, or null without a selfie.
livenessPresentation-attack result: pad_score (higher is more likely an attack), verdict, is_live, or null.
videotemporal_score and verdict when a video was sent, else null.
injectionscore, verdict, virtual_camera, reasons from device_signals, or null.
face_matchmatch_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.
documenttamper_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.
evidenceThe findings behind the decision: check, finding, weight.
recommendationA plain-language summary for a reviewer.
consensus_familiesHow many independent signal families agreed on fraud.
signal_breakdownEvery signal with its raw and calibrated score and whether it was informative. Only calibrated, informative signals can drive the decision.
credits_remaining, processing_msYour 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."
}
Statuserror.codeMeaningChargedRetry?
400bad_requestNo 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.NoNo, fix the file
401unauthorizedThe X-API-Key header is missing.NoNo
402payment_requiredNo credits left. Top up in the dashboard.NoAfter top-up
403forbiddenThe key is unknown or revoked, the account is disabled, or the request came from an IP address outside the key's allowlist.NoNo
409conflictA request with the same Idempotency-Key is still running.NoYes, same key
413payload_too_largeThe file is over 15 MB, or the image is over 40 megapixels.NoNo, resize
422validation_errorA required field is missing or malformed (for example no image part). error.details lists each field and why.NoNo, fix the request
429rate_limitedYour account's per-minute limit is used up.NoYes, back off
503engine_unavailableThe analysis service is temporarily unavailable or at capacity.NoYes, back off
502, 504engine_bad_response, engine_timeoutIdentity checks only: the analysis service gave an unexpected answer or took too long.NoYes, back off
500internal_errorAn error on our side. Write to us with the request id.NoYes, 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.

PlanRequests a minute
Free and pay as you go120
Starter240
Scale600
Enterprise1,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.

EventSent when
check.completedEvery completed live check, whatever the outcome.
check.flaggedA live check came back flag.
check.reviewA live check came back review.
credits.lowYour balance reached 20 or 5 credits.
webhook.testYou 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

HeaderValue
TrueSecurix-Signaturet=<unix seconds>,v1=<hex HMAC-SHA256(secret, "<t>.<raw body>")>
TrueSecurix-EventThe event type.
TrueSecurix-DeliveryThe delivery id. It stays the same on every retry of this event to this endpoint.
Content-Typeapplication/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 2xx within 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 id to ignore a repeat.
  • Endpoints must be https addresses 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 and device_signals are sent with a direct HTTP call.
  • 429, 502, 503, 504 and network errors are retried with jittered backoff of several seconds. Pass an idempotency_key so a retry is never charged twice.
  • A final failure raises TrueSecurixError with status_code, code and request_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, suspected class 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 the reference.
  • --labels takes a CSV of file,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.
  • --workers sets parallel requests (default 4). Raising it rarely makes a run faster. --base-url or TRUESECURIX_BASE_URL changes 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.

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, currently 2026-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 (currently ia-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

EndpointAuthReturns
GET /v1/statusNoneEach part of the service now, and measured uptime per day for 90 days. Shown on the status page.
GET /healthNone{"status": "healthy"} while the API process is up.
GET /readyNone{"ready": true} when the analysis service is ready for checks.
GET /v1/demo/quota, POST /v1/demo/verifyNoneThe 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.