Documentation
Reference for workspaces, collections, selectors, scripts, and secrets.
1. First launch
On first launch, Synapse asks for a workspaces directory: the folder where every workspace (one per API project) is saved. The choice is remembered.
| Start option | Result |
|---|---|
| Try sample project | Exports the two sample workspaces below into an empty folder of your choice. Both are ready to send. |
| Start from scratch | Creates an empty workspace folder named after your project (lowercase, hyphenated) containing workspace.json. |
| Sample workspace | Contents |
|---|---|
| DummyJSON | JWT login, Bearer-protected endpoints, GET/POST/PUT, script-line extraction, and a .kts script that decodes the JWT. |
| Showcases | One request per Synapse feature. |
2. Workspace structure
The workspaces directory holds one folder per workspace. Workspaces are completely independent: each has its own requests, selectors and scripts, and nothing is shared between them. Each workspace has the same layout:
synapse-workspaces/ ← Synapse home directory (workspaces directory)
├── workspace-one/ ← independent workspace
│ ├── workspace.json ← workspace name (required)
│ ├── collections/ ← HTTP requests (required)
│ ├── selectors/ ← dropdown definitions (optional)
│ └── scripts/ ← .kts automation scripts (optional)
├── workspace-two/ ← independent workspace
│ ├── workspace.json
│ └── collections/
└── workspace-three/ ← independent workspace
├── workspace.json
├── collections/
└── selectors/Secrets are not stored in the workspace. They are 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.
{
"name": "DummyJSON API"
}Live file watching
Synapse watches the workspace folder. A JSON file saved from any editor is reloaded immediately, without a restart.
3. Collections — request files
Each .json file in collections/ defines one HTTP request. Sub-folders become groups. Filenames prefixed with 01-, 02-, … sort by prefix; the rest sort alphabetically.
Requests and folders can also be created, duplicated, moved, and deleted inside Synapse, so the files never have to be written by hand.
Responses
Status, headers, and body are shown for every response. Binary payloads (PDF, ZIP, images) are not rendered and can be saved to a file. Response bodies can be searched with plain text or a regular expression, case-insensitive by default.
Field reference
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
id | string | ✅ | — | Stable unique identifier, lowercase-kebab-case. |
name | string | ✅ | — | Display label. |
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. |
auth | object or null | — | null | Per-request authentication. See Auth. |
settings | object or null | — | null | HTTP version, redirects, cookies, timeout. See Settings. |
bodyType | string | — | "none" | none, raw, table, multipart, or graphql. Selects the editor only. See How the body is encoded. |
body | string or null | — | null | Raw request body. For JSON, escape inner quotes with \". Supports templates. |
bodyFilePath | string or null | — | null | Absolute path of a file sent as the raw body. See Binary body. |
bodyParts | array | — | [] | Multipart form-data parts. See Multipart form-data body. |
graphql | object or null | — | null | GraphQL query, variables and operation name. See GraphQL body. |
description | string or null | — | null | Info text shown next to the request. |
preScriptLines | array of strings | — | [] | Script lines run before the request is sent. See Scripts. |
postScriptLines | array of strings | — | [] | Script lines run after the response arrives. See Scripts. |
Templates
Templates work in every string field: url, headers, queryParams, body, graphql, and all auth fields. They resolve at send time.
| Template | Resolves to | Example | Defined in |
|---|---|---|---|
{{selector.<id>}} | The selected option's value | {{selector.version}} → v2 | selectors/ |
{{selector.<id>.<KEY>}} | A variable of the selected option | {{selector.environment.BASE_URL}} → https://dummyjson.com | selectors/ |
{{script.<NAME>}} | A variable captured by a script | {{script.TOKEN}} → eyJhbGciOiJIUzI1… | preScriptLines, postScriptLines, .kts scripts |
{{script.<function>()}} | The output of a built-in function | {{script.uuid()}} → 550e8400-e29b-41d4-a716-446655440000 | Built in |
{{secrets.<KEY>}} | A decrypted, device-local secret | {{secrets.API_KEY}} → sk-live-abc123 | Secrets vault |
{{var.<KEY>}} | A path-param value saved with the request | {{var.userId}} → 5 | pathParams |
Templates can be combined in one string and nested: an inner {{…}} resolves first and becomes part of the outer expression.
| Expression | Resolution |
|---|---|
{{selector.environment.BASE_URL}}/users/{{var.userId}}?ts={{script.timestamp()}} | https://dummyjson.com/users/5?ts=1718358792000 |
{{secrets.CLIENT_SECRET_{{selector.brand}}_{{selector.environment}}}} | With Brand IKEA and Environment TEST1: reads the secret CLIENT_SECRET_IKEA_TEST1 |
{{script.sign({{var.payload}},{{secrets.SIGNING_KEY}})}} | Both arguments resolve first, then sign() runs on the resolved values |
Resolution rules:
| Case | Behavior |
|---|---|
A secret or script variable contains {{…}} | Sent as-is, never re-resolved. Data returned by an API cannot inject a template. |
A selector variable contains {{…}} | Resolved again, so one selector can be composed from another. See Composed from another selector. |
| A template cannot be resolved | Sent literally, e.g. a typo like {{selector.enviroment.BASE_URL}} is transmitted unchanged. |
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. An auth-generated header overrides a manual 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 query parameter <key>=<value> |
digest | username, password | Answers the server's 401 digest challenge (RFC 2617, MD5) automatically |
oauth1 | oauth1 (nested object) | Authorization: OAuth ..., signed per RFC 5849, header only |
oauth2 | oauth2 (nested object), addTo (header/query) | Authorization: <headerPrefix> <token>, or query parameter access_token |
"auth": { "type": "bearer", "token": "{{script.TOKEN}}" }Keep real credentials out of the file and reference them as {{secrets.KEY}}. The Showcases workspace contains a working example of each type.
OAuth 1.0 (oauth1)
| Field | Description |
|---|---|
consumerKey | OAuth consumer key |
consumerSecret | OAuth consumer secret |
token | OAuth token |
tokenSecret | OAuth token secret |
signatureMethod | HMAC-SHA1 (default), PLAINTEXT, or RSA-SHA1 |
realm | Optional realm included in the Authorization header |
callback | Optional oauth_callback value |
verifier | Optional oauth_verifier value |
privateKey | PEM-encoded PKCS#8 private key, used only when signatureMethod is RSA-SHA1 |
The request is signed locally at send time per RFC 5849: the signature base string covers the method, the normalized URL, and all query and form parameters, with a fresh oauth_nonce and oauth_timestamp on every send. The result always goes in the Authorization header.
"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)
| Field | Description |
|---|---|
grantType | client_credentials (default), authorization_code, password, refresh_token, or implicit |
authUrl | Authorization endpoint, used by authorization_code and implicit |
tokenUrl | Token endpoint, used by every grant except implicit |
clientId | OAuth client ID |
clientSecret | OAuth client secret |
scope | Space-separated scopes |
redirectUrl | Redirect URI registered with the provider |
username, password | Used by the password grant |
refreshToken | Used by the refresh_token grant |
accessToken | A manually set token; always wins over a fetched token |
headerPrefix | Scheme prefix sent with the token, default Bearer |
clientAuth | How clientId/clientSecret reach the token endpoint: body (default) or basic |
usePkce | Boolean, adds S256 PKCE to the authorization_code flow, default false |
autoRefresh | Boolean, default true |
| Grant | Token acquisition |
|---|---|
client_credentials, password, refresh_token | Fetched from tokenUrl at send time; a cached token is reused until it goes stale. |
authorization_code, implicit | Requires a browser authorization step first. |
Fetched tokens are held in memory only, never written to the request file, and cleared on workspace switch. Only a manually set accessToken is saved to disk.
"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"
}
}Settings
Per-request transport behavior is stored in the request file as a settings block.
| Field | Values | Default | Description |
|---|---|---|---|
httpVersion | http/1.1, http/2 | http/2 | http/2 is negotiated over TLS and falls back to HTTP/1.1 when the server does not support it. http/1.1 forces HTTP/1.1. |
followRedirects | true, false | false | Follow 30x responses via their Location header. |
cookiesEnabled | true, false | false | Send and store cookies via the shared in-memory cookie jar. |
timeoutMs | 1000–600000 | null | Single budget for the whole call. Unset keeps the staged defaults: 30 s connect, 60 s read, 30 s write. Out-of-range values are clamped. |
The cookie jar is one in-memory store shared by every request with cookiesEnabled: true. It is not isolated per request and resets on every app restart.
"settings": { "httpVersion": "http/1.1", "followRedirects": true, "cookiesEnabled": true, "timeoutMs": 5000 }How the body is encoded
bodyType selects the editor, not the encoding. raw and table both produce a JSON object; the Content-Type header decides what goes on the wire.
Content-Type header | Sent as |
|---|---|
| absent | No Content-Type header. Synapse never guesses a media type. |
absent, with bodyFilePath set | application/octet-stream |
absent, with bodyType: graphql | application/json; charset=utf-8 |
application/x-www-form-urlencoded | Form-urlencoded; a JSON object body is converted to form fields |
| anything else | That value, body sent verbatim |
| Rule | Behavior |
|---|---|
Text body, media type without charset | ; charset=utf-8 is appended, e.g. application/json; charset=utf-8. |
| File body | Sent untouched. |
bodyParts holds a named part on a body-carrying method | Synapse writes the multipart Content-Type itself and ignores a manual one. |
GET, HEAD, DELETE with empty body | No body is sent. |
POST, PUT, PATCH with empty body | Sent with Content-Length: 0. |
Table body field types
With bodyType: table, the JSON body is built from key/value pairs. Each value is sent as a string (default) or as raw JSON. A raw-JSON value that is not valid JSON falls back to a string.
| Key | Value | Sent as | Resulting JSON |
|---|---|---|---|
name | Alice | string | "name": "Alice" |
age | 30 | JSON | "age": 30 |
active | true | JSON | "active": true |
note | null | JSON | "note": null |
address | {"city":"Prague"} | JSON | "address": {"city":"Prague"} |
tags | ["a","b"] | JSON | "tags": ["a","b"] |
Binary body (file upload)
A file's raw bytes are sent as the body. 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; a manual header (e.g. image/png) overrides it. The path is machine-specific and does not resolve on another computer.
Multipart form-data body
Text fields and file uploads are combined in one multipart/form-data request. The parts are saved with the request as bodyParts.
| Part field | Type | Default | Description |
|---|---|---|---|
name | string | — | Form field name. Required; unnamed parts are not sent. |
type | text, file | text | Part kind. Unknown values are treated as text. |
value | string | "" | Text parts only. Supports templates. |
filePath | string or null | null | File parts only. Absolute path of the file; machine-specific. |
fileName | string or null | file's own name | File parts only. Overrides the filename= parameter. |
contentType | string or null | see description | Per-part Content-Type. Default: detected from the extension for files, omitted for text. |
enabled | boolean | true | Disabled parts are kept in the file but not sent. |
Synapse writes Content-Type: multipart/form-data; boundary=... itself, with a fresh random boundary on every send, and ignores a manual one. Parts are never sent on GET or HEAD.
GraphQL body
With bodyType: graphql, the query and its variables are stored separately and unescaped in a graphql block, and sent as the standard JSON envelope {"query", "variables", "operationName"}.
| Field | Type | Default | Description |
|---|---|---|---|
query | string | "" | The GraphQL document. Supports templates. |
variables | object or null | null | A native JSON object, not an escaped string. Templates resolve inside string values. |
operationName | string or null | null | Needed only when query holds several operations. |
| Rule | Behavior |
|---|---|
| Templates | Resolved first, then JSON-encoded, so a value with quotes or newlines stays valid JSON. |
variables null or empty | Left out of the envelope. |
operationName null or blank | Left out of the envelope. |
No Content-Type header | Sent as application/json; charset=utf-8. A manual header overrides it. |
bodyType other than graphql | The graphql block is kept but not sent; body is sent instead. |
The GraphQL-over-HTTP specification recommends Accept: application/graphql-response+json, application/json. GraphQL is sent as a POST; a GET is not encoded into query parameters.
{
"id": "graphql-character",
"name": "Get Character",
"method": "POST",
"url": "https://rickandmortyapi.com/graphql",
"bodyType": "graphql",
"graphql": {
"query": "query Character($id: ID!) {\n character(id: $id) { name status }\n}",
"variables": { "id": "{{var.characterId}}" }
}
}The encoded body is what request.body and response.body script paths see, e.g. request.body.variables.id or response.body.data.character.name.
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": [
"saveVariable(\"TOKEN\", response.body.accessToken)",
"saveVariable(\"REFRESH\", response.body.refreshToken)",
"saveVariable(\"USER_ID\", response.body.id)",
"runScript(\"parse-token.kts\")"
]
}| Feature | Where | Effect |
|---|---|---|
| Selector in the URL | {{selector.environment.BASE_URL}} | The Environment dropdown switches the base URL. |
| Selector in headers | {{selector.device.USER_AGENT}}, {{selector.device.OS_VERSION}} | The Device dropdown switches both headers. |
| Post-response script lines | saveVariable("TOKEN", response.body.accessToken), … | {{script.TOKEN}} is available in every later request. |
| Custom script | runScript("parse-token.kts") | Runs scripts/parse-token.kts, which decodes the JWT into JWT_USER_ID. |
4. Selectors — dynamic dropdowns
Each .json file in selectors/ defines one dropdown. Switching a selector retargets every request that references it. Selectors are independent, so each testing axis (environment, profile, device, API version, region, locale) is one selector rather than one copy of the whole environment per combination.
The DummyJSON sample ships three:
| Selector | Provides | Templates |
|---|---|---|
environment | Base URL per environment (Local, Staging, Live) | {{selector.environment.BASE_URL}} |
profile | One option per teammate with their own test data | {{selector.profile.PRODUCT_ID}}, {{selector.profile.PRODUCT_NAME}} |
device | User-Agent and OS version headers per platform | {{selector.device.USER_AGENT}}, {{selector.device.OS_VERSION}} |
Selectors, options, and variables can also be managed inside Synapse.
Field reference
| Field | Type | Required | Description |
|---|---|---|---|
id | string | ✅ | Used in templates: {{selector.<id>}} or {{selector.<id>.<KEY>}}. |
name | string | ✅ | Display label. |
defaultValue | string or null | — | Pre-selected option value. Falls back to the first option. |
options | array | ✅ | List of options (below). |
Each option has:
| Field | Type | Required | Description |
|---|---|---|---|
value | string | ✅ | Injected by {{selector.<id>}}. Never expanded as a template. |
label | string | ✅ | Display text. |
variables | object (string→string) | — | Extra key/value pairs accessed as {{selector.<id>.<KEY>}}. |
Three patterns
With variables: several values per option.
{
"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" }
}
]
}Without variables: the option's value is used directly.
{
"id": "version",
"name": "API version",
"defaultValue": "v1",
"options": [
{ "value": "v1", "label": "v1 (stable)" },
{ "value": "v2", "label": "v2 (beta)" }
]
}Composed from another selector: a variable may itself contain templates.
{
"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}}" } }
]
}| Selection | Template | Resolves to |
|---|---|---|
Device ios | {{selector.device.USER_AGENT}} | DummyJSON-App/3.2.0 (iPhone; iOS 17.5) |
API version v2 | {{selector.version}} | v2 |
Environment live, API version v2 | {{selector.environment.BASE_URL}} | https://api.example.com/v2 |
A variable can reference anything a request can: another selector, {{secrets.<KEY>}}, {{script.<function>()}}. Chains of any length are followed. A reference that leads back to itself is left unresolved.
5. Scripts — dynamic values & automation
Built-in functions
Always available, no setup. Usable 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 |
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. To pass a resolved value as an argument, nest the template: {{script.sign({{var.payload}},{{secrets.SIGNING_KEY}})}}.
Script lines (pre-request and post-response extraction)
A request holds two script-line lists with the same syntax: preScriptLines runs before the send, postScriptLines after the response arrives.
Every line is a function call:
| Script line | Effect |
|---|---|
saveVariable("NAME", source.path) | Stores the value at source.path as {{script.NAME}}. The name is in double quotes. |
runScript("name.kts") | Runs scripts/name.kts. See Custom Kotlin scripts. |
clearScriptVariables() | Discards all session variables. See Session variables. |
Lines starting with // or # are comments.
Breaking change: older versions usedNAME = source.pathandrun:name. These lines no longer run. Each one fails with an error that shows the line in the current form, e.g.saveVariable("NAME", source.path)orrunScript("name.kts").
| Source | Available in | Extracts |
|---|---|---|
request.body.<key>.<key>… | pre, post | Request JSON body (dot-separated key chain, integer = array index) |
request.headers.<header-name> | pre, post | Request header |
request.url | pre, post | Resolved request URL (after template substitution) |
request.method | pre, post | HTTP method, e.g. POST |
response.body.<key>.<key>… | post | Response JSON body (dot-separated key chain, integer = array index) |
response.headers.<header-name> | post | Response header |
response.status | post | HTTP status code, e.g. 200 (0 if the server was never reached) |
response.statusText | post | HTTP status message, e.g. Not Found |
response.duration | post | Round-trip time in milliseconds |
The body path is a plain dot-separated key walk, not JSONPath. Wildcards, $, and filters are not supported.
| Script line | Captures | Access afterward |
|---|---|---|
saveVariable("TOKEN", response.body.accessToken) | accessToken field of the response JSON | {{script.TOKEN}} |
saveVariable("FIRST_ITEM", response.body.products.0.title) | title of the first item in the products array | {{script.FIRST_ITEM}} |
saveVariable("TRACE_ID", response.headers.X-Request-Id) | X-Request-Id response header | {{script.TRACE_ID}} |
saveVariable("CODE", response.status) | Status code | {{script.CODE}} |
saveVariable("SEED", request.headers.X-Seed) | X-Seed request header, usable in the same request (pre) | {{script.SEED}} |
Pre-request (preScriptLines)
A variable set here is substituted into the same request's {{script.VAR}} templates (URL, headers, query and path params, body, auth fields) before it is sent.
Post-response (postScriptLines)
Values captured here become session variables for later requests.
Failure behavior
| Phase | Failure | Effect |
|---|---|---|
| Pre-request | A line reads response.* | Send is aborted, no HTTP call is made. |
| Pre-request | A line is not a valid script line | Send is aborted, no HTTP call is made. |
| Pre-request | A runScript(...) script fails | Send is aborted, no HTTP call is made. |
| Post-response | A line is invalid or a runScript(...) script fails | Nothing is aborted; the response is shown and saved to history. |
Example abort message:
Pre-request script cannot read "response.body.token": there is no response yet —
move "saveVariable("TOKEN", response.body.token)" to the post-request script.Session variables and clearScriptVariables()
Captured variables are kept in memory for the session and discarded when Synapse closes. They can also be reviewed and cleared inside Synapse.
A line clearScriptVariables() discards all variables at that point: variables captured above it are dropped, lines below it are kept. It is valid in both lists and runs before that phase's runScript(...) scripts.
| Script lines | Result after the phase |
|---|---|
clearScriptVariables() | No session variables remain |
clearScriptVariables()saveVariable("TOKEN", response.body.accessToken) | Only TOKEN remains |
Custom Kotlin scripts (.kts)
Script lines only read what the request and response already contain. A .kts script in scripts/ can run any code: call another API, run a command-line tool, read a file, query a database, sign or decode a token. Trigger it with runScript("<name>.kts") from either script-line list. Every entry the script writes into result becomes a {{script.KEY}} value.
Scripts are not sandboxed. They run with the same permissions as Synapse itself. Only run scripts you trust.
| 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 |
context.phase | Pre or Post | if (context.phase == ScriptPhase.Pre) … |
result (output) | MutableMap<String, String> | result["JWT_USER_ID"] = "42" |
| Situation | Binding values |
|---|---|
Pre phase | responseBody, responseHeaders, statusMessage are empty; statusCode and durationMs are 0. |
Server never reached (Post) | statusCode is 0, which distinguishes a transport failure from an HTTP error response. |
A script used in both phases should branch on context.phase.
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) ?: ""Trigger in a request's postScriptLines:
saveVariable("TOKEN", response.body.accessToken)
runScript("parse-token.kts")After Login, {{script.JWT_USER_ID}} is available in every request.
6. Secrets — encrypted local credentials
API keys, tokens, and passwords are kept in an encrypted, device-local vault saved outside the workspace and outside any repository. They never appear in collection files.
| Property | Value |
|---|---|
| Location | Synapse's local config folder (next to config.json), per workspace. Nothing about secrets is tracked in Git. |
| Key | Valid only on the current device and never written to disk. |
| Sharing | Not shared through the repository. Each developer imports their own .env once per machine. |
| Display | A resolved secret is shown and copied as its {{secrets.KEY}} placeholder in request and response views by default. Hiding can be turned off. |
Import from a .env file
API_KEY=sk-live-abc123
DB_PASSWORD=hunter2
WEBHOOK_SECRET=whsec_xyzSynapse reads the file, encrypts each value, and saves the result in its local config folder. The .env file itself is not copied into the workspace. Keep it outside the repository.
Manage secrets
Secrets can be added, edited, and removed one at a time. Export writes all current secrets to a plain <workspace>_secrets.env file for backup or another machine. Store it outside the repository.
Use them in any request
Reference a secret with {{secrets.KEY}} wherever templates work. The value is decrypted at send time.
{
"headers": {
"Authorization": "Bearer {{secrets.API_KEY}}"
}
}7. Tips & recipes
| Goal | How |
|---|---|
| Chain requests | Capture saveVariable("TOKEN", response.body.accessToken) in Login's postScriptLines; later requests use {{script.TOKEN}} in Authorization. |
| Switch environments or identities | URLs in an environment selector, per-teammate test data in a profile selector, device headers in a device selector. |
| Control sidebar order | Prefix filenames and folders: 01-login.json, 02-current-user.json. |
| Combine 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. Any LLM can generate collections/ and selectors/ files from a description of the API. Secrets stay encrypted and out of the files.
Import an existing collection
Three skills, built on the agent-skill standard, work with any compatible AI coding assistant.
| Skill | What it does |
|---|---|
synapse-import-workspace | Builds a workspace from OpenAPI/Swagger, Postman, Insomnia, Bruno, RapidAPI, HAR, or curl, with an environment selector for every base URL and {{secrets.KEY}} placeholders for credentials. |
synapse-validate-workspace | Checks a workspace's files, ids, templates and scripts, and flags credentials written in plain text. |
synapse-docs | Gives the assistant this documentation, which the other two skills read. |
All three are one download in the download section. Unpack it into your assistant's skills folder and keep the three skills side by side.