API — Ports & traffic

Open, close, list and configure proxy ports, and test an upstream before you commit to it. These are the endpoints you'll use most when integrating BlankTrail Proxy.

Open a port

A single request raises a local proxy port and sets all of its behaviour at once. There are many fields but only one is required — port; the rest take their defaults, and can be changed later on the live port.

POST/api/v1/ports/openAuth required

Raises a proxy port with the given identity and behaviour.

ParameterTypeRequiredDescription
portintYesThe port number, 1–65535.
protocolstringNohttp (the default), socks5 or mtproto.
upstreamstringNoThe egress proxy; empty means direct.
modestringNoHow the identity is chosen: random, db, auto, specific, custom.
browserstringNoThe browser filter; incompatible with mode=random.
osstringNoThe OS filter; incompatible with mode=random.
Request body
{
  "port": 20134,
  "protocol": "socks5",
  "mode": "db",
  "browser": "chrome",
  "os": "windows",
  "upstream": "socks5://user:pass@host:1080"
}
Example (curl)
curl -X POST -H "X-API-Key: YOUR_API_KEY" -H "Content-Type: application/json" \
  -d '{"port":20134,"protocol":"socks5","mode":"db","browser":"chrome","os":"windows"}' \
  http://127.0.0.1:8891/api/v1/ports/open
