HTTP API

Two public endpoints: POST /api/scan (detect AI-writing signals) and POST /api/cleanup (strip machine typography, no rewriting). Base URL https://api.scanforai.com. Anonymous works; an API key raises your limits and saves history. The score is a signal, not a verdict.

Interactive docs & Postman. Live, always-accurate reference with a "try it" console: Swagger UI. Machine-readable spec: openapi.json — paste that URL straight into Postman or Insomnia (Import → Link) to get every endpoint and parameter as a ready collection.

Authentication

Send an API key (minted in the dashboard) as a bearer token — same header on every request:

Authorization: Bearer at_sk_your_key_here

Scripted/API calls need a key — registration is free (no card) and takes a minute. Anonymous access is reserved for the web scanner and the browser extension. Key scopes: full, or read (scan/read only). Your current usage-vs-limits and recent request statuses are on the dashboard's API-keys page.

Connect your AI agent (MCP)

ScanForAI is an MCP server — Claude Code, Cursor, claude.ai custom connectors and any MCP-capable agent can use scan_text and cleanup_text as native tools, no integration code. Point the agent at the endpoint with your API key as a header:

claude mcp add --transport http scanforai https://api.scanforai.com/api/mcp \
    --header "Authorization: Bearer at_sk_your_key_here"

That's the whole setup. Your agent can then score any text it reads or writes (0–100, band, plain-English reasons — a signal, not a verdict) and strip machine typography, under your key's normal limits. Other MCP clients: add the same URL and header wherever the client configures remote MCP servers.

POST /api/scan

Body can be JSON, a form field, or raw text. Quickest taste — pipe a file in, get a human-readable report back:

# plain-text report, straight from a file:
curl -s --data-binary @draft.txt -H 'Authorization: Bearer at_sk_...' \
  "https://api.scanforai.com/api/scan?format=text"

# JSON (a JSON body overrides the query-param knobs):
curl -s https://api.scanforai.com/api/scan \
  -H 'Authorization: Bearer at_sk_...' \
  -H 'Content-Type: application/json' \
  -d '{"text": "...", "include_fixes": true}'

# file upload (multipart; .txt .md .markdown .html .htm .docx .odt .pdf — extracted server-side, 1 MB max):
curl -s -F "file=@essay.docx" -H 'Authorization: Bearer at_sk_...' \
  https://api.scanforai.com/api/scan

Parameters (JSON body):

Response (fields are stable; new ones may be added):

{
  "score": 68.4,            // 0..100, higher = more AI-like — NOT a probability
  "band": "elevated",       // low | moderate | elevated | high
  "label": "strong signs of AI writing",
  "confidence": "high",     // high | moderate | low — how far to trust THIS read
  "confidence_note": "a clear, consistent signal across enough text",
  "model_used": false,      // whether the deeper model was part of this scan
  "word_count": 1240,
  "coverage": {"active_signals": 11, "total_signals": 12, "words": 1240},
  "signals": [ {"name": "sentence_rhythm", "score": 0.82, "weight": 3.0,
                "detail": "sentences are unusually even in length", "value": 0.24}, ... ],
  "fixes": ["vary sentence length: ...", ...],
  "scan_id": null,          // an account + save:true returns the stored id
  "disclaimer": "A signal, not a verdict. ..."
}

Errors: 413 over the character cap, 429 rate-limited (message includes retry seconds), 402 monthly word quota reached, 422 empty text. If the model worker is briefly unavailable the scan still succeeds heuristics-only, with "model_used": false and a "degraded" note.

POST /api/cleanup

Strips machine-typography artifacts and normalizes to plain text without rewriting a word (deterministic).

curl -s https://api.scanforai.com/api/cleanup \
  -H 'Authorization: Bearer at_sk_...' \
  -H 'Content-Type: application/json' \
  -d '{"text": "The results—truly—are “remarkable”…"}'

Parameters: text (required), contractions (bool, default false — also apply everyday contractions), and account-only title / save (saved to history like scans).

{
  "mode": "light",
  "output_text": "The results - truly - are \"remarkable\"...",
  "changes": {"em/en dash": 2, "smart quotes": 2, "ellipsis glyph": 1},
  "total_changes": 5,
  "chars_in": 40, "chars_out": 41,
  "id": null                // an account + save:true returns the stored id
}

Text types — what each one covers

Different kinds of writing are formal, rhythmic or persuasive on purpose, and a detector that ignores that flags good human writing. The type adjusts which signals are allowed to count. Picking the right one gives a fairer score; the safe default is auto. The type used is recorded with every scan, so a disputed result can always say how it was judged.

