ChronoVerify

Method and limits

ChronoVerify answers one question about a photo: when it was captured, where if it still carries location data, and whether it shows signs of editing. It returns one plain-language verdict, a 0 to 100 evidence-strength number, and the per-signal breakdown that produced it. It is provenance-first, works on any photo, signed or not, and is not a deepfake-only detector.

Two layers, kept separate. Every report has an integrity substrate (the file hash, the method version, and an Ed25519 signature over the file-derived verdict) that is reproducible from the file alone, and a probabilistic verdict built from provenance, metadata, and pixel signals. The processing time and request id are recorded as unsigned context. We never use the strong cryptographic facts to imply that the verdict itself is proven.

The verdicts

By design the verdict biases toward inconclusive, and no single pixel signal can produce a manipulation verdict. At least two distinct corroborating signals must agree.

The signals (method cards)

Content Credentials (C2PA)

ChronoVerify is a C2PA Conformant Validator, listed on the C2PA Conforming Products List as record 019f8a20-6452-7a43-b11b-59d0b0e4a84a: specification 2.2, conformance program version 0.1, covering validation of image/jpeg, image/png, image/webp and image/avif. The listing applies to the validation described in this section and to nothing else on this page. It is not a certification, an audit, or an endorsement of any verdict. Check the record or read the full scope.

For signed images, the embedded C2PA manifest is read and its signature is cryptographically validated against the official C2PA and CAI trust lists. A signer is reported as validated only when its certificate chains to a recognized trust anchor; a self-signed or untrusted credential is reported as present but unverified, and a tampered one as failed. This is the strongest available provenance signal. It establishes who signed the image and what they asserted about how it was made, which is not the same as establishing that the image is a camera photograph: a validated credential can itself declare that the image was generated by AI, and ChronoVerify reports that declaration rather than reading it as evidence of authenticity.

Failure modes: most images in the wild are unsigned, so this fires rarely. Manifests are routinely stripped by social platforms, screenshots, and re-saves. A valid manifest proves what the signer asserted, not that the depicted scene is true, and the absence of a credential is not evidence of anything.

EXIF / XMP internal consistency

We extract embedded metadata (capture time, device, GPS, software) and check whether its parts agree with each other. A real internal contradiction (for example a last-saved time earlier than the capture time) is flagged.

Failure modes: timestamps and GPS can be edited, so we report internal consistency only, never the truth of an embedded value. A later save time or an editor software tag is reported neutrally: re-saving and normal editing are common and are not evidence that content changed.

Error Level Analysis (localized)

The image is re-saved at a known quality and compared to itself. We look for a local region whose re-save error is far above the rest of the frame, which can indicate a region with a different compression history.

Failure modes: ELA is contested and unreliable on its own. It mislabels altered and authentic images in both directions, is meaningful mainly on JPEGs, and degrades on recompressed, resized, or screenshotted images. We use it only as one corroborating signal and down-weight it accordingly.

Noise dispersion

A natural photo has fairly consistent sensor-noise texture. We measure how much local high-frequency energy varies across the frame; strongly inconsistent regions can corroborate a spliced area.

Failure modes: probabilistic and noisy on heavily compressed or low-texture images. We use it for corroboration only; it is never decisive alone.

JPEG quality estimate

Estimated last-saved quality from the quantization tables, reported for context.

We report this neutrally: a low quality or evidence of re-saving is normal and is not evidence of content change.

Limits of use

ChronoVerify returns investigative triage and risk scoring. It is not courtroom proof and should never be a sole basis for an automated decision such as rejecting a claim, a listing, or an account. Route flagged results to human review.

Current status

As of this build: a deterministic verification pipeline and a signed-PDF report (Ed25519 over the canonical verdict, with an optional RFC 3161 trusted timestamp) are working. Cryptographic C2PA validation is active: embedded Content Credentials are checked against the official C2PA and CAI trust lists, a signer is confirmed only when it chains to a recognized trust anchor, and validation fails closed. A regression benchmark is in place: on a synthetic corpus the tool flags no authentic or normally re-saved image and returns "inconclusive" only on provenance-stripped images. Synthetic error-level analysis is not representative of real photos, so that set only checks that the verdict logic does not over-fire; it is not a detection measurement. Detection is demonstrated on real edits instead: the benchmark and calibration report documents how it is measured (no authentic capture in the corpus wrongly flagged, no known edit cleared as authentic, and most edits read as inconclusive for a human to review).