Response
{
  "port": 20134,
  "protocol": "socks5",
  "status": "opened",
  "current_profile": {
    "name": "chrome_152_windows",
    "user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) …",
    "browser": "chrome",
    "os": "windows"
  }
}
  • 400 “port is required” — the field is absent or 0; 400 “invalid port number: N (must be 1-65535)”.
  • 400 for an invalid mode, browser, os, vdns_mode, resolver_strategy or intercept_scope — the refusal text lists what is allowed.
  • 400 if mode=random is combined with a browser or os filter; 400 “require_udp_dns requires vdns_mode to be on_leak or forced”; 400 if a gateway was requested but the gateway manager is off; 400 “gateway config … not found — upload it first via POST /api/v1/ovpn” if upstream_gateway or chain_gateway names a gateway that is not in the saved list — a typo in the name gives the same refusal as a gateway that was never uploaded, so check GET /api/v1/ovpn before you blame the manager.
  • 403 “the debug fingerprint port requires a Pro license” for debug_capture without Pro.
  • 409 if the port is already open or taken by another program — the text comes from the port manager.
  • With protocol=mtproto the response carries two more fields: tg_link (the tg://proxy… link handed to a Telegram client) and mtproto_secret (the canonical ee… secret, generated one included).

Egress

FieldTypeDefaultWhat it does
upstreamstring—The egress proxy: scheme://[user:pass@]host:port, schemes socks5, socks5h, http. Empty means direct egress.
chain_proxystring—The first link of the chain in front of the egress proxy.
upstream_gatewaystring—The name of a saved gateway; its local SOCKS5 becomes the egress.
chain_gatewaystring—The name of a saved gateway for the first link of the chain.
ovpn_configstring—A legacy alias for upstream_gateway, kept for older clients.
upstream_tls_insecureboolfalseDrop certificate verification for an https proxy. Only for your own proxy with a self-signed certificate.
allow_mitm_upstreamboolfalseAllow an egress that opens TLS itself. Forbidden by default: such an egress erases the fingerprint.
egress_force_ipv4booltrueEgress over IPv4 only — protection against an IPv6 leak.
block_private_targetsbooltrueRefuse connections to private, loopback, link-local and CGNAT literals: otherwise the proxy becomes a map of the local network.

Identity and protocol

FieldTypeDefaultWhat it does
modestringrandomrandom, db (alias database), auto, specific or custom. Mode random is incompatible with the browser/os filters.
browserstring—The browser filter: chrome, firefox, safari, edge, random; a version may be added — chrome_145.
osstring—The OS filter: windows, macos, linux, ios, android, random.
specific_profilestring—The profile name for mode=specific, e.g. chrome_152_windows.
custom_tlsobject—A captured fingerprint as a whole for mode=custom; the same shape as PUT /port/{port}/custom_tls.
auto_profile_from_uaboolfalsePick the profile from the request User-Agent.
h2_spoofingbool—Spoof the HTTP/2 settings to match the browser profile.
spoof_user_agentbool—Spoof the User-Agent. Off relays the client's own header byte for byte.
spoof_headersbool—Bring the header set and order in line with the browser's.
tls_passthroughboolfalsePass TLS straight through without opening it: the client's fingerprint survives, the content is not read.
tls_mirrorboolfalseOpen TLS but replay the client's own captured fingerprint outward.
session_resumptionbool—Allow TLS session resumption (tickets).
enable_http3boolfalseRe-originate over HTTP/3 where the site offers h3 and the egress can carry UDP.
decompressbool—Decode br/gzip/zstd before handing the body to the client. The challenge solver forces it on.

Load and timeouts

FieldTypeDefaultWhat it does
max_concurrentint—The cap on concurrent requests; 0 means no cap.
sem_timeoutint—How many seconds a request waits for a slot within that cap. 🔴 Here the field is called sem_timeout — unlike the standalone endpoint, where it is timeout_seconds.
skip_retryboolfalseDo not retry a request after a network error.
retry_delay_msint—The pause between retries, in milliseconds.
timeout_secondsint—The idle timeout of a CONNECTION (not of the port), in seconds.
connect_timeout_secondsint5The ceiling on ONE dial attempt.
request_timeout_secondsint30The ceiling on the whole establishment phase including retries; 0 means no limit.
idle_secondsint | nullnullThe idle timeout of the PORT, overriding the global one (30 minutes by default); 0 never closes it.

Cache, log and capture

FieldTypeDefaultWhat it does
cache_enabledboolfalseEnable the response cache on the port.
cache_modestringnormalnormal, hard, hard-media or hard-autowarm.
cache_ignore_no_cacheboolfalseCache in spite of a no-cache header.
traffic_logboolfalseWrite the port's request log to data/traffic_port_<port>.jsonl.
debug_captureboolfalseOpen a fingerprint capture port. 🔴 Requires a Pro license: 403 otherwise.
debug_capture_nint500The size of the capture ring on a capture port.

Challenges and sessions

FieldTypeDefaultWhat it does
js_solverboolfalseThe challenge solver: requests that hit a challenge go to the solver pool. Requires opening TLS (incompatible with tls_passthrough).
keep_sessionsboolfalseThe port keeps its own per-domain cookie jar: it absorbs Set-Cookie, injects cookies and follows redirects. Independent of js_solver.

DNS and leak protection

FieldTypeDefaultWhat it does
leak_guardstring—The pre-start egress DNS/IPv6 leak check: off, warn or enforce.
vdns_modestringoffVirtual DNS: off, on_leak (turn on when a leak is found) or forced. Standard plan and above: on Lite the field is accepted, the port opens with vdns off, and its vdns_path_reason reads plan.
resolver_strategystringautoauto or custom — where the resolvers come from.
custom_resolversarray—A list of host:port for resolver_strategy=custom.
ecs_enabledbooltruePass the client subnet in the DNS query (EDNS Client Subnet).
vdns_strict_bypassboolfalseStrict bypass: only an IP literal goes out, the hostname never leaves the machine.
require_udp_dnsboolfalseOpen the port ONLY if the egress has proved it can relay UDP for DNS. Requires vdns_mode on_leak or forced, otherwise 400.

System traffic interception

Warningintercept_scope defaults to system — that is, the WHOLE machine, not selected programs. A port with interception steers all traffic into the tunnel, including your own remote control of that machine. To steer only chosen applications, set intercept_scope=process and list them in intercept_apps.
FieldTypeDefaultWhat it does
interceptboolfalseSteer system traffic into the port with no proxy configured in the application.
intercept_scopestringsystem🔴 system means the WHOLE machine (the default), process only the listed programs.
intercept_appsarray—Paths to the executables for scope=process.

MTProto (a proxy for Telegram)

The fields below are meaningful only with protocol=mtproto. In that mode the port speaks Telegram's protocol rather than HTTP or SOCKS5, and a Telegram client connects to it through the link in the response.

FieldTypeDefaultWhat it does
mtproto_secretstring—The canonical secret of the form ee…; empty generates a fresh one.
mtproto_camouflage_domainstringwww.google.comThe camouflage domain — also the SNI the Telegram client presents.
mtproto_fallback_realbooltrueSplice probers and clients with a bad secret to the real camouflage host.
mtproto_egressstringautoauto, obfuscated or faketls — how to reach an upstream MTProto proxy.
mtproto_faketls_upstreamstring—The host:port of the upstream MTProto proxy for egress=faketls.
mtproto_faketls_secretstring—The ee… secret of that upstream proxy.

Close a port

POST/api/v1/ports/closeAuth required

Closes an open port and drops every connection going through it.

ParameterTypeRequiredDescription
portintYesThe port to close.
Request body
{ "port": 20134 }
Example (curl)
curl -X POST -H "X-API-Key: YOUR_API_KEY" -H "Content-Type: application/json" \
  -d '{"port":20134}' \
  http://127.0.0.1:8891/api/v1/ports/close
Response
{
  "port": 20134,
  "status": "closed"
}
  • 400 “port is required” if the field is absent or 0; 404 if the port is not open.
  • A port with interception drops its interception rule as it closes — the traffic returns to its ordinary route.

The list of open ports

GET/api/v1/portsAuth required

Returns every open port with its full configuration and state.

Example (curl)
curl -H "X-API-Key: YOUR_API_KEY" \
  http://127.0.0.1:8891/api/v1/ports
Response
{
  "ports": [
    {
      "port": 20134,
      "protocol": "socks5",
      "created_at": "2026-09-06T09:12:44Z",
      "last_activity": "2026-09-06T11:03:01Z",
      "current_profile": "chrome_152_windows",
      "mode": "db",
      "upstream": "socks5://host:1080",
      "chain_proxy": "",
      "browser_filter": "chrome",
      "os_filter": "windows",
      "h2_spoofing": true,
      "spoof_user_agent": true,
      "spoof_headers": true,
      "decompress": true,
      "max_concurrent": 0,
      "skip_retry": false,
      "retry_delay_ms": 0,
      "cache_enabled": false,
      "cache_mode": "",
      "cache_normalize_ids": false,
      "debug_capture": false,
      "capture_count": 0,
      "auto_profile_from_ua": false,
      "effective_idle_seconds": 1800,
      "js_solver": false,
      "keep_sessions": false,
      "captcha_action": "rotate_retry",
      "captcha_max_attempts": 3,
      "intercept": false,
      "vdns_active": false
    }
  ],
  "total_open": 1,
  "max_ports": 1000
}
  • max_ports is the binding cap on concurrently open ports: the lower of the configured maximum (portmanager.max_ports) and your license cap. It is exactly the number GET /api/v1/status returns — the two endpoints no longer need to be cross-checked.
  • 🔴 In builds before this release the field here returned the constant 1000 regardless of plan or configuration, while /status already carried the real cap under the same name. If you see exactly 1000 with a knowingly smaller limit, update the client; until then size the pool by max_ports from /status.
  • There is no status field on a list item: an open port is open. The fields leak_report, last_ua, last_auto_profile, idle_override_seconds, leak_guard, upstream_gateway, chain_gateway, intercept_scope, intercept_apps, intercept_state, intercept_reason, vdns_mode, vdns_transport and vdns_udp_reason appear only when they have something to say, and on a protocol=mtproto port so do tg_link and mtproto_secret (the same two fields the open call returns). Every other field is present in EVERY item even when empty — including cache_normalize_ids, captcha_action, captcha_max_attempts and vdns_active, shown in the example above.
  • effective_idle_seconds is the resolved idle threshold after the port's override is applied over the global one (30 minutes by default); 0 means never close.
  • vdns_active tells you whether virtual DNS is rewriting dials RIGHT NOW — unlike vdns_mode, which only states the configured mode. vdns_transport is the transport that ACTUALLY carried the most recent successful resolution: udp, tcp, dot or doh; empty means there has not been one yet. vdns_udp_reason appears when VDNS is active but not currently on udp, and names why: refused, accepted_but_silent, control_error, chain_unsupported or not_probed.

Suggest a free port

GET/api/v1/ports/suggestAuth required

Returns the lowest number in the range 20000–29999 that is neither taken by the manager nor unbindable at the OS level.

Example (curl)
curl -H "X-API-Key: YOUR_API_KEY" \
  http://127.0.0.1:8891/api/v1/ports/suggest
Response
{
  "port": 20134
}
  • 503 “no_free_port” if nothing free was found in the whole range.
  • Between the suggestion and the open, someone else may take the port — that is normal: the open then answers 409 and you simply ask for the next one.

Test an egress before opening a port

The test assembles the egress chain for the duration of the request and runs the chosen probes through it without opening anything. This is how you learn that a proxy is alive, relays UDP for DNS and does not leak — before building work on it.

POST/api/v1/upstream/testAuth required

Checks that the egress is reachable, that UDP over SOCKS5 works, and that DNS and IPv6 do not leak.

ParameterTypeRequiredDescription
checksarrayYesAny of http, udp, leak.
protocolstringNosocks5 or http — the protocol of the future port.
upstreamstringNoThe egress proxy; empty tests a direct connection.
chain_proxystringNoThe first link of the chain.
upstream_gatewaystringNoThe name of a saved gateway instead of an egress address.
chain_gatewaystringNoThe name of a saved gateway for the first link.
upstream_tls_insecureboolNoMirrors the port setting of the same name: without it the test would check a different configuration from the one you are about to open.
Request body
{
  "checks": ["http", "udp", "leak"],
  "protocol": "socks5",
  "upstream": "socks5://user:pass@host:1080"
}
Example (curl)
curl -X POST -H "X-API-Key: YOUR_API_KEY" -H "Content-Type: application/json" \
  -d '{"checks":["http","leak"],"upstream":"socks5://user:pass@host:1080"}' \
  http://127.0.0.1:8891/api/v1/upstream/test
Response
{
  "http": { "ok": true, "detail": "200 in 45ms" },
  "udp": { "ok": false, "detail": "UDP ASSOCIATE granted but nothing came back",
            "code": "accepted_but_silent" },
  "leak": { "ok": true, "detail": "no DNS/IPv6 leak — exit 203.0.113.45" }
}
  • Besides ok and detail, each probe may carry skipped (the probe did not run) and code — a machine-readable reason convenient to branch on.
  • 403 if the license is inactive: this endpoint sits behind the license. 400 “invalid JSON body”; 405 for any method other than POST. The body is capped at 64 KiB.

The port's state and configuration

Three endpoints about the same thing at different depths: /status is a summary of the live port, /config a full snapshot of its configuration, and PUT /config the only way to change what has no endpoint of its own.

GET/api/v1/port/{port}/statusAuth required

A summary of the live port: identity, behaviour, egress, refusal counters and the time of last activity.

Example (curl)
curl -H "X-API-Key: YOUR_API_KEY" \
  http://127.0.0.1:8891/api/v1/port/20134/status
Response
{
  "port": 20134,
  "protocol": "socks5",
  "mode": "db",
  "browser_filter": "chrome",
  "os_filter": "windows",
  "h2_spoofing": true,
  "spoof_user_agent": true,
  "spoof_headers": true,
  "session_resumption": true,
  "max_concurrent": 0,
  "sem_timeout_seconds": 30,
  "connect_timeout_seconds": 5,
  "request_timeout_seconds": 30,
  "skip_retry": false,
  "retry_delay_ms": 0,
  "cache_enabled": false,
  "cache_mode": "",
  "traffic_log": false,
  "tls_passthrough": false,
  "tls_mirror": false,
  "cache_ignore_no_cache": false,
  "allow_mitm_upstream": false,
  "mitm_blocked": 0,
  "current_profile": { "name": "chrome_152_windows", "user_agent": "Mozilla/5.0 …",
                       "browser": "chrome", "os": "windows" },
  "upstream": "socks5://host:1080",
  "chain_proxy": "",
  "created_at": "2026-09-06T09:12:44Z",
  "last_activity": "2026-09-06T11:03:01Z"
}
  • mitm_blocked and mitm_last_issuer are read as one snapshot: they change together, and reading them separately would show the count of one refusal next to the culprit of another.
  • last_activity is moved not only by traffic but by any call to this port's endpoints.
GET/api/v1/port/{port}/configAuth required

A full snapshot of the port's configuration — all sixty-odd keys, including those with no endpoint of their own. Keys with no value are left out of the snapshot.

Example (curl)
curl -H "X-API-Key: YOUR_API_KEY" \
  http://127.0.0.1:8891/api/v1/port/20134/config
Response
{
  "port": 20134,
  "protocol": "socks5",
  "upstream": "socks5://host:1080",
  "mode": "db",
  "browser": "chrome",
  "os": "windows",
  "timeout_seconds": 0,
  "connect_timeout_seconds": 5,
  "request_timeout_seconds": 30,
  "egress_force_ipv4": true,
  "block_private_targets": true,
  "js_solver": false,
  "keep_sessions": false,
  "sessions_per_port": 1,
  "captcha_action": "rotate_retry",
  "captcha_max_attempts": 3,
  "ecs_enabled": true
}
  • The response is trimmed for the example, but the keys shown are ones a port really returns. Keys with no value are omitted entirely: leak_guard, vdns_mode and resolver_strategy are absent at their defaults rather than returned as empty strings; idle_seconds appears only on a port with its OWN idle threshold — when it inherits the global one the key is missing, and null is never sent; intercept and intercept_scope appear only on a port opened with interception, and intercept is then always true while intercept_scope is system or process. So cfg.intercept === false and cfg.idle_seconds === null read undefined: test for the key instead, e.g. "intercept" in cfg.
  • The port's boolean and numeric settings, by contrast, ALWAYS come back — zero and false included: js_solver, keep_sessions, timeout_seconds, egress_force_ipv4 and the rest. The key names match the body fields of POST /api/v1/ports/open — and the merge in PUT /config goes by the same names.
PUT/api/v1/port/{port}/configAuth required

Changes the configuration of a live port. The fields you send are MERGED onto the current snapshot: what is not in the body stays as it was.

Request body
{ "mode": "auto", "browser": "firefox", "os": "macos" }
Example (curl)
curl -X PUT -H "X-API-Key: YOUR_API_KEY" -H "Content-Type: application/json" \
  -d '{"mode":"auto","browser":"firefox","os":"macos"}' \
  http://127.0.0.1:8891/api/v1/port/20134/config
Response
{
  "port": 20134,
  "protocol": "socks5",
  "status": "reconfigured",
  "current_profile": { "name": "firefox_152_macos", "user_agent": "Mozilla/5.0 …",
                       "browser": "firefox", "os": "macos" }
}
  • 🔴 The status field in the response is not the port's state but the name of the action performed: this endpoint always returns exactly reconfigured, while POST /api/v1/ports/open returns opened. Checking either response against open never matches.
  • It takes the same fields and values as POST /api/v1/ports/open, and refuses with the same 400s — including the ban on combining mode=random with the filters.
  • 500 “cannot read the port's current configuration” if the snapshot could not be read — the merge is then impossible and the port is left untouched.
  • This is the only way to change js_solver, keep_sessions, leak_guard, vdns_mode, resolver_strategy, custom_resolvers, ecs_enabled, timeout_seconds, connect_timeout_seconds, request_timeout_seconds, intercept and the other keys with no endpoint of their own.
  • 400 if the body changes interception on an already-open port: it cannot be switched on, off or reconfigured on a live port — close the port and open it again. A body that does not mention interception passes: states are compared, not the presence of a key.

Per-port settings

Every setting lives at its own path of the form /api/v1/port/{port}/name. GET reads the current value, PUT changes it on a live port: the port is neither closed nor restarted.

WarningThere is no value field on any of these endpoints. Each has its own field name — the table below gives it its own column, and on three endpoints (/sem_timeout, /retry_delay, /idle) it does not match the last path segment. The server drops an unknown field silently: {"value":true} on a boolean setting answers 200 and TURNS IT OFF, and on a string setting it clears the filter.

Failures shared by every per-port endpoint: 400 “invalid port number” when {port} is not a number; 404 “port N is not open” when the port is closed; 400 “invalid JSON body” when the body does not parse. A 404 here means the port is closed rather than the path is missing. For an unknown setting name the answer depends on the method: a GET falls through to the dashboard catch-all and gives 404 as the line “404 page not found”, while PUT, POST and DELETE give 405. A known name with an unsupported method gives that same 405, but only when the method is not GET (PUT /profile, say): a GET on an endpoint with no GET registration falls through to the catch-all and gives 404 again.

NoteAny call to a per-port endpoint counts as activity and resets the idle timeout. A monitor polling GET /status once a minute will keep the port open forever.
PathMethodsBody fieldTypeValues and caveats
/modeGET, PUTmodestringrandom, db (alias database), auto, specific
/browserGET, PUTbrowserstringempty, random, chrome, firefox, safari, edge; a version may be added: chrome_145
/osGET, PUTosstringempty, random, windows, macos, linux, ios, android
/profileGET——read-only; pin a profile through /mode or /config
/profile/viewGET——read-only: the composition of the current profile
/rotatePOST——no body; issues a new profile immediately
/custom_tlsPUTja3, ja4, …objecta captured fingerprint as a whole; switches the port into custom mode
/upstreamGET, PUTupstreamstringthe egress proxy address; an empty string means direct egress
/chain_proxyGET, PUTchain_proxystringthe first link of the chain in front of the egress proxy
/allow_mitm_upstreamGET, PUTallow_mitm_upstreamboolthe field is required: without it the answer is 400, not false
/h2_spoofingGET, PUTenabledbooltrue or false
/spoof_user_agentGET, PUTenabledbooltrue or false
/spoof_headersGET, PUTenabledbooltrue or false
/tls_passthroughGET, PUTenabledbooltrue or false
/tls_mirrorGET, PUTenabledbooltrue or false
/http3GET, PUTenabledbooltrue or false
/session_resumptionGET, PUTenabledbooltrue or false
/decompressGET, PUTenabledbooltrue or false
/auto_profile_from_uaGET, PUTenabledboolGET also returns last_ua
/cache_ignore_no_cacheGET, PUTenabledbooltrue or false
/max_concurrentGET, PUTmax_concurrentint0 or more; 0 means no cap
/sem_timeoutGET, PUTtimeout_secondsint1 or more — the field name does NOT match the path
/skip_retryGET, PUTskip_retrybooltrue or false
/retry_delayGET, PUTretry_delay_msint0 or more — the field name does NOT match the path
/idlePUTsecondsint | nullnull returns to the global timeout, 0 never closes; there is no GET
/cacheGET, PUT, DELETEenabled, modebool, stringsee the Response cache section
/traffic_logGET, PUT, DELETEenabledboolsee the port's request log section
/capturesGET, DELETE——only on a port opened with debug_capture

The remaining port configuration keys have NO endpoint of their own: timeout_seconds, connect_timeout_seconds, request_timeout_seconds, js_solver, keep_sessions, sessions_per_port, captcha_action, captcha_max_attempts, leak_guard, egress_force_ipv4, block_private_targets, h3_profile, cache_normalize_ids, vdns_mode, resolver_strategy, custom_resolvers, ecs_enabled, require_udp_dns, intercept, intercept_scope, intercept_apps, debug_capture, debug_capture_n and the mtproto_* fields. Read them with GET /api/v1/port/{port}/config and change them with PUT /api/v1/port/{port}/config; they have no path of their own. A GET to /api/v1/port/{port}/js_solver returns 404 as the plain line “404 page not found” — served by the dashboard catch-all — while PUT, POST and DELETE on that same path return 405 with Allow: GET, HEAD and the body “Method Not Allowed”. Neither answer is JSON with an error field.

🔴 Seven of the keys listed above are NOT applied by PUT /api/v1/port/{port}/config, even though it answers 200 “reconfigured”: sessions_per_port, captcha_action, captcha_max_attempts, cache_normalize_ids, h3_profile, debug_capture and debug_capture_n. The handler does not carry them into the configuration it hands to the port manager, so there is no refusal and the port is left as it was. debug_capture and debug_capture_n are set when the port is OPENED, in the body of POST /api/v1/ports/open; to change them, close the port and open it again. You can see it in GET /api/v1/port/{port}/captures, which on a port that was sent debug_capture by PUT keeps answering 400 “port is not a debug fingerprint-capture port”. The other five are not accepted by the open body at all: captcha_action and captcha_max_attempts are configured on the port pool only (see “Port pool”), sessions_per_port is always 1 in the current version, and h3_profile and cache_normalize_ids are set by no endpoint — h3_profile is also always empty, so the key is absent from the GET /config response.

The mtproto_* fields follow the general rule in PUT /config: what the body does not name stays as it was, what it names is applied. The secret and the camouflage domain survive an edit to any other setting, so a tg://proxy link already handed out keeps working; and sending mtproto_secret explicitly is the supported way to rotate the secret without closing the port. Existing connections are not cut by the change; new ones use the new secret.

🔴 A secret that fails to parse is NOT refused: the endpoint quietly issues a fresh one and still answers 200 “reconfigured”. Check the secret in the GET /api/v1/port/{port}/config response against the one you sent.

🔴 In builds before this release it was the other way round: editing any setting on an mtproto port issued a new secret and reset the camouflage domain, killing a link already handed out. If the client is not updated yet, do not edit an mtproto port through PUT /config — and if you already did, re-read the secret and hand out the link again.

The port's identity

Who the port presents itself as: how the profile is chosen, what narrows the choice, and how to supply your own captured fingerprint.

GET/api/v1/port/{port}/modeAuth required

Returns the profile selection mode.

Example (curl)
curl -H "X-API-Key: YOUR_API_KEY" \
  http://127.0.0.1:8891/api/v1/port/20134/mode
Response
{
  "mode": "db"
}
PUT/api/v1/port/{port}/modeAuth required

Changes the profile selection mode on a live port.

ParameterTypeRequiredDescription
modestringYesrandom builds a synthetic fingerprint; db (alias database) takes a real profile from the database; auto generates one for the requested browser and OS; specific pins one by name.
specific_profilestringNoThe profile name for mode=specific, e.g. chrome_152_windows.
Request body
{ "mode": "specific", "specific_profile": "chrome_152_windows" }
Example (curl)
curl -X PUT -H "X-API-Key: YOUR_API_KEY" -H "Content-Type: application/json" \
  -d '{"mode":"specific","specific_profile":"chrome_152_windows"}' \
  http://127.0.0.1:8891/api/v1/port/20134/mode
Response
{
  "mode": "specific"
}
  • This endpoint does NOT accept custom, even though the refusal text names it: “mode must be one of: random, db, auto, specific, custom”. A port enters custom only through PUT /custom_tls or PUT /config.
  • 400 if the port carries a browser or os filter and the mode is switched to random: a synthetic fingerprint belongs to no real browser and cannot honour them. Clear the filters with an empty string first.
  • 400 with the engine's own text if specific_profile is unknown — but the mode has ALREADY been switched by then. After that error re-read GET /api/v1/port/{port}/config: the port is left in specific with the old name.
  • The new value takes effect from the next connection: already-open keep-alive connections are not dropped.
GET/api/v1/port/{port}/browserAuth required

Returns the browser filter. An empty string means no filter.

Example (curl)
curl -H "X-API-Key: YOUR_API_KEY" \
  http://127.0.0.1:8891/api/v1/port/20134/browser
Response
{
  "browser": "chrome"
}
PUT/api/v1/port/{port}/browserAuth required

Narrows the profile choice to one browser, or removes the narrowing.

ParameterTypeRequiredDescription
browserstringYeschrome, firefox, safari, edge, random, or an empty string (clear the filter). A version may be pinned with a suffix: chrome_145.
Request body
{ "browser": "chrome_145" }
Example (curl)
curl -X PUT -H "X-API-Key: YOUR_API_KEY" -H "Content-Type: application/json" \
  -d '{"browser":"chrome_145"}' \
  http://127.0.0.1:8891/api/v1/port/20134/browser
Response
{
  "browser": "chrome_145"
}
  • 400 “browser must be one of: "", random, chrome, firefox, safari, edge (optionally with version: chrome_145)” for anything else.
  • 400 if the port is in random mode: it builds a synthetic fingerprint and cannot honour a filter. Clearing the filter with an empty string on a random port is still allowed.
  • The field must be named exactly this. The server drops unknown names silently and the request applies the zero value instead: false for booleans, an empty string for strings — with a 200 response.
GET/api/v1/port/{port}/osAuth required

Returns the operating-system filter.

Example (curl)
curl -H "X-API-Key: YOUR_API_KEY" \
  http://127.0.0.1:8891/api/v1/port/20134/os
Response
{
  "os": "windows"
}
PUT/api/v1/port/{port}/osAuth required

Narrows the profile choice to one operating system, or removes the narrowing.

ParameterTypeRequiredDescription
osstringYeswindows, macos, linux, ios, android, random, or an empty string (clear the filter).
Request body
{ "os": "macos" }
Example (curl)
curl -X PUT -H "X-API-Key: YOUR_API_KEY" -H "Content-Type: application/json" \
  -d '{"os":"macos"}' \
  http://127.0.0.1:8891/api/v1/port/20134/os
Response
{
  "os": "macos"
}
  • 400 “os must be one of: "", random, windows, macos, linux, ios, android” for anything else.
  • 400 on a random-mode port — for the same reason as the browser filter.
  • The field must be named exactly this. The server drops unknown names silently and the request applies the zero value instead: false for booleans, an empty string for strings — with a 200 response.
GET/api/v1/port/{port}/profileAuth required

Returns the profile the port presents right now. Read-only: a PUT to this path gives 405.

Example (curl)
curl -H "X-API-Key: YOUR_API_KEY" \
  http://127.0.0.1:8891/api/v1/port/20134/profile
Response
{
  "name": "chrome_152_windows",
  "user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) …",
  "browser": "chrome",
  "os": "windows"
}
  • To pin a profile by name use PUT /api/v1/port/{port}/mode with the body {"mode":"specific","specific_profile":"…"}, or PUT /api/v1/port/{port}/config.
PUT/api/v1/port/{port}/custom_tlsAuth required

Feeds the port a fingerprint captured elsewhere, as a whole, and switches it into custom mode. This is how a port takes a shape captured from a real browser — through ChromeApi or tls.peet.ws, say.

ParameterTypeRequiredDescription
ja3stringNoThe full JA3 string.
ja3_hashstringNoThe JA3 hash, if captured.
ja4stringNoThe JA4 string.
ciphersarrayYesThe cipher list in ClientHello order. The only mandatory field. Names are matched against a fixed table; names starting with TLS_GREASE keep their slot as GREASE, and unknown names are dropped silently. If nothing recognizable is left, the answer is 400. Ciphers are NOT derived from ja3 — that string is only used for extension order.
extensionsarray<object>NoThe extension list in ClientHello order. Each element is an object: a required name plus optional supported_groups, signature_algorithms, versions and protocols.
supportedGroupsarrayNoThe supported groups.
signatureAlgorithmsarrayNoThe signature algorithms.
alpnarrayNoThe ALPN list.
h2objectNoHTTP/2 parameters: settings, windowUpdate, akamai_fingerprint, headerOrder.
userAgentstringNoThe User-Agent the fingerprint belongs to.
Example (curl)
curl -X PUT -H "X-API-Key: YOUR_API_KEY" -H "Content-Type: application/json" \
  --data-binary @captured.json \
  http://127.0.0.1:8891/api/v1/port/20134/custom_tls
Response
{
  "ok": true,
  "mode": "custom",
  "profile": {
    "name": "custom_1757116800123",
    "ja3_hash": "cd08e31494f9531f560d64c695473da9",
    "ja4": "t13d1516h2_8daaf6152771_b0da82dd1658",
    "userAgent": "Mozilla/5.0 …"
  }
}
  • Side effect: h2_spoofing and spoof_user_agent are forced off — spoofing HTTP/2 on top of the supplied shape broke redialling. If you need them, turn them on with their own endpoints AFTER this request.
  • Second side effect: the port's previous custom profiles are deleted and existing connections are closed — otherwise part of the traffic would keep going with the old shape.
  • Third side effect: the port's solved challenges are dropped — the new TLS shape and User-Agent invalidate the clearance harvested under the previous identity, so the next request to a protected site goes through a challenge again. An ACTUAL address change in PUT /api/v1/port/{port}/upstream does the same.
  • 400 “invalid JSON: …” if the body does not parse, and 400 “failed to set custom TLS: …” if the engine rejects the shape. The commonest cause of the second is a body with no ciphers, or with names outside the table: 400 “failed to set custom TLS: building custom profile: building ClientHelloSpec: no recognized cipher suites”. A trimmed dump of ja3, ja4 and userAgent alone is not enough; POST /api/v1/fingerprint/parse returns a body of the right shape.
  • There is no GET on this path, and the request falls through to the dashboard catch-all: you get 404 as the plain string “404 page not found”, not JSON, and that 404 does NOT mean the port is closed — do not reopen the port on the strength of it. Read the current composition via /profile/view; other methods (POST, DELETE) give 405.
  • 🔴 The profile name in the response is not custom: the server builds a fresh one on every request as custom_<unix-milliseconds> (custom_1757116800123, say) and returns that same name from GET /api/v1/port/{port}/profile and GET /api/v1/port/{port}/profile/view. Do not compare it against a fixed string, and do not try to pin it with mode=specific plus specific_profile=custom — that returns 400 “fingerprint: profile "custom" not found”.

Egress

How the port reaches the outside world, and what to do when the egress substitutes TLS.

GET/api/v1/port/{port}/upstreamAuth required

Returns the port's egress proxy. Empty values mean direct egress.

Example (curl)
curl -H "X-API-Key: YOUR_API_KEY" \
  http://127.0.0.1:8891/api/v1/port/20134/upstream
Response
{
  "socks5_addr": "socks5://user:pass@host:1080",
  "upstream": "socks5://user:pass@host:1080"
}
  • The socks5_addr field is a legacy name kept for older clients; it always repeats upstream.
PUT/api/v1/port/{port}/upstreamAuth required

Changes the egress proxy on a live port — this is how proxies are rotated without closing the port.

ParameterTypeRequiredDescription
upstreamstringNoAn address of the form scheme://[user:pass@]host:port; schemes socks5, socks5h, http. An empty body or an empty string returns the port to direct egress.
socks5_addrstringNoA legacy alias for the same field; read only when upstream is empty.
Request body
{ "upstream": "socks5://user:pass@host:1080" }
Example (curl)
curl -X PUT -H "X-API-Key: YOUR_API_KEY" -H "Content-Type: application/json" \
  -d '{"upstream":"socks5://user:pass@host:1080"}' \
  http://127.0.0.1:8891/api/v1/port/20134/upstream
Response
{
  "socks5_addr": "socks5://user:pass@host:1080",
  "upstream": "socks5://user:pass@host:1080"
}
  • Side effect when the address ACTUALLY changes: the port's solved challenges are dropped, because the clearance was issued to the previous egress address and does not work from the new one. The next request to a protected site will go through a challenge again.
  • The cached dial is reset and idle connections to the previous egress are closed.
GET/api/v1/port/{port}/chain_proxyAuth required

Returns the intermediate proxy — the first link of the chain in front of the egress one.

Example (curl)
curl -H "X-API-Key: YOUR_API_KEY" \
  http://127.0.0.1:8891/api/v1/port/20134/chain_proxy
Response
{
  "chain_proxy": "http://10.0.0.5:3128"
}
PUT/api/v1/port/{port}/chain_proxyAuth required

Sets or clears the intermediate proxy: traffic goes client → chain → egress → site.

ParameterTypeRequiredDescription
chain_proxystringYesAn address of the same form as upstream. An empty string removes the link.
Request body
{ "chain_proxy": "http://10.0.0.5:3128" }
Example (curl)
curl -X PUT -H "X-API-Key: YOUR_API_KEY" -H "Content-Type: application/json" \
  -d '{"chain_proxy":"http://10.0.0.5:3128"}' \
  http://127.0.0.1:8891/api/v1/port/20134/chain_proxy
Response
{
  "chain_proxy": "http://10.0.0.5:3128"
}
  • The field must be named exactly this. The server drops unknown names silently and the request applies the zero value instead: false for booleans, an empty string for strings — with a 200 response.
GET/api/v1/port/{port}/allow_mitm_upstreamAuth required

Reports whether an egress that substitutes TLS is allowed, and how many connections have already been refused for that reason.

Example (curl)
curl -H "X-API-Key: YOUR_API_KEY" \
  http://127.0.0.1:8891/api/v1/port/20134/allow_mitm_upstream
Response
{
  "allow_mitm_upstream": false,
  "mitm_blocked": 17,
  "mitm_last_issuer": "CN=Corporate Proxy CA"
}
  • mitm_blocked and mitm_last_issuer answer the question “why doesn't the fingerprint arrive” outright: the issuer of the last substituted certificate is named.
PUT/api/v1/port/{port}/allow_mitm_upstreamAuth required

Allows or forbids working through an egress that opens and re-assembles TLS.

ParameterTypeRequiredDescription
allow_mitm_upstreamboolYestrue works even through such an egress; false refuses connections through it. Off by default: such an egress erases the fingerprint, and the port silently stops doing what it was opened for.
Request body
{ "allow_mitm_upstream": true }
Example (curl)
curl -X PUT -H "X-API-Key: YOUR_API_KEY" -H "Content-Type: application/json" \
  -d '{"allow_mitm_upstream":true}' \
  http://127.0.0.1:8891/api/v1/port/20134/allow_mitm_upstream
Response
{
  "allow_mitm_upstream": true,
  "mitm_blocked": 17,
  "mitm_last_issuer": "CN=Corporate Proxy CA"
}
  • 400 “allow_mitm_upstream is required” if the field is absent. This is the only boolean port endpoint that tells “not sent” from false and refuses instead of silently turning off.

Protocol behaviour

What the port does with TLS, HTTP/2 and headers. Every endpoint in this group is built the same way: the body field is called enabled and the response repeats the applied value.

GET/api/v1/port/{port}/h2_spoofingAuth required

Reports whether HTTP/2 settings are spoofed to match the chosen browser.

Example (curl)
curl -H "X-API-Key: YOUR_API_KEY" \
  http://127.0.0.1:8891/api/v1/port/20134/h2_spoofing
Response
{
  "enabled": true
}
PUT/api/v1/port/{port}/h2_spoofingAuth required

Turns HTTP/2 settings spoofing on or off.

ParameterTypeRequiredDescription
enabledboolYestrue takes the SETTINGS frame, priorities and pseudo-header order from the browser profile.
Request body
{ "enabled": true }
Example (curl)
curl -X PUT -H "X-API-Key: YOUR_API_KEY" -H "Content-Type: application/json" \
  -d '{"enabled":true}' \
  http://127.0.0.1:8891/api/v1/port/20134/h2_spoofing
Response
{
  "enabled": true
}
  • PUT /custom_tls forces this off — turn it on AFTER supplying your own fingerprint.
  • The field must be named exactly this. The server drops unknown names silently and the request applies the zero value instead: false for booleans, an empty string for strings — with a 200 response.
GET/api/v1/port/{port}/spoof_user_agentAuth required

Reports whether the request User-Agent is spoofed.

Example (curl)
curl -H "X-API-Key: YOUR_API_KEY" \
  http://127.0.0.1:8891/api/v1/port/20134/spoof_user_agent
Response
{
  "enabled": true
}
PUT/api/v1/port/{port}/spoof_user_agentAuth required

Turns User-Agent spoofing on or off.

ParameterTypeRequiredDescription
enabledboolYestrue takes the header from the profile; false relays the client's own header byte for byte.
Request body
{ "enabled": true }
Example (curl)
curl -X PUT -H "X-API-Key: YOUR_API_KEY" -H "Content-Type: application/json" \
  -d '{"enabled":true}' \
  http://127.0.0.1:8891/api/v1/port/20134/spoof_user_agent
Response
{
  "enabled": true
}
  • The field must be named exactly this. The server drops unknown names silently and the request applies the zero value instead: false for booleans, an empty string for strings — with a 200 response.
GET/api/v1/port/{port}/spoof_headersAuth required

Reports whether the header set and order are brought in line with the browser's.

Example (curl)
curl -H "X-API-Key: YOUR_API_KEY" \
  http://127.0.0.1:8891/api/v1/port/20134/spoof_headers
Response
{
  "enabled": true
}
PUT/api/v1/port/{port}/spoof_headersAuth required

Turns browser-shaped headers on or off.

ParameterTypeRequiredDescription
enabledboolYestrue makes the header set, casing and order match the browser profile.
Request body
{ "enabled": true }
Example (curl)
curl -X PUT -H "X-API-Key: YOUR_API_KEY" -H "Content-Type: application/json" \
  -d '{"enabled":true}' \
  http://127.0.0.1:8891/api/v1/port/20134/spoof_headers
Response
{
  "enabled": true
}
  • The field must be named exactly this. The server drops unknown names silently and the request applies the zero value instead: false for booleans, an empty string for strings — with a 200 response.
GET/api/v1/port/{port}/tls_passthroughAuth required

Reports whether TLS is passed through without being opened.

Example (curl)
curl -H "X-API-Key: YOUR_API_KEY" \
  http://127.0.0.1:8891/api/v1/port/20134/tls_passthrough
Response
{
  "enabled": true
}
PUT/api/v1/port/{port}/tls_passthroughAuth required

Turns TLS pass-through on or off.

ParameterTypeRequiredDescription
enabledboolYestrue passes the connection straight through: the client's own fingerprint survives, the content is not read, and cache and request log are useless on such a port.
Request body
{ "enabled": true }
Example (curl)
curl -X PUT -H "X-API-Key: YOUR_API_KEY" -H "Content-Type: application/json" \
  -d '{"enabled":true}' \
  http://127.0.0.1:8891/api/v1/port/20134/tls_passthrough
Response
{
  "enabled": true
}
  • The field must be named exactly this. The server drops unknown names silently and the request applies the zero value instead: false for booleans, an empty string for strings — with a 200 response.
GET/api/v1/port/{port}/tls_mirrorAuth required

Reports whether the client's own TLS parameters are mirrored instead of the profile's.

Example (curl)
curl -H "X-API-Key: YOUR_API_KEY" \
  http://127.0.0.1:8891/api/v1/port/20134/tls_mirror
Response
{
  "enabled": true
}
PUT/api/v1/port/{port}/tls_mirrorAuth required

Turns mirroring of the client's TLS parameters on or off.

ParameterTypeRequiredDescription
enabledboolYestrue sends outward the shape taken from the client itself rather than from the profile.
Request body
{ "enabled": true }
Example (curl)
curl -X PUT -H "X-API-Key: YOUR_API_KEY" -H "Content-Type: application/json" \
  -d '{"enabled":true}' \
  http://127.0.0.1:8891/api/v1/port/20134/tls_mirror
Response
{
  "enabled": true
}
  • The field must be named exactly this. The server drops unknown names silently and the request applies the zero value instead: false for booleans, an empty string for strings — with a 200 response.
GET/api/v1/port/{port}/http3Auth required

Reports whether HTTP/3 (QUIC) is allowed on the port.

Example (curl)
curl -H "X-API-Key: YOUR_API_KEY" \
  http://127.0.0.1:8891/api/v1/port/20134/http3
Response
{
  "enabled": true
}
PUT/api/v1/port/{port}/http3Auth required

Allows or forbids HTTP/3 (QUIC).

ParameterTypeRequiredDescription
enabledboolYestrue makes the port use HTTP/3 wherever a site offers it.
Request body
{ "enabled": true }
Example (curl)
curl -X PUT -H "X-API-Key: YOUR_API_KEY" -H "Content-Type: application/json" \
  -d '{"enabled":true}' \
  http://127.0.0.1:8891/api/v1/port/20134/http3
Response
{
  "enabled": true
}
  • The field must be named exactly this. The server drops unknown names silently and the request applies the zero value instead: false for booleans, an empty string for strings — with a 200 response.
GET/api/v1/port/{port}/session_resumptionAuth required

Reports whether TLS session resumption is allowed.

Example (curl)
curl -H "X-API-Key: YOUR_API_KEY" \
  http://127.0.0.1:8891/api/v1/port/20134/session_resumption
Response
{
  "enabled": true
}
PUT/api/v1/port/{port}/session_resumptionAuth required

Allows or forbids TLS session resumption (tickets).

ParameterTypeRequiredDescription
enabledboolYestrue accepts and reuses session tickets; false starts every connection with a full handshake.
Request body
{ "enabled": true }
Example (curl)
curl -X PUT -H "X-API-Key: YOUR_API_KEY" -H "Content-Type: application/json" \
  -d '{"enabled":true}' \
  http://127.0.0.1:8891/api/v1/port/20134/session_resumption
Response
{
  "enabled": true
}
  • The field must be named exactly this. The server drops unknown names silently and the request applies the zero value instead: false for booleans, an empty string for strings — with a 200 response.
GET/api/v1/port/{port}/decompressAuth required

Reports whether compressed response bodies are decoded for the client.

Example (curl)
curl -H "X-API-Key: YOUR_API_KEY" \
  http://127.0.0.1:8891/api/v1/port/20134/decompress
Response
{
  "enabled": true
}
PUT/api/v1/port/{port}/decompressAuth required

Turns body decoding on or off.

ParameterTypeRequiredDescription
enabledboolYestrue decodes br, gzip and zstd into plain bytes before handing the body to the client.
Request body
{ "enabled": true }
Example (curl)
curl -X PUT -H "X-API-Key: YOUR_API_KEY" -H "Content-Type: application/json" \
  -d '{"enabled":true}' \
  http://127.0.0.1:8891/api/v1/port/20134/decompress
Response
{
  "enabled": true
}
  • The field must be named exactly this. The server drops unknown names silently and the request applies the zero value instead: false for booleans, an empty string for strings — with a 200 response.
GET/api/v1/port/{port}/cache_ignore_no_cacheAuth required

Reports whether responses are cached in spite of a no-cache header.

Example (curl)
curl -H "X-API-Key: YOUR_API_KEY" \
  http://127.0.0.1:8891/api/v1/port/20134/cache_ignore_no_cache
Response
{
  "enabled": true
}
PUT/api/v1/port/{port}/cache_ignore_no_cacheAuth required

Turns caching in spite of no-cache on or off.

ParameterTypeRequiredDescription
enabledboolYestrue stores the response in the cache even when the site asked not to.
Request body
{ "enabled": true }
Example (curl)
curl -X PUT -H "X-API-Key: YOUR_API_KEY" -H "Content-Type: application/json" \
  -d '{"enabled":true}' \
  http://127.0.0.1:8891/api/v1/port/20134/cache_ignore_no_cache
Response
{
  "enabled": true
}
  • The field must be named exactly this. The server drops unknown names silently and the request applies the zero value instead: false for booleans, an empty string for strings — with a 200 response.
GET/api/v1/port/{port}/auto_profile_from_uaAuth required

Reports whether the profile is picked from the request User-Agent, and shows the last such header.

Example (curl)
curl -H "X-API-Key: YOUR_API_KEY" \
  http://127.0.0.1:8891/api/v1/port/20134/auto_profile_from_ua
Response
{
  "enabled": true,
  "last_ua": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) …"
}
PUT/api/v1/port/{port}/auto_profile_from_uaAuth required

