Quick start
Send the image file in the image field of a multipart/form-data request. No key is needed.
curl
curl -sS https://wonjun12-aeye-backend.hf.space/api/analyze \
-F "image=@photo.jpg;filename=image.jpg"
Python (requests)
import time
import requests
BASE = "https://wonjun12-aeye-backend.hf.space"
def analyze(path, api_key=None):
headers = {"X-API-Key": api_key} if api_key else {}
for attempt in range(2):
with open(path, "rb") as f:
# the file name can affect the verdict, so send a neutral one
r = requests.post(f"{BASE}/api/analyze", headers=headers,
files={"image": ("image.jpg", f)}, timeout=120)
wait = int(r.headers.get("Retry-After", "0"))
if r.status_code in (429, 503) and attempt == 0 and wait <= 60:
time.sleep(wait) # per-minute limit or a full line: wait as told, try once more
continue
r.raise_for_status()
return r.json()
result = analyze("photo.jpg")
print(result["display_band"], result["display_score"])
Read the verdict from display_band (see Reading the verdict).
Keys
Today the API can be called without a key; such requests are counted per IP address. With a key, requests are counted for that key. Send the key in either header:
X-API-Key: <key>
Authorization: Bearer <key>
- A key that is not registered gets
401 Invalid API key. The request is not handled as a keyless one instead. - When the server is set to require a key, a request without one gets
401 API key required (X-API-Key header). - The server stores only a hash of each key, so a lost key cannot be looked up. Ask for a new one.
- Keep the key out of app and web page code. Send it from your server only.
Getting a key
Where to ask: oscar43@naver.com. Tell us the service name, the expected requests per day and whether you send your users’ photos.
If your service sends its users’ photos to A-EYE, you must tell those users that their photos are sent to the A-EYE analysis server (processing by a third party and transfer abroad). How A-EYE handles them is in the privacy policy. A key is issued after we have confirmed that you tell your users this and that the use is for evaluation or research.
Rate limits
| Caller | Per minute | Per day | Counted per |
|---|---|---|---|
| No key | 20 | 300 | IP address |
| With a key | 10 | 1,000 | Key |
A key is counted on its own, never together with others on the same IP address, and allows 1,000 requests a day. Its per-minute limit is 10 because one free server analyzes one image at a time and is shared with A-EYE app users.
- A minute is 60 seconds from the first request of the window, and a day is 24 hours (86,400 seconds) from the first request of its window. Nothing resets at midnight.
POST /api/analyzeandPOST /predictshare one count. A request that passes the key check counts once even if it then fails as a bad upload (400, 413, 415, 422) or a full line (503). Requests stopped at the Receive step (401, and the 400 or 413 decided from theContent-Lengthheader alone) and 429s do not count. GET endpoints do not count.- Counted responses carry
X-RateLimit-Remaining, the requests left this minute. A429carriesRetry-After, the seconds until its window ends. - Images are analyzed one at a time. Up to 8 requests can wait for their turn; one more gets an immediate
503(Retry-After: 2). A waiting request whose turn does not come within 30 seconds also gets a503(Retry-After: 30).
POST/api/analyze
Request
multipart/form-datawith exactly one file field, namedimage. Any other name gets a422.- Formats: JPEG, PNG, WebP, decided from the first bytes of the file, not from its extension or
Content-Type. HEIC, GIF and animated PNG or WebP get a415. - Size: files of at most 29,360,128 bytes (28 MiB). Pixels (width × height): at most 64,000,000 for JPEG and 32,000,000 for PNG and WebP.
- Send the file as you received it. C2PA and metadata records inside the file are part of the verdict, and saving or compressing it again weakens those records and traces.
- The server also reads the uploaded file name. A name a generator gives its exports, or a screenshot’s name, can change the result, so if you do not need the original name, send a neutral one such as
image.jpg.
Response fields
Build only on the fields below. The response carries more fields than this table; they are internal diagnostics whose names and values can change or disappear without notice. New fields can be added, so ignore fields you do not know.
| Field | Value | Meaning |
|---|---|---|
display_band | "real", "uncertain", "ai" | The final three-way verdict, the same as the app’s Likely real, Inconclusive and AI suspected. Decide on this value. |
display_score | Integer, out of 100 | The AI detection score the app shows, and the one number to show people. It is not score multiplied by 100: it is always redrawn to agree with display_band. Higher means closer to an AI-generated image. It is not a rate of correct answers. |
verdict | "REAL", "AI" | The two-way verdict. Today the server sends only these two values and never a third (an inconclusive result is told apart by display_band alone). Inconclusive results also come as "REAL", so this value alone cannot tell you a result was inconclusive. |
score | Decimal from zero to one | The score after file records and the server’s adjustments. Closer to one means closer to AI, but it can be close to one even for Inconclusive and Likely real results, so do not show it to people as it is. The verdict’s thresholds are not guaranteed and change from model to model, so do not set your own threshold on it; use display_band. |
basis | String | What the verdict rests on: "model" is the image analysis, "provenance" an AI declaration in a C2PA manifest inside the image (its signature is not verified), "provenance_hint" a real-looking photo lifted to Inconclusive by an unsigned record (a metadata mark or a generator’s file name), "local_edit" a suspected partial edit. Other values can appear. Use it for explanations only. |
partial_edit_suspect | true, false | A sign that part of a real photo may have been edited. When true, display_band is "uncertain" and basis is "local_edit". |
heatmap_b64 | PNG, base64 | When verdict is "AI" or partial_edit_suspect is true: a heatmap of suspicious areas, at most 768 px on the long side. Otherwise (verdict is not "AI" and partial_edit_suspect is false) an empty string; show the image you sent instead. |
overlay_b64 | PNG, base64 | Under the same condition, the original with the suspicious areas marked. Otherwise the original scaled down, which may become an empty string later. |
heatmap_style | "spotlight", "jet" | How overlay_b64 was drawn: spotlight darkens everything outside the suspicious areas and outlines them, jet lays a color heatmap over the image. |
input_kind | "photo", "screen_capture", "unknown" | What kind of input the server took the file for. A hint computed after the verdict; when the feature is off on the server, the field is absent. |
model_version | String | The name of the model that gave the verdict. Its format, including any suffix, can change. If you store results, store it with them. |
elapsed_ms | Integer | Milliseconds the analysis took inside the server, not counting time spent waiting in line. |
The image fields have no data: prefix. To show one, put data:image/png;base64, in front of it.
Example response (illustrative values, images shortened)
{
"display_band": "uncertain",
"display_score": 49,
"verdict": "REAL",
"score": 0.91,
"basis": "model",
"partial_edit_suspect": false,
"heatmap_b64": "iVBORw0KGgo…",
"overlay_b64": "iVBORw0KGgo…",
"heatmap_style": "spotlight",
"input_kind": "photo",
"model_version": "best91(v6)+best91_loc",
"elapsed_ms": 3140,
…
}
In this example the verdict is Inconclusive and the number to show is 49. verdict says "REAL" and score is 0.91, so reading either one alone gets the verdict wrong.
Reading the verdict
display_band is the verdict and basis adds why. This is how the A-EYE app shows them.
display_band | The app says | Worth adding |
|---|---|---|
"real" | Likely real | Signs commonly found in AI-generated images were weak or absent. It is not proof that the image is real. |
"uncertain" | Inconclusive, or Edit suspected when basis is "local_edit" | Hard to call either way; it is not an error. Real photos that were forwarded in messengers or compressed several times come out this way too. When basis is "provenance_hint", the photo itself looks real but an unsigned AI record (a metadata mark or a file name) held the verdict back. |
"ai" | AI suspected, or a result from a C2PA record in the file when basis is "provenance" | "provenance" means a C2PA manifest inside the image declares AI generation. The server does not verify the record’s signature. An unsigned metadata mark or file name alone never gives "ai". |
POST/predict
A compatibility endpoint that keeps the response format older app versions used. For a new integration, use /api/analyze: this response has no display_band, so it cannot tell you a result was inconclusive.
- The request is the same as for
/api/analyze: the sameimagefield, the same size limits, the same count. fake_probisscoremultiplied by 100, andreal_probis 100 minusfake_prob. Do not showfake_probto people as a score: it cannot tell an inconclusive result and differs fromdisplay_score.verdict,heatmap_b64,overlay_b64,heatmap_style,model_versionandinput_kindare as in/api/analyze. Other fields are internal diagnostics.- One error differs: when the analysis itself fails, the answer is not a
422but a200with{"error": "Invalid image"}. On a200, look for anerrorkey first. - Empty, broken, unsupported and oversized files are refused exactly as on
/api/analyze, with400,413and415. A missingimagefield also gets the same422with a list indetail.
GET/api/health
200 with {"status": "ok"} means the server has finished starting and has loaded its models. The server only starts taking requests once the models are loaded, so while it starts this endpoint does not answer either. There is no other status value, and it does not tell you whether the line is full. It does not count toward the rate limit.
GET/api/version
Returns the name of the model now giving verdicts as model_version. The other fields that come with it are for operations. It does not count toward the rate limit.
{"model_version": "best91(v6)+best91_loc", …}
Errors
An error body is {"detail": "…"} with a string in detail. There are two exceptions: a 422 for a malformed request (a FastAPI validation error) has a list in detail, and an analysis failure on /predict is the 200 answer described above.
| Code | detail | Meaning and what to do |
|---|---|---|
400 | Empty upload | The file is empty. |
400 | Invalid or corrupt image | The format marker is right but the image cannot be opened. Check whether the file is broken. |
400 | Malformed Content-Length | The Content-Length header is not a number. Let your HTTP library set it. |
401 | Invalid API key | The key is not registered. |
401 | API key required (X-API-Key header) | The server requires a key and none was sent. |
404 | Not found | No such address, called with GET. Calling an upload endpoint with GET, such as GET /api/analyze, also ends here. |
405 | Method Not Allowed | The address is wrong (a trailing / included) or the method is. If the Allow header says GET, that address takes no POST: check its spelling and any trailing /. If it says POST, switch the method to POST. |
413 | Upload larger than 29360128 bytes | The file is larger than 28 MiB. |
413 | Image has too many pixels (6000x6000); limit for PNG is 32000000 | Too many pixels. The parentheses hold the size you sent; the limit is 64000000 for JPEG and 32000000 for PNG and WEBP. Lower the resolution. |
413 | Image has too many pixels; limit is 32000000 | The size written in the file header is so large that the file was refused before it was opened (no size is given here, and the limit depends on the format). Lower the resolution. |
415 | Only JPEG, PNG and WebP images are accepted | Unsupported format. Convert HEIC and other formats to JPEG first. |
415 | Animated images are not supported | An animated PNG or WebP. |
415 | Image structure not supported (more than 256 JPEG APPn/COM segments) | A file with far too many metadata segments or a broken structure; the reason in brackets varies by file. Save (export) the photo again and send it. |
422 | A list (below) | The image field is missing. Check the field name. |
422 | Image is too small (20x20); the shorter side must be at least 32 px | The shorter side must be at least 32 px for the image to be analyzed. |
422 | Image is too narrow (4000x32); the long side may be at most 100 times the short side | Images whose long side is more than 100 times the short side are not analyzed. |
422 | Image could not be analysed (ref 665a47c3) | The file passed every check but the analysis failed. Sending the same file again will most likely fail the same way; tell us the ref value. Unlike the size and shape 422s above, this one carries a ref. |
429 | Too many requests | Per-minute limit reached. Send again after Retry-After seconds. |
429 | Daily request quota exceeded | Daily limit reached. Retry-After is the seconds until that window ends. |
503 | Server busy, retry shortly | The line was full, or the turn did not come within 30 seconds. Send once more after Retry-After seconds. |
503 | Model not loaded | The models are not ready yet. Retry-After: 10. |
500 | None (not JSON) | An unexpected server error. Tell us the X-Request-Id. |
502 504 | None | An error from the hosting layer in front of the A-EYE server; the body may not be JSON. Send once more after a short wait. |
A 422 for a malformed request looks like this. Whether detail is a string or a list tells it apart from an analysis failure.
{
"detail": [
{
"type": "missing",
"loc": ["body", "image"],
"msg": "Field required",
"input": null
}
]
}
Responses from the A-EYE server almost always carry an X-Request-Id header (8 hexadecimal digits). It is the same value as the ref of an analysis failure; include it when you contact us.
Terms of use
- For evaluation and pilots. Availability and response times are not guaranteed, and there is no service level agreement (SLA).
- Photos are not stored. A photo you send is discarded as soon as the result is returned and is not used for model training.
- When you show a result to others, say it is an estimate. Do not present a verdict as a fact check or as evidence.
- To use the API in a commercial service, ask first. During the pilot period, each request is considered case by case.
Limitations
- The verdict is an estimate. A real photo can be judged AI and an AI image real, and images from newly released generators are the easiest to miss. Do not use a result as the only basis for an important decision such as legal evidence, news reporting or a hiring decision; check the source, the context in which it was taken and the metadata as well.
- It is one free server. One CPU server analyzes one image at a time and is shared with the users of the A-EYE app. Sending many requests at once makes app users get
503s too. If you need to process a large batch, contact us first. Availability is not guaranteed, and the server does not answer while it restarts. - Screenshots often come out Inconclusive. The verdict criteria are set to fit photos taken with a camera, including ones shared in messages. For some PNG images that look like screenshots, a server-side screenshot step can move an Inconclusive result down to Likely real;
scorethen stays on the photo scale. Followdisplay_band. A Likely real screenshot does not mean the messages or amounts it shows are true. - It cannot be called from a browser directly. Browser code on another website is blocked by the cross-origin (CORS) settings. Call the API from your server.
- Images are not kept. An uploaded image is held in server memory only while it is analyzed, is discarded as soon as the result is returned, and is never used to train models. See the privacy policy.
Contact
API questions: oscar43@naver.com. When reporting an error, include the X-Request-Id and the time. For help with the app, see the support page.