Documentation

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

1. First launch

On first launch, Synapse asks for a workspaces directory, the folder where all your API projects (called workspaces) are saved. The choice is remembered, so the app goes straight there next time.

There are two ways to start.

Try sample project

Synapse exports two ready-made sample workspaces into an empty folder of your choice:

Both are ready to send right away.

Start from scratch

Synapse creates an empty workspace folder named after your project (lowercase, hyphenated). It contains a workspace.json and is ready for 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 workspace. 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 workspace folder for changes. Save a JSON file from any editor and Synapse reloads it instantly, without a restart.

3. Collections — request files

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

You don't have to write these files by hand. Requests and folders can also be created, duplicated, moved, and deleted inside Synapse.

Responses

Status, headers, and body are shown for every response. Binary payloads such as PDFs, ZIPs, or images are not rendered; save them to a file instead.

Response bodies can be searched using plain text or a regular expression, matching case-insensitively by default.

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.
authobject or nullnullPer-request authentication — see Auth.
settingsobject or nullnullPer-request HTTP version, redirect, cookie, and timeout behavior — see Settings.
bodyTypestring"none"none, raw, table, or multipart. Selects the editor only — see How the body is encoded.
bodystring or nullnullRaw request body. For JSON, escape inner quotes with \". Supports templates.
bodyFilePathstring or nullnullAbsolute path to a file sent as the raw request body — see Binary body.
bodyPartsarray[]Multipart form-data parts. Each part has name, type (text/file), value, filePath, fileName, contentType, enabled.
descriptionstring or nullnullInfo text shown next to the request.
preScriptLinesarray of strings[]Extraction rules run before the request is sent. See Scripts.
postScriptLinesarray of strings[]Extraction rules run after the response arrives. See Scripts.

Templates

Templates work in any field that accepts a string value: url, headers, queryParams, body, and every auth field. They resolve at send time, in these forms:

TemplateResolves toExampleDefined in
{{selector.<id>}}The selected option's value{{selector.version}}v2selectors/
{{selector.<id>.<KEY>}}A variable of the selected option{{selector.environment.BASE_URL}}https://dummyjson.comselectors/
{{script.<NAME>}}A variable extracted by a script{{script.TOKEN}}eyJhbGciOiJIUzI1…preScriptLines, postScriptLines, .kts scripts
{{script.<function>()}}A fresh value generated by a built-in function{{script.uuid()}}550e8400-e29b-41d4-a716-446655440000Built in — no setup
{{secrets.<KEY>}}A decrypted, device-local secret{{secrets.API_KEY}}sk-live-abc123Secrets vault
{{var.<KEY>}}A path-param value saved with the request{{var.userId}}5pathParams

Mix them freely in one string: {{selector.environment.BASE_URL}}/users/{{var.userId}}?ts={{script.timestamp()}}.

Templates can be nested — an inner {{…}} resolves first and becomes part of the expression around it. That is how a value keyed by more than one selector is looked up:

{{secrets.CLIENT_SECRET_{{selector.brand}}_{{selector.environment}}}} — with Brand IKEA and Environment TEST1 this reads the secret CLIENT_SECRET_IKEA_TEST1. Brand and Environment stay independent dropdowns; adding a brand is one secret plus one selector option.

Nesting also lets a built-in function take resolved arguments: {{script.sign({{var.payload}},{{secrets.SIGNING_KEY}})}}.

A resolved value is not re-read — except a selector variable. Selector files are your own, so one selector may be composed from another, e.g. "BASE_URL": "https://api.example.com/{{selector.version}}". See Composed from another selector. A secret or a script variable whose value happens to contain {{…}} is sent exactly as it is, so data returned by an API can never inject a template.
Unresolved templates are sent as they are. A typo like {{selector.enviroment.BASE_URL}} is transmitted literally instead of being replaced.

Path params

A URL can contain {{var.KEY}} placeholders, e.g. https://api.example.com/users/{{var.userId}}. Their values are saved with the request under pathParams.

Auth

Per-request authentication is 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
oauth1oauth1 (nested object)Authorization: OAuth ... — request signed per RFC 5849, header-only
oauth2oauth2 (nested object), addTo (header/query)Authorization: <headerPrefix> <token>, or an access_token query parameter
"auth": { "type": "bearer", "token": "{{script.TOKEN}}" }

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

OAuth 1.0 (oauth1)