Turns picking the profile from the request User-Agent on or off.

ParameterTypeRequiredDescription
enabledboolYestrue lets the application name who to impersonate: the port picks a profile matching the User-Agent it receives.
Request body
{ "enabled": true }
Example (curl)
curl -X PUT -H "X-API-Key: YOUR_API_KEY" -H "Content-Type: application/json" \
  -d '{"enabled":true}' \
  http://127.0.0.1:8891/api/v1/port/20134/auto_profile_from_ua
Response
{
  "enabled": true
}
  • The PUT response carries no last_ua — it appears only in the GET response, and only after the port has seen its first request. This is the only way to check that the mode fires on live traffic.
  • The field must be named exactly this. The server drops unknown names silently and the request applies the zero value instead: false for booleans, an empty string for strings — with a 200 response.

Load, retries and idling

How many requests the port holds at once, what it does after a network error, and when it closes itself.

GET/api/v1/port/{port}/max_concurrentAuth required

Returns the cap on concurrent requests on the port.

Example (curl)
curl -H "X-API-Key: YOUR_API_KEY" \
  http://127.0.0.1:8891/api/v1/port/20134/max_concurrent
Response
{
  "max_concurrent": 32
}
PUT/api/v1/port/{port}/max_concurrentAuth required

Changes the cap on concurrent requests.

