Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Turly

Turly

A Postman-style API client that lives in your terminal. Vim keys, plain-file workspaces and OpenAPI import.

cargo install turly
turly -i https://petstore3.swagger.io/api/v3/openapi.json

Three-pane TUI

Request tree, request editor and response viewer, with the mouse if you want it.

Vim keys

Modal editing everywhere, a command box, search, and every key remappable.

Plain-file workspaces

One YAML file per request. Diff them, review them, commit them.

OpenAPI import

Build a whole workspace from an OpenAPI 3.x or Swagger 2.0 spec, file or URL.

Real auth

Bearer, Basic, API key and OAuth2 with browser sign-in, plus a workspace default.

Secrets stay out of git

Environments and variables, with secret values kept in a gitignored file.

Responses are syntax-highlighted and searchable, every request is kept in history, and any request can be copied as curl. Ten themes ship in the box, and you can add your own in TOML.

New here? Start with Getting started, then read Editing requests and Authentication.

Getting started

Install

# from crates.io
cargo install turly

# from source
git clone https://github.com/mwithington/turly && cd turly
cargo install --path .

Prebuilt binaries for macOS, Linux and Windows are attached to each GitHub release.

Try a spec without leaving anything behind

turly -i petstore-openapi.json

-i / --import accepts a file or URL. It imports into a temporary workspace that is deleted when you quit and is never registered.

Build a workspace from a spec

turly import https://petstore3.swagger.io/api/v3/openapi.json --dir petstore
turly petstore

Start empty

turly init my-api
turly my-api

turly with no arguments opens the workspace in the current directory, or the most recently used one. Inside the app, :wsnew <path> and :import <spec> do the same as the commands above.

First request

Imported requests point at {{base_url}}. Set it for your environment with :set base_url=https://api.example.com, and secrets with :secret token=.... Or edit them all at once with :vars.

Move to a request with j/k, press Enter to open and send it, and press ? for the key reference.

Editing requests

Press i or a on the URL or on a tab to edit it in the Vim editor. Esc leaves it, Ctrl-s saves, and Ctrl-r sends.

Tabs

Params     status=available          one key=value per line, "# " disables a line
Headers    Accept: application/json  one Name: value per line
Auth       token: {{token}}          fields depend on the type (press t to change)
Body       { "name": "doggie" }      json, text or form (key=value lines)
Example    { "name": "doggie" }      read-only sample from the OpenAPI spec

:example copies the Example tab into Body. Ctrl-e in editor normal mode opens the buffer in $EDITOR.

Variables inline

{{vars}} show their resolved value (red) when the cursor is not on that line, and the raw {{name}} while it is. Unresolved ones stay as {{name}} in yellow.

Responses

The response pane shows the body (pretty-printed and syntax-highlighted), headers, and status. / searches the body. :save <path> writes the full body to a file, and y copies the request as curl.

Authentication

Each request has an auth type, shown on the Auth tab. Press t to cycle through them:

none → bearer → basic → apikey → oauth2 → inherit

Every field accepts {{variables}}, so keep real values in the environment (:secret token=...) and put {{token}} in the request.

TypeWhat is sent
bearerAuthorization: Bearer <token>
basicAuthorization: Basic base64(user:pass)
apikeya custom header (default X-API-Key), or a query parameter with in: query
oauth2Authorization: Bearer <access token>, fetched for you (below)
inheritwhatever the workspace default is

A header you add yourself on the Headers tab named Authorization always wins over bearer, basic and oauth2.

Workspace default

Set the auth on one request, then run :authws. It is saved as auth: in turly.yaml, and every request set to inherit uses it. New requests start as inherit. :authws clear removes it.

# turly.yaml
name: my-api
auth:
  type: bearer
  token: "{{token}}"

A request set to none stays unauthenticated, which is what OpenAPI import uses for public endpoints.

OAuth2

The Auth tab for oauth2 is one field: value per line:

grant: client_credentials
token_url: https://auth.example.com/oauth/token
auth_url:
client_id: {{client_id}}
client_secret: {{client_secret}}
scope: read write
username:
password:
grantUseFields needed
client_credentialsmachine to machinetoken_url, client_id, client_secret
passwordlegacy username and password loginplus username, password
codesign in with a browser (authorization code with PKCE)auth_url, token_url, client_id

On send, Turly gets a token, adds it as a bearer header, and caches it in memory for the session. It reuses the token until shortly before it expires, then refreshes it with the refresh token if the server gave one, or gets a new one. :logout clears the cache. Credentials are sent in the request body (client_secret_post).