FieldDescription
consumerKeyOAuth consumer key
consumerSecretOAuth consumer secret
tokenOAuth token
tokenSecretOAuth token secret
signatureMethodHMAC-SHA1 (default), PLAINTEXT, or RSA-SHA1
realmOptional realm included in the Authorization header
callbackOptional oauth_callback value
verifierOptional oauth_verifier value
privateKeyPEM-encoded PKCS#8 private key, used only when signatureMethod is RSA-SHA1

Synapse signs the request at send time per RFC 5849, building the signature base string from the method, the normalized URL, and all query and form parameters, and generating a fresh oauth_nonce/oauth_timestamp. Signing happens locally, with no extra network round-trip, and the result always goes in the Authorization header; unlike apikey or oauth2, there is no query or body placement.

"auth": {
  "type": "oauth1",
  "oauth1": {
    "consumerKey": "{{secrets.CONSUMER_KEY}}",
    "consumerSecret": "{{secrets.CONSUMER_SECRET}}",
    "token": "{{secrets.OAUTH_TOKEN}}",
    "tokenSecret": "{{secrets.OAUTH_TOKEN_SECRET}}",
    "signatureMethod": "HMAC-SHA1"
  }
}

OAuth 2.0 (oauth2)

FieldDescription
grantTypeclient_credentials (default), authorization_code, password, refresh_token, or implicit
authUrlAuthorization endpoint, used by authorization_code and implicit
tokenUrlToken endpoint, used by every grant except implicit
clientIdOAuth client ID
clientSecretOAuth client secret
scopeSpace-separated scopes
redirectUrlRedirect URI registered with the provider
username, passwordUsed by the password grant
refreshTokenUsed by the refresh_token grant
accessTokenA manually set token — always wins over any fetched token
headerPrefixScheme prefix sent with the token, default Bearer
clientAuthHow clientId/clientSecret are sent to the token endpoint: body (default) or basic
usePkceBoolean — adds S256 PKCE to the authorization_code flow, default false
autoRefreshBoolean, default true

For client_credentials, password, and refresh_token, Synapse fetches an access token from tokenUrl at send time, reusing a cached one until it goes stale, and attaches it as Authorization: <headerPrefix> <token>, or as an access_token query parameter when addTo is query. authorization_code and implicit need a browser authorization step first.

Fetched tokens are held in memory only, never written to the request file, and cleared when you switch workspaces. Only a manually set accessToken is saved to disk, like Bearer's token.

"auth": {
  "type": "oauth2",
  "oauth2": {
    "grantType": "client_credentials",
    "tokenUrl": "https://auth.example.com/oauth/token",
    "clientId": "{{secrets.CLIENT_ID}}",
    "clientSecret": "{{secrets.CLIENT_SECRET}}",
    "scope": "read write",
    "clientAuth": "basic"
  }
}

See the Showcases workspace for a working example of each.

Settings

Per-request transport behavior (HTTP version, redirect-following, cookie handling, and the call timeout) is 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.
cookiesEnabledtrue, falsefalseWhether this request sends and stores cookies via Synapse's cookie jar.
timeoutMs1000600000nullWhole-call budget in ms; absent uses 30s connect / 60s read.

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

The cookie jar is a single in-memory store shared by every request with cookiesEnabled: true, anywhere in the app. It is not isolated per request, and it resets on every app restart. There is no separate screen to view or clear individual cookies.

Leaving timeoutMs unset keeps the built-in staged timeouts (30s connect, 60s read, 30s write). Setting a value replaces those with a single budget for the whole call instead; values outside 1000–600000 ms are clamped into that range rather than rejected.

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

How the body is encoded

bodyType records how the body is edited, not how it is encoded. raw and table both produce a JSON object, and the Content-Type header alone decides what goes on the wire.

Content-Type headerSent as
absentNo Content-Type header — Synapse never guesses a media type
absent, with bodyFilePath setapplication/octet-stream
application/x-www-form-urlencodedForm-urlencoded — a JSON object body is converted to form fields
anything elseThat value, with the body sent verbatim

A text body whose media type declares no charset gets ; charset=utf-8 appended, so application/json goes out as application/json; charset=utf-8. File bodies are sent untouched.

Multipart is the one exception: whenever bodyParts holds a named part on a method that carries a body, Synapse writes the Content-Type itself and ignores a manual one.

