# Migrating from Postman to an OpenAPI-First Workflow — Routebase

> The migration everyone postpones is almost entirely mechanical — and mechanical work is what agents are for. How an AI with an MCP connection imports your collections, rebuilds your assertions, and verifies its own work against your real API.

Canonical page: https://routebase.dev/blog/migrating-from-postman-to-openapi-first/
Published: 2026-08-14 · The Routebase Team · OpenAPI, Postman, Migration

Almost nobody migrates off Postman because Postman is bad at being Postman.
Teams migrate because of something that happened without anyone deciding it:
the collection became the documentation. Then it became the contract. And a
collection was never designed to be either.

The difference is easy to state and easy to miss. A collection is a record of
*calls that worked* — this URL, these headers, this body, this is what came
back that one time. A spec is a statement about *what the API promises* — these
fields always exist, this one is optional, this enum has exactly four values,
this endpoint can fail this way. One is a transcript. The other is a contract.

Most teams know this. They postpone the move anyway, and always for the same
reason: somebody estimates it in weeks. Six hundred requests, years of
accumulated test scripts, environments nobody fully understands — a quarter of
work with no feature at the end of it.

That estimate was correct until recently. It isn't anymore, and the reason is
worth being precise about.

## Why this is agent work

Look closely at what a migration actually consists of, and there are no hard
decisions in it. There is a large amount of **transformation**: this Postman
field becomes that OpenAPI field; this `pm.test` line becomes that assertion
row; this `{{authToken}}` becomes that environment variable. Every step is
well-defined, every step is verifiable, and there are thousands of them.

That is the exact shape of work that is miserable for a person and trivial for
a machine. What made it a quarter-long project was never difficulty — it was
volume.

_Figure: Broken down, a migration is several hundred transformations of half a dozen kinds, each one mechanical and verifiable, and a handful of calls that genuinely need judgement — which enums are really enums, what the saved examples never showed, which scripts stay code. The quarter-long estimate came from the first number; the second is the part that stays with you._

The reason an agent can now do it end to end is that every one of those steps
is a **tool call**, not a click. Routebase ships an
[MCP server](/mcp-server/) on every plan: 400+ tools covering the same
operations your team performs in the UI, under the same role-based
permissions. Connect Claude, Cursor, or your agent of choice, point it at your
collection export, and the migration becomes something it can actually
perform — import the spec, correct the schemas, rebuild the suites, run them,
read the failures, fix them.

Here is what that looks like, step by step, and where you still matter.

## Step 1: Import — with a dry run first

Hand the agent your collection export and it can preview the result before
anything is written: a validation call in import mode returns exactly what the
real import would create — endpoint, schema and folder counts — plus every
warning, and writes nothing. The collection is recognized as a Postman file
automatically; nobody has to tell it what format it's looking at.

The structural mapping is more faithful than people expect:

- **Folders become tags.** A request inside a `Customers` folder comes out
  tagged `Customers`. Nested folders collapse to the innermost name — a request
  in `API / v2 / Customers` gets one tag, `Customers`.
- **Request names become summaries.** Whatever you called it in the sidebar is
  now the human-readable name of the operation.
- **`:id` segments become path parameters.** `{{baseUrl}}/customers/:customerId`
  becomes `/customers/{customerId}` with a declared, required path parameter.
- **Query params and headers become parameters** — minus the ones the client
  generates anyway (`Content-Type`, `Host`, `User-Agent` and friends), which
  would be noise in a spec.
- **Saved responses become response schemas**, inferred from the example body.
- **Auth becomes a security scheme**, at collection level and per request.

So this request:

```json
{
  "name": "Get customer",
  "request": {
    "method": "GET",
    "url": {
      "raw": "{{baseUrl}}/customers/:customerId?include=invoices",
      "path": ["customers", ":customerId"],
      "query": [{ "key": "include", "value": "invoices" }],
      "variable": [{ "key": "customerId", "value": "c-1024" }]
    },
    "header": [{ "key": "X-Tenant-Id", "value": "acme" }]
  },
  "response": [{ "name": "200 OK", "code": 200, "body": "…" }]
}
```

— with that saved `200 OK` body being an ordinary customer record:

```json
{
  "id": "9f2c1d4e-5b6a-4c3d-8e7f-0a1b2c3d4e5f",
  "email": "ada@example.com",
  "createdAt": "2026-03-04T09:12:00Z",
  "plan": "pro",
  "seats": 12
}
```

comes out as this operation:

```yaml
/customers/{customerId}:
  get:
    summary: Get customer
    tags: [Customers]
    parameters:
      - name: customerId
        in: path
        required: true
        schema: { type: string }
      - name: include
        in: query
        required: true
        schema: { type: string }
      - name: X-Tenant-Id
        in: header
        required: false
        schema: { type: string }
    responses:
      "200":
        description: 200 OK
        content:
          application/json:
            schema:
              type: object
              required: [id, email, createdAt, plan, seats]
              properties:
                id: { type: string, format: uuid }
                email: { type: string, format: email }
                createdAt: { type: string, format: date-time }
                plan: { type: string }
                seats: { type: integer }
```

Look closely at `include`. In Postman it was an enabled row in the query table,
which is all a query row can be — so it arrived as `required: true`. If
`include` is really an optional filter, that line is wrong.

Which is the point of the whole exercise: the import produces a structurally
valid spec, and then somebody has to supply the intent that was never written
down. The warnings tell you where. Pre-request and test scripts, collection
variables beyond the base URL, duplicate requests merged into one operation,
duplicate parameters dropped — each one is flagged, with the request it came
from.

Traditionally that list is the reason migrations stall: a few hundred lines of
"you'll want to look at this." For an agent it's the opposite — it's a
work queue, already itemized, already located.

## Step 2: Closing the inference gap

Inferred schemas are better than nothing and worse than they look. The example
above is a fair result — types are real, `uuid`, `email` and `date-time`
formats were recognized from the values, `seats` came through as an integer
rather than a string. But every line of it was derived from **one recorded
response**, which produces three predictable errors:

**Everything present became required.** `plan` and `seats` happened to have
values in that body, so both are now required. If a trial account returns no
`seats`, the spec is wrong in the direction that breaks generated clients.

**Everything absent doesn't exist.** Optional fields the example didn't include
aren't marked optional — they're missing entirely. Consumers will discover them
in production and depend on them anyway.

**Nothing knows its own range.** `plan: "pro"` is `type: string`. It is almost
certainly an enum with three or four members, and that enum is one of the most
valuable things you can write down.

_Figure: Every failure has the same cause: a single saved example is the entire evidence base — a narrow window onto the contract. Everything inside the window hardens on its way into the schema (present values become required, a string that is really an enum stays a string), and everything outside it simply doesn't exist: the fields the example never carried aren't marked optional, they are missing entirely, with nothing to flag the gap._

Here the agent has an advantage no single import pass has: it can read your
*entire* collection at once. Four saved responses for the same endpoint,
scattered across four duplicate requests, are four observations of the same
schema — and a field that's present in three of them and absent in the fourth
is optional, demonstrably. The same sweep surfaces the enum members, the
nullable fields, and the formats that only one example happened to show. It
then corrects the spec directly: parameters updated, schemas rewritten,
responses added.

Then there's the half of the contract collections rarely record at all: the
failures. Most saved responses are the happy path, because that's what you were
debugging when you hit **Save** — so a freshly imported spec often documents
every success and no errors, while `401`, `404`, `409` and `422` are exactly
the responses consumers write code against. An agent that has read your
handlers or your error middleware can draft those too, ideally in a
[consistent error format](/blog/consistent-error-handling-rfc-9457/), and you
review the result rather than typing it.

## Step 3: Every script has a destination

This is the part everyone braces for, and the fear is misplaced. Yes: your
`pm.test("status is 200", …)` JavaScript does not survive the import, because
assertions were never part of the OpenAPI contract. But "doesn't import"
is not the same as "has nowhere to go." Sort your scripts by what they actually
do and each group has a specific home:

**Checks become assertions.** The overwhelming majority of `pm.test` bodies
verify something about a response, and that translates into a row of data —
type, operator, target, expected value — across six assertion types and ten
operators. `pm.response.to.have.status(200)` becomes a `status` / `equals` /
`200` assertion. `pm.expect(json.plan).to.eql("pro")` becomes a `jsonPath`
assertion on `$.plan`. This is the single best case for handing work to a
machine: the target is structured data, so the agent isn't writing logic you'd
have to audit line by line — it's filling in fields you can read in a diff.