ParameterTypeRequiredDescription
max_concurrentintYes0 or more; 0 means no cap.
Request body
{ "max_concurrent": 32 }
Example (curl)
curl -X PUT -H "X-API-Key: YOUR_API_KEY" -H "Content-Type: application/json" \
  -d '{"max_concurrent":32}' \
  http://127.0.0.1:8891/api/v1/port/20134/max_concurrent
Response
{
  "max_concurrent": 32
}
  • 400 “max_concurrent must be >= 0 (0 = unlimited)” for a negative value.
  • The field must be named exactly this. The server drops unknown names silently and the request applies the zero value instead: false for booleans, an empty string for strings — with a 200 response.
GET/api/v1/port/{port}/sem_timeoutAuth required

Returns how many seconds a request waits for a free slot within the concurrency cap.

Example (curl)
curl -H "X-API-Key: YOUR_API_KEY" \
  http://127.0.0.1:8891/api/v1/port/20134/sem_timeout
Response
{
  "timeout_seconds": 30
}
PUT/api/v1/port/{port}/sem_timeoutAuth required

Changes the wait for a free slot. 🔴 The field is called timeout_seconds, not sem_timeout.

ParameterTypeRequiredDescription
timeout_secondsintYes1 or more — seconds to wait.
Request body
{ "timeout_seconds": 30 }
Example (curl)
curl -X PUT -H "X-API-Key: YOUR_API_KEY" -H "Content-Type: application/json" \
  -d '{"timeout_seconds":30}' \
  http://127.0.0.1:8891/api/v1/port/20134/sem_timeout