A GET, HEAD, or DELETE request with an empty body sends no body at all. POST, PUT, and PATCH always carry one; an empty body is sent as Content-Length: 0.

Table body field types

When bodyType is table, the JSON body is built from key/value pairs. Each value is sent either as a string (the default) or as raw JSON: a number, boolean, null, object, or array. A raw-JSON value that isn't valid JSON falls back to a string.

KeyValueSent asResulting JSON
nameAlicestring"name": "Alice"
age30JSON"age": 30
activetrueJSON"active": true
notenullJSON"note": null
address{"city":"Prague"}JSON"address": {"city":"Prague"}
tags["a","b"]JSON"tags": ["a","b"]

Binary body (file upload)

A request can send a file's raw bytes as its body: an image, a PDF, an archive, or any other non-text payload. The file's absolute path is saved with the request as bodyFilePath.

Without a Content-Type header the body goes out as application/octet-stream; set your own header (e.g. image/png) to override it.

Multipart form-data body

A multipart/form-data body mixes plain text fields and file uploads in one request, for example an image alongside form metadata. The parts are saved with the request as bodyParts.

A file part takes two optional overrides: fileName (defaults to the file's own name) and contentType (defaults to auto-detection from the file extension). Text parts carry no Content-Type unless one is set.

Synapse writes the Content-Type: multipart/form-data; boundary=... header itself, with a fresh random boundary on every send, and ignores a manual one. Parts are never sent on GET/HEAD, since those methods carry no body. As with a binary body, a file part's path is absolute and specific to your machine, so it won't resolve for a teammate on another 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.USER_AGENT}}",
    "X-APP-OS-SYSTEM-VERSION": "{{selector.device.OS_VERSION}}"
  },
  "bodyType": "raw",
  "body": "{\"username\":\"emilys\",\"password\":\"emilyspass\"}",
  "postScriptLines": [
    "TOKEN = response.body.accessToken",
    "REFRESH = response.body.refreshToken",
    "USER_ID = response.body.id",
    "run:parse-token"
  ]
}

One request, four features:

4. Selectors — dynamic dropdowns

Selectors are dropdowns in the toolbar. Switching one instantly retargets every request that references it, so you do not have to find-and-replace or edit requests one by one. Each selector is one .json file in selectors/.

Why selectors matter

Define as many selectors as your project needs, one per axis of your testing. Because they are independent, you switch each on its own instead of duplicating a whole environment for every combination.

The DummyJSON sample ships three:

Any other dimension, such as an API version, a region, or a locale, is added the same way.

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>}}.

Three patterns

With variables — bundle several config values in one option (e.g. environment URLs, device headers):

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

Use as {{selector.device.USER_AGENT}} and {{selector.device.OS_VERSION}}, for example as the User-Agent and X-APP-OS-SYSTEM-VERSION headers on any request.

Without variables — the option's 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.

Composed from another selector — a variable may contain templates itself, so one selector can be expressed in terms of another:

{
  "id": "environment",
  "name": "Environment",
  "defaultValue": "live",
  "options": [
    { "value": "live",    "label": "🌍 Live",    "variables": { "BASE_URL": "https://api.example.com/{{selector.version}}" } },
    { "value": "staging", "label": "🧪 Staging", "variables": { "BASE_URL": "https://staging.example.com/{{selector.version}}" } }
  ]
}

Requests keep using {{selector.environment.BASE_URL}}; with Environment Live and API version v2 it resolves to https://api.example.com/v2. Both dropdowns stay independent, and the version is written in one place instead of in every request.

A variable can reference anything a request can — another selector, {{secrets.<KEY>}}, {{script.<function>()}} — and chains of any length are followed. An option's own value is never expanded, because it identifies the option. A reference that leads back to itself is left unresolved instead of looping.

Tip: Selectors, their options, and their variables can also be managed inside Synapse, without editing files by hand.

5. Scripts — dynamic values & automation

Built-in functions

These are always available and need 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
base64(value){{script.base64(hello)}}aGVsbG8=
sign(payload,secret){{script.sign(data,mySecret)}}Base64 HMAC-SHA256 digest

Arguments are literal text separated by commas. A value containing a comma is split into two arguments, and a {{...}} template inside the parentheses is not resolved.

Script lines (pre-request and post-response extraction)

A request holds two independent script-line lists: preScriptLines runs before the request is sent, postScriptLines after the response arrives. Both share the same syntax.

