Migrating from Postman
An import is a one-off: it reads a Postman collection (and, optionally, its environment files), writes plain API Craft files into a new API, and steps aside. There is no live link to Postman, no re-import and no sync. What it writes is ordinary project content you edit with the normal tools.
What it does for you:
- every request becomes a request instance, grouped into resources and operations by the same mapper an OpenAPI import uses;
- each operation gets a contract (parameters, request body, responses) inferred from your requests and their saved examples;
- collection variables and environments become API Craft environments, with secrets kept out of committed files;
- bearer, API key and basic auth carry over as-is.
What it deliberately does not do is translate JavaScript. Postman scripts are copied into
your project commented out, under a throw, so nothing passes by accident before you rewrite it.
1. Export from Postman
- Collection: on the collection, ⋯ → Export, format Collection v2.1 (v2.0 works too; v1 is refused).
- Environments: Environments → ⋯ → Export, one file per environment you want to keep.
2. Preview the import
Nothing is written until you apply. Start with the plan:
craftr import postman plan ./petstore.postman_collection.json \
--environment ./dev.postman_environment.json \
--environment ./prod.postman_environment.json
The plan is JSON. The parts worth reading:
| Key | What it tells you |
|---|---|
api | The slug (derived from the collection name; --api picks another) and the base URL. |
resources | How requests were grouped into resources and operations, with method and path. |
instances | One per Postman request, with its name and concrete URL. |
secrets | The candidates you must decide before applying (see below). |
postman.environments | The environments that will be created, their level and variables. |
postman.middlewares / postman.workflows | Where your pre-request and test scripts will land. |
postman.standalone | Requests on another host, kept as standalone instances. |
postman.unconverted | Everything that could not be translated (see What isn't converted). |
In the app, open the New API wizard and pick Import a Postman collection under the name field. Choose the collection and its environment files, then Analyze: the wizard shows the same plan and the secret decisions on one screen.

3. Decide every secret
Postman doesn't really separate values from secrets, so the import proposes candidates and you
decide each one. Nothing is written until every candidate has a decision; there is no --yes.
A value is a candidate when its name looks like a credential (token, key, secret, password,
auth, session, credential…) or Postman marked it secret. Candidates come in three kinds:
| Key | Example | What it is |
|---|---|---|
variable:<name> | variable:access_token | A collection or environment variable. |
auth:<type> | auth:bearer, auth:basic, auth:apikey | A credential typed directly into an auth tab instead of a variable. |
header: / query: / cookie: | header:x-api-key | A literal credential in a request parameter. |
Two decisions:
- secret: the value goes to the gitignored
<environment>.secrets.envand requests reference it as{{variable}}. - drop: the value is not imported. A variable stays declared (secret, without value) so you fill it later; a literal header or query parameter is left out of the request.
craftr import postman apply ./petstore.postman_collection.json \
--environment ./dev.postman_environment.json \
--secret variable:access_token --drop auth:apikey
# or decide everything the same way
craftr import postman apply ./petstore.postman_collection.json --secret-all
On a terminal, candidates you didn't pass are asked interactively. In a pipeline the command fails and lists them. The import refuses an API slug that already exists: it always creates a new API.
A credential typed directly in an auth tab is always moved behind a variable (bearerToken,
basicPassword, apiKeyValue), whatever you decide: the decision only says whether its value is
written to .secrets.env.
What gets converted
| Postman | API Craft |
|---|---|
| Request | A request instance under a defined operation. Requests sharing a method and path share the operation. |
| Folders | Not reproduced: the mapper groups by path, as for an OpenAPI import. Regroup with schema rules if needed. |
{{baseUrl}}/… (any leading host variable) | The API base URL {{baseUrl}}, with one value per environment. Requests on another host become standalone instances. |
:id and {{var}} path segments | Path parameters ({id}), with the request's value on the instance. |
| Collection variables | The local environment. |
| Environment files | One environment each, named after the file's environment; the level comes from the name (prod → production, staging → staging, otherwise local). An environment value wins over the collection variable, as in Postman. |
| Variable names | camelCase (base_url → baseUrl, page-size → pageSize); every reference is rewritten. |
| Bearer, API key, basic auth | The same declarative auth. Collection auth becomes the API default; folder and request auth that differs lands on the instances; "No auth" stays explicit. |
| JSON, text, XML, urlencoded and GraphQL bodies | The instance body (GraphQL as JSON { query, variables }). |
| Saved examples | Response codes and body schemas in the operation's contract. |
| Pre-request scripts | One before middleware per script, chained on each instance in Postman order: collection, folders, request. |
| Test scripts | One workflow per tested request, <api>-<request>: a request block, then one assert block per test script (collection, folders, request). |
Scripts: copied, never translated
There is no pm.* compatibility layer. Each script is written in full, line by line in comments,
under a throw that fails until you rewrite it:
// apis/petstore/middlewares/collection-pre-request.ts
import type { BeforeRequestContext } from '@apicrafthq/script-sdk'
export const handle = async (ctx: BeforeRequestContext): Promise<void> => {
throw new Error('Imported Postman pre-request script, not converted. …')
// --- Postman pre-request script of Collection (not executable) ---
// pm.request.headers.add({ key: 'X-Request-Id', value: pm.variables.replaceIn('{{$guid}}') });
}
Line comments are used (never /* */), so a script containing */ can't break the file, and the
project always builds. Collection and folder pre-request scripts are one middleware shared by every
request they applied to, so you rewrite them once. Test scripts are copied into each workflow,
because workflow scripts belong to their workflow.
What isn't converted
Everything below is listed in the plan under postman.unconverted, with where it was found:
| Kind | Why, and what to do |
|---|---|
setNextRequest | Postman's runner ordering has no equivalent. Rebuild the sequence as one workflow by moving blocks. |
formdata / binaryBody | Multipart and file bodies are not supported yet; the body is left out. |
unparsedJson | A raw JSON body that doesn't parse (an unquoted {{var}}, a comment). Kept as text; fix it in the request. |
dynamicVariable | {{$guid}}, {{$timestamp}}… are kept as typed and won't resolve. Generate the value in a middleware. |
complexAuth | OAuth2, digest, AWS signature… become a custom auth script with the configuration commented out (literal values shown as <redacted>). |
4. Finish the migration
- Fill the secrets you dropped: in each environment's
.secrets.env, or the environment view in the app. - Rewrite the scripts. Start with the middlewares (every request using them fails until they
are rewritten), then the test workflows. A Postman test usually becomes a few lines:
// Postman: pm.test('found', () => pm.response.to.have.status(200)); import type { AssertContext } from '@apicrafthq/script-sdk' export const run = (ctx: AssertContext): void => { const status = ctx.blockResult('request').response?.status if (status !== 200) throw new Error(`expected 200, got ${status}`) }
Some assertions don't need a script at all: anassertblock can checkstatus, headers and body fields declaratively (seeassert). - Regroup the workflows. The import makes one workflow per tested request on purpose: whether a
folder was a sequence or just storage is not in the file. Drag blocks onto another workflow in
the sidebar (or
craftr workflow move-block) to build real scenarios. - Correct the contracts. Inferred schemas come from samples. Fix parameters, the request body and responses in the operation's Definition tab (see Defining & promoting operations).
- Promote to a spec when you're happy with it, to get a real OpenAPI file for the API.
Without a Pro license an API can have 2 environments and a project 3 workflows. An import that would go over refuses before writing anything; import fewer environment files or upgrade.