# API Drifter > Finds where your API drifted from its spec. Open-source CLI and MCP server that runs your OpenAPI spec against a live implementation and tells you which side to fix. Canonical site: https://apidrifter.com/. Open source (Apache-2.0). CLI package: `apidrifter` on npm (https://www.npmjs.com/package/apidrifter). Engine package: `@apidrifter/core`. Repository: https://github.com/apidrifter/apidrifter This file is the full-text companion to /llms.txt. Every research page also has a Markdown twin at the same path plus `index.md`. ## What API drift is API drift is the gap between what an API's published OpenAPI document says and what the live implementation does. It breaks generated clients, typed SDKs, mocks and contract tests. It appears when a spec is hand-maintained separately from the code, or generated once and edited afterwards. Three forms: the spec is ahead of the implementation; the implementation is ahead of the spec; both drifted from an older shared version. ## Commands - `npx apidrifter plan openapi.yaml`: turn a spec into a request plan with the values an agent or a person must fill in. - `npx apidrifter run openapi.yaml --base-url http://localhost:3000`: execute the plan against a live server and verify every response; each finding carries a fix for the spec and a fix for the implementation. - `npx apidrifter verify openapi.yaml --observations recorded.json`: verify from recorded request/response pairs. - `npx apidrifter breaking old.yaml new.yaml`: diff two spec versions, exit 1 on breaking changes. - `npx apidrifter mcp snapshot ` and `npx apidrifter mcp diff `: snapshot and diff MCP servers, including tool description changes. - `npx apidrifter serve`: run the same capabilities as an MCP server over stdio. ## Pages - https://apidrifter.com/docs/cli/ : CLI reference. Every apidrifter command and flag: plan, run and verify a live API against its OpenAPI spec, diff specs and MCP servers, and serve the engine over MCP. - https://apidrifter.com/docs/rules/ : Rules reference. Every change rule and verification finding the engine can report, with its stable ID and default severity, so CI config and reports can refer to them. - https://apidrifter.com/docs/design/ : Design. How API Drifter is built and why: the pure diff engine, polarity-aware classification, the CLI and MCP server, the hosted service and its security model. - https://apidrifter.com/research/public-api-spec-drift-2026-11/ : We checked 13 public APIs against their own OpenAPI specs. 9 had hard mismatches. # Research: We checked 13 public APIs against their own OpenAPI specs. 9 had hard mismatches. Published 2026-11-02 by API Drifter. Canonical: https://apidrifter.com/research/public-api-spec-drift-2026-11/. Markdown: https://apidrifter.com/research/public-api-spec-drift-2026-11/index.md Every API that publishes an OpenAPI document is making a promise: send this, get that back. Generated clients, typed SDKs, mocks and contract tests all depend on it. We wanted to know how often the promise holds, so we pointed [API Drifter](/) at thirteen public APIs that publish their own spec, sent one read-only request per operation, and compared what came back to what the document said. The short answer: nine of the thirteen returned at least one successful response that a client generated from their own spec would choke on. ## What we counted, and what we did not Numbers like this are easy to inflate, so the strict rules first. A finding only counts as **hard** when a typed client generated from the spec would break on it: a field with the wrong type, a required field missing, a field present where the schema forbids unknown fields, a null where the schema does not allow null, or a success response in a media type the spec does not list. Nothing else is in the headline number. Three categories are reported separately and deliberately kept out of it: - **Undocumented fields.** OpenAPI allows extra properties unless a schema says `additionalProperties: false`. An extra field is a documentation gap, not a violation. - **Undocumented error statuses.** We sent no credentials, so we received 401, 403 and 429 responses that the specs do not mention. Almost no spec documents those, and nobody treats that as drift. - **Our own mistakes.** A handful of requests used values that violated the spec's constraints. Those are our fault and are labeled as such. We also found one false positive in our own validator during review, in how OpenAPI 3.0's `nullable: true` combines with `oneOf`. We fixed it, added a test, and re-verified every API from the recorded responses before writing this. ## Results The table below is generated from the survey data. Each row links to the full findings, where every finding carries the exact request we sent, the status we got, the location in the response body, and two suggested fixes: one if the spec is the source of truth, one if the implementation is. | API | Spec | Ops in spec | GET sent | 2xx | Hard | Body | Media | Errors | Input | Reviewer note | |-----|------|-------------|----------|-----|------|------|-------|--------|-------|---------------| | [Swagger Petstore (reference demo)](https://apidrifter.com/research/public-api-spec-drift-2026-11/petstore/) | [OpenAPI 3](https://petstore3.swagger.io/api/v3/openapi.json) | 19 | 8 | 6 | 1 | 1 | 0 | 0 | 0 | Confirmed: GET /user/login returns a plain-text session string under application/json. | | [APIs.guru directory API](https://apidrifter.com/research/public-api-spec-drift-2026-11/apis-guru/) | [OpenAPI 3](https://api.apis.guru/v2/openapi.yaml) | 7 | 6 | 6 | 13 | 13 | 0 | 0 | 0 | Confirmed: GET /specs/{provider}/{api}.json returns the OpenAPI document itself; the spec documents the API entry object (preferred, versions). | | [PokeAPI](https://apidrifter.com/research/public-api-spec-drift-2026-11/pokeapi/) | [OpenAPI 3](https://raw.githubusercontent.com/PokeAPI/pokeapi/master/openapi.yml) | 102 | 30 | 30 | 1 | 1 | 0 | 0 | 0 | Confirmed: evolution-chain baby_trigger_item is null; the spec does not allow null. | | [Wikimedia REST API (en.wikipedia.org)](https://apidrifter.com/research/public-api-spec-drift-2026-11/wikipedia-rest/) | [OpenAPI 3](https://en.wikipedia.org/api/rest_v1/?spec) | 49 | 19 | 13 | 8 | 20 | 0 | 9 | 0 | Confirmed: media-list items carry undocumented leadImage and srcset; /page/lint returns an array where the spec says object; 400 errors come back as application/json, documented as application/problem+json. | | [Discourse (meta.discourse.org)](https://apidrifter.com/research/public-api-spec-drift-2026-11/discourse-meta/) | [OpenAPI 3](https://docs.discourse.org/openapi.json) | 95 | 21 | 13 | 106 | 126 | 0 | 8 | 0 | Confirmed: category custom_fields holds plugin keys the spec forbids; topic tags are objects where the spec says strings; permission is null. The spec is generated from test fixtures and lags the live site. | | [Open Library](https://apidrifter.com/research/public-api-spec-drift-2026-11/openlibrary/) | [OpenAPI 3](https://openlibrary.org/openapi.json) | 58 | 20 | 9 | 1 | 0 | 1 | 9 | 4 | Confirmed: /health returns text/plain, documented as application/json; the spec's own example for search.json spellcheck_count violates its schema. Account endpoints 303-redirect when logged out. | | [GitHub REST API (unauthenticated)](https://apidrifter.com/research/public-api-spec-drift-2026-11/github/) | [OpenAPI 3](https://raw.githubusercontent.com/github/rest-api-description/main/descriptions/api.github.com/api.github.com.json) | 1254 | 40 | 9 | 0 | 0 | 0 | 28 | 0 | First run: gist file language is null but not nullable; event payloads match two oneOf branches. The rerun hit the anonymous rate limit (403) after nine successes, so this row understates GitHub; rerun with a token pending. | | [Gitea (gitea.com, self-served Swagger 2.0)](https://apidrifter.com/research/public-api-spec-drift-2026-11/gitea/) | [Swagger 2.0](https://gitea.com/swagger.v1.json) | 537 | 40 | 19 | 15 | 30 | 0 | 14 | 0 | Confirmed: activity feed comment and user objects are null; User carries an undocumented username field. Swagger 2.0 cannot express null, so the null findings are partly a format limitation. | | [Forgejo (codeberg.org, self-served Swagger 2.0)](https://apidrifter.com/research/public-api-spec-drift-2026-11/codeberg/) | [Swagger 2.0](https://codeberg.org/swagger.v1.json) | 506 | 40 | 16 | 23 | 35 | 0 | 18 | 1 | Same drift as Gitea (Forgejo fork), plus repo.parent null on feeds. | | [httpbin.org (self-served Swagger 2.0)](https://apidrifter.com/research/public-api-spec-drift-2026-11/httpbin/) | [Swagger 2.0](https://httpbin.org/spec.json) | 78 | 20 | 18 | 0 | 0 | 0 | 2 | 0 | Confirmed: GET /cookies/set and /cookies/delete respond 302, documented as 200 only. Earlier input mistakes fixed. | | [Nager.Date public holidays](https://apidrifter.com/research/public-api-spec-drift-2026-11/nager-date/) | [OpenAPI 3](https://date.nager.at/openapi/community-v3.json) | 8 | 8 | 7 | 0 | 0 | 0 | 0 | 0 | One 502 on LongWeekend during the run; transient, not drift. | | [Open-Meteo forecast API](https://apidrifter.com/research/public-api-spec-drift-2026-11/open-meteo/) | [OpenAPI 3](https://raw.githubusercontent.com/open-meteo/open-meteo/main/openapi/forecast.yml) | 1 | 1 | 1 | 0 | 1 | 0 | 0 | 0 | Minor: current_units.interval is undocumented. | | [GitLab REST API (gitlab.com, unauthenticated)](https://apidrifter.com/research/public-api-spec-drift-2026-11/gitlab/) | [OpenAPI 3](https://gitlab.com/gitlab-org/gitlab/-/raw/master/doc/api/openapi/openapi_v3.yaml) | 1864 | 15 | 8 | 3 | 3 | 0 | 6 | 0 | Confirmed: GET /projects/{id}/issues and /repository/contributors return arrays where the generated spec says object; namespace.parent_id is null but not nullable. gitlab.com rate-limits anonymous clients after a dozen calls, so coverage is thin. | **9 of 13 APIs** had at least one hard violation on a successful response, across 171 distinct schema locations. Data generated 2026-11-02, source: https://github.com/apidrifter/apidrifter/tree/main/tools/survey/results ## The ones worth reading **Discourse** publishes a spec generated from its own test fixtures, and it has drifted furthest from the live site. Topic tags come back as objects where the spec says strings. Category `custom_fields` carries plugin keys that the schema forbids with `additionalProperties: false`. Several fields the spec marks required only appear for logged-in users. None of this is subtle, and all of it would break a generated client on the first page it fetched. **Wikimedia's REST API** returns an array from its lint endpoint where the spec says object, its media lists carry `leadImage` and `srcset` fields the spec has never heard of, and its 400 errors come back as plain `application/json` rather than the documented `application/problem+json`. **GitLab's** generated OpenAPI document declares several list endpoints as returning an object. They return arrays. The namespace `parent_id` is null for top-level groups, and the spec does not allow null. **APIs.guru**, the directory most of us use to find specs, documents its per-API endpoint as returning an index entry with `preferred` and `versions`. It returns the full OpenAPI document instead. **Gitea and Forgejo** serve their own Swagger 2.0 documents, and Swagger 2.0 has no way to say a field can be null. The live API returns null for nested objects constantly. That is less an API that drifted than a spec format that cannot describe the API, which is its own kind of problem for anyone generating a client from it. **Swagger Petstore**, the reference demo for the whole format, returns a plain-text session string under an `application/json` content type on login. **GitHub** is the one we cannot yet report properly. The first pass found gist files with a null `language` the spec forbids, and event payloads matching two `oneOf` branches at once. The second pass hit the anonymous rate limit after nine successful responses, so GitHub's row understates it until we rerun with a token. ## What this does and does not show It shows that published specs and live behavior disagree often enough that checking is worth automating, and that the disagreements are the kind that break real clients, not pedantry about descriptions. It does not show a rate. Thirteen APIs chosen because they publish a spec, one request per operation, read-only, no credentials, is a sample, not a measurement. Treat the nine of thirteen as "common," not as a percentage. ## Method - One `GET` per operation, values from the spec's own examples or a small hand-written set per API, listed in the open-source target file. - A delay between requests, a User-Agent naming the survey, one polite retry on 429, and a hard request budget per API. - Swagger 2.0 documents were converted to OpenAPI 3 before checking. - Every request, status and finding is in the repository. The tool, the runner and the targets are open source, so anyone can rerun it. If you maintain one of these APIs and a finding is wrong, we want to know. If it is right, we are opening fixing pull requests on the smaller specs first. ## Per-API summaries ### Swagger Petstore (reference demo) - Page: https://apidrifter.com/research/public-api-spec-drift-2026-11/petstore/ (Markdown: https://apidrifter.com/research/public-api-spec-drift-2026-11/petstore/index.md) - Spec: https://petstore3.swagger.io/api/v3/openapi.json - 19 operations in spec, 8 requests sent, 6 successful; hard violations: 1; other body findings: 1; undocumented error statuses: 0 - Reviewer note: Confirmed: GET /user/login returns a plain-text session string under application/json. ### APIs.guru directory API - Page: https://apidrifter.com/research/public-api-spec-drift-2026-11/apis-guru/ (Markdown: https://apidrifter.com/research/public-api-spec-drift-2026-11/apis-guru/index.md) - Spec: https://api.apis.guru/v2/openapi.yaml - 7 operations in spec, 6 requests sent, 6 successful; hard violations: 13; other body findings: 13; undocumented error statuses: 0 - Reviewer note: Confirmed: GET /specs/{provider}/{api}.json returns the OpenAPI document itself; the spec documents the API entry object (preferred, versions). ### PokeAPI - Page: https://apidrifter.com/research/public-api-spec-drift-2026-11/pokeapi/ (Markdown: https://apidrifter.com/research/public-api-spec-drift-2026-11/pokeapi/index.md) - Spec: https://raw.githubusercontent.com/PokeAPI/pokeapi/master/openapi.yml - 102 operations in spec, 30 requests sent, 30 successful; hard violations: 1; other body findings: 1; undocumented error statuses: 0 - Reviewer note: Confirmed: evolution-chain baby_trigger_item is null; the spec does not allow null. ### Wikimedia REST API (en.wikipedia.org) - Page: https://apidrifter.com/research/public-api-spec-drift-2026-11/wikipedia-rest/ (Markdown: https://apidrifter.com/research/public-api-spec-drift-2026-11/wikipedia-rest/index.md) - Spec: https://en.wikipedia.org/api/rest_v1/?spec - 49 operations in spec, 19 requests sent, 13 successful; hard violations: 8; other body findings: 20; undocumented error statuses: 9 - Reviewer note: Confirmed: media-list items carry undocumented leadImage and srcset; /page/lint returns an array where the spec says object; 400 errors come back as application/json, documented as application/problem+json. ### Discourse (meta.discourse.org) - Page: https://apidrifter.com/research/public-api-spec-drift-2026-11/discourse-meta/ (Markdown: https://apidrifter.com/research/public-api-spec-drift-2026-11/discourse-meta/index.md) - Spec: https://docs.discourse.org/openapi.json - 95 operations in spec, 21 requests sent, 13 successful; hard violations: 106; other body findings: 126; undocumented error statuses: 8 - Reviewer note: Confirmed: category custom_fields holds plugin keys the spec forbids; topic tags are objects where the spec says strings; permission is null. The spec is generated from test fixtures and lags the live site. ### Open Library - Page: https://apidrifter.com/research/public-api-spec-drift-2026-11/openlibrary/ (Markdown: https://apidrifter.com/research/public-api-spec-drift-2026-11/openlibrary/index.md) - Spec: https://openlibrary.org/openapi.json - 58 operations in spec, 20 requests sent, 9 successful; hard violations: 1; other body findings: 0; undocumented error statuses: 9 - Reviewer note: Confirmed: /health returns text/plain, documented as application/json; the spec's own example for search.json spellcheck_count violates its schema. Account endpoints 303-redirect when logged out. ### GitHub REST API (unauthenticated) - Page: https://apidrifter.com/research/public-api-spec-drift-2026-11/github/ (Markdown: https://apidrifter.com/research/public-api-spec-drift-2026-11/github/index.md) - Spec: https://raw.githubusercontent.com/github/rest-api-description/main/descriptions/api.github.com/api.github.com.json - 1254 operations in spec, 40 requests sent, 9 successful; hard violations: 0; other body findings: 0; undocumented error statuses: 28 - Reviewer note: First run: gist file language is null but not nullable; event payloads match two oneOf branches. The rerun hit the anonymous rate limit (403) after nine successes, so this row understates GitHub; rerun with a token pending. ### Gitea (gitea.com, self-served Swagger 2.0) - Page: https://apidrifter.com/research/public-api-spec-drift-2026-11/gitea/ (Markdown: https://apidrifter.com/research/public-api-spec-drift-2026-11/gitea/index.md) - Spec: https://gitea.com/swagger.v1.json - 537 operations in spec, 40 requests sent, 19 successful; hard violations: 15; other body findings: 30; undocumented error statuses: 14 - Reviewer note: Confirmed: activity feed comment and user objects are null; User carries an undocumented username field. Swagger 2.0 cannot express null, so the null findings are partly a format limitation. ### Forgejo (codeberg.org, self-served Swagger 2.0) - Page: https://apidrifter.com/research/public-api-spec-drift-2026-11/codeberg/ (Markdown: https://apidrifter.com/research/public-api-spec-drift-2026-11/codeberg/index.md) - Spec: https://codeberg.org/swagger.v1.json - 506 operations in spec, 40 requests sent, 16 successful; hard violations: 23; other body findings: 35; undocumented error statuses: 18 - Reviewer note: Same drift as Gitea (Forgejo fork), plus repo.parent null on feeds. ### httpbin.org (self-served Swagger 2.0) - Page: https://apidrifter.com/research/public-api-spec-drift-2026-11/httpbin/ (Markdown: https://apidrifter.com/research/public-api-spec-drift-2026-11/httpbin/index.md) - Spec: https://httpbin.org/spec.json - 78 operations in spec, 20 requests sent, 18 successful; hard violations: 0; other body findings: 0; undocumented error statuses: 2 - Reviewer note: Confirmed: GET /cookies/set and /cookies/delete respond 302, documented as 200 only. Earlier input mistakes fixed. ### Nager.Date public holidays - Page: https://apidrifter.com/research/public-api-spec-drift-2026-11/nager-date/ (Markdown: https://apidrifter.com/research/public-api-spec-drift-2026-11/nager-date/index.md) - Spec: https://date.nager.at/openapi/community-v3.json - 8 operations in spec, 8 requests sent, 7 successful; hard violations: 0; other body findings: 0; undocumented error statuses: 0 - Reviewer note: One 502 on LongWeekend during the run; transient, not drift. ### Open-Meteo forecast API - Page: https://apidrifter.com/research/public-api-spec-drift-2026-11/open-meteo/ (Markdown: https://apidrifter.com/research/public-api-spec-drift-2026-11/open-meteo/index.md) - Spec: https://raw.githubusercontent.com/open-meteo/open-meteo/main/openapi/forecast.yml - 1 operations in spec, 1 requests sent, 1 successful; hard violations: 0; other body findings: 1; undocumented error statuses: 0 - Reviewer note: Minor: current_units.interval is undocumented. ### GitLab REST API (gitlab.com, unauthenticated) - Page: https://apidrifter.com/research/public-api-spec-drift-2026-11/gitlab/ (Markdown: https://apidrifter.com/research/public-api-spec-drift-2026-11/gitlab/index.md) - Spec: https://gitlab.com/gitlab-org/gitlab/-/raw/master/doc/api/openapi/openapi_v3.yaml - 1864 operations in spec, 15 requests sent, 8 successful; hard violations: 3; other body findings: 3; undocumented error statuses: 6 - Reviewer note: Confirmed: GET /projects/{id}/issues and /repository/contributors return arrays where the generated spec says object; namespace.parent_id is null but not nullable. gitlab.com rate-limits anonymous clients after a dozen calls, so coverage is thin. # CLI reference (https://apidrifter.com/docs/cli/) # apidrifter (CLI) Diff OpenAPI documents and MCP servers, classify breaking changes with stable rule IDs, verify a running implementation against its spec, and expose all of it to agents as an MCP server. ``` npx apidrifter breaking ./openapi-v1.yaml ./openapi-v2.yaml npx apidrifter mcp snapshot https://mcp.example.com/mcp --out snapshot.json npx apidrifter run ./openapi.yaml --base-url http://localhost:3000 --plan filled.json ``` Inputs are file paths or `https://` URLs. Plain `http://` is accepted only for localhost, because the CLI's job includes hitting your dev server. Documents are parsed with the engine's size, depth and reference limits; YAML is parsed with the core schema only. ## Use it from an agent `apidrifter serve` runs the same capabilities as an MCP server over stdio. Claude Code: `claude mcp add apidrifter -- npx -y apidrifter serve` Any client that takes a JSON config: ```json { "mcpServers": { "apidrifter": { "command": "npx", "args": ["-y", "apidrifter", "serve"] } } } ``` Tools: `plan_requests`, `run_requests`, `verify_observations`, `diff_specs`, `breaking_changes`, `changelog`, `mcp_snapshot`, `mcp_diff`, `list_rules`. ## Commands ### `apidrifter diff ` Every change between two OpenAPI documents or two MCP snapshots. The kind is detected; force it with `--kind openapi|mcp`. ### `apidrifter breaking ` Breaking changes only. Exit 1 if there are any. ### `apidrifter changelog ` Markdown changelog grouped by severity. ### `apidrifter mcp snapshot [--out file]` Connect to an MCP server over Streamable HTTP and write a snapshot of its tools, resources, resource templates and prompts, plus server name, version, instructions and capabilities. Servers that lack a capability are skipped for that list, not failed. `apidrifter mcp snapshot --stdio -- [args...]` spawns a local stdio server instead. The command is run without a shell; its environment contains only `PATH`, `HOME` and anything passed with `--env KEY=VALUE`. ### `apidrifter mcp diff ` Diff a saved snapshot against a live server or another snapshot. `--stdio -- ` works here too. ### `apidrifter plan [--out file]` The requests the spec implies, as JSON, with the parameter and body slots a caller must fill. The engine does not invent values; an agent or a person does. ### `apidrifter run --base-url [--plan filled.json]` Build requests from the plan and the filled values, execute them, verify the responses against the spec, and report findings with a fix for each side (spec as truth, implementation as truth). Values come from the filled plan first, then from `example`, `examples` or `default` in the spec. Operations whose required slots are still empty are skipped and listed as `skipped : missing values for ...` so an agent knows what to supply next. The filled plan is an array of `{ operationKey, values, body, mediaType, headers }` where `values` is keyed by `in:name` (`path:petId`, `query:limit`, `header:X-Tenant`, `cookie:session`). Flags: - `--header "Name: value"`, repeatable. Values are redacted in stored observations. - `--bearer-env VAR`: send `Authorization: Bearer` from an environment variable. The token never appears in output. - `--observations-out file`: write the redacted observations for later `verify` runs. - `--max-body-bytes n`: response body cap (default 8 MiB). Larger responses fail the run rather than being truncated. - `--timeout ms`: per-request timeout (default 30000). Redirects are not followed; the redirect status is recorded as the response. ### `apidrifter verify --observations ` Verify from recorded observations only, without sending anything. ### `apidrifter serve` Run as an MCP server over stdio. Tools: `diff_specs`, `breaking_changes`, `changelog`, `mcp_snapshot`, `mcp_diff`, `plan_requests`, `run_requests`, `verify_observations`, `list_rules`. Documents are given as `{ path }`, `{ url }` or `{ content }`. Paths must resolve inside the server's working directory; URLs follow the same policy as the CLI; `mcp_snapshot` accepts URLs only (the server never spawns processes on an agent's behalf). Results come back as structured content; the text summary carries counts and rule IDs only, so third-party text never reaches an agent outside a data field. Example client configuration: ```json { "mcpServers": { "apidrifter": { "command": "npx", "args": ["apidrifter", "serve"] } } } ``` ## Common options | Flag | Meaning | | ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | | `--format json\|markdown\|text` | Default: text on a terminal, json otherwise. | | `--config ` | Severity overrides, ignores and baseline. `apidrifter.config.json` or `.apidrifter.yaml` in the working directory is loaded automatically. | | `--fail-on breaking\|warning\|info` | Exit 1 when anything at or above this level is found. Default `breaking`. | | `--timeout ` | Network timeout. Default 30000. | ## Exit codes | Code | Meaning | | ---- | ----------------------------------------------------------------------- | | 0 | Nothing at or above the `--fail-on` level. | | 1 | Threshold exceeded. | | 2 | Usage or input error (bad flags, missing file, URL rejected by policy). | | 3 | The engine refused a document (too large, too deep, unsupported kind). | ## Configuration ```json { "severity": { "response-property-enum-value-added": "breaking", "api-version-changed": "ignore" }, "ignore": { "ids": ["endpoint-description-changed"], "subjects": ["GET /internal/**"] }, "baseline": [""] } ``` Configuration can make the engine quieter or louder. It never changes what is detected.