Skip to content

Content provenance (was this AI-generated?)

Check a file for provider provenance signals and read a structured verdict: what was detected, whether it validated, and which model and issuer the manifest names.

Moderation asks is this content harmful. Provenance asks a different question: does this file carry a signal saying a model made it. Use it when accepting user uploads, labelling media, or building an audit trail.

import { checkProvenance } from '@combycode/llm-sdk';
const res = await checkProvenance({
file: bytes,
filename: 'upload.png',
mimeType: 'image/png',
});
for (const s of res.signals) {
console.log(s.kind, s.detected, s.validationState, s.issuer, s.model);
}

Two signal kinds are normalised into one list: C2PA (a signed Content Credentials manifest carrying issuer / model / timestamp) and SynthID (Google’s watermark; for audio it is the only one available).

detected and trusted are different questions, and the difference is the whole point. A C2PA manifest is metadata: anyone can attach one, and it can be stripped, forged, or invalidated by a re-encode. detected means a manifest was found. trusted means its signature validated against a known issuer.

valid is not trusted, and you meet this immediately. An image generated by gpt-image-1 and checked minutes later returns detected: true, validationState: 'valid', issuer: "OpenAI OpCo, LLC", model: 'gpt-image' — and trusted: false, because the issuer is not on the checker’s trust list. For most uses the signal you actually want is detected && validationState === 'valid', not trusted alone.

Nothing detected says nothing at all. Signals are routinely lost to a screenshot, a crop, or a re-encode. Absence is absence of evidence, not evidence of human authorship — no policy should be built as though it were.

OptionTypeRequiredDescription
fileUint8ArrayYesThe bytes to check.
filenamestringYesWith an extension — the API uses it to pick a decoder.
mimeTypestringYese.g. image/png, audio/wav.
apiKeystringNoFalls back to engine.apiKeys.openai.
engineEngineHandleNoShare an existing engine instance.

ProvenanceCheckResult carries detected, trusted, and signals[] (kind, detected, validationState, issuer, model, generatedAt).

import { readFileSync } from 'node:fs';
import { checkProvenance } from '@combycode/llm-sdk';

// "Was this file made by a model?" — same shape as moderate(): bytes in, structured
// verdict out. C2PA (a signed manifest) and SynthID (a watermark) are normalised into one
// `signals` list, so you read the answer the same way regardless of which was found.
const t0 = performance.now();
const r = await checkProvenance({
  file: readFileSync('../../official-samples/_fixtures/ai-image.png'),
  filename: 'ai-image.png',
  mimeType: 'image/png',
  // Which backend runs the check. OpenAI is the only one today and the default;
  // naming it keeps the call honest the day that changes.
  provider: 'openai',
  apiKey: process.env.LLM_API_KEY,
});

const c2pa = r.signals.find((s) => s.kind === 'c2pa');
console.log(
  JSON.stringify({
    // `detected` is the signal; `trusted` is the stronger claim, and they differ in
    // practice — an OpenAI image validates as `valid` but is not on the trust list.
    result: `${c2pa?.detected ? 'detected' : 'not_detected'}/${c2pa?.validationState ?? 'none'}`,
    issuer: c2pa?.issuer ?? null,
    model: c2pa?.model ?? null,
    trusted: r.trusted,
    ms: Math.round(performance.now() - t0),
  }),
);

OpenAI is the only tracked SDK that ships a provenance endpoint at all; Anthropic, Google, xAI and OpenRouter have none. The call itself is a multipart upload to /v1/content_provenance_checks, and the raw response splits results by type — checkProvenance() normalises them into one signals list and separates detected from trusted so the weaker claim cannot be mistaken for the stronger.

The call is free, and still recorded. No tokens are billed, but an onCostEntry is emitted with an honest zero so the ledger can distinguish “free” from “never happened”.

Non-OpenAI providers throw immediately, naming the provider — there is no silent fallback, because a provenance check that quietly did nothing is worse than an error.

Next steps:

  • Moderation — classifying content as harmful, a different question
  • Image generation — producing the media whose provenance this reads