Response
{
  "timeout_seconds": 30
}
  • 400 “timeout_seconds must be >= 1” for zero and negatives.
  • A body of {"sem_timeout": 30} — named after the path rather than the field — silently means 0 and is refused with 400.
GET/api/v1/port/{port}/skip_retryAuth required

Reports whether a request is retried after a network error.

Example (curl)
curl -H "X-API-Key: YOUR_API_KEY" \
  http://127.0.0.1:8891/api/v1/port/20134/skip_retry
Response
{
  "skip_retry": false
}
PUT/api/v1/port/{port}/skip_retryAuth required

Turns skipping retries on or off.

ParameterTypeRequiredDescription
skip_retryboolYestrue does not retry after a network error and returns the error to the client at once.
Request body
{ "skip_retry": true }
Example (curl)
curl -X PUT -H "X-API-Key: YOUR_API_KEY" -H "Content-Type: application/json" \
  -d '{"skip_retry":true}' \
  http://127.0.0.1:8891/api/v1/port/20134/skip_retry
Response
{
  "skip_retry": true
}
  • The field must be named exactly this. The server drops unknown names silently and the request applies the zero value instead: false for booleans, an empty string for strings — with a 200 response.
GET/api/v1/port/{port}/retry_delayAuth required

Returns the pause between retries, in milliseconds.

