API Documentation

The AI Visibility Checker exposes two public JSON endpoints. GET /api/report/:domain.json needs no API key. POST /api/scan can be authenticated three ways — the public web form's Turnstile widget, a free self-serve customer API key, or an internal bearer token for direct integrations the site owner sets up — see below. Both endpoints are rate-limited by IP (customer API keys additionally get their own per-key limit — see "Rate limits" and "Authentication" below).

Rate limits

  • 8 fresh scans per IP per hour (anonymous / Turnstile requests)
  • 3 fresh scans per target domain per hour
  • 60 fresh scans per hour per self-serve customer API key — its own bucket, not shared with other customers' keys or with anonymous traffic. See "Authentication" below for how to get one — it's free.

Cached results (within 24 hours) are served instantly and do not count against any of these limits.

POST /api/scan

Start a scan or fetch a cached result for a domain.

Authentication

There are three ways to authenticate a request to this endpoint:

  • Turnstile widget (browser only) — the public scan form on this page solves an invisible Cloudflare Turnstile challenge and submits the resulting turnstileToken in the request body. This path only works from a real browser loading the form; it is not available to direct API callers.
  • Self-serve customer API key (free) — sign up for a free account at /account/signup, then generate a named API key from your account dashboard. Send it as Authorization: Bearer uvk_... and omit turnstileToken entirely. Each key gets its own rate-limit bucket of 60 scans/hour — higher than the 8/hour anonymous limit, and not shared with other customers' keys or with anonymous traffic. There is no payment and nothing to upgrade; the only differences from anonymous use are the higher limit and a persistent, named, individually revocable key. Keys are shown in full only once, at creation, and a revoked key stops working immediately for new requests (a background scan already in flight just before revocation may still finish). Signup needs only a valid-looking email and a password of at least 12 characters — there is no email verification or password-reset flow yet (this app has no transactional email sending wired up at all currently), and signup itself is protected by the same Turnstile check and rate limiting as the admin login.
  • Internal PIPELINE_TOKEN (programmatic callers the site owner sets up) — send Authorization: Bearer <token> and omit turnstileToken entirely. There is no self-serve signup for this token — contact the site owner to have one issued. (If you just want your own key today, use the free self-serve option above instead.)

Don't have a token? Sign up for a free account at /account/signup to generate your own API key with a higher rate limit (60 scans/hour vs 8/hour anonymous).

If an Authorization header is present but the token doesn't match — a wrong/unknown PIPELINE_TOKEN, or a self-serve key that's invalid, unknown, or revoked — the request fails with 401 { "error": "Unauthorized" }. If no Authorization header is sent at all, behavior is unchanged from before: a valid turnstileToken is required, or the request fails with 403 { "error": "Verification failed. Please retry." }.

AI agents or tools that speak MCP can skip all of these and call POST /mcp instead, which needs no authentication at all — see the README's "MCP server + WebMCP" section.

Rate limiting for the internal PIPELINE_TOKEN is unchanged from browser visitors: keyed callers share the same per-IP and per-target-domain limits described above, so heavy programmatic use can still hit the per-IP limit. Self-serve customer API keys work differently — each key has its own 60/hour bucket (see "Rate limits" above), independent of the caller's IP.

Request body

{
  "url": "example.com",
  "forceRescan": false
}

Responses

Status Body Meaning
200 ScanRecord Cache hit — result already fresh (within 24 h).
202 { "domain": "example.com" } Scan started in background — poll GET /api/report/:domain.json until it returns 200.
400 { "error": "..." } Bad or unresolvable URL.
401 { "error": "Unauthorized" } An Authorization: Bearer header was sent but the token did not match.
403 { "error": "Verification failed. Please retry." } No Authorization header was sent, and the Turnstile check failed or turnstileToken was missing/invalid.
429 { "error": "...", "retryAfterSeconds": N } Rate limit exceeded. Wait retryAfterSeconds before retrying.

curl example

curl -X POST https://aivisibility.unomage.com/api/scan \
  -H 'Authorization: Bearer YOUR_TOKEN' \
  -H 'Content-Type: application/json' \
  -d '{"url":"example.com"}'

Omit the Authorization header only when calling this endpoint from a browser that already supplies a Turnstile token in the request body — direct/scripted callers need the bearer token shown above. YOUR_TOKEN can be either a self-serve uvk_... API key from your account dashboard or an internal PIPELINE_TOKEN — both are accepted the same way.

GET /api/report/:domain.json

Fetch the full ScanRecord for a domain. Returns 404 if the domain has never been scanned.

Responses

Status Body Meaning
200 ScanRecord Full record for the domain.
404 { "error": "Not found" } Domain has not been scanned yet.

curl example

curl https://aivisibility.unomage.com/api/report/example.com.json

ScanRecord shape

{
  "domain": "example.com",
  "status": "ok" | "failed",
  "scannedAt": "ISO 8601 string",
  "result": {
    "score": 0-100,
    "grade": "A" | "B" | "C" | "D" | "F",
    "label": "string",
    "pillars": [
      {
        "id": "string",
        "title": "string",
        "score": 0-100,
        "points": 0-100,
        "weight": 0-100,
        "signals": [ /* see below */ ]
      }
    ],
    "topIssues": [
      {
        "id": "string",
        "title": "string",
        "status": "fail" | "warn",
        "impact": "critical" | "high" | "medium" | "low",
        "detail": "string",
        "fix": {
          "what": "string",
          "why": "string",
          "how": "string",
          "effort": "S" | "M" | "L"
        }
      }
    ]
  } | null,
  "offSite": {
    "wikidata":    { "status": "found" | "not_found" | "unknown", "detail": "string", "url": "string | null" },
    "wikipedia":   { "status": "found" | "not_found" | "unknown", "detail": "string", "url": "string | null" },
    "commonCrawl": { "status": "found" | "not_found" | "unknown", "detail": "string", "url": "string | null" }
  } | null
}

result is null when status is "failed". offSite is null for scans run before off-site checking was added, or when the check itself failed.

Polling pattern

When POST /api/scan returns 202, the scan is running in the background. Poll GET /api/report/:domain.json every 1–2 seconds until it returns 200:

# 1. Start the scan
curl -s -X POST https://aivisibility.unomage.com/api/scan \
  -H 'Authorization: Bearer YOUR_TOKEN' \
  -H 'Content-Type: application/json' \
  -d '{"url":"example.com"}' | jq .

# 2. Poll until ready (bash loop)
while true; do
  result=$(curl -s https://aivisibility.unomage.com/api/report/example.com.json)
  echo "$result" | jq '.status' && break || sleep 2
done