← Back to home

Documentation

Your guide to workspaces, collections, selectors, and scripts.

1. First launch

When you open Synapse for the first time, a Welcome Screen appears and asks you to choose a workspaces directory — a folder on your computer where all your API projects (called workspaces) will live.

Synapse remembers your choice. Next time you open the app, it goes straight to your workspaces.

Try sample project

Don't have a workspace yet? Click "Try Sample Project…" on the Welcome Screen. Select an empty folder, and Synapse exports two ready-made sample workspaces into it:

The app opens the exported folder immediately — you can start sending requests right away.

Start from scratch

Want an empty project instead? Click "Start from scratch" on the Welcome Screen, choose a home folder for your workspaces, and type a project name. Synapse creates a new workspace folder — named after your project (lowercase, hyphenated) — containing a workspace.json, then opens it, ready for you to add your own collections and selectors.

2. Workspace structure

Each workspace is a plain folder inside your workspaces directory:

my-workspace/
├── workspace.json        ← workspace name (required)
├── collections/          ← your HTTP requests (required)
├── selectors/            ← dropdown definitions (required)
└── scripts/              ← automation scripts (optional)
Secrets are not stored in the space. They're encrypted and saved in Synapse's local config folder, outside any repository — see Secrets.

workspace.json

The only file required at the root. It holds the display name shown in the toolbar.

{
  "name": "DummyJSON API"
}

Live file watching

Synapse watches your space folder for changes. Save a JSON file from any editor, and the UI updates instantly — no reload needed.

3. Collections — request files

Each .json file in collections/ defines one HTTP request. Sub-folders become collapsible groups in the sidebar. Prefix filenames with 01-, 02-, … to control sort order.

Creating requests & folders in the app

You don't have to create these files by hand. In the Collections sidebar:

Files created this way have no 01- sort prefix, so they sort alphabetically. Rename them on disk (e.g. add a 01- prefix) if you want a specific order.

Right-click a request in the sidebar for three more actions:

Editing & sending

The editor's tabs — Auth, Headers, Query Params, Path Params, Body, Script, Settings — show a small dot whenever they contain data, so you can see where a request's data is located without clicking through them.

Keyboard shortcuts (they also work while typing in any field):

ShortcutAction
Ctrl + Enter (macOS: Cmd + Enter)Send the request
Ctrl + S (macOS: Cmd + S)Save the request

Viewing & saving responses

The response viewer shows status, headers, and body, with a Save as... button next to the body. Synapse doesn't render binary payloads like PDFs, ZIPs, or images in-app — instead, Save as... lets you download the body to disk as the file type declared in the response, so you can open it in whatever viewer you'd use normally.

Field reference

FieldTypeRequiredDefaultDescription
idstringStable unique identifier. Use lowercase-kebab-case.
namestringDisplay label in the sidebar and response tabs.
methodstringGET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS.
urlstringFull URL. Supports {{...}} templates.
headersobject (string→string){}Request headers. Values support templates.
queryParamsobject (string→string){}Query parameters (?key=value). Values support templates.
pathParamsobject (string→string){}Values for {{var.KEY}} placeholders in the URL. Edited in the Path Params tab.
authobject or nullnullPer-request authentication. Edited in the Auth tab — see Auth.
settingsobject or nullnullPer-request HTTP version and redirect behavior. Edited in the Settings tab — see Settings.
bodyTypestring"none"none, json, raw, form, or multipart.
bodystring or nullnullRaw request body. For JSON, escape inner quotes with \". Supports templates.
bodyFilePathstring or nullnullAbsolute path to a file to send as the raw request body (Binary body mode).
bodyPartsarray[]Multipart form-data parts (Multipart body mode). Each part has name, type (text/file), value, filePath, fileName, contentType, enabled.
descriptionstring or nullnullInfo text shown next to the request.
scriptLinesarray of strings[]Post-response extraction rules. See Scripts.

Templates work in url, headers, queryParams, and body — any field that accepts a string value.

Path params

A URL can contain {{var.KEY}} placeholders, e.g. https://api.example.com/users/{{var.userId}}. As you type such a placeholder into the URL, a matching row appears in the Path Params tab where you fill in its value; removing the placeholder removes the row again (unless you already entered a value). The values are stored in the request file under pathParams.

Auth

The Auth tab configures per-request authentication, stored in the request file as an auth block. All values support {{...}} templates, and an auth-generated header overrides a manually added header of the same name.