Syntax: VARIABLE_NAME = source.path.to.value

Pre-request (preScriptLines)

A variable set here is substituted into that same request's {{script.VAR}} templates (URL, headers, query/path params, body, auth fields) before it goes out.

Source prefixExtracts from
request.body.<key>.<key>…Request JSON body (dot-separated key chain, integer = array index)
request.headers.<header-name>Request header
request.urlResolved request URL (after template substitution)
request.methodHTTP method, e.g. POST

response.* is not available in this phase, because there is no response yet. Referencing it aborts the send entirely, and no HTTP call is made:

Pre-request script cannot read "response.body.token": there is no response yet —
move "TOKEN = response.body.token" to the post-request script.

A failing run:<name> custom script in this phase aborts the send the same way.

Post-response (postScriptLines)

Runs after a response arrives. Extract values into session variables for use in later requests.

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}}
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}}

A failing post-response script line or run:<name> script aborts nothing, since the response has already arrived and is still shown and saved to history.

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

Session variables and clearScriptVariables()

Captured variables are kept in memory for the whole session and discarded when Synapse closes.

A line reading clearScriptVariables() discards them where it stands: variables captured above it go too, lines below it are kept. Valid in both lists, and it runs before that phase's run:<name> scripts.

Script linesResult after the phase
clearScriptVariables()No session variables remain
clearScriptVariables()
TOKEN = response.body.accessToken
Only TOKEN remains
Tip: session variables can also be reviewed and cleared inside Synapse.

Custom Kotlin scripts (.kts)

Script lines only read what the request and response already contain. A .kts script has no such limit and can perform any action a request needs. Place .kts files in the scripts/ folder and trigger them with run:<name> from either script-line list.

Whatever the script writes into result becomes a {{script.KEY}} value in the request:

NeedWhat a script can do
A value from a databaseQuery it through the database's command-line client or an HTTP data API
A secret from a vaultCall HashiCorp Vault or a cloud secret manager over its HTTP API, or run its CLI
A derived credentialHMAC/RSA signing, JWT decoding, or any custom authentication scheme
A value from another systemCall another API, read a local file, or run any command-line tool

Combined with preScriptLines, a request can be assembled from data that exists nowhere in the workspace: values fetched, computed, or decrypted just before it is sent.

Scripts are not sandboxed. They run with the same permissions as Synapse itself, so only run scripts you trust.

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
context.phasePre or Postif (context.phase == ScriptPhase.Pre) …
result (write-only output)MutableMap<String, String>result["JWT_USER_ID"] = "42"

context.statusCode is 0 when the request never reached the server, so a script can tell a transport failure apart from an HTTP error response. In the Pre phase there is no response yet, so responseBody, responseHeaders, statusCode, statusMessage, and durationMs all hold their empty/zero defaults. A script used in both phases should branch on context.phase.

Trigger in a request's postScriptLines:

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 saved outside your repository, so they never appear in your collections or anything an AI agent can read.

Import from a .env file

Secrets are imported from a .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 workspace or repository. Synapse reads it, encrypts each value, and saves 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

Secrets can also be added, edited, and removed one at a time. Export writes all current secrets back out as a plain <workspace>_secrets.env file, which is handy for a backup or another machine (store it 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

By default, a resolved secret is shown and copied as its {{secrets.KEY}} placeholder instead of the real value, in both the request and the response view. Hiding can be turned off to reveal and copy the actual values.

7. Tips & recipes

Chain requests

Add postScriptLines to Login, send it once, and every following request can use {{script.TOKEN}} in its Authorization header instead of a copy-pasted value.

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. Switch a dropdown and the whole workspace retargets, without touching a colleague's setup.

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

Combine templates freely

Selectors, script variables, secrets, and path params can be mixed in any string field. See Templates:

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

8. Importing and creating workspaces with AI

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.

Import an existing collection

Synapse also ships an import-synapse-workspace skill, built on the agent-skill standard and usable by any compatible AI coding assistant. Give it an existing API collection export (OpenAPI/Swagger, Postman, Insomnia, Bruno, RapidAPI, HAR, or curl) and a workspace name, and it builds the workspace for you: collections, an environment selector for every base URL, and {{secrets.KEY}} placeholders in place of any credentials.

The skill is a separate download, available in the download section. Unpack it into your assistant's skills folder.