Picking a type the text is not costs accuracy in both directions — an AI essay scored as fiction, or fiction scored as an essay, is judged by the wrong rules. When unsure, leave auto on.

What happens to an uploaded file

Only the prose is scored — never the formatting. Every upload is reduced to plain text server-side before scanning, deliberately: layout, styling and markup are not writing, and scoring them would let a pretty template move a score. Per type:

After extraction, whitespace is normalized (runs of spaces collapsed, paragraph breaks kept) and your plan’s character limit applies to the extracted text. The upload itself is processed in memory for extraction and is not stored; on saved scans, what is stored is the extracted text, exactly like a paste.

POST /api/injection/image Free account

Reads an image the way a model would and scores how much of the recovered text is phrased as an instruction to a model rather than as content — image prompt injection, with free test images you can try against your own pipeline today. Live now; free with a free account.

curl -s -F "file=@upload.png" \
  -H 'Authorization: Bearer at_sk_...' \
  https://api.scanforai.com/api/injection/image

Parameters (multipart form fields; the same names also work as query parameters, and a raw binary body with an X-Filename header works for scripts):

{
  "instruction_density": 78.0,     // 0-100
  "band": "high",                  // low | moderate | elevated | high | null
  "confidence": "high",            // how sure the reads are, in plain words
  "leading_read": "instruction_text",
  "reads": [
    {
      "name": "instruction_text",
      "score": 0.78,
      "detail": "text in png:tEXt:Comment tells the reader to set aside what it was
                 already told, speaks directly to an AI assistant",
      "value": { "source": "png:tEXt:Comment",
                 "text": "AI assistant: ignore previous instructions and…" },
      "region": null,
      "active": true               // false = this read abstained
    }
  ],
  "image": { "format": "png", "bytes": 855, "width": 320, "height": 240 },
  "layers_read": ["filename", "png"],
  "degraded": null,                // set when something could NOT be read at this
                                   // tier (e.g. printed text without deep=true) —
                                   // always show it; never treat a degraded low
                                   // score as an all-clear
  "abstained": false,
  "disclaimer": "This describes what a model could read in this file…"
}

No verdict field, by design. There is no is_safe, blocked or threat_level to branch on, for the same reason /api/scan has no is_ai: we are not in a position to promise it. band is null when there was too little to read — an abstention, not a low score.

The extracted text comes back defused. It is hostile by definition, so invisible codepoints are escaped and every payload is confined to its own field. If you pipe our JSON into a model of your own, use redact. A scanner that reports an injection by repeating it into your prompt has just delivered it.

Two tiers, metered separately. The light pass runs on every image. The deep pass only runs when the light pass finds something worth opening, so a pipeline scanning clean uploads all day barely touches it. If the deep budget is spent, you get the light result with a degraded note rather than a 429 — the same contract /api/scan has when no model worker answers. Limits get published at launch.

Note the namespace: /api/injection/… answers “what does this file say to a model”. Whether a picture was made by a model is a different question, answered by /api/scan/image below.

POST /api/scan/image Free account

Reads what an image declares about how it was made: C2PA Content Credentials, the IPTC digitalSourceType field, EXIF/XMP generator tags, and the Stable-Diffusion toolchain's PNG prompt block.

curl -s -F "file=@picture.jpg" -H 'Authorization: Bearer at_sk_...' \
  https://api.scanforai.com/api/scan/image

Returns declared (bool), mode (generated / edited / null), generator (the tool named in the file, when one is), source_type, c2pa, fields (the provenance fields read) and evidence — one plain-English line per finding, saying which field said what.

Read the answer the right way round. declared: true is a certainty — the file says so itself. declared: false only means the file carries no disclosure, and most platforms strip metadata on upload, so it is never evidence a person made the picture. This layer reports disclosure, not a judgement; there are no verdict fields here for the same reason there are none on /api/scan.

GET /api/config

Your tier's limits (what the counter uses): char cap, monthly word quota + usage, per-hour caps, and whether the deep model is available to you.

Accounts & API keys

A free account (dashboard) raises the per-scan limit to 50,000 characters, adds a monthly word quota, saves scan + cleanup history, and issues API keys. Authenticated scans/cleanups are saved by default — pass "save": false to opt out.

Fair use

Please don't wrap this in a product that presents scores as verdicts about people. The response's disclaimer field exists to be shown.