TypeFieldsWhat is sent
bearertokenAuthorization: Bearer <token>
basicusername, passwordAuthorization: Basic base64(username:password)
apikeykey, value, addTo (header/query)Header <key>: <value>, or a query parameter
digestusername, passwordAnswers the server's 401 digest challenge (RFC 2617, MD5) automatically
"auth": { "type": "bearer", "token": "{{script.TOKEN}}" }

Keep real credentials out of the file — reference them as {{secrets.KEY}}.

Settings

The Settings tab configures per-request transport behavior — HTTP version and redirect-following — stored in the request file as a settings block.

FieldValuesDefaultDescription
httpVersionhttp/1.1, http/2http/2The only two protocols Synapse's HTTP client can send.
followRedirectstrue, falsefalseWhether a 30x response is automatically followed via its Location header.

Http version — HTTP/2 is negotiated over TLS and falls back to HTTP/1.1 when the server doesn't support it. HTTP/1.1 forces the older protocol.

Redirect — Off (default). A switch for following redirects.

"settings": { "httpVersion": "http/1.1", "followRedirects": true }

Form body field types

When bodyType is form, the body editor shows a key/value table with a String checkbox per row:

KeyValueStringResulting JSON
nameAlice✅ checked"name": "Alice"
age30⬜ unchecked"age": 30
activetrue⬜ unchecked"active": true
notenull⬜ unchecked"note": null
address{"city":"Prague"}⬜ unchecked"address": {"city":"Prague"}
tags["a","b"]⬜ unchecked"tags": ["a","b"]

The body editor opens in Raw mode unless the request file sets "bodyType": "form"; select Form to edit the JSON body as a key/value table — a nested object or array shows up as a single row containing its JSON text, so clear String to keep editing it as an object/array.

Binary body (file upload)

The Binary body mode sends a file's raw bytes as the request body — for uploading images, PDFs, archives, or any other non-text payload. In the Body tab, either drag a file onto the drop zone or click it to browse. The chosen file's absolute path is saved with the request as bodyFilePath, so it's remembered next time the request is opened.

If no Content-Type header is set, Synapse defaults to application/octet-stream; add your own Content-Type header (e.g. image/png) to override it.

Multipart form-data body

The Multipart body mode sends a multipart/form-data body — for mixing plain text fields and file uploads in a single request, e.g. an image upload alongside form metadata.

A file part accepts two optional overrides: a filename (defaults to the file's own name) and an explicit Content-Type (defaults to auto-detection from the file's extension). Text parts have no Content-Type unless you set one explicitly.

Synapse generates the actual Content-Type: multipart/form-data; boundary=... header itself, with a fresh random boundary on every send; a manually added Content-Type header is ignored while Multipart mode is active. Parts are never sent on GET/HEAD requests, since those methods carry no body. As with Binary body mode, a file part's path is absolute and specific to your machine, so it won't resolve for a teammate opening the same workspace on their own computer.

Example — login with token extraction

{
  "id": "login",
  "name": "Login",
  "method": "POST",
  "url": "{{selector.environment.BASE_URL}}/auth/login",
  "headers": {
    "Content-Type": "application/json",
    "Accept": "application/json",
    "User-Agent": "{{selector.device.USERAGENT}}",
    "X-APP-OS-SYSTEM-VERSION": "{{selector.device.OS_VERSION}}"
  },
  "bodyType": "json",
  "body": "{\"username\":\"emilys\",\"password\":\"emilyspass\"}",
  "scriptLines": [
    "TOKEN = response.body.accessToken",
    "REFRESH = response.body.refreshToken",
    "USER_ID = response.body.id",
    "run:parse-token"
  ]
}

This single request demonstrates:

4. Selectors — dynamic dropdowns

Selectors appear as dropdown chips in the toolbar. Switching one instantly retargets every request that references it — no find-and-replace, no editing requests one by one. Each selector is one .json file in selectors/.

Why selectors matter

Selectors let you model every axis of your testing independently. Define as many as your project needs — each covering a different dimension of a request — and combine them freely. Because they are independent, you switch each one on its own, without duplicating a whole environment for every combination.

The DummyJSON sample ships three:

Add any custom dimension your project needs the same way — an API version, a region, a locale — as its own selector. That is the whole point: model each axis of your testing once, then switch them independently from the toolbar.

Field reference

