Table of contents
An agent that wants to call your API has to know what it can ask for. An OpenAPI file answers that in one fetch: every path, every parameter, every response shape, in JSON a program can read. Publishing it takes about ten minutes if you already have the file, and the check that grades it (E2) is a pass or a miss with no partial credit for a file that exists but does not parse. This guide covers where to serve it, which version to write, how to advertise it, and how to prove it parses.
Quick summary
Short answer: serve an OpenAPI 3.1 JSON document at /openapi.json with a JSON content type, list your real API host in servers, and add a service-desc link so agents do not have to guess the path.
| If you want to... | Do this | Check | Time |
|---|---|---|---|
| Pass the check fastest | Put openapi.json at the site root | check:E2 | 10 minutes |
| Have your framework write the file | Turn on the built-in export (FastAPI does by default) | check:E2 | 5 minutes |
| Make the file discoverable | Add a Link header and an API catalog entry | check:P1, check:D3 | 10 minutes |
| Confirm it works | Run the script below, or scan the site | check:E2 | 2 minutes |
According to the OpenAPI Specification 3.1.2 (fetched 12 October 2026), it is RECOMMENDED that the entry document be named openapi.json or openapi.yaml. That is the whole reason /openapi.json is the path every scanner tries first.
What an agent does with the file
Think of the file as the menu a restaurant hands over before anyone orders. Without it, an agent reads your human-facing docs, guesses a parameter name, gets a 400 and tries again. With it, the agent reads the menu once and sends a correct request first time.
The spec sets a low bar for what counts as a document. In its own words, an OpenAPI description must contain at least one of paths, components or webhooks. A file with only openapi and info is valid JSON and useless, and AIScan treats that kind of file as a miss. A file with real paths is what an agent can act on.
AIScan's E2 check (the capability checks reference, verified on 12 October 2026) looks for /openapi.json, /openapi.yaml, /api/openapi.json, /.well-known/openapi.json, /swagger.json, or a service-desc link in your RFC 9727 API catalog. E2 has a weight of 3 and is marked recommended. On sites detected as pure content sites, the report calls it an optional improvement rather than a failure.
Who should skip this
If your site has no endpoints a stranger could call, there is nothing to describe. A brochure site, a blog, a store whose only API is the platform's own: skip E2 and spend the time on llms.txt and server-rendered HTML. A WordPress site already exposes /wp-json/, which is WordPress's own route index, not an OpenAPI file. If you only run a blog on WordPress, the useful files for you are llms.txt, robots rules and schema, and ThinkRank handles all three from one plugin and migrates from Rank Math, Yoast, AIOSEO and SEOPress. If you run a Shopify store, StoreSEO generates llms.txt from live products and collections. Neither of those publishes an OpenAPI file for an API you wrote yourself, which is what this guide is about.
Publish the file when you run your own API: a SaaS product, a headless backend, a public data endpoint, a booking or quote service.
Choose a version
Write 3.1. The spec site at spec.openapis.org lists 3.1.2 and 3.2.0 as published (fetched 12 October 2026), and the spec says tooling that supports OAS 3.1 should be compatible with all 3.1.x patch versions. AIScan's own reference file uses 3.1.0 and its docs describe the target as OpenAPI 3.1.
Two version traps:
- A
swaggerkey at the top instead ofopenapimarks an older Swagger 2.x file. AIScan still looks for/swagger.json, but new files should useopenapi. - The
openapifield is the spec version.info.versionis your API's own version. Mixing them up is the most common first-draft mistake.
Path one: write the file by hand
Use this when you have a handful of endpoints and no framework that exports a spec.
- Create a file named
openapi.json. - Start with the three required top-level pieces:
openapi,info(withtitleandversion) andpaths. - Add a
serversarray with your real, absolute API URL. The spec says that whenserversis missing, the default is a single server with aurlof/, which is relative to wherever the file was fetched and tells an agent nothing about a separate API host. - Describe each operation with a
summary, its parameters and at least one response. - Serve the file from the site root as a static file with the content type
application/json.
A minimal file that passes the check:
{
"openapi": "3.1.0",
"info": { "title": "Example Quote API", "version": "1.0.0" },
"servers": [{ "url": "https://api.example.com" }],
"paths": {
"/quotes": {
"get": {
"summary": "List quotes",
"parameters": [
{ "name": "limit", "in": "query", "schema": { "type": "integer" } }
],
"responses": { "200": { "description": "A list of quotes" } }
}
}
}
}
Where the static file goes depends on your stack:
| Stack | Where the file goes | Note |
|---|---|---|
| Next.js | public/openapi.json | The Next.js docs (version 16.4.0 page, fetched 12 October 2026) say files in public are served from the base URL / |
| Plain nginx or Apache | The web root, next to index.html | Confirm the content type with curl -I |
| WordPress with an API of your own | The site root, over SFTP or your host's file manager | A file at the root is served as is; do not put it in a theme folder |
| Cloudflare Pages or Netlify | The publish directory root | Check that a catch-all rewrite does not swallow it |
Path two: let your framework write it
If your API is built with a framework that already generates a spec, publishing is a setting, not a writing job.
- Find the framework's export or docs-URL setting.
- Confirm it produces OpenAPI 3.x, not Swagger 2.
- Make the URL one of the paths E2 tries, or add a redirect from
/openapi.json. - Re-export on every release so the file never describes last quarter's API.
FastAPI is the clean example. According to its docs (fetched 12 October 2026), the schema is served at /openapi.json by default, can move with the openapi_url parameter, and setting openapi_url=None turns it off along with the documentation pages that use it. If you set it to /api/v1/openapi.json, E2 will not find it, so add a link as described next.
Other frameworks vary by version, so check yours before copying a command from a blog post, including this one.
Advertise it so agents do not guess
A file at a guessable path passes E2. A file that is linked is found by agents that do not know your conventions. Add two pointers.
A Link header. The service-desc relation comes from RFC 8631. On nginx:
add_header Link '</openapi.json>; rel="service-desc"; type="application/json"' always;
I have not run that nginx line in this session (no nginx in the sandbox), so confirm it with curl -I after you reload.
An API catalog entry. RFC 9727 defines /.well-known/api-catalog. Add your spec as a service-desc entry. Our walkthrough, publish an API catalog agents can find, covers the full file, so this guide stays on the OpenAPI document itself.
AIScan's own setup shows the pattern. Fetched on 12 October 2026, aiscan.site/openapi.json returns 200 with application/json, declares 3.1.0, lists two paths and names https://aiscan.site as its server. The catalog at /.well-known/api-catalog returns application/linkset+json and lists that file first under service-desc. One honest gap: the Link header on the homepage has a service-desc entry that points at /api/scan (the SSE stream), not at the OpenAPI file, so an agent reading only headers would miss the spec and need the catalog.
Verify it parses
With AIScan
Run npx aiscan-cli yoursite.com, or paste the URL at aiscan.site. It is free and needs no account. Read these rows:
| Row | What it tells you |
|---|---|
| check:E2 | A machine-readable API description was found |
| check:P1 | An API catalog exists and lists it |
| check:D3 | Discovery links are present in response headers |
AIScan cannot judge whether your spec is accurate. It can find a file and confirm it is there. It will not notice that your spec describes an endpoint you removed in March. That part is a review job, and it is yours.
By hand
If you would rather check by hand, run this script. It tries each path E2 uses and refuses to count a soft 200 (an HTML page returned with status 200) as a pass.
#!/usr/bin/env python3
import json, sys, urllib.request, urllib.error
PATHS = ["/openapi.json", "/api/openapi.json", "/.well-known/openapi.json", "/swagger.json"]
UA = "Mozilla/5.0 (compatible; openapi-check/1.0)"
def fetch(url):
req = urllib.request.Request(url, headers={"User-Agent": UA})
try:
with urllib.request.urlopen(req, timeout=15) as r:
return r.status, r.headers.get("content-type", ""), r.read(5_000_000)
except urllib.error.HTTPError as e:
return e.code, e.headers.get("content-type", ""), b""
def main(site):
base = "https://" + site.removeprefix("https://").strip("/")
problems, found = [], False
for p in PATHS:
status, ctype, body = fetch(base + p)
if status != 200:
print(f"- {p}: {status}")
continue
try:
doc = json.loads(body)
except ValueError:
problems.append(f"{p}: 200 but not JSON ({ctype or 'no type'}), a soft 200")
continue
version = doc.get("openapi") or doc.get("swagger")
if not version:
problems.append(f"{p}: JSON without an openapi or swagger key")
elif not any(k in doc for k in ("paths", "components", "webhooks")):
problems.append(f"{p}: version {version} but no paths, components or webhooks")
elif "json" not in ctype:
problems.append(f"{p}: parses, but content-type is {ctype!r}")
else:
print(f"PASS {p}: {version}, {len(doc.get('paths', {}))} paths, {ctype}")
found = True
for m in problems:
print("FAIL", m)
return 0 if found and not problems else 1
if __name__ == "__main__":
sys.exit(main(sys.argv[1]))
Save it as check_openapi.py and run python3 check_openapi.py yoursite.com. A pass prints the version and the path count and exits 0.
I ran it on 12 October 2026 against aiscan.site (PASS on /openapi.json: 3.1.0, 2 paths; the other three paths returned 404) and against a local test server built to fail. That server returned an HTML shell with status 200 for every unknown path, a {"hello":1} file at /openapi.json, and an openapi file with no paths at /swagger.json. The script reported all four as FAIL and exited 1. The soft 200 is the trap worth remembering: a status check alone calls an HTML page a pass.
Success thresholds, in order of strictness:
/openapi.jsonreturns 200 and a JSON content type.- The body parses and has
openapiand at least one ofpaths,components,webhooks. - A path that should not exist returns 404, not 200. Try
/definitely-not-here.json.
If check 3 fails, your server answers everything with 200 and every other check in this guide is unreliable until you fix it.
When the file is there and agents still fail
Three failures show up after the file is live. Each has a first thing to try.
| Symptom | Likely cause | First fix |
|---|---|---|
| The scan reports no API description although the URL opens in your browser | The path returns HTML with status 200, or the type is text/html | Run the script above and read the FAIL line |
| The file parses but an agent calls the wrong host | servers is missing, so the default / applies | Add the absolute API URL to servers |
| The file is found on one path and 404s on the other three | Normal; E2 needs one hit, not four | Leave it, or add the link header so the path is explicit |
A related question is whether an agent can finish a job once it has found your API. That is a separate test, covered in how to test whether an AI agent can complete a task on your site, and it is worth running once the spec is live. The spec tells an agent what exists, and the test tells you whether the calls work. The wider set of well-known files is mapped in our well-known files corpus, with figures fetched from the live sites it measured.
Maintenance
- Regenerate the file in your release pipeline. A stale spec is worse than none, because an agent trusts it.
- Re-run the script after any change to caching, redirects or a CDN rule. A rule that rewrites unknown paths to the home page will turn your 404s into soft 200s.
- Keep the file public. If it contains internal-only routes, remove them before you serve it.
Next step
Scan your site at aiscan.site and read check:E2 and check:P1 in the Capabilities group. Then browse the other fixes in the AIScan guides, starting with the capability checks reference.
Frequently asked questions
Where should I publish my OpenAPI file?
At /openapi.json on the site root. The OpenAPI Specification recommends naming the entry document openapi.json or openapi.yaml, and AIScan's E2 check tries /openapi.json first, then /api/openapi.json, /.well-known/openapi.json and /swagger.json.
Should I use OpenAPI 3.1 or Swagger 2.0?
Use 3.1. The spec site lists 3.1.2 and 3.2.0 as published. A top-level swagger key marks an older 2.x file; AIScan still looks for /swagger.json, but a new file should start with an openapi key.
Does my site need an OpenAPI file at all?
Only if you run an API that others can call. A blog or brochure site has nothing to describe, and AIScan reports E2 as an optional improvement on sites it detects as pure content sites.
My OpenAPI URL opens in the browser but the scan says no API description was found. Why?
The most common cause is a soft 200: the path returns an HTML page with status 200, or a JSON body without an openapi key. Run the check_openapi.py script in this guide and read the FAIL line. It names the path and the reason.
The scan finds my file but an agent sends requests to the wrong host. What is wrong?
The servers array is probably missing. The spec says the default is a single server with a url of /, which is relative to where the file was fetched. Add your absolute API URL, for example https://api.example.com.
My framework serves the spec at /api/v1/openapi.json and E2 shows nothing. What do I do?
Move the URL to /openapi.json, or add a redirect, or advertise the real path with a Link header using rel="service-desc" and an entry in /.well-known/api-catalog. FastAPI, for one, serves /openapi.json by default and changes with the openapi_url parameter.
Is a file with only openapi and info valid?
It is valid JSON, but the spec says an OpenAPI description must contain at least one of paths, components or webhooks. The script in this guide marks a file without any of them as FAIL.
How do I stop the spec going stale?
Generate it in your release pipeline instead of editing it by hand, and re-run the check after any caching, redirect or CDN change. A stale spec misleads an agent more than a missing one.
Related guides
Platform League Table: WordPress, Next.js, Astro and Shopify Across 728 Sites
Every few weeks someone asks which platform produces the most agentready sites. WordPress, Next.js, Astro and Shopify all have fans, and a ranked table is the answer people expect. We have the data…
Which Checks Do Sites Fix? 349 Repeat Scans Compared
Most scanner posts tell you what to fix. Fewer tell you what people actually fixed. AIScan keeps every scan it has ever run, and many sites get scanned more than once, so we can answer a plainer…
Redirect www and http to one canonical host, the way an agent needs it
Type http://www.example.com/pricing into a terminal and count the hops it takes to reach the page. On a clean setup the answer is one, a single 301 straight to the canonical address. On many sites it…
Agent Discovery Files Census: P1, P3, P4 and E2 Across 835 Sites
Only 301 of the 835 sites in our scan corpus pass any one of four agentdiscovery checks that depend on a /.wellknown/ file or an API description. All four pass on 99 sites. Take out one platform and…
