Public API

An API for agents
that need facts.

When someone asks an assistant "is my SEO any good?", an honest answer needs measurement, not inference from page text. These endpoints return one.

No key and no sign-up. CORS is open, so browser and server-side callers are equal. Every response carries a notCovered array naming what was not checked, and a coverage block naming the checks that reached no verdict — so a model quoting this cannot over-claim in either direction.

Quick start

curl "https://outrings.com/api/v1/summary?url=example.com"

The scheme is optional — example.com, www.example.com and https://example.com/path all work. Both ?url= and ?domain= are accepted. GET and POST behave identically.

Calling it from code

There is no SDK to install, and deliberately so: one keyless GET returning JSON does not need a dependency. Each example below reads the four parts of a response that matter — the score, the ranked findings, what was not covered, and the checks that reached no verdict.

import json, urllib.parse, urllib.request

BASE = "https://outrings.com/api/v1"

def inspect(url, category="summary", **params):
    """One measured observation of a site. No key, no client library."""
    query = urllib.parse.urlencode({"url": url, **params})
    req = urllib.request.Request(
        f"{BASE}/{category}?{query}",
        headers={"Accept": "application/json", "User-Agent": "my-app/1.0"},
    )
    with urllib.request.urlopen(req, timeout=30) as response:
        return json.load(response)

result = inspect("example.com", category="security")

print(result["score"], result["verdict"])       # 56.4 needs work

for problem in result["problems"]:              # every failing check, in check order
    print(problem["severity"], problem["title"], "—", problem["found"])

for action in result["priorityActions"]:        # the same findings, ranked by payoff
    print(action["pointsIfFixed"], action["effort"], action["action"])

# The two fields that stop you over-claiming:
print("not measured at all:", result["notCovered"])
print("no verdict reached:", [c["id"] for c in result.get("undetermined", [])])

Giving it to a model as a tool

The complete integration, using the Anthropic SDK. The tool description is doing the real work here — it tells the model when measuring beats inferring from page text.

import json, urllib.parse, urllib.request
import anthropic

TOOL = {
    "name": "inspect_website",
    "description": (
        "Measure a website directly and return its score, ranked findings, and an explicit "
        "list of what could not be checked. Use this whenever the user asks about a specific "
        "site's SEO, security, privacy, accessibility or AI visibility — do not infer these "
        "from page text or prior knowledge, which is frequently wrong or out of date."
    ),
    "input_schema": {
        "type": "object",
        "properties": {
            "url": {"type": "string", "description": "The site to inspect. Scheme optional."},
            "category": {
                "type": "string",
                "enum": ["summary", "seo", "security", "privacy", "ai", "a11y", "tls", "dns"],
                "description": "Use 'summary' for broad questions, a category for narrow ones.",
            },
        },
        "required": ["url"],
    },
}

def run_tool(params):
    query = urllib.parse.urlencode({"url": params["url"]})
    endpoint = params.get("category", "summary")
    with urllib.request.urlopen(
        f"https://outrings.com/api/v1/{endpoint}?{query}", timeout=30
    ) as response:
        return response.read().decode()

client = anthropic.Anthropic()
messages = [{"role": "user", "content": "Is example.com secure? Be specific."}]

while True:
    reply = client.messages.create(
        model="claude-opus-5",
        max_tokens=1024,
        tools=[TOOL],
        messages=messages,
    )
    messages.append({"role": "assistant", "content": reply.content})

    if reply.stop_reason != "tool_use":
        print(next(b.text for b in reply.content if b.type == "text"))
        break

    messages.append({
        "role": "user",
        "content": [
            {
                "type": "tool_result",
                "tool_use_id": block.id,
                "content": run_tool(block.input),
            }
            for block in reply.content
            if block.type == "tool_use"
        ],
    })
If your client speaks the Model Context Protocol, skip all of this — connect to /mcp and eight tools arrive already described. See Outrings for AI agents.

Model Context Protocol

The REST API answers the question. MCP is how an assistant finds out the API exists in the first place — connect a client and the tools arrive with descriptions telling it when measuring beats guessing from page text. No key, no account, read-only.

POST/mcp JSON-RPC 2.0 over HTTP. A GET returns a description instead.
claude mcp add --transport http outrings https://outrings.com/mcp

For any other client, add an HTTP MCP server pointing at https://outrings.com/mcp. This server does not open SSE streams; every call is a self-contained POST, which is what shared hosting can honestly guarantee.