**Token stashing becomes response extraction.** The classic
`pm.environment.set("token", json.access_token)` at the end of a login request
becomes an extraction rule: capture `$.access_token` from the response into a
runtime variable that later steps reference as `{{token}}`. Declarative,
inspectable, and no longer invisible in a script tab.

**Chained requests become scenarios.** The sequence you encoded by ordering
requests in a folder and passing variables between them becomes an explicit
multi-step scenario, with the extraction rules wiring one step's output to the
next step's input.

**Environments become environments.** Every `{{apiVersion}}`, `{{tenantId}}`,
`{{authToken}}` the importer warned about is a value with a proper place —
environment variables and auth schemes, with secrets marked as secrets instead
of sitting in a collection file that gets shared.

**And genuinely computational logic stays code.** Signing a request, computing
an HMAC, deriving a nonce — that is real work a script should do, and it
belongs in a reusable pre-request or post-response snippet. Note the word
*reusable*: the same signing logic copy-pasted into forty Postman requests
becomes one snippet referenced forty times. The migration is where that
duplication gets collapsed.

So the honest count isn't "your scripts are lost." It's that most of them stop
being scripts, a few stay scripts and get deduplicated, and an agent can place
every one of them.

## Step 4: The loop that makes this trustworthy

Everything above would still be a leap of faith if the output were only
generated. It isn't — the agent can check its own work, and this is the part
that separates a migration you can trust from a pile of plausible YAML.

The same MCP connection that creates a test suite can **run** it. So the loop
closes:

1. Import the collection, correct the schemas, rebuild the suites.
2. Lint the spec against your style rules; fix what the linter reports.
3. Run the suites against a real environment.
4. Read the failures — and a failure here is *information*, not a setback. Each
   one is either a test the agent got wrong, or a place where your collection
   was already lying about your API.
5. Fix and repeat until it's green.

That fourth step is the one worth sitting with. A failing assertion after
migration usually isn't a migration bug. It's a contract violation that has
been in your API for months, invisible because the collection recorded what the
API *did* rather than what it *promised*. The migration finds them because,
for the first time, something is comparing the two.

None of this runs unsupervised in a way you can't see. The agent works through
the same permission system as your team — creating specs needs `specs:write`,
touching tests needs `tests:write` — so what an agent may do is what its role
allows, and every change is a diff you can review before it ships.

## Step 5: One artifact, not four

Once the spec is honest, the payoff isn't "we have a spec now." It's that
mocks, docs, and tests stop being separate objects kept aligned by hand.

In the collection world, a [mock server](/api-mocking/), a docs page, and a
monitor each encode their own copy of the contract, and each drifts on its own
schedule. Derive all three from one spec and there is nothing left to
synchronize — not because the sync got better, but because there is only one
document to change. That is also the most practical defence against
[contract drift](/blog/why-openapi-specs-drift/): the drift you never have is
the drift between artifacts that don't exist separately.

## What stays in Postman

Probably a scratchpad, and that's fine. Poking at a third-party endpoint,
reproducing a bug with a hand-edited header, trying something once — that's
what a request client is good at, and none of this removes the need. The
distinction worth keeping is which artifact is allowed to be the source of
truth. A scratchpad nobody treats as documentation is a useful tool. A
scratchpad that quietly became the contract is how you got here.

## Start with one folder

You do not have to commit to the whole collection to find out whether this
works. Export one folder — a dozen requests you know well — connect an agent to
your workspace, and let it run the loop: import, correct, rebuild, run. You'll
have a governed spec, working test suites, and a mock server for that folder in
an afternoon, and a precise sense of what the remaining folders will cost.

The reason to do it now rather than next quarter is simply that the estimate
changed. The migration was always mechanical; what's new is that mechanical
work no longer has to be done by the person who understands the API. Their job
is to review it — which is the only part that ever needed them.

[Import your collection](/postman-alternative/) and see what the first pass
gives you. Even the warning list is a useful read.

---

[Routebase](https://routebase.dev/) — [Sign up](https://app.routebase.dev/): Every account starts with a 14-day Pro trial — no credit card required.
