A-EYE

Send one image, get the verdict back as JSON

How to call the A-EYE analysis server from your own service. It is the same server, and the same verdict, as the A-EYE app. You can try it right away without a key. If you send more than 300 requests a day, or call it from a server that shares its IP address with other services (such as a cloud function), ask for a key.

Server https://wonjun12-aeye-backend.hf.space · Based on the server code of October 15, 2026

  1. Receive

    Checks the size the request declares and the API key.

    Stops with 400 401 413

  2. Count

    Counts requests per minute and per day, per key or, without a key, per IP. From here on a request counts.

    Stops with 429

  3. Check

    Looks at the image field, the file format, the bytes and the pixels.

    Stops with 400 413 415 422

  4. Wait

    One image is analyzed at a time. Up to 8 requests wait in line, each for at most 30 seconds.

    Stops with 503

  5. Analyze

    Returns the verdict and the images of suspicious areas. A failure during analysis is a 422.

    Ends with 200

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

CallerPer minutePer dayCounted per
No key20300IP address
With a key101,000Key

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/analyze and POST /predict share 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 the Content-Length header alone) and 429s do not count. GET endpoints do not count.
  • Counted responses carry X-RateLimit-Remaining, the requests left this minute. A 429 carries Retry-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 a 503 (Retry-After: 30).

POST/api/analyze

Request

  • multipart/form-data with exactly one file field, named image. Any other name gets a 422.
  • 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 a 415.
  • 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.

FieldValueMeaning
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_scoreInteger, out of 100The 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.
scoreDecimal from zero to oneThe 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.
basisStringWhat 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_suspecttrue, falseA sign that part of a real photo may have been edited. When true, display_band is "uncertain" and basis is "local_edit".
heatmap_b64PNG, base64When 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_b64PNG, base64Under 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_versionStringThe name of the model that gave the verdict. Its format, including any suffix, can change. If you store results, store it with them.
elapsed_msIntegerMilliseconds 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_bandThe app saysWorth adding
"real"Likely realSigns 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 same image field, the same size limits, the same count.
  • fake_prob is score multiplied by 100, and real_prob is 100 minus fake_prob. Do not show fake_prob to people as a score: it cannot tell an inconclusive result and differs from display_score. verdict, heatmap_b64, overlay_b64, heatmap_style, model_version and input_kind are as in /api/analyze. Other fields are internal diagnostics.
  • One error differs: when the analysis itself fails, the answer is not a 422 but a 200 with {"error": "Invalid image"}. On a 200, look for an error key first.
  • Empty, broken, unsupported and oversized files are refused exactly as on /api/analyze, with 400, 413 and 415. A missing image field also gets the same 422 with a list in detail.

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.

CodedetailMeaning and what to do
400Empty uploadThe file is empty.
400Invalid or corrupt imageThe format marker is right but the image cannot be opened. Check whether the file is broken.
400Malformed Content-LengthThe Content-Length header is not a number. Let your HTTP library set it.
401Invalid API keyThe key is not registered.
401API key required (X-API-Key header)The server requires a key and none was sent.
404Not foundNo such address, called with GET. Calling an upload endpoint with GET, such as GET /api/analyze, also ends here.
405Method Not AllowedThe 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.
413Upload larger than 29360128 bytesThe file is larger than 28 MiB.
413Image has too many pixels (6000x6000); limit for PNG is 32000000Too many pixels. The parentheses hold the size you sent; the limit is 64000000 for JPEG and 32000000 for PNG and WEBP. Lower the resolution.
413Image has too many pixels; limit is 32000000The 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.
415Only JPEG, PNG and WebP images are acceptedUnsupported format. Convert HEIC and other formats to JPEG first.
415Animated images are not supportedAn animated PNG or WebP.
415Image 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.
422A list (below)The image field is missing. Check the field name.
422Image is too small (20x20); the shorter side must be at least 32 pxThe shorter side must be at least 32 px for the image to be analyzed.
422Image is too narrow (4000x32); the long side may be at most 100 times the short sideImages whose long side is more than 100 times the short side are not analyzed.
422Image 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.
429Too many requestsPer-minute limit reached. Send again after Retry-After seconds.
429Daily request quota exceededDaily limit reached. Retry-After is the seconds until that window ends.
503Server busy, retry shortlyThe line was full, or the turn did not come within 30 seconds. Send once more after Retry-After seconds.
503Model not loadedThe models are not ready yet. Retry-After: 10.
500None (not JSON)An unexpected server error. Tell us the X-Request-Id.
502 504NoneAn 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; score then stays on the photo scale. Follow display_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.