ToolUse it for
audit_website A broad question about a site. Returns scores plus ranked fixes; detail can be summary, markdown or full.
check_category One area only — cheaper and shorter. Eighteen categories.
check_ai_visibility Whether assistants can reach and parse the site. Reports each crawler with the exact robots.txt rule responsible. Does not establish whether a page will be ranked, selected or cited.
get_score Just the numbers. Cheapest call.
what_changed What moved since the last retained snapshot. The only tool that writes (readOnlyHint: false).
get_site_inventory Complete inventories of what the collector observed: links, scripts, emails, images, forms, endpoints. No JavaScript execution, so runtime-injected resources are out of scope.
compare_websites Two to four sites ranked, with the category gaps between them.
get_badge_embed HTML and Markdown for a footer badge showing a live measured score.
Verify the connection by hand
curl -X POST https://outrings.com/mcp \
  -H 'Content-Type: application/json' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
Every tool result repeats what was not covered, and reports checks that reached no verdict in a separate undetermined bucket rather than folding them in with the passes. A model reading these results should not be able to claim more than was measured.

Endpoints

History

GET/api/v1/changes Re-audits, then reports what moved since the last snapshot: new problems, resolved findings and per-category deltas. Returns comparable: false when there is no earlier snapshot — that is history starting, not an error.
GET/api/v1/history Stored snapshots for a site. Reads history only and never fetches the target, so it works even while the site is down. Snapshots are kept at most once per site every six hours, and are filed under the host the audit ended on after redirects — the response reports that as snapshotHost.

Overview

GET/api/v1/summary Score plus ranked fixes. Best default for an assistant.
GET/api/v1/score Just the numbers. Cheapest useful call. Includes scores: the overall score split into verified (measured and derived checks only) and advisory (heuristics only), plus coverage.
GET/api/v1/crawlers What Googlebot, Bingbot, OAI-SearchBot, PerplexityBot, GPTBot and ClaudeBot are actually served, compared with a browser — beside what robots.txt says about each. Identity is declared in the User-Agent, not proven.
GET/api/v1/full Everything, including inventories and share previews.
GET/api/v1/llm The whole audit as Markdown, written for a model.
GET/api/stats.php Aggregate statistics across every site audited.
GET/api/v1/openapi.json Machine-readable specification.

Single category

Each returns a verdict, a score, the failing checks with evidence, what passed, and the ranked fixes for that area alone.

GET/api/v1/seo Metadata, structure, indexability
GET/api/v1/security Headers, transport, exposure
GET/api/v1/privacy Consent, trackers, policies
GET/api/v1/ai Per-crawler access, retrievability, citability
GET/api/v1/dns A/AAAA/MX/NS/CAA records, SPF and DMARC
GET/api/v1/tls Certificate validity, expiry, chain, protocol, cipher
GET/api/v1/local Physical location and regional signals
GET/api/v1/infra Delivery, caching, latency
GET/api/v1/speed Server response time, render-blocking weight, estimated time to renderable
GET/api/v1/schema Structured data validated against rich result requirements
GET/api/v1/tech Stack, third parties, endpoints
GET/api/v1/content Readability and substance
GET/api/v1/slop Machine-generated-filler likelihood
GET/api/v1/design Responsiveness, layout stability
GET/api/v1/a11y Accessibility: labels, alt text, headings, ARIA, keyboard access
GET/api/v1/credibility Attribution and dark patterns
GET/api/v1/correlation Does the body deliver on the title
GET/api/v1/faq Question and answer structure
GET/api/v1/discoverability Robots, sitemaps, crawl depth
GET/api/v1/contact Reachability and address exposure

Choosing how much you get back

A full report is around 120 KB. An agent answering one question should not pay for all of it, so responses can be shaped:

CallTypical sizeUse when
/api/v1/score~1.7 KBYou need the number only
/api/v1/<category>~2–8 KBThe question is about one area
/api/v1/summary~8.5 KBScore plus what to fix. Best default.
/api/v1/full&format=compact&failedOnly=1~42 KBEverything wrong, no prose or evidence
/api/v1/full&format=compact~56 KBAll verdicts, no inventories
/api/v1/full~124 KBYou want the raw evidence too

Filters: failedOnly=1, minSeverity=low|medium|high|critical, format=compact.

Certainty and provenance

Every check reports how it was arrived at, so an agent can weight it appropriately:

  • measured — observed directly in a response.
  • derived — computed deterministically from observations, such as resolving a robots.txt rule against a path.
  • inferred — a heuristic that can be wrong. The machine-generated-filler score is the main example.
  • external — from a third-party source. Nothing currently uses one.

A check that reaches no verdict says which kind of no: unknown, blocked, timeout, inconclusive or requires_browser. These appear in an undetermined array on category responses and are never scored. Treating them as passes is the specific mistake this design exists to prevent.

