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.
Postman v2.1
Section titled “Postman v2.1”Import a Postman collection. The mapping is deliberate about what it can and cannot carry:
| Postman | Becomes |
|---|---|
| Folders | A name prefix on each request (Users / Admin / Create user) |
{{var}} | Kept verbatim — Postman and Tinspec use the same placeholder syntax |
:id path params | Rewritten to {{id}} |
| Structured query | The request’s first-class query map |
| raw / urlencoded / formdata / file / graphql bodies | The matching Tinspec body |
| Bearer / Basic / API-key auth | Materialised as headers or query parameters |
| Collection-level auth | Inherited by each request (a folder’s auth overrides it below that folder) |
pm.test assertions | Real test rows on the request |
pm.*.set(…) calls | Chain extractions, wired into a chain |
| Anything else in a script | A commented-out pre/post hook, with a header saying why |
| Collection-level scripts | Not imported — reported, so you can re-create them once in the collection’s script defaults |
| Unsupported auth types | Not 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.
What a pm.* script becomes
Section titled “What a pm.* script becomes”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.
Assertions become tests
Section titled “Assertions become tests”| Postman | Test row |
|---|---|
pm.response.to.have.status(200) | status eq 200 |
pm.response.to.not.have.status(404) | status ne 404 |
pm.response.to.be.ok | status 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.exist | body 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 theichanges 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.
About that auth row
Section titled “About that auth row”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:
- Inline auth on the request. The credential becomes a keychain reference —
bearer,basicorapiKeywith 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. - 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.
Where to find them
Section titled “Where to find them”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.
Not supported
Section titled “Not supported”Insomnia, Bruno, and OpenAPI-as-import (a spec is bound, not imported) are not import sources at v0.1.0-preview.10.