Skip to content

Import from curl & Postman

Three import paths, all producing ordinary Tinspec requests that you can then save, edit, and chain.

The fastest on-ramp. Copy a curl command from a terminal, from your browser’s network panel (“Copy as cURL”), or from a colleague’s message.

On an empty pane: with a project open and no tab, just press ⌘V. If the clipboard starts with curl, Tinspec parses it and opens each command as an unsaved request tab. Nothing touches disk until you save.

It handles the flags that matter: method, headers, data bodies, form fields (-F), and --data-binary '@path' for a file body.

Shell variable references survive the trip. $BASE_URL, ${TOKEN} and %API_KEY% are rewritten to {{BASE_URL}}, {{TOKEN}} and {{API_KEY}} so the request resolves against a Tinspec environment instead of against a shell that is not there. Variables the active environment does not define are offered, never silently written: a toast names them and adds them empty if you accept.

Copying back out is equally faithful — Copy as cURL from the navigator or the Docs view emits -F and --data-binary '@path' for multipart and file bodies rather than silently dropping them.

Import a HAR capture to turn recorded traffic into requests. Useful when the API has no spec and you would otherwise be reading a network panel by hand.

Import a Postman collection. The mapping is deliberate about what it can and cannot carry:

PostmanBecomes
FoldersA name prefix on each request (Users / Admin / Create user)
{{var}}Kept verbatim — Postman and Tinspec use the same placeholder syntax
:id path paramsRewritten to {{id}}
Structured queryThe request’s first-class query map
raw / urlencoded / formdata / file / graphql bodiesThe matching Tinspec body
Bearer / Basic / API-key authMaterialised as headers or query parameters
Collection-level authInherited by each request (a folder’s auth overrides it below that folder)
pm.test assertionsReal test rows on the request
pm.*.set(…) callsChain extractions, wired into a chain
Anything else in a scriptA commented-out pre/post hook, with a header saying why
Collection-level scriptsNot imported — reported, so you can re-create them once in the collection’s script defaults
Unsupported auth typesNot imported — reported as a per-request note

Nothing is silently dropped: anything that could not be carried across comes back as a note on the request it belonged to, so you can see what needs doing by hand.

Every other migration tool rewrites pm.test(…) line by line into its own scripting dialect. The result runs, and that is the whole of its virtue: the assertion is still a closure in a string, so the chain linter cannot see it, spec validation cannot check it, and contract drift cannot notice when it goes stale. Every line that stays a script is a line the rest of the app is blind to.

Tinspec has better targets for the three things a Postman script usually does: an assertion becomes a test row, a variable it publishes becomes an extraction, and anything genuinely imperative becomes a hook you port by hand.

PostmanTest row
pm.response.to.have.status(200)status eq 200
pm.response.to.not.have.status(404)status ne 404
pm.response.to.be.okstatus between 200..299 — Postman’s own definition of “ok”, not an invented range
pm.response.to.have.header("ETag")header ETag exists
pm.expect(jsonData.id).to.existbody id exists
pm.expect(jsonData.items).to.have.lengthOf(3)body items length 3
pm.expect(pm.response.responseTime).to.be.below(500)duration lt 500
pm.expect(pm.response.code).to.be.oneOf([200, 201])status in 200, 201

const jsonData = pm.response.json() and its variants are recognised, which is what lets pm.expect(jsonData.data.id) resolve to the body path data.id. Header, cookie, duration and size subjects map to the sources of the same name.

Variables become extractions, and then a chain

Section titled “Variables become extractions, and then a chain”

pm.environment.set("token", jsonData.access_token) is not really a script statement; it is an extraction with a pm. prefix. pm.collectionVariables.set, pm.globals.set and pm.variables.set are treated the same way, and the value may come from the body, a response header, or the status code.

After the requests are saved, the importer builds a linear chain — but only when a variable actually crosses from one request to a later one. Collections routinely set variables nothing reads, and a one-node chain for those is noise. Requests that neither produce nor consume are left out of the chain entirely; a variable that nothing downstream reads is reported as an orphan rather than wired. The order is the collection’s own order, because that is the only dependency evidence a Postman export carries — postman.setNextRequest is a jump, not a dependency, so it is left untranslated rather than read as an edge.

The import summary makes a checkable claim — “Scripts on 7 requests became 12 tests, 3 extractions, 2 hooks left to port. Wired 2 variables into the chain …” — so you can verify it against the collection afterwards.

What it refuses, and why that is the point

Section titled “What it refuses, and why that is the point”

A wrong translation is worse than no translation: a mistranslated assertion is a test that passes for the wrong reason, which is the single worst thing a migration can leave behind. So the translator matches a fixed table of shapes exactly or refuses, and a pm.test containing even one unrecognised statement is refused whole rather than partly translated — a half-translated test is one whose name promises more than it checks.

Named refusals include:

  • a status name — to.have.status("Created"), because that mapping is Postman’s table, not the HTTP spec’s, and a silent 201-for-200 is exactly the error class a migration must not introduce;
  • type assertions like to.be.a("string"), which have no equivalent in Tinspec’s vocabulary;
  • a regex with flags — /^ab/i — because dropping the i changes what matches;
  • pm.environment.set("stamp", Date.now()) and friends, where the value did not come out of the response, so calling it an extraction would invent a dependency that does not exist.

Everything untranslated is preserved as a commented-out hook on the request, under a header explaining that pm.* does not exist in Tinspec’s runtime and pointing at the ts.* standard library to port it with. It is deliberately not rewritten line by line: the parts that could not be rewritten would throw on the next send, and a hook that half-works is worse than one that visibly does nothing. The code survives the migration inside the collection file, the request still sends, and the port happens in the Scripts tab with completion in front of you.

If nothing at all in a script was understood, the original is kept whole rather than reshuffled.

A Postman request carrying a bearer token still imports as a request with that token in a literal header — the importer materialises credentials the way the curl importer does.

That is a starting point, not a destination. A literal Authorization: value is the worst outcome, because it goes into a file you commit. Two better places for it:

  1. Inline auth on the request. The credential becomes a keychain reference — bearer, basic or apiKey with a name and placement — and the committed file carries the name, never the value. This is the right home for a long-lived partner key or a staging shared secret that no token dance produces.
  2. An auth provider. The right home for anything obtained by logging in: the provider performs the real flow and injects the result, so nothing is pasted and the login itself enters your test coverage.

Either way, delete the literal header once it has a better home. Otherwise you have moved a pasted token from one tool to another — and this one is going into git.

All three live in the Import stage of the add-source dialog. curl also has the ⌘V shortcut above and a Paste a curl command action on the welcome pane.

Insomnia, Bruno, and OpenAPI-as-import (a spec is bound, not imported) are not import sources at v0.1.0-preview.10.