---
title: "How to Publish an OpenAPI File AI Agents Can Find"
slug: publish-openapi-json-for-agents
published: 2026-10-12T08:17:00.691883+00:00
updated: 2026-10-12T08:17:00.691883+00:00
author: "AIScan Content Team"
author_url: https://aiscan.site/about
category: "AI Readiness"
tags: check:E2, check:P1, check:D3, platform:nextjs, OpenAPI, API description, how-to, AI readiness
description: "Where to serve /openapi.json, which version to write, how to advertise it, and a script that catches soft 200s. Fixes AIScan check E2, with steps for two paths."
url: https://aiscan.site/blog/publish-openapi-json-for-agents
---

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](https://spec.openapis.org/oas/v3.1.2.html) (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](https://aiscan.site/docs/checks/capabilities), 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](https://thinkrank.ai) handles all three from one plugin and migrates from Rank Math, Yoast, AIOSEO and SEOPress. If you run a Shopify store, [StoreSEO](https://storeseo.com/) 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 `swagger` key at the top instead of `openapi` marks an older Swagger 2.x file. AIScan still looks for `/swagger.json`, but new files should use `openapi`.
- The `openapi` field is the spec version. `info.version` is 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.

1. Create a file named `openapi.json`.
2. Start with the three required top-level pieces: `openapi`, `info` (with `title` and `version`) and `paths`.
3. Add a `servers` array with your real, absolute API URL. The spec says that when `servers` is missing, the default is a single server with a `url` of `/`, which is relative to wherever the file was fetched and tells an agent nothing about a separate API host.
4. Describe each operation with a `summary`, its parameters and at least one response.
5. Serve the file from the site root as a static file with the content type `application/json`.

A minimal file that passes the check:

```json
{
  "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.

1. Find the framework's export or docs-URL setting.
2. Confirm it produces OpenAPI 3.x, not Swagger 2.
3. Make the URL one of the paths E2 tries, or add a redirect from `/openapi.json`.
4. Re-export on every release so the file never describes last quarter's API.

[FastAPI](https://fastapi.tiangolo.com/tutorial/metadata/) 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](https://www.rfc-editor.org/rfc/rfc8631). On nginx:

```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](https://www.rfc-editor.org/rfc/rfc9727) defines `/.well-known/api-catalog`. Add your spec as a `service-desc` entry. Our walkthrough, [publish an API catalog agents can find](https://aiscan.site/blog/publish-api-catalog-rfc-9727), 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](https://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.

```python
#!/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:

1. `/openapi.json` returns 200 and a JSON content type.
2. The body parses and has `openapi` and at least one of `paths`, `components`, `webhooks`.
3. 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](https://aiscan.site/blog/test-agent-can-complete-task), 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](https://aiscan.site/blog/well-known-agent-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](https://aiscan.site/) and read check:E2 and check:P1 in the Capabilities group. Then browse the other fixes in the [AIScan guides](https://aiscan.site/guides), starting with the [capability checks reference](https://aiscan.site/docs/checks/capabilities).

