Plenty of production APIs have no OpenAPI description at all. That has always been a problem for documentation and SDKs. In the AI era it is also an integration problem, because an OpenAPI description is the contract AI agents read to learn how to call an API.
There are two usual fixes: write the description by hand, or ask an AI assistant to derive it from the source code. Writing by hand is slow. The source-code approach fails on large codebases: the model loses context, hallucinates API behavior, and generates convincing but subtly inaccurate descriptions that are impossible to verify.
Instead of guessing from source code, the new redocly generate-spec command uses actual recorded HTTP traffic to build an accurate, initial API specification. First, it automatically creates a base description from the traffic. Then, it uses AI selectively - processing one endpoint at a time, grounding every change in real data, and verifying all output.
The command accepts HAR files, Kong logs, Nginx/Apache JSON logs, and NDJSON - a single file or a whole folder of them.
From the recorded exchanges it builds a baseline deterministically:
- Identifier-like path segments (numeric IDs, UUIDs, prefixed tokens like
prd_…) become named path parameters, so a hundred URLs become one templated path. - Request and response schemas are merged across all observations; a property becomes optional as soon as one sample omits it.
- Alternative body shapes for the same operation are preserved as
oneOfvariants, and object shapes that repeat across the document are extracted intocomponents/schemas. - String values are analyzed conservatively: strings that consistently match well-known patterns get a
format(uuid,date-time,email,uri), and strings that only ever take a small set of repeated values become anenum.
Here is what that looks like end to end, on Redocly Cafe - our public demo API, so you can follow along on the same traffic if you want. Pretend for a moment that its OpenAPI description doesn't exist, and let's reconstruct it from traffic.
First, record some. The proxy command starts a local reverse proxy that captures everything passing through into a HAR file:
redocly proxy --target https://api.cafe.redocly.com --har ./cafe.harProxy listening on http://127.0.0.1:4040 → forwarding to https://api.cafe.redocly.com/
Recording traffic to ./cafe.har
Press Ctrl+C to stop.The proxy records complete exchanges - URLs, headers, cookies, and request and response bodies. A capture taken against production contains whatever that traffic contained, including credentials and personal data. Record against a test environment with synthetic data. This matters in every mode, not only with AI: observed values end up in the generated description as enums and examples, so a description inferred from real user data is not safe to share either.
Send a few requests through it, the way a real client would: browse the menu, filter it, then take menu item IDs from the response and download some photos:
curl http://127.0.0.1:4040/menu
curl "http://127.0.0.1:4040/menu?category=dessert"
curl "http://127.0.0.1:4040/menu?category=beverage"
for id in $(curl -s http://127.0.0.1:4040/menu | jq -r '.items[:3][].id'); do
curl -o "$id.png" "http://127.0.0.1:4040/menu-item-images/$id"
donePress Ctrl + C to stop the proxy - it reports how many exchanges it captured and writes the HAR file.
Now ask for a description:
redocly generate-spec ./cafe.har --title "Cafe API" -o cafe-openapi.yamlInferred a baseline OpenAPI description from traffic: 2 operation(s).
Written to: cafe-openapi.yaml
Done in 0s.It outputs a valid OpenAPI 3.2 spec (~100 lines long) from just those few captured requests. The file sets the server URL and outlines a path for each endpoint found:
openapi: 3.2.0
info:
title: Cafe API
version: 1.0.0
servers:
- url: https://api.cafe.redocly.com
paths:
/menu:
# …
/menu-item-images/{menu-item-imageId}:
# …Let's look at what the inference did.
Start with the paths: the photo URLs became one templated path, because the prd_… identifiers were recognized as IDs and turned into a required path parameter:
/menu-item-images/{menu-item-imageId}:
get:
operationId: get-menu-item-images-menu-item-imageId
responses:
'200':
description: OK
parameters:
- name: menu-item-imageId
in: path
required: true
schema:
type: stringInside the /menu response schema, every menu item property got a type, and the observed values were analyzed for more detail:
properties:
# …
price:
type: integer
category:
type: string
enum:
- beverage
- dessert
createdAt:
type: string
format: date-time
photoUrl:
type: string
format: uricategory became an enum because every observed value was one of the two, and createdAt and photoUrl matched well-known patterns in every sample. The category query parameter on the same operation stayed a plain string - two observations are not enough evidence, so the inference stays conservative:
parameters:
- name: category
in: query
required: false
schema:
type: stringThe merge across samples also detected which properties are not always present. Beverages have volume, desserts have calories, so volume, containsCaffeine, and calories are typed but absent from the required list:
properties:
# …
volume:
type: integer
containsCaffeine:
type: boolean
calories:
type: integer
required:
- id
- name
- price
- photoTextDescription
- category
- createdAt
- updatedAt
- object
- photoUrlThe result is still only a hypothesis - the description knows only what the traffic showed. price is an integer because every observed price happened to be a whole number. name became enum of the handful of menu items in the capture. Endpoints that nobody called are missing, there are no human-readable descriptions, and names like {menu-item-imageId} are generated mechanically - rename them when you review. More traffic makes the hypothesis stronger. You can record traffic in your e2e tests using Redocly CLI proxy command and then feed it to generate-spec command.
The baseline is structurally correct, but it can't explain anything. That can be improved with AI. Let's explore with --with-ai parameter:
redocly generate-spec ./cafe.har --title "Cafe API" --with-ai --ai-provider claude -o cafe-openapi.yamlAs a result, the AI fills in everything the deterministic engine couldn't:
- Documentation - summaries and descriptions for every operation, parameter, and property.
- Semantic types and constraints - business logic like
minimum: 0on prices, ID pattern matches, and data formats inferred from field meaning rather than repeated values. - Real API design - variant payloads modeled as
oneOfunions with discriminators, and shared structures extracted intoallOfcomponents. - Over-fitting cleanup - overly restrictive enums converted into plain typed fields with realistic examples, preserving only true enums.
"Ask AI for an OpenAPI description" usually fails for one reason: context. Give a model a whole codebase - or a whole traffic dump - and it loses track, then fills the gaps with plausible guesses. generate-spec structures the work so this cannot happen:
- One operation per prompt. Each prompt contains a single operation from the baseline, the component schemas it references, and a small sample of its recorded exchanges - a few real requests, picked so that every observed payload variant is included.
- Determinism and AI work together, not against each other. The AI does not rebuild anything from scratch - it refines the baseline.
- Nothing is trusted blindly. Each AI response is validated against baseline. If operations differ too much, the response is treated as rejected.
Three providers are supported - claude (Claude Code), codex (Codex CLI), and cursor (Cursor CLI). Each one runs the locally installed CLI in non-interactive mode, so the subscription you already use and pay for does the work. --ai-provider is optional and defaults to claude; pick a model with --ai-model or let the provider use its default.
Operations are refined in parallel. --ai-concurrency (default 4) is the main way to make it faster.
--with-ai sends captured traffic samples - URLs, query strings, and payloads to your AI provider.
Three built-in safeguards minimize data exposure:
- headers are omitted to keep auth tokens and cookies strictly local;
- environments are isolated by running the CLI in an empty directory, preventing local files or custom rules from leaking into the prompt;
- secrets are scrubbed via model instructions that block credential-like values from ending up in examples.
These guardrails reduce risk, but they aren't foolproof. Run captures in a sandbox and verify traffic is clean before sending data to an external provider.
With Cafe API we can answer it precisely: its real, handwritten OpenAPI description exists - we only pretended it doesn't.
We recorded a fuller session than the small capture above - one that covers every endpoint: the OAuth2 client registration flow, menu items created in both categories, orders placed, updated, and deleted, photo downloads, and the errors a real session produces along the way (a 400, a few 404s, even a 409). Then we generated a description twice from that one capture - once deterministically, once with --with-ai - and scored both against the handwritten description.
Let's look at the results:
For response schemas:
| Metric | Deterministic | --with-ai |
|---|---|---|
| Response properties recovered | 97.5% | 98.3% |
| Correct types | 100% | 100% |
Correct required | 69.2% | 72.2% |
| Formats documented, recovered | 53.1% | 62.5% |
| Enums documented, recovered | 66.7% | 66.7% |
required documented, recovered | 91.3% | 94.2% |
| Properties carrying a description | 0% | 97.5% |
| Numeric and length constraints | 0 | 21 |
| Run time | under 1s | 1-15 min¹ |
¹ Depends heavily on the model and --ai-concurrency - the largest models at the default concurrency are the slowest, while a rerun of the same capture with --ai-concurrency 6 finished in under a minute.
What --with-ai adds is what determinism cannot produce at all: descriptions on nearly every property, constraints, examples, and formats inferred from context rather than repetition.
For request bodies we could see more improvements with AI:
| Metric | Deterministic | --with-ai |
|---|---|---|
| Request properties recovered | 55.9% | 61.8% |
| Correct types | 78.9% | 100% |
Correct required | 81.8% | 100% |
| Properties carrying a description | 0% | 85.7% |
POST /menu accepts multipart/form-data, and every value in a multipart form is sent as a string. The deterministic baseline can only write down what it saw:
requestBody:
content:
multipart/form-data:
schema:
type: object
properties:
name:
type: string
price:
type: string
category:
type: string
volume:
type: string
containsCaffeine:
type: string
calories:
type: string
required:
- name
- price
- categoryThis is the one place in the whole experiment where the baseline was wrong rather than just incomplete - price, volume, calories, and containsCaffeine are not strings - and it is exactly what the AI fixed. The same request body after --with-ai:
requestBody:
content:
multipart/form-data:
schema:
oneOf:
- $ref: '#/components/schemas/BeverageCreate'
- $ref: '#/components/schemas/DessertCreate'
discriminator:
propertyName: category
mapping:
beverage: '#/components/schemas/BeverageCreate'
dessert: '#/components/schemas/DessertCreate'BeverageCreate:
description: Creation request for a beverage menu item.
allOf:
- $ref: '#/components/schemas/MenuItemCreateBase'
- type: object
properties:
category:
type: string
enum:
- beverage
volume:
type: integer
minimum: 0
description: Serving volume in millilitres.
example: 180
containsCaffeine:
type: boolean
description: Whether the beverage contains caffeine.
example: true
MenuItemCreateBase:
type: object
description: Attributes shared by every menu item creation request.
properties:
name:
type: string
description: Human-readable name of the menu item.
example: flat-white
price:
type: integer
minimum: 0
description: Price in the smallest currency unit (for example cents).
example: 450
# …
required:
- name
- price
- categoryEvery type is corrected, and constraints, descriptions, and examples appeared - but the bigger change is the shape itself. The AI noticed from the samples that beverages and desserts carry different fields, and modeled the union explicitly: allOf composition over the shared attributes, selected by a category discriminator. The handwritten description models menu items exactly the same way - oneOf beverage or dessert, discriminated by category. Traffic plus AI arrived at the same design the API team chose by hand; the baseline could only offer one merged object with everything optional.
One caveat applies to every API: path parameters. Every Cafe path parameter was recognized, because its identifiers are prefixed tokens (prd_…, ord_…) that the deterministic inference detects. On APIs whose path segments are ordinary words - organization names, repository names, branches - those segments stay hardcoded, and AI refinement cannot fix them, because a refined operation must keep its path. Reviewing paths by hand is the one step you cannot skip.
The generate-spec command is experimental. Flags, output, and behavior may change - including breaking changes - in upcoming releases while we shape it with your feedback.
The generate-spec command is available now in the latest Redocly CLI - see the command reference for all options.
Once you have your spec generated don't let it go stale. Use drift command to ensure it stays up-to-date.