Example (curl)
curl -H "X-API-Key: YOUR_API_KEY" \
  http://127.0.0.1:8891/api/v1/port/20134/retry_delay
Response
{
  "retry_delay_ms": 500
}
PUT/api/v1/port/{port}/retry_delayAuth required

Changes the pause between retries. 🔴 The field is called retry_delay_ms, not retry_delay.

ParameterTypeRequiredDescription
retry_delay_msintYes0 or more — milliseconds.
Request body
{ "retry_delay_ms": 500 }
Example (curl)
curl -X PUT -H "X-API-Key: YOUR_API_KEY" -H "Content-Type: application/json" \
  -d '{"retry_delay_ms":500}' \
  http://127.0.0.1:8891/api/v1/port/20134/retry_delay
Response
{
  "retry_delay_ms": 500
}
  • 400 “retry_delay_ms must be >= 0” for a negative value.
  • A body of {"retry_delay": 500} passes validation as 0 and SETS a 0 ms pause with a 200 response.
PUT/api/v1/port/{port}/idleAuth required

Sets the idle timeout of THIS port, overriding the global one. It has no GET — the current value is visible as idle_seconds in the response of GET /api/v1/port/{port}/config.

ParameterTypeRequiredDescription
secondsint | nullYesSeconds of idling before the port closes itself. null removes the override and returns the port to the global timeout; 0 never closes it.
Request body
{ "seconds": 1800 }
Example (curl)
curl -X PUT -H "X-API-Key: YOUR_API_KEY" -H "Content-Type: application/json" \
  -d '{"seconds":1800}' \
  http://127.0.0.1:8891/api/v1/port/20134/idle
Response
{
  "status": "ok"
}
  • The response does NOT contain the value that was set — only status. Verify through GET /config.
  • 400 “seconds must be >= 0” for a negative value; 404 if the port is closed.
  • This is the only per-port endpoint that does NOT count the call as activity: it parses the port number itself, bypassing the shared helper.

Debug fingerprint capture

A port opened with debug_capture keeps the fingerprints of everyone who connected to it in a ring buffer. This is how you check what the network itself sees. A capture port requires a Pro license: without one, POST /api/v1/ports/open with debug_capture answers 403 “the debug fingerprint port requires a Pro license”.

GET/api/v1/port/{port}/capturesAuth required

Returns the fingerprints captured by a debug capture port, oldest first.

ParameterTypeRequiredDescription
sinceintNoReturn only captures newer than this sequence number.
limitintNoMaximum number of captures to return; 0 or absent means all.
Example (curl)
curl -H "X-API-Key: YOUR_API_KEY" \
  "http://127.0.0.1:8891/api/v1/port/20134/captures?since=0&limit=50"
Response
{
  "count": 128,
  "captures": [
    {
      "seq": 1,
      "time": "2026-09-06T12:34:56.789+03:00",
      "domain": "example.com",
      "client_addr": "127.0.0.1:54321",
      "alpn": "h2",
      "method": "GET",
      "path": "/",
      "ua": "Mozilla/5.0 …",
      "status": "ok",
      "tls": {
        "ja3": "771,4865-4866-4867-…",
        "ja3_hash": "cd08e31494f9531f560d64c695473da9",
        "ja4": "t13d1516h2_8daaf6152771_b0da82dd1658",
        "client_hello_hex": "16030103…"
      },
      "h2": {
        "available": true,
        "akamai": "1:65536;2:0;4:6291456;6:262144|15663105|0|m,a,s,p",
        "settings": [ { "id": 1, "val": 65536 } ],
        "window_update": 15663105,
        "pseudo_order": ["m", "a", "s", "p"],
        "header_order": ["accept", "user-agent", "accept-encoding"]
      }
    }
  ]
}
  • 🔴 The fingerprint lives INSIDE the capture: TLS in captures[].tls (ja3, ja3_hash, ja4, client_hello_hex), HTTP/2 in captures[].h2 (available, akamai, settings, window_update, pseudo_order, header_order), and the User-Agent is captures[].ua. There are NO top-level ja3_hash, ja4 or user_agent fields.
  • status is ok when both TLS and HTTP/2 were captured, and tls-only when the client never completed the h2 handshake (cert pinning, or plain HTTP/1.1): then captures[].h2.available is false and the rest of the h2 block is absent.
  • method, path, ua, tls.client_hello_hex and every h2 field except available are optional: when a value was not captured, the key is simply missing from the response.
  • count is how many captures the buffer holds IN TOTAL, regardless of since and limit.
  • 400 “port is not a debug fingerprint-capture port” if the port was opened without debug_capture.
  • The ring size is set when the port is opened, with the debug_capture_n field, 500 captures by default; an overflow evicts the oldest.
DELETE/api/v1/port/{port}/capturesAuth required

Clears the capture ring of a debug capture port.

Example (curl)
curl -X DELETE -H "X-API-Key: YOUR_API_KEY" \
  http://127.0.0.1:8891/api/v1/port/20134/captures
Response
{
  "count": 0,
  "captures": []
}
  • 400 “port is not a debug fingerprint-capture port” on an ordinary port.

Response cache

The cache is enabled per port and works in one of four modes. The ordinary and the hard cache are different stores, not different settings of one.

ModeWhat it means
normalThe ordinary cache: it lives as long as the application runs, honours response headers and revalidates stale entries with the server.
hardThe hard cache: it survives a restart (a SQLite store) and NEVER revalidates — which is why volatile responses are not admitted into it at all.
hard-mediaThe hard cache for images, fonts, audio and video only: everything else passes by.
hard-autowarmThe hard cache that admits an address only after three consecutive identical response bodies — that is, once the content has proved itself static.
WarningEntries are SHARED by every port: the key is the method, scheme, host and path with query — the port number is not part of it. The store has no per-identity separation, and the protection works differently: nothing is admitted that carries Set-Cookie, Cache-Control: no-store or private, or Vary: *, Vary: Cookie or Vary: Authorization; and ETag and Last-Modified are stripped from what is served, so a validator issued to one identity cannot echo back from another and link the two.
GET/api/v1/port/{port}/cacheAuth required

The state of the port's cache: whether it is on, in which mode, and how much it has already saved.

Example (curl)
curl -H "X-API-Key: YOUR_API_KEY" \
  http://127.0.0.1:8891/api/v1/port/20134/cache
Response
{
  "enabled": true,
  "mode": "hard",
  "hits": 18420,
  "misses": 2210,
  "entries": 1842,
  "used_bytes": 268435456,
  "max_bytes": 2147483648,
  "saved_bytes": 913000000
}
  • mode is empty when the cache is off. saved_bytes is how many bytes did not have to be downloaded again.
PUT/api/v1/port/{port}/cacheAuth required

Turns the cache on or off, or switches its mode, on a live port.

ParameterTypeRequiredDescription
modestringNonormal, hard, hard-media, hard-autowarm, or empty.
enabledboolNoRead ONLY when the mode is not a hard one. false turns the cache off; in a hard mode the field is ignored.
Request body
{ "mode": "hard-media" }
Example (curl)
curl -X PUT -H "X-API-Key: YOUR_API_KEY" -H "Content-Type: application/json" \
  -d '{"mode":"hard-media"}' \
  http://127.0.0.1:8891/api/v1/port/20134/cache
Response
{
  "enabled": true,
  "mode": "hard-media",
  "hits": 18420,
  "misses": 2210,
  "entries": 1842,
  "used_bytes": 268435456,
  "max_bytes": 2147483648,
  "saved_bytes": 913000000
}
  • 🔴 There is exactly one body that turns the cache off: {"enabled": false} with no mode. Any other body — an empty {} included — TURNS THE CACHE ON in normal mode.
  • 400 “invalid cache mode: must be one of normal, hard, hard-media, hard-autowarm” for any other mode.
  • 🔴 The response carries the EXACT mode name: hard-media and hard-autowarm do NOT collapse to hard. Check that the mode was applied by comparing mode to the value you SENT, not to the string hard. The same string comes back from GET /api/v1/port/{port}/cache and in the cache_mode field of /status and /ports.
  • The response is the same state GET returns: the handler answers with it.
