Check an image
Send an image by URL, as base64 or as an upload, and read the verdict, the score and the three lanes behind it.
Send an image by URL
curl -X POST "https://realpng.com/api/v1/audit" \
-H "X-API-Key: $REALPNG_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"image_url": "https://images.example.com/beach.jpg"
}'{
"verdict": "ai",
"score": 91,
"decided_by": "pixels",
"lanes": {
"pixels": {
"verdict": "ai",
"score": 91
},
"watermark": {
"verdict": "none",
"score": 3
},
"provenance": {
"verdict": "nothing",
"issuer": null
}
},
"media": "image"
}realpng fetches the URL itself, so it must be a public http or https address. Redirects are followed. A private address, a host that is slow to answer or a URL that answers with an error comes back as a 400, listed in errors.
Each call sends exactly one of image_url, image_base64 or image_upload_id. It needs an API key and spends credits from your plan.
Send a small file as base64
curl -X POST "https://realpng.com/api/v1/audit" \
-H "X-API-Key: $REALPNG_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"image_base64": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAIAAAD91JpzAAAAF0lEQVR4nGM4UREQteASg0bUtGd3TgAALTYG/9S1V3UAAAAASUVORK5CYII="
}'Send the bytes as plain base64 or as a data URL. The data: prefix is removed before decoding.
Base64 travels inside the request, so its size limit is lower than for a URL or an upload. Send anything bigger as an upload.
Send an uploaded file
curl -X POST "https://realpng.com/api/v1/audit" \
-H "X-API-Key: $REALPNG_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"image_upload_id": "5d1f7a3c-2b8e-4f6a-9c0d-8e3b1a7f4c62"
}'Any file on disk can go through POST /uploads first. You PUT the bytes to a one-time URL, then send its id as image_upload_id. Upload a local file covers the full flow, including what a retry needs.
Read the verdict
A 200 carries these fields. Agents get the same body from the audit_image tool. See MCP tools.
| Field | Type | What it holds |
|---|---|---|
verdict | string | ai, real, uncertain or unavailable. The field to act on |
score | integer or null | 0 to 100, higher reads more like AI. Null only when the verdict is unavailable |
decided_by | string | Which evidence set the verdict |
lanes | object | What each lane found on its own: pixels, watermark and provenance |
media | string | image, or video for a video |
detail | object | Only with detail=full. See get the full engine report |
Verdict
| Value | Meaning | What to do |
|---|---|---|
ai | The image reads as AI generated, or carries proof that it is | Handle it as AI generated |
real | The image reads as a photograph, or carries a signed camera capture | Handle it as real, within the limits |
uncertain | The evidence sits in between, or proof points both ways | Have a person look, or treat it as unknown |
unavailable | No lane found proof and the pixel lane could not read the image | Retry later |
Score
The score orders images by how strongly they read as AI, from 0 to 100. Use it to sort or rank, such as reviewing the highest first.
Branch on verdict, not on the score. The cut points between real, uncertain and ai are internal and may change, so a threshold you pick today can drift. When proof decides, the score is pinned to a set value whatever the pixels said, as the next table shows.
What decided it
decided_by | When | Verdict and score |
|---|---|---|
watermark | A SynthID watermark was found | ai, score pinned near 100 |
provenance | A valid signed C2PA manifest was found | ai near 100 for signed_ai, real near 0 for signed_capture |
pixels | No proof and nothing to weigh. Also the value when the verdict is unavailable | The pixel lane's own verdict and score |
weighed | No proof, but an unsigned AI label or a faint watermark raised the pixel score | The raised score, which never reaches either end, and the verdict it falls in |
conflict | AI proof and camera proof at once, such as a watermark on a signed capture | uncertain, score in the middle |
A watermark decides even when the pixels are unsure:
{
"verdict": "ai",
"score": 99,
"decided_by": "watermark",
"lanes": {
"pixels": { "verdict": "uncertain", "score": 38 },
"watermark": { "verdict": "present", "score": 96 },
"provenance": { "verdict": "nothing", "issuer": null }
}
}Read the lanes
Each lane keeps its own vocabulary, so the watermark lane reads present or none, never ai or real.
Pixels
A panel of trained pixel detectors reads the image. The lane reads pixels only and knows nothing about who made the file.
It averages the detectors on several overlapping crops and takes the median crop, so a caption or sticker over one part does not decide the verdict. An image too small to crop is read whole.
verdict | Meaning |
|---|---|
ai | The pixels read as AI generated |
real | The pixels read as a photograph |
uncertain | The pixels sit in between |
unavailable | Too few detectors returned a reading, and score is null |
score is 0 to 100, or null.
What it cannot see. Detection is measured on original files. An ordinary JPEG re-encode makes it markedly weaker, so screenshots, social media copies and recompressed files are caught less often. Generators the panel was not tuned on are caught less reliably.
Watermark
The lane looks for SynthID, the invisible watermark Google embeds in images from its generators.
verdict | Meaning |
|---|---|
present | SynthID was found. This is proof of AI and decides the verdict |
faint | A weak trace. It raises the score but never decides on its own |
none | No SynthID was found. This does not mean the image is real |
unavailable | The watermark model did not run, and score is null |
score is the watermark model's own reading, 0 to 100, or null.
What it cannot see. Any watermark other than SynthID. An image from a generator that does not embed SynthID reads none.
Provenance
The lane reads the file's metadata, not its pixels. It uses no model and always answers, so it has no unavailable value.
verdict | Meaning | issuer |
|---|---|---|
signed_ai | A C2PA manifest that validates, from a known AI issuer or declaring an AI source. Proof of AI | The signer, such as OpenAI |
signed_capture | A C2PA manifest that validates and records a digital capture, such as from a signing camera. Proof of a real capture | The signer |
ai_label | An unsigned AI label: an AI digital source type in IPTC XMP, an unverified C2PA AI action, or generator settings in PNG text | null |
nothing | None of the above | null |
An ai_label raises the score but never decides on its own. Anyone can add or remove unsigned metadata, so only a signature counts as proof. Stray bytes that happen to spell a generator's name are ignored.
What it cannot see. Most re-encodes, screenshots and social platforms strip metadata, so nothing is common and says nothing either way. A missing signature is not evidence of AI.
Handle an unavailable verdict
The verdict is unavailable when no lane found proof and the pixel lane could not read the image. With a watermark present or a signed manifest, the verdict is still decided even if the pixel lane is unavailable.
{
"verdict": "unavailable",
"score": null,
"decided_by": "pixels",
"lanes": {
"pixels": { "verdict": "unavailable", "score": null },
"watermark": { "verdict": "none", "score": 4 },
"provenance": { "verdict": "nothing", "issuer": null }
}
}It is a normal 200, not an error, and it is charged as a completed check. Retry the same image later. The full engine report names the detector that dropped out.
The watermark lane can also answer unavailable on its own. The verdict then comes from the other lanes as usual.
An engine that is still warming up is different. It answers 503 before any lane runs, as described in errors.
Get the full engine report
curl -X POST "https://realpng.com/api/v1/audit?detail=full" \
-H "X-API-Key: $REALPNG_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"image_url": "https://images.example.com/beach.jpg"
}'Add detail=full to the query to get detail beside the usual fields. It holds the raw engine report: each detector's own reading, the evidence per lane, the decoded image's size and format, and how long the engine took (engine_ms).
The shape of detail can change without notice. Read verdict, score, decided_by and lanes
in code, and keep detail for debugging and support.
Know what a check cannot tell you
- Only
watermark,provenanceandconflictindecided_byrest on proof. Apixelsorweighedverdict is a statistical reading and can be wrong. - A
realverdict from the pixels means the image does not read as AI. It does not prove a camera took it. - Each lane has blind spots of its own. Recompressed copies weaken the pixels, the watermark lane sees SynthID only, and most platforms strip metadata. Read the lanes has the detail.
Handle errors
Every error comes back as JSON with a stable code. Errors lists each one with its status and fix.
Next steps
Credits and limits
What a check and an optimization cost, when credits come back and reset, each plan's rate limits and jobs at once, and the limits on images and videos.
Optimize an image
Start an optimization, follow the job by polling or with a signed webhook, and download the optimized image with its optimization score.