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:
- DummyJSON — login with JWT, Bearer-token-protected endpoints, GET/POST/PUT, script line extraction, and a custom
.ktsscript that decodes the JWT. - Showcases — a collection of selected requests demonstrating specific Synapse features.
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:
- The + in the header creates a new top-level collection folder; the + that appears when you hover over a folder creates a sub-folder inside it. Type the name inline and press Enter (Esc, or clicking away, cancels).
- The new-request icon (in the header, or on any folder when hovered) adds a request and opens it in the editor. When you first Save it, Synapse names the file and its
idfrom the request's name and method — lowercase and hyphenated, e.g. aPOSTnamed "Login User" becomeslogin-user-post.jsonwith idlogin-user-post.
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:
- Duplicate — creates a sibling copy with " copy" appended to the name and a freshly generated id/filename (the source's id is never reused).
- Delete — immediately removes the request's file from disk.
- Move to… — opens a folder picker and moves the request's file into the chosen collection folder (or back to the collections root).
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.
- Send executes exactly what the editor currently shows — including edits you haven't saved yet. Send stays disabled while the URL is empty.
- Save writes the request back to its JSON file. A small dot on the Save button marks unsaved changes; after clicking, the button reports whether the save actually succeeded.
- Navigating away with unsaved changes — selecting another request, switching space, or closing the app — asks whether to save, discard, or cancel instead of silently dropping the edits.
Keyboard shortcuts (they also work while typing in any field):
| Shortcut | Action |
|---|---|
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
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
id | string | ✅ | — | Stable unique identifier. Use lowercase-kebab-case. |
name | string | ✅ | — | Display label in the sidebar and response tabs. |
method | string | ✅ | — | GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS. |
url | string | ✅ | — | Full URL. Supports {{...}} templates. |
headers | object (string→string) | — | {} | Request headers. Values support templates. |
queryParams | object (string→string) | — | {} | Query parameters (?key=value). Values support templates. |
pathParams | object (string→string) | — | {} | Values for {{var.KEY}} placeholders in the URL. Edited in the Path Params tab. |
auth | object or null | — | null | Per-request authentication. Edited in the Auth tab — see Auth. |
settings | object or null | — | null | Per-request HTTP version and redirect behavior. Edited in the Settings tab — see Settings. |
bodyType | string | — | "none" | none, json, raw, form, or multipart. |
body | string or null | — | null | Raw request body. For JSON, escape inner quotes with \". Supports templates. |
bodyFilePath | string or null | — | null | Absolute path to a file to send as the raw request body (Binary body mode). |
bodyParts | array | — | [] | Multipart form-data parts (Multipart body mode). Each part has name, type (text/file), value, filePath, fileName, contentType, enabled. |
description | string or null | — | null | Info text shown next to the request. |
scriptLines | array 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.
| Type | Fields | What is sent |
|---|---|---|
bearer | token | Authorization: Bearer <token> |
basic | username, password | Authorization: Basic base64(username:password) |
apikey | key, value, addTo (header/query) | Header <key>: <value>, or a query parameter |
digest | username, password | Answers 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.
| Field | Values | Default | Description |
|---|---|---|---|
httpVersion | http/1.1, http/2 | http/2 | The only two protocols Synapse's HTTP client can send. |
followRedirects | true, false | false | Whether 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:
- Checked (default) — the value is sent as a JSON string, e.g.
"42". - Unchecked — the value is parsed as a JSON number, boolean, null literal, or a nested object/array, e.g.
42,true,false,null,{"city":"Prague"},[1,2,3]. If the value isn't valid JSON, it falls back to a string.
| Key | Value | String | Resulting JSON |
|---|---|---|---|
name | Alice | ✅ checked | "name": "Alice" |
age | 30 | ⬜ unchecked | "age": 30 |
active | true | ⬜ unchecked | "active": true |
note | null | ⬜ 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:
- Selectors in URL —
{{selector.environment.BASE_URL}}switches the base URL when you change the Environment dropdown. - Selectors in headers —
{{selector.device.USERAGENT}}and{{selector.device.OS_VERSION}}change theUser-AgentandX-APP-OS-SYSTEM-VERSIONheaders when you switch the Device dropdown. - Script lines — extract the JWT token from the response.
TOKENis then available as{{script.TOKEN}}in every subsequent request. - Custom script —
run:parse-tokentriggers a.ktsfile that decodes the JWT to extract the user ID.
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:
- Environment — base URLs and other environment-specific values used to build requests. Reference as
{{selector.environment.BASE_URL}}and swap Local, Staging, or Live without rewriting a single URL. - Profile — one option per teammate (Claudia, Peter, Alex), each carrying their own dummy test data (e.g.
PRODUCT_ID,PRODUCT_NAME). Pick your profile and every request uses your data — change it freely without affecting a colleague, because each person's data is stored in its own option. - Device — the
User-Agentand OS headers that let you exercise how the API behaves per operating system, via{{selector.device.USERAGENT}}and{{selector.device.OS_VERSION}}.
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
| Field | Type | Required | Description |
|---|---|---|---|
id | string | ✅ | Used in templates: {{selector.<id>}} or {{selector.<id>.<KEY>}}. |
name | string | ✅ | Label shown in the toolbar. |
defaultValue | string or null | — | Pre-selected option value. Falls back to first option. |
options | array | ✅ | List of choices (see below). |
Each option has:
| Field | Type | Required | Description |
|---|---|---|---|
value | string | ✅ | Injected by {{selector.<id>}}. |
label | string | ✅ | Display text in the dropdown. |
variables | object (string→string) | — | Extra key/value pairs accessed as {{selector.<id>.<KEY>}}. |
Two patterns
| Pattern | Use case | Access template |
|---|---|---|
| With variables | Bundle multiple config values per option (e.g. environment URLs, device headers) | {{selector.<id>.<variable>}} |
| Without variables | Use 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.
| Function | Template | Output 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 prefix | Extracts from |
|---|---|
response.body.<key>.<key>… | Response JSON body (dot-separated key chain, integer = array index) |
response.headers.<header-name> | Response header |
response.status | HTTP status code, e.g. 200 (0 if the server was never reached) |
response.statusText | HTTP status message, e.g. Not Found |
response.duration | Round-trip time in milliseconds |
request.body.<key>.<key>… | Request JSON body |
request.headers.<header-name> | Request header |
request.url | Resolved request URL (after template substitution) |
request.method | HTTP method, e.g. POST |
Examples:
| Script line | What it captures | Access afterward |
|---|---|---|
TOKEN = response.body.accessToken | accessToken field from the response JSON | {{script.TOKEN}} |
USER_ID = response.body.id | id field from the response JSON | {{script.USER_ID}} |
FIRST_ITEM = response.body.products.0.title | title of the first item in the products array (integer = index) | {{script.FIRST_ITEM}} |
TRACE_ID = response.headers.X-Request-Id | X-Request-Id response header | {{script.TRACE_ID}} |
CODE = response.status | Status code of the response | {{script.CODE}} |
TOOK_MS = response.duration | How 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:
| Binding | Type | Example usage |
|---|---|---|
context.variables | Map<String, String> | context.variables["TOKEN"] |
context.responseBody | String | context.responseBody |
context.responseHeaders | Map<String, String> | context.responseHeaders["X-Request-Id"] |
context.statusCode | Int | if (context.statusCode == 401) … |
context.statusMessage | String | context.statusMessage |
context.durationMs | Long | if (context.durationMs > 1000) … |
context.requestHeaders | Map<String, String> | context.requestHeaders["Authorization"] |
context.requestBody | String | context.requestBody |
context.url | String | context.url |
context.method | String | context.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
- The encryption key is valid only on your current device — it is never written to disk.
- Encrypted secrets are stored per project in Synapse's local config directory, never in the space/repository. Nothing about your secrets is tracked in Git.
- Because the values never enter the repo, neither your teammates' checkouts nor an AI agent reading the repository can see them. Each developer imports their own
.envonce.
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.
Control sidebar order
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()}}