DELETE/api/v1/port/{port}/cacheAuth required

Clears this port's cache without touching the others.

Example (curl)
curl -X DELETE -H "X-API-Key: YOUR_API_KEY" \
  http://127.0.0.1:8891/api/v1/port/20134/cache
Response
{
  "status": "cleared"
}
  • A port in a hard mode clears the SHARED hard store — the same one DELETE /api/v1/cache/hard clears.
GET/api/v1/cache/hardAuth required

The state of the hard cache as a whole: how many entries, how much space, and how much has been saved.

Example (curl)
curl -H "X-API-Key: YOUR_API_KEY" \
  http://127.0.0.1:8891/api/v1/cache/hard
Response
{
  "enabled": true,
  "mode": "hard",
  "hits": 18420,
  "misses": 2210,
  "entries": 1842,
  "used_bytes": 268435456,
  "max_bytes": 2147483648,
  "saved_bytes": 913000000
}
DELETE/api/v1/cache/hardAuth required

Clears the hard cache as a whole — for every port at once.

Example (curl)
curl -X DELETE -H "X-API-Key: YOUR_API_KEY" \
  http://127.0.0.1:8891/api/v1/cache/hard
Response
{
  "status": "hard cache cleared"
}
POST/api/v1/cache/hard/evictAuth required

Evicts entries from the hard cache by a list of patterns — surgically, instead of clearing everything.

ParameterTypeRequiredDescription
patternsarrayYesDomains or full URLs. The list is required.
Request body
{ "patterns": ["example.com", "https://cdn.example.net/img/logo.png"] }
Example (curl)
curl -X POST -H "X-API-Key: YOUR_API_KEY" -H "Content-Type: application/json" \
  -d '{"patterns":["example.com"]}' \
  http://127.0.0.1:8891/api/v1/cache/hard/evict
Response
{
  "evicted": 37
}
  • 400 “patterns list is required” for an empty list. There is no domain field on this endpoint.
POST/api/v1/cache/evictAuth required

The same for the ordinary cache: evicts entries by a list of patterns.

ParameterTypeRequiredDescription
patternsarrayYesDomains or full URLs. The list is required.
Request body
{ "patterns": ["example.com"] }
Example (curl)
curl -X POST -H "X-API-Key: YOUR_API_KEY" -H "Content-Type: application/json" \
  -d '{"patterns":["example.com"]}' \
  http://127.0.0.1:8891/api/v1/cache/evict
Response
{
  "evicted": 12
}
  • 400 “patterns list is required” for an empty list.
POST/api/v1/cache/hard/saveAuth required

Kept for older clients: the hard cache lives in SQLite and is written to disk on every put, so there is nothing to flush. The endpoint simply reports the state.

Example (curl)
curl -X POST -H "X-API-Key: YOUR_API_KEY" \
  http://127.0.0.1:8891/api/v1/cache/hard/save
Response
{
  "status": "persisted",
  "entries": 1842,
  "info": "SQLite-backed cache is already persistent"
}
GET/api/v1/cache/hard/exclusionsAuth required

The domains and addresses the hard cache steers clear of.

Example (curl)
curl -H "X-API-Key: YOUR_API_KEY" \
  http://127.0.0.1:8891/api/v1/cache/hard/exclusions
Response
{
  "exclusions": ["api.example.com", "*.example.net/checkout"]
}
PUT/api/v1/cache/hard/exclusionsAuth required

APPENDS the given patterns to the hard cache exclusion list rather than replacing it.

ParameterTypeRequiredDescription
exclusionsarrayYesDomains or address masks that must not be cached.
Request body
{ "exclusions": ["api.example.com"] }
Example (curl)
curl -X PUT -H "X-API-Key: YOUR_API_KEY" -H "Content-Type: application/json" \
  -d '{"exclusions":["api.example.com"]}' \
  http://127.0.0.1:8891/api/v1/cache/hard/exclusions
Response
{
  "exclusions": ["api.example.com", "*.example.net/checkout"]
}
  • 🔴 A shortened list sent with PUT does NOT remove anything: the list only grows, and what you send is merged with what was there, without duplicates. Removal is what DELETE is for.
  • The response is the full list after the merge.
DELETE/api/v1/cache/hard/exclusionsAuth required

Removes the LISTED patterns from the hard cache exclusion list. A body is required.

ParameterTypeRequiredDescription
exclusionsarrayYesWhat to remove from the list.
Request body
{ "exclusions": ["api.example.com"] }
Example (curl)
curl -X DELETE -H "X-API-Key: YOUR_API_KEY" -H "Content-Type: application/json" \
  -d '{"exclusions":["api.example.com"]}' \
  http://127.0.0.1:8891/api/v1/cache/hard/exclusions
Response
{
  "exclusions": ["*.example.net/checkout"]
}
  • 🔴 This is NOT “clear the list”. A DELETE with no body answers 400 “invalid JSON body”. To empty the list, name everything GET returned in the body.
  • 🔴 If you name everything and the list ends up empty, the answer is {"exclusions": null}, not an empty array — exactly as with the ordinary cache.
GET/api/v1/cache/exclusionsAuth required

The same for the ordinary cache: the exclusion list.

Example (curl)
curl -H "X-API-Key: YOUR_API_KEY" \
  http://127.0.0.1:8891/api/v1/cache/exclusions
Response
{
  "exclusions": ["login.example.com"]
}
PUT/api/v1/cache/exclusionsAuth required

APPENDS patterns to the ordinary cache exclusion list.

ParameterTypeRequiredDescription
exclusionsarrayYesDomains or address masks that must not be cached.
Request body
{ "exclusions": ["login.example.com"] }
Example (curl)
curl -X PUT -H "X-API-Key: YOUR_API_KEY" -H "Content-Type: application/json" \
  -d '{"exclusions":["login.example.com"]}' \
  http://127.0.0.1:8891/api/v1/cache/exclusions
Response
{
  "exclusions": ["login.example.com"]
}
  • 🔴 This endpoint's response shows only WHAT YOU SENT, not the full list after the merge — unlike the hard cache one. GET returns the full list.
DELETE/api/v1/cache/exclusionsAuth required

Removes the listed patterns from the ordinary cache exclusion list. A body is required.

ParameterTypeRequiredDescription
exclusionsarrayYesWhat to remove from the list.
Request body
{ "exclusions": ["login.example.com"] }
Example (curl)
curl -X DELETE -H "X-API-Key: YOUR_API_KEY" -H "Content-Type: application/json" \
  -d '{"exclusions":["login.example.com"]}' \
  http://127.0.0.1:8891/api/v1/cache/exclusions
Response
{
  "exclusions": null
}
  • 🔴 When nothing is left after the removal the field comes back as null, NOT as []. This endpoint never returns an empty array, so a client written as resp.exclusions.map(…) or for … of throws on exactly the successful full clear — test for null. A GET of the same list in that state returns []: GET and DELETE have DIFFERENT response shapes.
  • A DELETE with no body — 400 “invalid JSON body”.

The port's request log

The log writes one JSON line per request that passed through the port: time, method, host, path, status code, body type and size, the caching headers and the cache verdict. It is what you use to work out why the cache does not fire and where the traffic goes.

WarningThe log is a browsing history on disk. It is written to data/traffic_port_<port>.jsonl next to the application, survives a restart (the file is APPENDED to, not started afresh) and grows to a cap beyond which old records are evicted. Turning the log off closes the file but does NOT delete it.
GET/api/v1/port/{port}/traffic_logAuth required

Reports whether the log is on, where the file is, and how many records it holds.

Example (curl)
curl -H "X-API-Key: YOUR_API_KEY" \
  http://127.0.0.1:8891/api/v1/port/20134/traffic_log
Response
{
  "enabled": true,
  "path": "data/traffic_port_20134.jsonl",
  "entries": 4821
}
  • 🔴 entries is the NUMBER of records, not the records themselves. The records come from /traffic_log/download.
  • The counter describes the FILE, not the session: after a restart it shows everything already on disk.
  • When the log is off the response is {"enabled": false, "entries": 0}: path is absent, but entries is ALWAYS there and reads 0. Tell off from on-but-empty by enabled — not by whether the entries key is present.
PUT/api/v1/port/{port}/traffic_logAuth required

Turns the log on or off on a live port. Enabling creates the file if it is not there yet.

ParameterTypeRequiredDescription
enabledboolYestrue starts writing; false closes the file and stops.
Request body
{ "enabled": true }
Example (curl)
curl -X PUT -H "X-API-Key: YOUR_API_KEY" -H "Content-Type: application/json" \
  -d '{"enabled":true}' \
  http://127.0.0.1:8891/api/v1/port/20134/traffic_log
Response
{
  "enabled": true,
  "path": "data/traffic_port_20134.jsonl",
  "entries": 4821
}
  • The response is the same state GET returns.
  • 500 “failed to create traffic log: …” if the file could not be created — the data directory is not writable, say.
  • The field must be named enabled: an unknown name means false, which TURNS THE LOG OFF with a 200 response.
GET/api/v1/port/{port}/traffic_log/downloadAuth required

Returns the whole log file — one JSON line per request (NDJSON).

Example (curl)
curl -H "X-API-Key: YOUR_API_KEY" -o traffic.jsonl \
  http://127.0.0.1:8891/api/v1/port/20134/traffic_log/download
Response
{"ts":"2026-09-06T11:22:33Z","method":"GET","scheme":"https","host":"example.com",
 "path":"/static/app.js","status":200,"content_type":"application/javascript",
 "content_len":184320,"cache_control":"max-age=31536000","cache":"hit","proto":"h2","port":20134}
  • The Content-Type is application/x-ndjson and the attachment is named traffic_port_<port>.jsonl. The cache field takes the values hit, miss, stale, excluded and skip.
  • 400 “traffic logging is not enabled” if the log is off.