Not done yet: an independently reproduced, third-party benchmark is still pending, and no comparative claim against any competitor is made before a shared-input test exists; SOC 2 Type II and ISO 27001 are not held; a self-hosted deployment is planned.

API

One endpoint does the work: send an image, get the verdict object back as JSON. The public endpoint is keyless and free, rate limited per IP. For programmatic use, send your API key.

Get a key

A free key comes from POST /v1/keys/free with an email field: 100 verifications a month, no card, one key per email. For volume or signed reports, buy credits on the pricing page ($5 minimum). Your key, which starts with cv_live_, is shown once on the welcome screen right after payment. View usage and your credit balance any time on your dashboard. The same key works for every endpoint below.

Official libraries and AI agents

Skip the boilerplate with a maintained client, or wire ChronoVerify straight into an AI agent. The key is optional in every case: omit it to use the free, rate-limited public path.

JavaScript and TypeScript (npm: chronoverify):

npm install chronoverify
import { ChronoVerify } from "chronoverify";

const cv = new ChronoVerify("cv_live_...");
const r = await cv.verify({ file: "photo.jpg" });
console.log(r.verdict, r.confidence);

It ships a CLI too: npx chronoverify verify photo.jpg.

Python (PyPI: chronoverify):

pip install chronoverify
from chronoverify import Client

cv = Client("cv_live_...")
r = cv.verify(file="photo.jpg")
print(r["verdict"], r["confidence"])

With a CLI: chronoverify verify photo.jpg.

AI agents (MCP): give Claude Desktop, Cursor, Cline, or any MCP client a verify_image tool (npm: chronoverify-mcp). Add this to your MCP config:

{
  "mcpServers": {
    "chronoverify": {
      "command": "npx",
      "args": ["-y", "chronoverify-mcp"],
      "env": { "CHRONOVERIFY_API_KEY": "cv_live_..." }
    }
  }
}

Then ask the agent to "verify the provenance of this photo." It can pass a URL, a local file path, or base64 image bytes.

Prefer raw HTTP? The quickstart, with a verdict decision table for pipelines, lives in the developer docs; the full machine-readable spec is at /openapi.json. Plain curl, Python, and Node examples follow.

Verify an image

curl -X POST https://chronoverify.com/v1/verify \
  -H "Authorization: Bearer cv_live_..." \
  -F "file=@photo.jpg"

Or point it at an image already online:

curl -X POST https://chronoverify.com/v1/verify \
  -H "Authorization: Bearer cv_live_..." \
  -F "url=https://example.com/photo.jpg"

The url must be a direct link to an image. Redirects and hosts that block automated fetches are not followed (an SSRF safeguard), so if a URL fails with a 400, upload the file directly instead.

Shareable verdict links (opt-in)

curl -X POST https://chronoverify.com/v1/verify \
  -H "Authorization: Bearer cv_live_..." \
  -F "file=@photo.jpg" \
  -F "permalink=true"

With permalink=true the verdict (never the image) is also stored, and the response gains a permalink field with a shareable URL. Links are unlisted and never indexed: anyone with the link can view the verdict at /v/{id}, or fetch the raw record at /v/{id}.json. Links created with an API key do not expire; links from the free verifier expire after 90 days. Nothing is stored unless you opt in.

Python

import requests

r = requests.post(
    "https://chronoverify.com/v1/verify",
    headers={"Authorization": "Bearer cv_live_..."},
    files={"file": open("photo.jpg", "rb")},
)
data = r.json()
print(data["verdict"], data["confidence"])

Node.js

import fs from "node:fs";

const fd = new FormData();
fd.append("file", new Blob([fs.readFileSync("photo.jpg")]), "photo.jpg");

const res = await fetch("https://chronoverify.com/v1/verify", {
  method: "POST",
  headers: { Authorization: "Bearer cv_live_..." },
  body: fd,
});
const data = await res.json();
console.log(data.verdict, data.confidence);

The response

You get one JSON verdict object, the exact data the browser verifier renders. Abbreviated:

{
  "schema_version": "v1",
  "request_id": "9f2c1a7e4b...",
  "verdict": "consistent",
  "confidence": 61,
  "headline": "Metadata is internally consistent. No manipulation signals fired.",
  "summary": "The image carries metadata that is internally consistent and no manipulation signals fired...",
  "capture_time":     { "value": "2026-05-18T14:32:10", "source": "exif" },
  "capture_location": { "present": true, "lat": 37.7749, "lon": -122.4194,
                        "place": "San Francisco, California, United States",
                        "source": "exif" },
  "capture_device":   { "make": "Apple", "model": "iPhone 15 Pro", "source": "exif" },
  "c2pa":             { "present": false, "validated": null, "validation_state": null,
                        "signature_valid": null, "trust_list_match": null, "signer": null,
                        "note": "No Content Credentials (C2PA) found." },
  "metadata_consistency": { "exif_present": true, "xmp_present": false,
                            "checks": [ ... ], "anomalies": [] },
  "pixel_forensics":  { "estimated_jpeg_quality": 98, "ela_mean": 0.18, "ela_max": 7.4,
                        "ela_localization": 9.2, "noise_dispersion": 0.31 },
  "signals": [ { "name": "exif_present", "layer": "metadata",
                 "direction": "supports_authentic", "weight": 0.2,
                 "detail": "EXIF metadata is present and was parsed." } ],
  "integrity": { "sha256": "c255...ff53", "sha512": "0a1b...e9", "format": "JPEG",
                 "width": 1200, "height": 800, "bytes": 52083,
                 "processed_at_utc": "2026-05-18T20:15:04+00:00",
                 "pipeline_version": "chronoverify-pipeline-0.1.0",
                 "method_version": "fusion-0.1.0", "forensics_input": "original",
                 "c2pa_validator_enabled": true },
  "limits": "Investigative triage and risk scoring, not courtroom proof.",
  "processing_ms": 142
}

The signals array is the auditable trail: every check that moved the score, each with a layer (provenance, metadata, or pixel), a direction (supports_authentic, supports_edited, or neutral), a weight, and a human-readable detail. Branch your logic on verdict, and use confidence (0 to 100) for your own thresholds. The /openapi.json schema is the canonical contract; this example is abbreviated.

The c2pa object reports the cryptographic validation result. validation_state is Trusted (signature and content binding intact and the signer chains to a root on the official C2PA or CAI trust list), Valid (cryptographically intact but the signer is not on a trust list), or Invalid (tampered or broken). validated is true only when the state is Trusted; signature_valid is true for both Trusted and Valid; trust_list_match mirrors the trusted signer; and signer carries the issuer and common name when present. Validation fails closed: any error leaves these unverified.

Verdict values

verdictwhat it means
provenance_confirmedC2PA Content Credentials were found and validated against the official trust list, so the signer is trusted. The strongest result.
consistentMetadata is internally consistent and no manipulation signals fired. Not proof of authenticity.
inconclusiveToo little signal to judge. Common for screenshots and images stripped of metadata.
metadata_anomalyAn internal contradiction in the metadata, for example a save time that precedes the capture time.
manipulation_indicatedPixel-forensic or metadata signals point to likely editing. Send to human review.

Status codes

codemeaning
200Verdict returned.
400No image provided, or the supplied URL could not be fetched.
401Missing or invalid API key.
402Out of monthly quota and prepaid credit. Top up on the pricing page.
413Image exceeds 25 MB or the pixel-dimension limit.
415File is not a readable image, or the format is unsupported.
429Rate limit exceeded. Back off and retry.

Headers, limits, and billing

Signed PDF report

For a shareable, independently verifiable audit artifact, call /v1/report with the same key. It returns a signed PDF carrying the capture time, device, location, the signals, and the file hashes. The PDF embeds an Ed25519 signature over a file-derived view of the verdict, with an optional RFC 3161 trusted timestamp; verify it against the public key published at /v1/key. A signed report is a premium unit at $2.00 each from a prepaid balance, or included in a monthly subscription's quota, so it is the chain-of-custody record for audits and review files.

curl -X POST https://chronoverify.com/v1/report \
  -H "Authorization: Bearer cv_live_..." \
  -F "file=@photo.jpg" -o report.pdf

Interactive reference

An interactive API explorer lives at /docs, with the machine-readable schema at /openapi.json and a versioned JSON Schema of the verify response at /v1/verify.schema.json. The verdict you get from the API is the same object that renders in the public verifier, so the homepage doubles as a live example.

See also

Wire ChronoVerify into your stack from the integrations hub (MCP, OpenAI, Claude, LangChain, LlamaIndex, CrewAI, n8n, and REST), add the one-click MCP server, browse use cases by vertical, or look up a term in the glossary.