# Convert curl commands and Postman collections into OpenAPI (without losing fields)

Two artifacts appear in every API integration: a curl command someone pasted into Slack, and a Postman collection exported from someone's account. Both are presented as "the API docs." Neither is documentation — but both are salvageable as raw material for an OpenAPI spec, as long as you know what each conversion preserves and what it silently destroys.

## What a curl command actually contains

Take a realistic capture:

``` bash
curl -X POST 'https://api.example.com/v1/projects?dry_run=false' \
  -H 'authorization: Bearer eyJhbGciOi...' \
  -H 'content-type: application/json' \
  -H 'idempotency-key: 7b32f1' \
  --data '{"name":"Apollo","plan":"TEAM","region":"us-east-1"}'
```

A faithful parser extracts: method, base server (`https://api.example.com`), path (`/v1/projects`), one query parameter, three headers, and a JSON body inferring three string properties. That is the complete list. Everything else is missing:

-   Which headers are required on *every* request versus one-off (the bearer token is really a security scheme, not a header parameter).
-   Whether `dry_run` accepts only `true`/`false` or also other values; whether it defaults.
-   Whether `plan` is an enum (it is — `PRO`, `TEAM`).
-   Whether `name` has a max length or is required.
-   Every response — success shape, error envelope, status codes.
-   Path parameters (there are none here, but a URL with `/projects/12` requires inferring that `12` is an `{id}`).

A converter that emits a full operation with response schemas from this input is **making things up**. The honest output is a partial operation with gaps marked, which you then complete against the live service or the spec.

## The conversion workflow that does not lie

1.  **Paste the command** into the importer. The parser should normalize shell quoting, multiple `--data` fragments, `-u user:pass` into basic auth, and `-F` fields into multipart form bodies — these are the places naive regex parsers corrupt data.
2.  **Parameterize the URL.** Replace concrete ids with `{project_id}` and add the path parameter; group everything under a server URL so the spec is environment-agnostic.
3.  **Promote auth to a security scheme.** A bearer header becomes `bearerAuth` at the operation or global level; the captured token is discarded, never written into the spec.
4.  **Infer, then tighten.** The inferred body gets string types; you promote `plan` to an enum, `region` to an enum, and mark `name` required in one review pass.
5.  **Capture the response from a real send.** Send the request once against a sandbox and let the response body seed the 201 schema — observed, not invented.
6.  **Save the operation into the spec** under the right tag, not into a standalone scratch file.

In Powerduck this is the scratch-request flow: paste or record the command, debug it like a normal request, then promote it into the OpenAPI document through a dialog that asks for path, method, tag, and conflict handling (what happens if `POST /projects` already exists).

## Postman collections: the mapping

Postman Collection v2.1 carries more structure than curl, and the mapping is mostly mechanical:

| Postman                                      | OpenAPI                                                  |
|----------------------------------------------|----------------------------------------------------------|
| Collection `variable` baseUrl                | `servers[0].url` with templated variables                |
| Folder hierarchy                             | `tags` (usually one level — nested folders collapse)     |
| Request name + description                   | `summary` / `description`                                |
| Request headers / query params               | parameter objects (`required` from the `disabled` flag)  |
| Collection or request auth                   | `securitySchemes` + `security`                           |
| Body `raw` JSON                              | `requestBody.content.application/json.schema` (inferred) |
| Body `formdata`                              | `multipart/form-data` schema                             |
| Saved example responses                      | response examples / media-type `examples`                |
| Pre-request scripts                          | Nothing — these are test harness, not contract           |
| Postman dynamic variables `{{$randomEmail}}` | Example values, stripped of the runtime syntax           |

What routinely gets lost or mangled:

-   **Response schemas.** Postman stores example *bodies*, not schemas. They seed examples well; deriving `required` and nullability from one example is guessing and should be labeled as such.
-   **Multiple responses per status.** A request with five saved 200 examples becomes one example; distinct error statuses only exist if someone saved them as examples with the right code.
-   **Auth inheritance.** Auth set at the folder level is easy to miss; the converter must walk the inheritance chain or the spec comes out with no security at all.
-   **Environment-specific URLs.** Collections reference `{{baseUrl}}` with the real value living in an environment JSON that is usually not exported. You end up with `https://{{baseUrl}}/...` and must fix servers by hand.
-   **Folder-level descriptions and ordering.** Nested folders beyond two levels do not map to tags; decide on a flat tag taxonomy before importing.

## HAR: the better bulk source

When the source of truth is "whatever the frontend actually calls," a browser HAR export beats a hand-curated collection. It contains every request and response from a recorded session — headers, query strings, bodies, status codes, timings — so response schemas are seeded from observed traffic, and endpoints the collection forgot show up anyway. The same caveats as all traffic-based recovery apply: it only covers exercised paths, recorded payloads may contain real customer data (scrub them before importing), and one observed shape does not prove optionality.

## The end state matters more than the import

Converting artifacts is a means, not a goal. The failure pattern is converting the collection into a 900-line YAML file, saving it once, and never touching it while the API moves on. To avoid that:

-   Land the imported operations in the **same document** used for debugging, mocking, and tests, so editing the spec is part of daily work rather than a docs project.
-   Re-run imports against the spec with conflict detection — duplicate path/method pairs should prompt to merge, not silently overwrite your edited descriptions.
-   Treat curl and HAR imports as *incremental*: one captured request becomes one reviewed operation, the way the scratch-to-spec promotion works in the workspace, rather than a big-bang regeneration.

The [online demo](https://www.powerduck.com/app/?ref=powerduck.com) accepts curl, Postman, and HAR on the import screen; the longer case for spec-as-source is in [a curl command is not an API handoff](https://www.powerduck.com/blog/a-curl-command-is-not-an-api-handoff/).

**What to read next:** [stop pasting curl into ChatGPT](https://www.powerduck.com/blog/stop-pasting-curl-into-chatgpt/) covers what happens when these artifacts get pasted into AI chats instead of a contract, and [generate OpenAPI from existing code](https://www.powerduck.com/blog/generate-openapi-from-existing-code/) is the path when you have the source instead of captures.