DELETE/api/v1/port/{port}/traffic_logAuth required

Empties the log file, leaving logging switched on.

Example (curl)
curl -X DELETE -H "X-API-Key: YOUR_API_KEY" \
  http://127.0.0.1:8891/api/v1/port/20134/traffic_log
Response
{
  "status": "cleared"
}
  • 400 “traffic logging is not enabled” if the log is off: this endpoint cannot empty the file of a switched-off log — turn it on first.

System traffic interception and the leak audit

Interception steers a program's or the whole machine's traffic into a port with no proxy configured in the application itself. The leak audit answers the reverse question: is the program under test going AROUND the interception.

GET/api/v1/system/interceptAuth required

Whether the privileged interception service is available right now. Ask this BEFORE opening a port: otherwise the refusal arrives only after the form is filled in.

Example (curl)
curl -H "X-API-Key: YOUR_API_KEY" \
  http://127.0.0.1:8891/api/v1/system/intercept
Response
{
  "available": false,
  "code": "not_installed",
  "reason": "привилегированная служба перехвата не установлена — установите её из установщика BlankTrail"
}
  • The code and reason fields are not duplicates: a program branches on code, a person reads reason. Both fields are always present — with available=true they are empty.
  • The code values: off — interception is unavailable in this build or on this platform; not_installed — the service is not installed; unreachable — the service does not answer; busy — another copy of the application already drives interception, and the service serves one client at a time.
GET/api/v1/system/processesAuth required

The list of applications from which per-process interception targets are chosen.

Example (curl)
curl -H "X-API-Key: YOUR_API_KEY" \
  http://127.0.0.1:8891/api/v1/system/processes
Response
[
  { "name": "chrome.exe", "path": "C:\\Program Files\\Google\\Chrome\\chrome.exe",
    "count": 7 },
  { "name": "curl.exe", "path": "C:\\Windows\\System32\\curl.exe", "count": 1 }
]
  • A list item is an EXECUTABLE, not a process: an interception rule attaches to the exe, so seven browser windows give one row with count = 7. The path is normalised — that is exactly what goes into intercept_apps.
  • 501 on a platform that cannot enumerate processes: an empty array would be indistinguishable from “there are no processes”, and a person would see an empty selection table instead of an explanation.
POST/api/v1/system/leak-auditAuth required

Starts an observation session: it watches whether the chosen program goes around the interception.

ParameterTypeRequiredDescription
exe_pathsarrayYesPaths to the executables to watch. 🔴 The field is called exe_paths, not paths.
policystringYesobserve only watches; block also cuts off what went around.
Request body
{ "exe_paths": ["C:\\Program Files\\App\\app.exe"], "policy": "observe" }
Example (curl)
curl -X POST -H "X-API-Key: YOUR_API_KEY" -H "Content-Type: application/json" \
  -d '{"exe_paths":["C:\\Program Files\\App\\app.exe"],"policy":"observe"}' \
  http://127.0.0.1:8891/api/v1/system/leak-audit
Response
{
  "started": true,
  "code": "",
  "reason": ""
}
  • 🔴 A refusal to start arrives with a 200 and started=false: a 200 here does NOT mean observation began. Check the started field, not the response status.
  • The refusal codes with a 200: ipv6_present — the machine has a live global IPv6 that the route capture does not cover; session_active — a session is already running; rule_conflict — the rule conflicts with the current interception; service_outdated and service_version_unknown — the service is old, or its version could not be determined.
  • 400 with the code no_paths for an empty list, and invalid_request when the body does not parse or the policy is neither observe nor block. 503 if the port manager is not up.
GET/api/v1/system/leak-auditAuth required

The state of the live session: the verdict, the aggregates, the honesty limits and a fresh tail of events.

Example (curl)
curl -H "X-API-Key: YOUR_API_KEY" \
  http://127.0.0.1:8891/api/v1/system/leak-audit
Response
{
  "exe_paths": ["C:\\Program Files\\App\\app.exe"],
  "policy": "observe",
  "started_at": "2026-09-06T11:00:00Z",
  "verdict": "leaking",
  "confirmed": true,
  "by_class": { "dns_direct": 12, "ip_direct": 3 },
  "top_targets": [ { "addr": "203.0.113.7:443", "count": 9 } ],
  "dropped": 0,
  "udp_exhausted": 0,
  "client_trimmed": 0,
  "connection_interrupted": false,
  "audit_unavailable": false,
  "stopped_by": "",
  "limits": ["attribution_by_exe"],
  "recent_events": []
}
  • verdict takes three values: clean, leaking and inconclusive. The last is an honest “we do not know” — when the audit log was unavailable or events were dropped, say.
  • limits lists the honesty limits of this run: attribution_by_exe (the rule attaches to an exe rather than a process), start_window (the window between requesting the rule and applying it), events_dropped, udp_exhausted, connection_interrupted, audit_unavailable, stopped_by_watchdog and others. The verdict must not be read without them.
  • 404 “сеанс аудита утечек не запущен” — сообщение приходит по-русски — until one is started: an empty report would be no more honest than silence.
GET/api/v1/system/leak-audit/reportAuth required

The report without the event feed — this is what you export. With no live session it returns the last finished one.

Example (curl)
curl -H "X-API-Key: YOUR_API_KEY" \
  http://127.0.0.1:8891/api/v1/system/leak-audit/report
  • 404 “аудит утечек ни разу не запускался” — the message comes in Russian — if there is no finished report either.
DELETE/api/v1/system/leak-auditAuth required

Stops the session and returns the final report in the same shape as GET.

Example (curl)
curl -X DELETE -H "X-API-Key: YOUR_API_KEY" \
  http://127.0.0.1:8891/api/v1/system/leak-audit
  • 404 if there is no session. On a stopped session the stopped_by field says who ended it — a person, or the watchdog that cuts an overlong observation short by itself.

Port checks

Two checks on a live port, and one helper. The full test sends a request THROUGH the port and compares how it was seen from outside with who it meant to impersonate.

POST/api/v1/port/{port}/testAuth required

The full configuration test of a port: egress leaks, the through-port fingerprint and UDP support.

Example (curl)
curl -X POST -H "X-API-Key: YOUR_API_KEY" \
  http://127.0.0.1:8891/api/v1/port/20134/test
Response
{
  "leak": {
    "exit_ip": "203.0.113.45",
    "egress_resolver": "203.0.113.53",
    "host_resolver": "192.0.2.1",
    "dns": "pass",
    "ipv6": "pass",
    "latency_ms": 214,
    "checked_at": "2026-09-06T11:03:01Z"
  },
  "leak_skipped": false,
  "fingerprint": {
    "ran": true,
    "skipped": false,
    "observed_ja3": "cd08e31494f9531f560d64c695473da9",
    "observed_ja4": "t13d1516h2_8daaf6152771_b0da82dd1658",
    "observed_ua": "Mozilla/5.0 …",
    "expected_ja4": "t13d1516h2_8daaf6152771_b0da82dd1658",
    "expected_ua": "Mozilla/5.0 …",
    "match": true
  },
  "udp": { "checked": true, "supported": true, "detail": "DNS round-trip via UDP ASSOCIATE",
           "verdict": "relays" },
  "ok": true,
  "messages": [],
  "checked_at": "2026-09-06T11:03:01Z"
}
  • The fingerprint check visits an external fingerprinting service through the port itself: match=false means what went out was not what the port promised.
  • leak_skipped=true with leak_skip_reason tells two things apart: disabled — the check is off in the port settings, direct — the egress is direct and there is nothing to probe. It is NOT “no leaks”.
  • fingerprint.skipped with the reason passthrough is not a failure: with TLS pass-through there is no substitution to observe.
  • udp.supported=true means a completed DNS round-trip through the egress. A granted UDP ASSOCIATE with no answer does NOT count as support — the verdict is accepted_but_silent.
  • The test is capped at 15 seconds.
POST/api/v1/port/{port}/leakcheckAuth required

The egress leak check alone: it compares whose resolver answers and whose IPv6 is visible.

Example (curl)
curl -X POST -H "X-API-Key: YOUR_API_KEY" \
  http://127.0.0.1:8891/api/v1/port/20134/leakcheck
Response
{
  "report": {
    "exit_ip": "203.0.113.45",
    "egress_resolver": "203.0.113.53",
    "host_resolver": "192.0.2.1",
    "dns": "pass",
    "ipv6": "leak",
    "ipv6_addr": "2001:db8::1",
    "host_ipv6": "2001:db8::1",
    "latency_ms": 214,
    "checked_at": "2026-09-06T11:03:01Z"
  }
}
  • The dns and ipv6 fields take three values: pass, leak and inconclusive. This endpoint has no webrtc and no verdict field.
  • 🔴 On a port with direct egress the answer is {"skipped": true} with NO report, and a 200 code. That means “not checked”, not “no leaks”.
  • ipv6_addr next to host_ipv6 is the proof itself: if they match, the machine's own address is visible outside, meaning the tunnel was bypassed. The check is capped at 12 seconds.
POST/api/v1/port/{port}/generateAuth required

Builds a profile for the requested browser and version and puts it on the port.

ParameterTypeRequiredDescription
browserstringYeschrome, firefox, safari or edge.
versionintNoThe browser version; 0 is the default one.
osstringNowindows, macos, linux, ios or android; empty means windows.
saveboolNoSave the profile in the database for reuse.
Request body
{ "browser": "chrome", "version": 152, "os": "windows", "save": true }
Example (curl)
curl -X POST -H "X-API-Key: YOUR_API_KEY" -H "Content-Type: application/json" \
  -d '{"browser":"chrome","version":152,"os":"windows"}' \
  http://127.0.0.1:8891/api/v1/port/20134/generate
  • 400 “browser is required (chrome, firefox, safari, edge)”, 400 “browser must be one of: chrome, firefox, safari, edge”, 400 “os must be one of: windows, macos, linux, ios, android”; 500 with the engine's text if the profile could not be built.