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:
- DummyJSON — login with JWT, Bearer-token-protected endpoints, GET/POST/PUT, script line extraction, and a custom
.ktsscript that decodes the JWT. - Showcases — selected requests demonstrating specific Synapse features.
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
| 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. |
auth | object or null | — | null | Per-request authentication — see Auth. |
settings | object or null | — | null | Per-request HTTP version, redirect, cookie, and timeout behavior — see Settings. |
bodyType | string | — | "none" | none, raw, table, or multipart. 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 to a file sent as the raw request body — see Binary body. |
bodyParts | array | — | [] | Multipart form-data parts. Each part has name, type (text/file), value, filePath, fileName, contentType, enabled. |
description | string or null | — | null | Info text shown next to the request. |
preScriptLines | array of strings | — | [] | Extraction rules run before the request is sent. See Scripts. |
postScriptLines | array 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:
| 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 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-446655440000 | Built in — no setup |
{{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 |
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.
| 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 |
oauth1 | oauth1 (nested object) | Authorization: OAuth ... — request signed per RFC 5849, header-only |
oauth2 | oauth2 (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)
| 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 |
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)
| 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 any fetched token |
headerPrefix | Scheme prefix sent with the token, default Bearer |
clientAuth | How clientId/clientSecret are sent to the token endpoint: body (default) or basic |
usePkce | Boolean — adds S256 PKCE to the authorization_code flow, default false |
autoRefresh | Boolean, 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.
| 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. |
cookiesEnabled | true, false | false | Whether this request sends and stores cookies via Synapse's cookie jar. |
timeoutMs | 1000–600000 | null | Whole-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 header | Sent as |
|---|---|
| absent | No Content-Type header — Synapse never guesses a media type |
absent, with bodyFilePath set | application/octet-stream |
application/x-www-form-urlencoded | Form-urlencoded — a JSON object body is converted to form fields |
| anything else | That 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.
| 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 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:
- Selectors in the URL —
{{selector.environment.BASE_URL}}switches the base URL with the Environment dropdown. - Selectors in headers —
{{selector.device.*}}swaps theUser-AgentandX-APP-OS-SYSTEM-VERSIONheaders with the Device dropdown. - Post-response script lines — extract the JWT from the response.
TOKENis then available as{{script.TOKEN}}in every later request. - Custom script —
run:parse-tokentriggers a.ktsfile that decodes the JWT to extract the user ID.
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:
- Environment — base URLs and other environment-specific values.
{{selector.environment.BASE_URL}}swaps Local, Staging, or Live without rewriting a single URL. - Profile — one option per teammate, each carrying its own dummy test data (e.g.
PRODUCT_ID,PRODUCT_NAME). Every request then uses your data, and changing it never affects a colleague. - Device — the
User-Agentand OS headers used to exercise how the API behaves per operating system, via{{selector.device.USER_AGENT}}and{{selector.device.OS_VERSION}}.
Any other dimension, such as an API version, a region, or a locale, is added the same way.
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>}}. |
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.
| 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, 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 prefix | Extracts from |
|---|---|
request.body.<key>.<key>… | Request JSON body (dot-separated key chain, integer = array index) |
request.headers.<header-name> | Request header |
request.url | Resolved request URL (after template substitution) |
request.method | HTTP 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 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}} |
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}} |
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 lines | Result 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:
| Need | What a script can do |
|---|---|
| A value from a database | Query it through the database's command-line client or an HTTP data API |
| A secret from a vault | Call HashiCorp Vault or a cloud secret manager over its HTTP API, or run its CLI |
| A derived credential | HMAC/RSA signing, JWT decoding, or any custom authentication scheme |
| A value from another system | Call 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:
| 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 (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-tokenAfter 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_xyzYou 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
- The encryption key is valid only on your current device, and it is never written to disk.
- Encrypted secrets are saved per project in Synapse's local config directory, never in the workspace or 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
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.
Control sidebar order
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.