Browser sign-in (code)

Sending the request opens your browser at auth_url. After you approve, the provider redirects to http://127.0.0.1:8765/callback, where Turly is listening, and the page tells you to return to the terminal. Turly waits two minutes. If the browser doesn't open, the sign-in URL is in the status line.

Register http://127.0.0.1:8765/callback as the redirect URI with your provider. Add a port: line to use a different port.

From OpenAPI

Importing a spec with an oauth2 scheme fills in the URLs and scopes from the spec, using the authorization code flow if it has one, then client credentials, then password. client_id and client_secret become secret variables for you to fill in.

Not supported

Digest auth, AWS SigV4, client certificates (mTLS), the OAuth2 implicit and device flows, and folder-level auth. Tokens are not saved to disk, so a code login repeats on each launch. The copied curl command (y) can't include an OAuth2 token because it is fetched at send time.

Environments and variables

Variables resolve from the active environment. Use them anywhere in a request as {{name}}. {{$NAME}} reads an OS environment variable.

Switch environments with e or :env <name>, and create one with :envnew <name>.

Editing

  • :set key=value sets one variable.
  • :secret key=value sets a secret.
  • :vars opens the whole environment as text, one key=value or secret key=value per line. Ctrl-s or :w saves, :wq saves and closes, and :q or Esc closes.

Values longer than 20 characters are shown cut off with … in the :vars editor. Move the cursor onto a line and the window widens to show it in full.

Secrets

Secret values live in .turly/secrets.yaml, which turly init adds to .gitignore. The environment file only records their names, so it is safe to commit.

Workspaces

A workspace is a plain directory:

my-api/
├── turly.yaml               name, timeout, active environment
├── requests/
│   └── pet/
│       └── find-pets-by-status.yaml
├── environments/
│   └── default.yaml         variables and the names of secret variables
└── .turly/                  gitignored: secrets.yaml, history.jsonl

A request file

name: findPetsByStatus
method: GET
url: "{{base_url}}/pet/findByStatus"
params:
  - { key: status, value: available, enabled: true }
auth: { type: bearer, token: "{{token}}" }
body: { type: none, content: "" }
docs: "Finds pets by status."
order: 3 # optional: pins the position in the sidebar

Folders sort first, then requests by order, then by name.

Several workspaces

All registered workspaces are remembered in ~/.config/turly/workspaces.yaml. Open one with turly <path>, or switch inside the app: press W (or run :ws with no argument) for a picker listing every known workspace, with a ● on the current one. :ws <name or path> switches directly.

Several workspaces in one folder

Launch Turly in a folder that holds workspaces, or point it at one (turly ~/apis). It finds every workspace in that folder and up to two levels below it, skipping hidden folders, target and node_modules. When there are two or more, they appear as the top-level rows of the sidebar. Expand as many as you like to see their requests side by side:

▾ alpha
  GET  Get users
▾ gamma ●
  GET  Ping
  ▸ history
▸ team-beta

The ● marks the active workspace: the one whose environment, variables and history the commands use. Opening a request in another workspace makes that workspace active. A folder with a single workspace shows the plain tree.

KeyOn a workspace row
l / hExpand / collapse it
Enter or clickCollapsed: expand and activate. Expanded but not active: activate. Active: collapse
z MCollapse every workspace
z RExpand every workspace

Activating a workspace closes the open request, so it is refused while that request has unsaved edits. Which workspace is active at startup is the most recently used one in the folder, else the first. W still lists every workspace Turly knows about, in any folder.

Switching is refused while the open request has unsaved edits. The open request and response are cleared on a switch, and each workspace keeps its own active environment.

OpenAPI import

turly import <spec> (or :import) accepts OpenAPI 3.0/3.1 and Swagger 2.0, as JSON or YAML, from a file or an http(s) URL. Swagger 2.0 is converted to 3.0 in memory first.

  • Tags become folders, operations become requests, in the spec's order.
  • servers become environments with a base_url variable. Path parameters become {{vars}} with example values.
  • Query and header parameters are listed, enabled when the spec marks them required.
  • JSON and form bodies are pre-filled from the spec's example/examples (or a schema's example/default), else generated from the schema's types. The original stays in the Example tab.
  • Bearer, Basic and API key security schemes become auth, with secret variables to fill in.
  • Summaries and descriptions show up in each request's Docs tab.

Import creates a new workspace. Re-importing over an existing one is not supported yet. For a throwaway one, use turly -i <spec>.