FieldTypeRequiredDescription
idstringUsed in templates: {{selector.<id>}} or {{selector.<id>.<KEY>}}.
namestringLabel shown in the toolbar.
defaultValuestring or nullPre-selected option value. Falls back to first option.
optionsarrayList of choices (see below).

Each option has:

FieldTypeRequiredDescription
valuestringInjected by {{selector.<id>}}.
labelstringDisplay text in the dropdown.
variablesobject (string→string)Extra key/value pairs accessed as {{selector.<id>.<KEY>}}.

Two patterns

PatternUse caseAccess template
With variablesBundle multiple config values per option (e.g. environment URLs, device headers){{selector.<id>.<variable>}}
Without variablesUse the option's value directly (e.g. a product ID in a URL path){{selector.<id>}}

With variables — bundle multiple config values per option (e.g. environment URLs, device headers):

{
  "id": "device",
  "name": "Device",
  "defaultValue": "android",
  "options": [
    {
      "value": "android",
      "label": "🤖 Android",
      "variables": { "USERAGENT": "DummyJSON-App/3.2.0 (Android 14; Pixel 8)", "OS_VERSION": "14" }
    },
    {
      "value": "ios",
      "label": "🍎 iOS",
      "variables": { "USERAGENT": "DummyJSON-App/3.2.0 (iPhone; iOS 17.5)", "OS_VERSION": "17.5" }
    },
    {
      "value": "desktop",
      "label": "🖥️ Desktop",
      "variables": { "USERAGENT": "DummyJSON-App/3.2.0 (Windows NT 10.0; Win64; x64)", "OS_VERSION": "10" }
    }
  ]
}

Use as {{selector.device.USERAGENT}} and {{selector.device.OS_VERSION}} — e.g. as the User-Agent and X-APP-OS-SYSTEM-VERSION headers on any request.

Without variables — the value is used directly (e.g. an API version in a URL path):

{
  "id": "version",
  "name": "API version",
  "defaultValue": "v1",
  "options": [
    { "value": "v1", "label": "v1 (stable)" },
    { "value": "v2", "label": "v2 (beta)" }
  ]
}

Use as {{selector.version}} → inserts v1 or v2 directly into the URL.

Tip: You can manage selectors directly inside Synapse — click the ⚙ Settings icon in the toolbar. Create a new selector with the next to the header, add options to a selector with the next to its name, and edit each option's variables — all without leaving the app.

5. Scripts — dynamic values & automation

Built-in functions

Always available, no setup. Use them in any template field.

FunctionTemplateOutput example
uuid(){{script.uuid()}}550e8400-e29b-41d4-a716-446655440000
timestamp(){{script.timestamp()}}1718358792000
isoTimestamp(){{script.isoTimestamp()}}2026-06-14T10:33:12.000Z
randomNumeric(n){{script.randomNumeric(6)}}482910
randomAlphabetic(n){{script.randomAlphabetic(9)}}xKvPtRmoQ
sign(payload,secret){{script.sign(data,mySecret)}}HMAC-SHA256 digest
base64(value){{script.base64(hello)}}aGVsbG8=

Script lines (post-response extraction)

Run automatically after a response. Extract values into session variables for use in subsequent requests.

Syntax: VARIABLE_NAME = source.path.to.value

Source prefixExtracts from
response.body.<key>.<key>…Response JSON body (dot-separated key chain, integer = array index)
response.headers.<header-name>Response header
response.statusHTTP status code, e.g. 200 (0 if the server was never reached)
response.statusTextHTTP status message, e.g. Not Found
response.durationRound-trip time in milliseconds
request.body.<key>.<key>…Request JSON body
request.headers.<header-name>Request header
request.urlResolved request URL (after template substitution)
request.methodHTTP method, e.g. POST

Examples:

Script lineWhat it capturesAccess afterward
TOKEN = response.body.accessTokenaccessToken field from the response JSON{{script.TOKEN}}
USER_ID = response.body.idid field from the response JSON{{script.USER_ID}}
FIRST_ITEM = response.body.products.0.titletitle of the first item in the products array (integer = index){{script.FIRST_ITEM}}
TRACE_ID = response.headers.X-Request-IdX-Request-Id response header{{script.TRACE_ID}}
CODE = response.statusStatus code of the response{{script.CODE}}
TOOK_MS = response.durationHow long the request took, in milliseconds{{script.TOOK_MS}}

Extracted variables are available as {{script.TOKEN}}, {{script.USER_ID}}, etc.