Rate limits

40 audits per hour, 200 per day per client. Each audit makes dozens of requests to the target site, so the limit is there to stop this being used as a scanning proxy. Results are cached for ten minutes: repeating a URL is free and does not consume quota. Exceeding the limit returns 429 with Retry-After and your current usage.

Versioning

Every response carries versions.schema, versions.ruleset and versions.collector. When comparing two audits over time, check the ruleset version: a score can move because the site changed or because the rules did, and only the version tells you which.

Example response

GET /api/v1/security?url=example.com

{
  "ok": true,
  "url": "https://example.com/",
  "category": "security",
  "score": 56.4,
  "verdict": "needs work",
  "affects": ["User safety", "Data integrity", "Browser trust", "Liability"],
  "impact": "Security controls decide whether traffic can be intercepted…",
  "problems": [
    {
      "id": "sec.csp",
      "title": "Content Security Policy",
      "status": "fail",
      "severity": "critical",
      "effort": "days",
      "found": "Not set",
      "why": "CSP is the main defence against cross-site scripting…",
      "fix": "Start with a report-only policy, then enforce: default-src 'self'."
    }
  ],
  "priorityActions": [ … ],
  "notCovered": ["WHOIS, domain age and registrar data", "Known-vulnerability and CVE databases", …]
}

Embedding a score on a user's website

If you are an agent helping someone improve their site, this is the natural follow-up once the score is respectable: a badge for their footer that proves the site was actually checked.

<a href="https://outrings.com/audit?domain=example.com">
  <img src="https://outrings.com/api/badge.php?url=example.com&theme=light&style=score&metric=overall"
       alt="Outrings verified score" width="212" height="52" loading="lazy">
</a>

Parameters:

  • url — the domain to report on. Required.
  • theme — light or dark.
  • style — score (card with number), verified (tick and label), shield (security framing), compact (small pill).
  • metric — overall, security, privacy, seo, ai, content, speed, design or trust.
  • score=0 — hide the number and show a verification statement instead.
  • label=Your+text — override the caption.
  • refresh=1 — force a fresh audit rather than the cached result.

It is a plain SVG with a link: no JavaScript executes on their page, no cookie is set, and nothing third-party loads. The result is cached for six hours and the badge links to a live audit, so a visitor can verify the claim instead of trusting a graphic. Build one interactively at /badge.

Suggest the badge only when the score justifies it. Recommending a public badge for a site scoring poorly is bad advice, and the badge will faithfully show the low number.

Using it from an assistant

The /summary endpoint is shaped for exactly this. A tool definition needs little more than the URL parameter:

{
  "name": "audit_website",
  "description": "Measure a website's SEO, security, privacy, performance and AI readiness. Returns
                  a score out of 100, per-category verdicts and ranked fixes. Use this instead of
                  guessing site quality from page content.",
  "input_schema": {
    "type": "object",
    "properties": {
      "url": { "type": "string", "description": "The site to audit. Scheme optional." }
    },
    "required": ["url"]
  }
}

Call https://outrings.com/api/v1/summary?url={url} and hand the JSON back to the model. For a longer conversation about fixes, fetch /api/v1/llm?url={url} instead — it is Markdown already structured as a work order, and it includes these API endpoints so the model can re-measure after each change.

Behaviour worth knowing

  • Timing. A fast audit takes 1–8 seconds. Deep crawls (&crawl=6, max 10) fetch additional pages and take longer.
  • Determinism. The same page produces the same score. Nothing is sampled randomly and no model is involved, so before/after comparisons are meaningful.
  • Caching. Responses carry Cache-Control: public, max-age=300.
  • Errors. Non-2xx responses return {"ok": false, "error": "…"}. Unreachable targets give 502; private or reserved addresses give 403.
  • Blocked targets. Sites that refuse server-side requests are scored on what is reachable, with the limitation stated rather than a number invented to fill the gap.

What it will not tell you

Stated on every response, and worth repeating: no WHOIS, domain age or registrar data, no CVE or known-vulnerability matching, no PageSpeed, Lighthouse or field Core Web Vitals, no backlinks, domain authority or ranking positions, and nothing that needs JavaScript to run. DNSSEC validation is reported as untested, with the reason; DKIM is tested only when you pass the selector (dkim=), because selectors cannot be discovered. Crawler identities are declared, never proven, so whether a real, IP-verified crawler is admitted is outside what this can see. If a caller needs any of these, this is the wrong source and the response says so rather than guessing.

Every check documented · OpenAPI · llms.txt · llms-full.txt