Keys and Vim motions

KeyAction
h j k lMove, collapse and expand folders, switch tabs (on a workspace row, l/h expand/collapse it)
gg / GTop / bottom
Ctrl-d / Ctrl-uHalf page down / up
Ctrl-w + h j k lFocus a pane (Ctrl-h/j/k/l also work)
Ctrl-w wNext pane
Tab / Shift-TabNext / previous tab
EnterOpen a request and send it
i / aEdit the URL or the open tab in the Vim editor
EscLeave the editor, or clear the sidebar filter
Ctrl-rSend the request (also while editing)
Ctrl-sSave the request
:Command box
/, n, NSearch the focused pane, next, previous
z M / z RCollapse / expand every workspace row
WPick a workspace (j/k or click, Enter opens, Esc closes)
oNew request
ddDelete a request (asks first)
m / tCycle the method / the auth or body type
eNext environment
y / YCopy the request as curl / copy the response (body, or headers on that tab)
Ctrl-w < > + -Resize panes (Ctrl-w = resets)
?Key reference
q / Ctrl-cQuit

/ is context-sensitive. In the sidebar it live-filters requests by name, method and URL. In the response it highlights matches in the body. In the editor it searches the buffer.

Editor motions

Each editable field is a modal Vim buffer (normal, insert, visual, . repeat). Beyond the built-in motions, normal mode adds F T ; , ^ C, and d/c/y with $ 0 ^ e b f F t T ; ,, for example d$, dtB and cF".

These custom operators aren't repeated by ..

Mouse

Click folders, requests, tabs and fields, drag to select, and scroll with the wheel. Drag the border between panes to resize them. Hold Shift for the terminal's native text selection, or set mouse = false in the config.

Remapping

Everything is remappable. See Configuration.

Commands

Press : and type. Tab completes, and the list shows what each command does.

CommandWhat it does
:w / :wqSave / save and quit
:q / :q! / :qaQuit (warns on unsaved changes) / quit and discard / quit out
:new [name], :mkdir <name>, :rm, :dup, :rename <name>, :discardManage requests and folders
:copyCopy the response (body, or headers on that tab)
:send, :method <M>, :curl, :save <path>Send, set the method, copy as curl, save the response body
:ws [path|name], :wsnew <path>Switch to a workspace (no argument opens the picker) / create one
:env [name], :envnew <name>Switch to / create an environment
:set k=v, :secret k=vSet a variable / a secret in the active environment
:varsEdit the active environment line by line
:authws [clear]Make this request's auth the workspace default
:logoutForget cached OAuth2 tokens
:exampleReset the body to the spec's example
:import <spec> [dir]Build a workspace from an OpenAPI or Swagger spec
:theme [name]Switch theme
:helpKey reference

/ opens the search box instead.

Configuration

~/.config/turly/config.toml (or $XDG_CONFIG_HOME/turly/):

theme = "nord"
mouse = true   # false keeps native terminal text selection

# Remap or add keys. Chords are space separated.
[keys]
"ctrl-s" = "save"
"g d"    = "delete_request"
"x"      = "quit"

Actions

up down left right top bottom half_down half_up focus_left focus_right focus_up focus_down focus_next next_tab prev_tab open edit send save command search search_next search_prev cancel quit help new_request delete_request cycle_method cycle_kind next_env copy_curl copy_response pane_wider pane_narrower pane_taller pane_shorter pane_reset workspaces collapse_all expand_all

Themes

Presets: turly (dark), turly-light, gruvbox, nord, catppuccin, catppuccin-latte, solarized, solarized-light, tokyonight, dracula and mono.

Switch with :theme <name>, or set theme in the config. The default follows your terminal background when it reports one (COLORFGBG), and truecolor falls back to 256 colors when COLORTERM doesn't advertise it.

Your own

Add ~/.config/turly/themes/<name>.toml, then :theme <name>:

base = "nord"        # optional starting preset
accent = "#ff8800"
string = "#a3be8c"

Slots: bg fg dim accent border selection search ok warn err info key string number keyword comment.

Limitations

  • OAuth2 covers client credentials, password and authorization code. Tokens live in memory only. See Authentication.
  • No scripting, tests or assertions, and no Postman collection import yet.
  • HTTP only: no WebSocket, GraphQL or gRPC.
  • The cookie jar lasts for the session and isn't saved to disk.
  • Responses over 10 MB are truncated in the viewer. :save <path> writes the full body.
  • Inline variable values are revealed per line, not per variable.
  • Import makes one folder level (the first tag).