Note: the body path is a simple dot-separated key walk, not JSONPath. Wildcards, $, and filters are not supported.

Custom Kotlin scripts (.kts)

For complex logic — JWT decoding, HMAC signatures, multi-step transformations. Place .kts files in the scripts/ folder and trigger them with run:<name> in scriptLines.

Example — decode a JWT and extract the user ID:

scripts/parse-token.kts

import java.util.Base64

val token = context.variables["TOKEN"] ?: error("TOKEN not set — send Login first")
val payloadJson = String(Base64.getUrlDecoder().decode(token.split(".")[1]))

val idMatch = """"id"\s*:\s*(\d+)""".toRegex().find(payloadJson)
result["JWT_USER_ID"] = idMatch?.groupValues?.get(1) ?: ""

Available bindings:

BindingTypeExample usage
context.variablesMap<String, String>context.variables["TOKEN"]
context.responseBodyStringcontext.responseBody
context.responseHeadersMap<String, String>context.responseHeaders["X-Request-Id"]
context.statusCodeIntif (context.statusCode == 401) …
context.statusMessageStringcontext.statusMessage
context.durationMsLongif (context.durationMs > 1000) …
context.requestHeadersMap<String, String>context.requestHeaders["Authorization"]
context.requestBodyStringcontext.requestBody
context.urlStringcontext.url
context.methodStringcontext.method
result (write-only output)MutableMap<String, String>result["JWT_USER_ID"] = "42"

context.statusCode is 0 when the request never reached the server (connection error), so a script can tell a transport failure apart from an HTTP error response.

Trigger in a request's scriptLines:

TOKEN = response.body.accessToken
run:parse-token

After Login, {{script.JWT_USER_ID}} is available everywhere.

6. Secrets — encrypted local credentials

API keys, tokens, and passwords don't belong in plain JSON or in your Git history. Synapse keeps them in an encrypted, device-local vault that lives outside your repository, so they never appear in your collections or anything an AI agent can read.

Import from a .env file

Click the 🔒 Secrets icon in the toolbar (next to ⚙ Settings) → Import .env, and select your .env file:

API_KEY=sk-live-abc123
DB_PASSWORD=hunter2
WEBHOOK_SECRET=whsec_xyz

You only need to do this once per machine. The .env file is not copied into your space or repository — Synapse reads it, encrypts each value, and stores the result in its own local config folder (next to config.json, outside the repo). Keep the original .env file safe and outside your repository.

Manage secrets in the dialog

The Secrets dialog also lets you add a single secret manually, edit a value in place, and remove one — removing takes a second, confirming click ("Delete?"), so a stray click can't destroy a secret. Export .env writes all current secrets back out as a plain <workspace>_secrets.env file to a folder you choose — handy for backing up or moving to another machine (store the exported file outside your repository).

Use them in any request

Reference a secret with the {{secrets.KEY}} template — anywhere templates work (url, headers, queryParams, body):

{
  "headers": {
    "Authorization": "Bearer {{secrets.API_KEY}}"
  }
}

At send time, Synapse decrypts the value on the fly and injects it.

How the encryption works

Hiding secrets in responses

In the Response/Request viewer, a Hide secrets checkbox sits next to Copy (on by default). While it's on, any resolved secret value is shown — and copied — as its {{secrets.KEY}} placeholder instead of the real value. Untick it to reveal and copy the actual values when you need them.

7. Tips & recipes

Chain requests

Add scriptLines to Login → send once → every request uses {{script.TOKEN}} in its Authorization header — no copy-pasting.

Switch environments or identities

Put URLs in an environment selector, per-teammate test data in a profile selector, and device headers in a device selector. Flip a dropdown — the whole workspace retargets, and you never touch a colleague's setup.

Prefix filenames: 01-login.json, 02-current-user.json. Folders work the same way.

Keep secrets safe

Import sensitive values via the 🔒 Secrets dialog instead of hardcoding them. They're encrypted with a local key and stored outside your repository — nothing is tracked in Git. Import your .env once per machine, keep the original file outside the repo, and reference values with {{secrets.KEY}}.

Let an AI write your collections

Every file is structured JSON in a predictable path. Tell any LLM what API you need and drop the output into collections/ and selectors/ — your secrets stay encrypted and out of reach.

Combine templates freely

Templates resolve at send time and can be mixed in any string field:

{{selector.environment.BASE_URL}}/products/{{selector.profile.PRODUCT_ID}}?ts={{script.timestamp()}}