Skip to content

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.

From traffic to description

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 oneOf variants, and object shapes that repeat across the document are extracted into components/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 an enum.

Example: the Cafe API

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.har
Proxy 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.
Record in a sandboxed environment

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"
done

Press 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.yaml
Inferred 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: string

Inside 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: uri

category 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: string

The 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
  - photoUrl

The 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.

Refine it with AI

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.yaml

As 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: 0 on prices, ID pattern matches, and data formats inferred from field meaning rather than repeated values.
  • Real API design - variant payloads modeled as oneOf unions with discriminators, and shared structures extracted into allOf components.
  • Over-fitting cleanup - overly restrictive enums converted into plain typed fields with realistic examples, preserving only true enums.

Built to keep the AI on track

"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.

Runs on the AI you already have

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.

Traffic leaves your machine

--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.

How much does --with-ai actually add?

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:

MetricDeterministic--with-ai
Response properties recovered97.5%98.3%
Correct types100%100%
Correct required69.2%72.2%
Formats documented, recovered53.1%62.5%
Enums documented, recovered66.7%66.7%
required documented, recovered91.3%94.2%
Properties carrying a description0%97.5%
Numeric and length constraints021
Run timeunder 1s1-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:

MetricDeterministic--with-ai
Request properties recovered55.9%61.8%
Correct types78.9%100%
Correct required81.8%100%
Properties carrying a description0%85.7%

What actually changed

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
          - category

This 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
    - category

Every 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.

Experimental

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.

Get started

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.

Latest from our blog

Generate OpenAPI from real traffic (with...

The new generate-spec command infers an OpenAPI description from recorded HTTP traffic - with AI refinement.

Document your MCP server with introspect...

The new introspect-mcp command asks a running MCP server what it can do and records its tools, prompts, and resources in the x-mcp extension of your OpenAPI description. The command also has a --check mode that fails CI when the docs drift.

llms.txt is still overhyped

A year after we called llms.txt overhyped, the server logs of the whole web agree. The index file goes unread. The Markdown behind it is what agents actually use.