# API Docs Finder - OpenAPI/Swagger Spec, llms.txt & Docs URLs (`neverempty/api-docs-finder`) Actor

For AI agents about to call an unfamiliar API: give a company domain, get its OpenAPI/Swagger spec (version, endpoints, auth), llms.txt, docs, API reference, status page, changelog, official npm/PyPI SDKs and GraphQL endpoint, each URL verified. On 42 API companies: llms.txt found for 35.

- **URL**: https://apify.com/neverempty/api-docs-finder.md
- **Developed by:** [NeverEmpty](https://apify.com/neverempty) (community)
- **Categories:** AI, Developer tools, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $3.85 / 1,000 domain locateds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## API Docs Finder - OpenAPI/Swagger Spec, llms.txt & Docs URLs

**Give your AI agent a company domain and get back where that company's developer resources live, each URL verified:** the OpenAPI/Swagger spec (downloaded and read: spec version, API version, endpoint count, auth types, servers), `llms.txt` and `llms-full.txt`, the developer docs home, the API reference, the status page (and its current status when allowed), the changelog (and the RSS/Atom feed it declares), the official npm and PyPI SDK packages with the evidence that they are official, and whether a GraphQL endpoint answers. One call, one row per domain, with a one-sentence `summary` the agent can read as is.

Only verified resources are returned as found: the URL answered 2xx and the content has the expected shape (a JSON/YAML file with an `openapi` or `swagger` version, a Markdown `llms.txt` rather than an HTML page, a docs page whose title or place says so). Links it could not verify (too large, over your download limit, behind a wall) are listed separately in `notVerified` with the reason. Every place it tried is listed in `searched` with what it answered, so "not found" comes with the list of places that were checked.

### Example

Input:

```json
{ "domains": ["stripe.com"] }
```

Output (production run pJMr2xEKNHE4SKcBg, shortened):

```json
{
  "domain": "stripe.com",
  "summary": "stripe.com: OpenAPI 3.0.0 spec \"Stripe API\" version 2026-09-30.endive (612 operations, auth: http basic, http bearer) at https://raw.githubusercontent.com/stripe/openapi/master/openapi/spec3.json; llms.txt at https://stripe.com/llms.txt; docs at https://docs.stripe.com/; API reference at https://docs.stripe.com/api; status page at https://status.stripe.com/; changelog at https://stripe.com/blog/changelog; npm: stripe@22.6.2, @stripe/extensibility-sdk@3.0.0, @stripe/ui-extension-sdk@9.3.0; PyPI: stripe 15.6.1. Not found: GraphQL endpoint.",
  "found": ["openapi", "llms", "docs", "apiReference", "status", "changelog", "sdks"],
  "missing": ["graphql"],
  "openapiUrl": "https://raw.githubusercontent.com/stripe/openapi/master/openapi/spec3.json",
  "openapiSpecVersion": "3.0.0",
  "apiVersion": "2026-09-30.endive",
  "endpointCount": 612,
  "authTypes": ["http basic", "http bearer"],
  "llmsTxtUrl": "https://stripe.com/llms.txt",
  "docsUrl": "https://docs.stripe.com/",
  "apiReferenceUrl": "https://docs.stripe.com/api",
  "statusPageUrl": "https://status.stripe.com/",
  "statusPageProvider": "custom",
  "changelogUrl": "https://stripe.com/blog/changelog",
  "npmPackages": [{ "name": "stripe", "latestVersion": "22.6.2", "evidence": "it is published or maintained by an @stripe.com account" }],
  "pypiPackages": [{ "name": "stripe", "latestStableVersion": "15.6.1", "evidence": "its releases are published with the author e-mail @stripe.com" }],
  "searchedCount": 60
}
```

With no input at all it searches `stripe.com`, so the Actor always returns a row.

### What it finds, per domain

| `found` value | How it is found | How it is verified |
|---|---|---|
| `openapi` | Well-known paths on `api.`, the site and docs hosts (`/openapi.json`, `/openapi.yaml`, `/swagger.json`, `/.well-known/openapi.json`, `/v3/api-docs` and more), spec links on the home page, docs pages and in `llms.txt`, and the free APIs.guru catalog (original source first, then its mirror copy) | Downloaded (up to 20 MB) and parsed as JSON or YAML; must carry `openapi: 3.x` or `swagger: 2.0`. You get spec version, title, API version, path and operation counts, auth types from the security schemes, server URLs, and `publishedBy` (`company-domain`, `company-github`, `apis-guru-mirror` or `third-party`) |
| `llms` | `/llms.txt` on the site and docs hosts, then `llms-full.txt` next to it | Must be Markdown, not HTML, and not a redirect to another page. You get its H1 title, the `>` summary, the `##` section names and the link count |
| `docs` | `docs.`, `developer.`, `developers.`, `platform.` hosts, `/docs`, `/developers`, and docs links on the home page | A real HTML page (not the home page, not a soft 404) whose title says docs/developers/reference, or that sits on a docs host or path |
| `apiReference` | `/api`, `/reference`, `/api-reference` on the docs hosts, and API reference links from the docs and `llms.txt` | Title says API reference/REST API/reference, or it sits on a docs host with such a path |
| `status` | `status.<domain>`, `<brand>status.com`, and status links on the site | The page is a status page (Atlassian Statuspage, incident.io, Instatus, Better Stack, Status.io and others); for Statuspage, `/api/v2/status.json` is read for `currentStatus` only when robots.txt allows it |
| `changelog` | `/changelog`, `/docs/changelog`, `/release-notes` on the site and docs hosts, and changelog links | Title or path says changelog/release notes/what's new. The RSS/Atom feed the page declares is returned as `changelogFeedUrl` as declared (not fetched) |
| `sdks` | npm registry search for the brand name, PyPI release feeds for the likely package names | npm: the package's homepage/repository points to the domain, or it is published or maintained by an account at the domain, or its GitHub org is linked from the company's own pages. PyPI: the release feed's author e-mail is at the domain. Platform-only binary packages are left out |
| `graphql` | `api.<domain>/graphql`, `/graphql`, `/api/graphql` | A GET answer only a GraphQL server gives ("Must provide query string", an `errors` list with `locations`/`extensions`, a 401 pointing to GraphQL docs) or a GraphiQL/Playground page. Nothing is queried; `authRequired` shows a 401/403 answer |

### Measured

One production run on 42 well-known API companies (Stripe, GitHub, Slack, OpenAI, Anthropic, Apify, Shopify, Twilio, Notion, Resend, Supabase, Vercel, Linear, Clerk, HubSpot, Cloudflare, Sentry, Airtable, Mailchimp, Render, Postmark, Zapier, Discord, Dropbox, SendGrid, Spotify, Atlassian, GitLab, DigitalOcean, PagerDuty, Datadog, Algolia, Mapbox, PayPal, Plaid, Zoom, Figma, freee, SmartHR, Chatwork, Asana, PAY.JP), default settings, 540 seconds (run XcCnurPxU1fUmbJux):

| found | domains |
|---|---|
| developer docs | 42 of 42 |
| official SDK packages | 40 |
| status page | 38 |
| llms.txt | 35 |
| API reference | 30 |
| changelog | 29 |
| OpenAPI/Swagger spec (downloaded and read) | 23 |
| GraphQL endpoint | 1 |

4,371 requests, 378 of them robots.txt; 312 URLs were not requested because robots.txt disallows them. A single domain takes about 8 seconds (stripe.com: 67 requests, including its 8 MB spec). Checked against a browser on three production runs (spec version, title and API version; llms.txt title; docs, status, changelog and API reference pages; npm latest version): 60 of 62 checks agreed, and the other 2 were limits of the checking script (an 11.6 MB spec the browser could not hold, and an llms.txt whose first line is not a heading, which this Actor correctly reports as having no title).

### Input

| Field | Default | What it does |
|---|---|---|
| `domains` | (example `stripe.com`) | Domains, 1 to 100 per run. URLs and subdomains are read as their domain (`https://docs.stripe.com/api` = `stripe.com`); the same domain twice is searched and charged once. |
| `include` | all | Any of `openapi`, `llms`, `docs`, `apiReference`, `status`, `changelog`, `sdks`, `graphql`. Fewer means a faster run; the price per domain is the same. |
| `verifySpecs` | 3 | How many linked or catalog-listed spec files are downloaded and read per domain (0 to 10). Specs at well-known paths are always read. |
| `onlyChanges` | false | Monitor mode: return only domains where something changed since the last run with the same `watchName`. |
| `watchName` | | Name of the remembered state for monitor mode. |
| `resetMonitoringState` | false | Forget the remembered state before this run. |
| `maxConcurrency` | 3 | Domains searched in parallel (1 to 5). Each site gets at most 3 requests at a time. |
| `requestTimeoutSecs` | 20 | Time limit per request (5 to 60 s); spec downloads get up to 45 s. Each domain gets at most 120 s in total. |

### Output columns

`status` (`ok`, `nothing-found`, `unreachable`, `bad-input`, `budget-reached`, `no-change`), `domain`, `input`, `summary`, `found`, `missing`, `openapiUrl`, `openapiTitle`, `openapiSpecVersion`, `apiVersion`, `endpointCount`, `authTypes`, `openapiSpecs` (each: `url`, `foundVia`, `publishedBy`, `format`, `specVersion`, `title`, `apiVersion`, `pathCount`, `operationCount`, `authTypes`, `serverUrls`, `sizeBytes`, `apisGuruKey`, `description`), `llmsTxtUrl`, `llmsFullTxtUrl`, `llmsTxt` (each: `url`, `title`, `summary`, `sections`, `linkCount`, `sizeBytes`, `followsSpec`), `docsUrl`, `docs`, `apiReferenceUrl`, `statusPageUrl`, `statusPageProvider`, `currentStatus`, `statusJsonUrl`, `changelogUrl`, `changelogFeedUrl`, `changelogs`, `npmPackages` and `pypiPackages` (each with `name`, `latestVersion`, `url`, `evidence`), `graphqlUrl`, `graphql` (`url`, `httpStatus`, `evidence`, `authRequired`), `githubOrgsLinked`, `notVerified` (`kind`, `url`, `why`), `skippedByRobots`, `timeLimitReached`, `searchedCount`, `searched` (`kind`, `url`, `result`), monitor columns (`changeType`, `changedFields`, `previous`, `previousCheckedAt`, `watchName`), `note`, `checkedAt`.

### Monitor mode

Set `watchName` and `onlyChanges: true` and schedule the run. The first run returns every domain. Later runs return only domains where something changed: a spec added or removed, a new API version or endpoint count, new `llms.txt` sections, a new docs, status or changelog URL, or a new SDK release, with `changedFields` and the previous values. A resource that could not be read this time (timeout, 5xx, 429, a challenge page) is never reported as removed; it is compared again on the next run.

### Pricing

Pay per event:

- **Domain located**: each domain where at least one resource was verified.
- **Domain re-checked** (monitor mode only): each domain that was searched and compared but had no change, so no row was returned.
- **Run start**: once per run that returned or re-checked at least one domain.

Free: domains where nothing could be verified (the row lists every place tried), domains that do not resolve, invalid input, and the rows that say a run hit its maximum total charge. A run whose maximum total charge cannot fit the start fee plus one domain searches nothing and costs nothing.

### Use it from an AI agent (MCP)

Through the Apify MCP server the input can be just `{"domains": ["example.com"]}`. Read `summary` first; use `openapiUrl` to load the spec, `llmsTxtUrl` to load the model-friendly docs, and `statusPageUrl`/`currentStatus` to check an outage before blaming your own code.

### Limits and rules it follows

- It reads a URL only when that site's robots.txt allows it (`ApiDocsFinder`, otherwise `User-agent: *`, RFC 9309 with prefix matching). URLs it skipped for that reason are listed in `skippedByRobots`. A robots.txt that answers 5xx or not at all means that host is treated as disallowed.
- It never signs in, never solves or bypasses a challenge page or CAPTCHA, and does not spoof browser fingerprints. It uses the Actor's own server, no proxy.
- pypi.org project pages answer with a JavaScript check, so PyPI packages are read from their public release feeds (`/rss/project/<name>/releases.xml`) instead; the npm registry search and APIs.guru are public APIs.
- Domains are searched in parallel (default 3) and each domain gets at most 120 seconds; places not tried in time are listed in `searched` as `skipped-time-limit` and the row says `timeLimitReached`.
- It finds what a company publishes on its own domain, subdomains and linked GitHub orgs. Docs on a separate domain that nothing links to (for example a product-branded developer site) may be missed; they then do not appear in `found`.
- Two-level country suffixes such as `co.jp`, `co.uk`, `com.pl` and `gov.au` are read as suffixes (`allegro.com.pl` is searched as `allegro.com.pl`); a bare suffix such as `com.pl` is refused as input.
- Not affiliated with any of the companies named here.

### Support

Open an issue on the Issues tab with the domain and the run ID.

# Actor input Schema

## `domains` (type: `array`):

Company or service domains, one per line (1 to 100 per run), for example stripe.com. A URL or a subdomain such as docs.stripe.com or https://api.stripe.com/v1 is read as its domain (stripe.com). The same domain given twice is searched and charged once. If this is empty, the example domain stripe.com is searched.

## `include` (type: `array`):

Which developer resources to search for. openapi = OpenAPI/Swagger spec files (downloaded and read); llms = llms.txt and llms-full.txt; docs = developer docs home; apiReference = API reference page; status = status page (and current status when its JSON is allowed); changelog = changelog or release notes page; sdks = official npm and PyPI packages; graphql = whether a GraphQL endpoint answers (nothing is queried). Leaving some out makes the run faster; the price per domain is the same.

## `verifySpecs` (type: `integer`):

How many linked or catalog-listed OpenAPI files (from the site, its llms.txt or the free APIs.guru catalog) are downloaded and read per domain, 0 to 10. Specs at well-known paths such as /openapi.json are always read. Others are listed under notVerified with the reason.

## `onlyChanges` (type: `boolean`):

Return only domains where something changed since the last run with the same watch name: a new or removed spec, a new API version or endpoint count, llms.txt sections, docs, status page or changelog URL, or a new SDK release. The first run returns every domain as the starting point. A resource that could not be read this time (timeout, 5xx, rate limit, challenge) is not reported as removed. A run in which nothing changed returns a free row saying so and charges only the run start fee.

## `watchName` (type: `string`):

Optional name for a separate watch list (letters, digits, dot, dash, underscore; up to 40). Use different names for different schedules. When set, rows also show changeType and changedFields even without monitor mode.

## `resetMonitoringState` (type: `boolean`):

Start the watch over: this run treats every domain as a first check.

## `maxConcurrency` (type: `integer`):

How many domains are searched at the same time (1 to 5). Each site gets at most 3 requests at a time.

## `requestTimeoutSecs` (type: `integer`):

How long to wait for each page (5 to 60). Spec downloads get up to 45 seconds.

## Actor input object example

```json
{
  "domains": [
    "stripe.com",
    "resend.com",
    "anthropic.com"
  ],
  "include": [
    "openapi",
    "llms",
    "docs",
    "apiReference",
    "status",
    "changelog",
    "sdks",
    "graphql"
  ],
  "verifySpecs": 3,
  "onlyChanges": false,
  "resetMonitoringState": false,
  "maxConcurrency": 3,
  "requestTimeoutSecs": 20
}
```

# Actor output Schema

## `results` (type: `string`):

One row per domain: a one-sentence summary for an AI agent, then each verified resource with its URL: OpenAPI/Swagger specs (spec version, title, API version, endpoint count, auth types, servers), llms.txt and llms-full.txt (title, sections), docs home, API reference, status page (provider, current status), changelog (and its RSS/Atom feed), official npm and PyPI packages with the evidence, and a GraphQL endpoint. Every place that was tried is listed in searched. A domain where nothing could be verified, or that does not resolve, comes back as a free row that says why.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "domains": [
        "stripe.com",
        "resend.com",
        "anthropic.com"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("neverempty/api-docs-finder").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = { "domains": [
        "stripe.com",
        "resend.com",
        "anthropic.com",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("neverempty/api-docs-finder").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "domains": [
    "stripe.com",
    "resend.com",
    "anthropic.com"
  ]
}' |
apify call neverempty/api-docs-finder --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,neverempty/api-docs-finder"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/5OMKaFsCluA0Fr2bI/builds/XOyIdMGNaPX9k9DcE/openapi.json
