Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
383 changes: 383 additions & 0 deletions blog/generate-openapi-from-traffic.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,383 @@
---
template: ../@theme/templates/BlogPost
title: Generate OpenAPI from real traffic (with AI)
description: The new generate-spec command infers an OpenAPI description from recorded HTTP traffic - with AI refinement.
seo:
title: Generate OpenAPI from real traffic (with AI)
description: The new generate-spec command infers an OpenAPI description from recorded HTTP traffic - with AI refinement.
author: adam-sobaniec
publishedDate: "2026-09-14"
categories:
- redocly:redocly-cli
- redocly:product-updates
- api-specifications:openapi
---

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:

```bash
redocly proxy --target https://api.cafe.redocly.com --har ./cafe.har
```
Comment thread
sobanieca-redocly marked this conversation as resolved.

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

{% admonition type="warning" name="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.
{% /admonition %}

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:

```bash
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 <kbd>`Ctrl`</kbd> + <kbd>`C`</kbd> to stop the proxy - it reports how many exchanges it captured and writes the HAR file.

Now ask for a description:

```bash
redocly generate-spec ./cafe.har --title "Cafe API" -o cafe-openapi.yaml
```

```sh
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:

```yaml
openapi: 3.2.0
Comment thread
sobanieca-redocly marked this conversation as resolved.
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:

```yaml
/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:

```yaml
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:

```yaml
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:

```yaml
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:

```bash
redocly generate-spec ./cafe.har --title "Cafe API" --with-ai --ai-provider claude -o cafe-openapi.yaml
Comment thread
sobanieca-redocly marked this conversation as resolved.
```

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.

{% admonition type="warning" name="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.
{% /admonition %}

## How much does `--with-ai` actually add?

With Cafe API we can answer it precisely: its real, handwritten [OpenAPI description](https://cafe.redocly.com/openapi/cafe) 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 `404`s, 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% |


## 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:

```yaml
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`:

```yaml
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'
```

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

{% admonition type="warning" name="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.
{% /admonition %}

## Get started
Comment thread
sobanieca-redocly marked this conversation as resolved.

The `generate-spec` command is available now in the latest [Redocly CLI](https://redocly.com/docs/cli) - see the [command reference](https://redocly.com/docs/cli/commands/generate-spec) for all options.

Once you have your spec generated don't let it go stale. Use [`drift` command](./catch-api-drift.md) to ensure it stays up-to-date.
Loading