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.
/api/v1/ports/openAuth requiredRaises a proxy port with the given identity and behaviour.
| Parameter | Type | Required | Description |
|---|---|---|---|
port | int | Yes | The port number, 1–65535. |
protocol | string | No | http (the default), socks5 or mtproto. |
upstream | string | No | The egress proxy; empty means direct. |
mode | string | No | How the identity is chosen: random, db, auto, specific, custom. |
browser | string | No | The browser filter; incompatible with mode=random. |
os | string | No | The OS filter; incompatible with mode=random. |
{
"port": 20134,
"protocol": "socks5",
"mode": "db",
"browser": "chrome",
"os": "windows",
"upstream": "socks5://user:pass@host:1080"
}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{
"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
| Field | Type | Default | What it does |
|---|---|---|---|
upstream | string | — | The egress proxy: scheme://[user:pass@]host:port, schemes socks5, socks5h, http. Empty means direct egress. |
chain_proxy | string | — | The first link of the chain in front of the egress proxy. |
upstream_gateway | string | — | The name of a saved gateway; its local SOCKS5 becomes the egress. |
chain_gateway | string | — | The name of a saved gateway for the first link of the chain. |
ovpn_config | string | — | A legacy alias for upstream_gateway, kept for older clients. |
upstream_tls_insecure | bool | false | Drop certificate verification for an https proxy. Only for your own proxy with a self-signed certificate. |
allow_mitm_upstream | bool | false | Allow an egress that opens TLS itself. Forbidden by default: such an egress erases the fingerprint. |
egress_force_ipv4 | bool | true | Egress over IPv4 only — protection against an IPv6 leak. |
block_private_targets | bool | true | Refuse connections to private, loopback, link-local and CGNAT literals: otherwise the proxy becomes a map of the local network. |
Identity and protocol
| Field | Type | Default | What it does |
|---|---|---|---|
mode | string | random | random, db (alias database), auto, specific or custom. Mode random is incompatible with the browser/os filters. |
browser | string | — | The browser filter: chrome, firefox, safari, edge, random; a version may be added — chrome_145. |
os | string | — | The OS filter: windows, macos, linux, ios, android, random. |
specific_profile | string | — | The profile name for mode=specific, e.g. chrome_152_windows. |
custom_tls | object | — | A captured fingerprint as a whole for mode=custom; the same shape as PUT /port/{port}/custom_tls. |
auto_profile_from_ua | bool | false | Pick the profile from the request User-Agent. |
h2_spoofing | bool | — | Spoof the HTTP/2 settings to match the browser profile. |
spoof_user_agent | bool | — | Spoof the User-Agent. Off relays the client's own header byte for byte. |
spoof_headers | bool | — | Bring the header set and order in line with the browser's. |
tls_passthrough | bool | false | Pass TLS straight through without opening it: the client's fingerprint survives, the content is not read. |
tls_mirror | bool | false | Open TLS but replay the client's own captured fingerprint outward. |
session_resumption | bool | — | Allow TLS session resumption (tickets). |
enable_http3 | bool | false | Re-originate over HTTP/3 where the site offers h3 and the egress can carry UDP. |
decompress | bool | — | Decode br/gzip/zstd before handing the body to the client. The challenge solver forces it on. |
Load and timeouts
| Field | Type | Default | What it does |
|---|---|---|---|
max_concurrent | int | — | The cap on concurrent requests; 0 means no cap. |
sem_timeout | int | — | 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_retry | bool | false | Do not retry a request after a network error. |
retry_delay_ms | int | — | The pause between retries, in milliseconds. |
timeout_seconds | int | — | The idle timeout of a CONNECTION (not of the port), in seconds. |
connect_timeout_seconds | int | 5 | The ceiling on ONE dial attempt. |
request_timeout_seconds | int | 30 | The ceiling on the whole establishment phase including retries; 0 means no limit. |
idle_seconds | int | null | null | The idle timeout of the PORT, overriding the global one (30 minutes by default); 0 never closes it. |
Cache, log and capture
| Field | Type | Default | What it does |
|---|---|---|---|
cache_enabled | bool | false | Enable the response cache on the port. |
cache_mode | string | normal | normal, hard, hard-media or hard-autowarm. |
cache_ignore_no_cache | bool | false | Cache in spite of a no-cache header. |
traffic_log | bool | false | Write the port's request log to data/traffic_port_<port>.jsonl. |
debug_capture | bool | false | Open a fingerprint capture port. 🔴 Requires a Pro license: 403 otherwise. |
debug_capture_n | int | 500 | The size of the capture ring on a capture port. |
Challenges and sessions
| Field | Type | Default | What it does |
|---|---|---|---|
js_solver | bool | false | The challenge solver: requests that hit a challenge go to the solver pool. Requires opening TLS (incompatible with tls_passthrough). |
keep_sessions | bool | false | The 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
| Field | Type | Default | What it does |
|---|---|---|---|
leak_guard | string | — | The pre-start egress DNS/IPv6 leak check: off, warn or enforce. |
vdns_mode | string | off | Virtual 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_strategy | string | auto | auto or custom — where the resolvers come from. |
custom_resolvers | array | — | A list of host:port for resolver_strategy=custom. |
ecs_enabled | bool | true | Pass the client subnet in the DNS query (EDNS Client Subnet). |
vdns_strict_bypass | bool | false | Strict bypass: only an IP literal goes out, the hostname never leaves the machine. |
require_udp_dns | bool | false | Open 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
| Field | Type | Default | What it does |
|---|---|---|---|
intercept | bool | false | Steer system traffic into the port with no proxy configured in the application. |
intercept_scope | string | system | 🔴 system means the WHOLE machine (the default), process only the listed programs. |
intercept_apps | array | — | 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.
| Field | Type | Default | What it does |
|---|---|---|---|
mtproto_secret | string | — | The canonical secret of the form ee…; empty generates a fresh one. |
mtproto_camouflage_domain | string | www.google.com | The camouflage domain — also the SNI the Telegram client presents. |
mtproto_fallback_real | bool | true | Splice probers and clients with a bad secret to the real camouflage host. |
mtproto_egress | string | auto | auto, obfuscated or faketls — how to reach an upstream MTProto proxy. |
mtproto_faketls_upstream | string | — | The host:port of the upstream MTProto proxy for egress=faketls. |
mtproto_faketls_secret | string | — | The ee… secret of that upstream proxy. |
Close a port
/api/v1/ports/closeAuth requiredCloses an open port and drops every connection going through it.
| Parameter | Type | Required | Description |
|---|---|---|---|
port | int | Yes | The port to close. |
{ "port": 20134 }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{
"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
/api/v1/portsAuth requiredReturns every open port with its full configuration and state.
curl -H "X-API-Key: YOUR_API_KEY" \
http://127.0.0.1:8891/api/v1/ports{
"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
/api/v1/ports/suggestAuth requiredReturns the lowest number in the range 20000–29999 that is neither taken by the manager nor unbindable at the OS level.
curl -H "X-API-Key: YOUR_API_KEY" \
http://127.0.0.1:8891/api/v1/ports/suggest{
"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.
/api/v1/upstream/testAuth requiredChecks that the egress is reachable, that UDP over SOCKS5 works, and that DNS and IPv6 do not leak.
| Parameter | Type | Required | Description |
|---|---|---|---|
checks | array | Yes | Any of http, udp, leak. |
protocol | string | No | socks5 or http — the protocol of the future port. |
upstream | string | No | The egress proxy; empty tests a direct connection. |
chain_proxy | string | No | The first link of the chain. |
upstream_gateway | string | No | The name of a saved gateway instead of an egress address. |
chain_gateway | string | No | The name of a saved gateway for the first link. |
upstream_tls_insecure | bool | No | Mirrors the port setting of the same name: without it the test would check a different configuration from the one you are about to open. |
{
"checks": ["http", "udp", "leak"],
"protocol": "socks5",
"upstream": "socks5://user:pass@host:1080"
}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{
"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.
/api/v1/port/{port}/statusAuth requiredA summary of the live port: identity, behaviour, egress, refusal counters and the time of last activity.
curl -H "X-API-Key: YOUR_API_KEY" \
http://127.0.0.1:8891/api/v1/port/20134/status{
"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.
/api/v1/port/{port}/configAuth requiredA 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.
curl -H "X-API-Key: YOUR_API_KEY" \
http://127.0.0.1:8891/api/v1/port/20134/config{
"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.
/api/v1/port/{port}/configAuth requiredChanges 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.
{ "mode": "auto", "browser": "firefox", "os": "macos" }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{
"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.
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.
| Path | Methods | Body field | Type | Values and caveats |
|---|---|---|---|---|
/mode | GET, PUT | mode | string | random, db (alias database), auto, specific |
/browser | GET, PUT | browser | string | empty, random, chrome, firefox, safari, edge; a version may be added: chrome_145 |
/os | GET, PUT | os | string | empty, random, windows, macos, linux, ios, android |
/profile | GET | — | — | read-only; pin a profile through /mode or /config |
/profile/view | GET | — | — | read-only: the composition of the current profile |
/rotate | POST | — | — | no body; issues a new profile immediately |
/custom_tls | PUT | ja3, ja4, … | object | a captured fingerprint as a whole; switches the port into custom mode |
/upstream | GET, PUT | upstream | string | the egress proxy address; an empty string means direct egress |
/chain_proxy | GET, PUT | chain_proxy | string | the first link of the chain in front of the egress proxy |
/allow_mitm_upstream | GET, PUT | allow_mitm_upstream | bool | the field is required: without it the answer is 400, not false |
/h2_spoofing | GET, PUT | enabled | bool | true or false |
/spoof_user_agent | GET, PUT | enabled | bool | true or false |
/spoof_headers | GET, PUT | enabled | bool | true or false |
/tls_passthrough | GET, PUT | enabled | bool | true or false |
/tls_mirror | GET, PUT | enabled | bool | true or false |
/http3 | GET, PUT | enabled | bool | true or false |
/session_resumption | GET, PUT | enabled | bool | true or false |
/decompress | GET, PUT | enabled | bool | true or false |
/auto_profile_from_ua | GET, PUT | enabled | bool | GET also returns last_ua |
/cache_ignore_no_cache | GET, PUT | enabled | bool | true or false |
/max_concurrent | GET, PUT | max_concurrent | int | 0 or more; 0 means no cap |
/sem_timeout | GET, PUT | timeout_seconds | int | 1 or more — the field name does NOT match the path |
/skip_retry | GET, PUT | skip_retry | bool | true or false |
/retry_delay | GET, PUT | retry_delay_ms | int | 0 or more — the field name does NOT match the path |
/idle | PUT | seconds | int | null | null returns to the global timeout, 0 never closes; there is no GET |
/cache | GET, PUT, DELETE | enabled, mode | bool, string | see the Response cache section |
/traffic_log | GET, PUT, DELETE | enabled | bool | see the port's request log section |
/captures | GET, 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.
/api/v1/port/{port}/modeAuth requiredReturns the profile selection mode.
curl -H "X-API-Key: YOUR_API_KEY" \
http://127.0.0.1:8891/api/v1/port/20134/mode{
"mode": "db"
}/api/v1/port/{port}/modeAuth requiredChanges the profile selection mode on a live port.
| Parameter | Type | Required | Description |
|---|---|---|---|
mode | string | Yes | random 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_profile | string | No | The profile name for mode=specific, e.g. chrome_152_windows. |
{ "mode": "specific", "specific_profile": "chrome_152_windows" }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{
"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.
/api/v1/port/{port}/browserAuth requiredReturns the browser filter. An empty string means no filter.
curl -H "X-API-Key: YOUR_API_KEY" \
http://127.0.0.1:8891/api/v1/port/20134/browser{
"browser": "chrome"
}/api/v1/port/{port}/browserAuth requiredNarrows the profile choice to one browser, or removes the narrowing.
| Parameter | Type | Required | Description |
|---|---|---|---|
browser | string | Yes | chrome, firefox, safari, edge, random, or an empty string (clear the filter). A version may be pinned with a suffix: chrome_145. |
{ "browser": "chrome_145" }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{
"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.
/api/v1/port/{port}/osAuth requiredReturns the operating-system filter.
curl -H "X-API-Key: YOUR_API_KEY" \
http://127.0.0.1:8891/api/v1/port/20134/os{
"os": "windows"
}/api/v1/port/{port}/osAuth requiredNarrows the profile choice to one operating system, or removes the narrowing.
| Parameter | Type | Required | Description |
|---|---|---|---|
os | string | Yes | windows, macos, linux, ios, android, random, or an empty string (clear the filter). |
{ "os": "macos" }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{
"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.
/api/v1/port/{port}/profileAuth requiredReturns the profile the port presents right now. Read-only: a PUT to this path gives 405.
curl -H "X-API-Key: YOUR_API_KEY" \
http://127.0.0.1:8891/api/v1/port/20134/profile{
"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.
- Change the port's identity right awayPOST /api/v1/port/{port}/rotate issues a new profile without waiting for a rotation, and GET /api/v1/port/{port}/profile/view shows the composition of the current one — both are in the profiles reference.
/api/v1/port/{port}/custom_tlsAuth requiredFeeds 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.
| Parameter | Type | Required | Description |
|---|---|---|---|
ja3 | string | No | The full JA3 string. |
ja3_hash | string | No | The JA3 hash, if captured. |
ja4 | string | No | The JA4 string. |
ciphers | array | Yes | The 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. |
extensions | array<object> | No | The extension list in ClientHello order. Each element is an object: a required name plus optional supported_groups, signature_algorithms, versions and protocols. |
supportedGroups | array | No | The supported groups. |
signatureAlgorithms | array | No | The signature algorithms. |
alpn | array | No | The ALPN list. |
h2 | object | No | HTTP/2 parameters: settings, windowUpdate, akamai_fingerprint, headerOrder. |
userAgent | string | No | The User-Agent the fingerprint belongs to. |
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{
"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.
/api/v1/port/{port}/upstreamAuth requiredReturns the port's egress proxy. Empty values mean direct egress.
curl -H "X-API-Key: YOUR_API_KEY" \
http://127.0.0.1:8891/api/v1/port/20134/upstream{
"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.
/api/v1/port/{port}/upstreamAuth requiredChanges the egress proxy on a live port — this is how proxies are rotated without closing the port.
| Parameter | Type | Required | Description |
|---|---|---|---|
upstream | string | No | An 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_addr | string | No | A legacy alias for the same field; read only when upstream is empty. |
{ "upstream": "socks5://user:pass@host:1080" }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{
"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.
/api/v1/port/{port}/chain_proxyAuth requiredReturns the intermediate proxy — the first link of the chain in front of the egress one.
curl -H "X-API-Key: YOUR_API_KEY" \
http://127.0.0.1:8891/api/v1/port/20134/chain_proxy{
"chain_proxy": "http://10.0.0.5:3128"
}/api/v1/port/{port}/chain_proxyAuth requiredSets or clears the intermediate proxy: traffic goes client → chain → egress → site.
| Parameter | Type | Required | Description |
|---|---|---|---|
chain_proxy | string | Yes | An address of the same form as upstream. An empty string removes the link. |
{ "chain_proxy": "http://10.0.0.5:3128" }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{
"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.
/api/v1/port/{port}/allow_mitm_upstreamAuth requiredReports whether an egress that substitutes TLS is allowed, and how many connections have already been refused for that reason.
curl -H "X-API-Key: YOUR_API_KEY" \
http://127.0.0.1:8891/api/v1/port/20134/allow_mitm_upstream{
"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.
/api/v1/port/{port}/allow_mitm_upstreamAuth requiredAllows or forbids working through an egress that opens and re-assembles TLS.
| Parameter | Type | Required | Description |
|---|---|---|---|
allow_mitm_upstream | bool | Yes | true 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. |
{ "allow_mitm_upstream": true }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{
"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.
/api/v1/port/{port}/h2_spoofingAuth requiredReports whether HTTP/2 settings are spoofed to match the chosen browser.
curl -H "X-API-Key: YOUR_API_KEY" \
http://127.0.0.1:8891/api/v1/port/20134/h2_spoofing{
"enabled": true
}/api/v1/port/{port}/h2_spoofingAuth requiredTurns HTTP/2 settings spoofing on or off.
| Parameter | Type | Required | Description |
|---|---|---|---|
enabled | bool | Yes | true takes the SETTINGS frame, priorities and pseudo-header order from the browser profile. |
{ "enabled": true }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{
"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.
/api/v1/port/{port}/spoof_user_agentAuth requiredReports whether the request User-Agent is spoofed.
curl -H "X-API-Key: YOUR_API_KEY" \
http://127.0.0.1:8891/api/v1/port/20134/spoof_user_agent{
"enabled": true
}/api/v1/port/{port}/spoof_user_agentAuth requiredTurns User-Agent spoofing on or off.
| Parameter | Type | Required | Description |
|---|---|---|---|
enabled | bool | Yes | true takes the header from the profile; false relays the client's own header byte for byte. |
{ "enabled": true }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{
"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.
/api/v1/port/{port}/spoof_headersAuth requiredReports whether the header set and order are brought in line with the browser's.
curl -H "X-API-Key: YOUR_API_KEY" \
http://127.0.0.1:8891/api/v1/port/20134/spoof_headers{
"enabled": true
}/api/v1/port/{port}/spoof_headersAuth requiredTurns browser-shaped headers on or off.
| Parameter | Type | Required | Description |
|---|---|---|---|
enabled | bool | Yes | true makes the header set, casing and order match the browser profile. |
{ "enabled": true }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{
"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.
/api/v1/port/{port}/tls_passthroughAuth requiredReports whether TLS is passed through without being opened.
curl -H "X-API-Key: YOUR_API_KEY" \
http://127.0.0.1:8891/api/v1/port/20134/tls_passthrough{
"enabled": true
}/api/v1/port/{port}/tls_passthroughAuth requiredTurns TLS pass-through on or off.
| Parameter | Type | Required | Description |
|---|---|---|---|
enabled | bool | Yes | true 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. |
{ "enabled": true }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{
"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.
/api/v1/port/{port}/tls_mirrorAuth requiredReports whether the client's own TLS parameters are mirrored instead of the profile's.
curl -H "X-API-Key: YOUR_API_KEY" \
http://127.0.0.1:8891/api/v1/port/20134/tls_mirror{
"enabled": true
}/api/v1/port/{port}/tls_mirrorAuth requiredTurns mirroring of the client's TLS parameters on or off.
| Parameter | Type | Required | Description |
|---|---|---|---|
enabled | bool | Yes | true sends outward the shape taken from the client itself rather than from the profile. |
{ "enabled": true }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{
"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.
/api/v1/port/{port}/http3Auth requiredReports whether HTTP/3 (QUIC) is allowed on the port.
curl -H "X-API-Key: YOUR_API_KEY" \
http://127.0.0.1:8891/api/v1/port/20134/http3{
"enabled": true
}/api/v1/port/{port}/http3Auth requiredAllows or forbids HTTP/3 (QUIC).
| Parameter | Type | Required | Description |
|---|---|---|---|
enabled | bool | Yes | true makes the port use HTTP/3 wherever a site offers it. |
{ "enabled": true }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{
"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.
/api/v1/port/{port}/session_resumptionAuth requiredReports whether TLS session resumption is allowed.
curl -H "X-API-Key: YOUR_API_KEY" \
http://127.0.0.1:8891/api/v1/port/20134/session_resumption{
"enabled": true
}/api/v1/port/{port}/session_resumptionAuth requiredAllows or forbids TLS session resumption (tickets).
| Parameter | Type | Required | Description |
|---|---|---|---|
enabled | bool | Yes | true accepts and reuses session tickets; false starts every connection with a full handshake. |
{ "enabled": true }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{
"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.
/api/v1/port/{port}/decompressAuth requiredReports whether compressed response bodies are decoded for the client.
curl -H "X-API-Key: YOUR_API_KEY" \
http://127.0.0.1:8891/api/v1/port/20134/decompress{
"enabled": true
}/api/v1/port/{port}/decompressAuth requiredTurns body decoding on or off.
| Parameter | Type | Required | Description |
|---|---|---|---|
enabled | bool | Yes | true decodes br, gzip and zstd into plain bytes before handing the body to the client. |
{ "enabled": true }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{
"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.
/api/v1/port/{port}/cache_ignore_no_cacheAuth requiredReports whether responses are cached in spite of a no-cache header.
curl -H "X-API-Key: YOUR_API_KEY" \
http://127.0.0.1:8891/api/v1/port/20134/cache_ignore_no_cache{
"enabled": true
}/api/v1/port/{port}/cache_ignore_no_cacheAuth requiredTurns caching in spite of no-cache on or off.
| Parameter | Type | Required | Description |
|---|---|---|---|
enabled | bool | Yes | true stores the response in the cache even when the site asked not to. |
{ "enabled": true }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{
"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.
/api/v1/port/{port}/auto_profile_from_uaAuth requiredReports whether the profile is picked from the request User-Agent, and shows the last such header.
curl -H "X-API-Key: YOUR_API_KEY" \
http://127.0.0.1:8891/api/v1/port/20134/auto_profile_from_ua{
"enabled": true,
"last_ua": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) …"
}/api/v1/port/{port}/auto_profile_from_uaAuth requiredTurns picking the profile from the request User-Agent on or off.
| Parameter | Type | Required | Description |
|---|---|---|---|
enabled | bool | Yes | true lets the application name who to impersonate: the port picks a profile matching the User-Agent it receives. |
{ "enabled": true }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{
"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.
/api/v1/port/{port}/max_concurrentAuth requiredReturns the cap on concurrent requests on the port.
curl -H "X-API-Key: YOUR_API_KEY" \
http://127.0.0.1:8891/api/v1/port/20134/max_concurrent{
"max_concurrent": 32
}/api/v1/port/{port}/max_concurrentAuth requiredChanges the cap on concurrent requests.
| Parameter | Type | Required | Description |
|---|---|---|---|
max_concurrent | int | Yes | 0 or more; 0 means no cap. |
{ "max_concurrent": 32 }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{
"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.
/api/v1/port/{port}/sem_timeoutAuth requiredReturns how many seconds a request waits for a free slot within the concurrency cap.
curl -H "X-API-Key: YOUR_API_KEY" \
http://127.0.0.1:8891/api/v1/port/20134/sem_timeout{
"timeout_seconds": 30
}/api/v1/port/{port}/sem_timeoutAuth requiredChanges the wait for a free slot. 🔴 The field is called timeout_seconds, not sem_timeout.
| Parameter | Type | Required | Description |
|---|---|---|---|
timeout_seconds | int | Yes | 1 or more — seconds to wait. |
{ "timeout_seconds": 30 }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{
"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.
/api/v1/port/{port}/skip_retryAuth requiredReports whether a request is retried after a network error.
curl -H "X-API-Key: YOUR_API_KEY" \
http://127.0.0.1:8891/api/v1/port/20134/skip_retry{
"skip_retry": false
}/api/v1/port/{port}/skip_retryAuth requiredTurns skipping retries on or off.
| Parameter | Type | Required | Description |
|---|---|---|---|
skip_retry | bool | Yes | true does not retry after a network error and returns the error to the client at once. |
{ "skip_retry": true }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{
"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.
/api/v1/port/{port}/retry_delayAuth requiredReturns the pause between retries, in milliseconds.
curl -H "X-API-Key: YOUR_API_KEY" \
http://127.0.0.1:8891/api/v1/port/20134/retry_delay{
"retry_delay_ms": 500
}/api/v1/port/{port}/retry_delayAuth requiredChanges the pause between retries. 🔴 The field is called retry_delay_ms, not retry_delay.
| Parameter | Type | Required | Description |
|---|---|---|---|
retry_delay_ms | int | Yes | 0 or more — milliseconds. |
{ "retry_delay_ms": 500 }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{
"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.
/api/v1/port/{port}/idleAuth requiredSets 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.
| Parameter | Type | Required | Description |
|---|---|---|---|
seconds | int | null | Yes | Seconds of idling before the port closes itself. null removes the override and returns the port to the global timeout; 0 never closes it. |
{ "seconds": 1800 }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{
"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”.
/api/v1/port/{port}/capturesAuth requiredReturns the fingerprints captured by a debug capture port, oldest first.
| Parameter | Type | Required | Description |
|---|---|---|---|
since | int | No | Return only captures newer than this sequence number. |
limit | int | No | Maximum number of captures to return; 0 or absent means all. |
curl -H "X-API-Key: YOUR_API_KEY" \
"http://127.0.0.1:8891/api/v1/port/20134/captures?since=0&limit=50"{
"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.
/api/v1/port/{port}/capturesAuth requiredClears the capture ring of a debug capture port.
curl -X DELETE -H "X-API-Key: YOUR_API_KEY" \
http://127.0.0.1:8891/api/v1/port/20134/captures{
"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.
| Mode | What it means |
|---|---|
normal | The ordinary cache: it lives as long as the application runs, honours response headers and revalidates stale entries with the server. |
hard | The 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-media | The hard cache for images, fonts, audio and video only: everything else passes by. |
hard-autowarm | The hard cache that admits an address only after three consecutive identical response bodies — that is, once the content has proved itself static. |
/api/v1/port/{port}/cacheAuth requiredThe state of the port's cache: whether it is on, in which mode, and how much it has already saved.
curl -H "X-API-Key: YOUR_API_KEY" \
http://127.0.0.1:8891/api/v1/port/20134/cache{
"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.
/api/v1/port/{port}/cacheAuth requiredTurns the cache on or off, or switches its mode, on a live port.
| Parameter | Type | Required | Description |
|---|---|---|---|
mode | string | No | normal, hard, hard-media, hard-autowarm, or empty. |
enabled | bool | No | Read ONLY when the mode is not a hard one. false turns the cache off; in a hard mode the field is ignored. |
{ "mode": "hard-media" }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{
"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.
/api/v1/port/{port}/cacheAuth requiredClears this port's cache without touching the others.
curl -X DELETE -H "X-API-Key: YOUR_API_KEY" \
http://127.0.0.1:8891/api/v1/port/20134/cache{
"status": "cleared"
}- A port in a hard mode clears the SHARED hard store — the same one DELETE /api/v1/cache/hard clears.
/api/v1/cache/hardAuth requiredThe state of the hard cache as a whole: how many entries, how much space, and how much has been saved.
curl -H "X-API-Key: YOUR_API_KEY" \
http://127.0.0.1:8891/api/v1/cache/hard{
"enabled": true,
"mode": "hard",
"hits": 18420,
"misses": 2210,
"entries": 1842,
"used_bytes": 268435456,
"max_bytes": 2147483648,
"saved_bytes": 913000000
}/api/v1/cache/hardAuth requiredClears the hard cache as a whole — for every port at once.
curl -X DELETE -H "X-API-Key: YOUR_API_KEY" \
http://127.0.0.1:8891/api/v1/cache/hard{
"status": "hard cache cleared"
}/api/v1/cache/hard/evictAuth requiredEvicts entries from the hard cache by a list of patterns — surgically, instead of clearing everything.
| Parameter | Type | Required | Description |
|---|---|---|---|
patterns | array | Yes | Domains or full URLs. The list is required. |
{ "patterns": ["example.com", "https://cdn.example.net/img/logo.png"] }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{
"evicted": 37
}- 400 “patterns list is required” for an empty list. There is no domain field on this endpoint.
/api/v1/cache/evictAuth requiredThe same for the ordinary cache: evicts entries by a list of patterns.
| Parameter | Type | Required | Description |
|---|---|---|---|
patterns | array | Yes | Domains or full URLs. The list is required. |
{ "patterns": ["example.com"] }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{
"evicted": 12
}- 400 “patterns list is required” for an empty list.
/api/v1/cache/hard/saveAuth requiredKept 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.
curl -X POST -H "X-API-Key: YOUR_API_KEY" \
http://127.0.0.1:8891/api/v1/cache/hard/save{
"status": "persisted",
"entries": 1842,
"info": "SQLite-backed cache is already persistent"
}/api/v1/cache/hard/exclusionsAuth requiredThe domains and addresses the hard cache steers clear of.
curl -H "X-API-Key: YOUR_API_KEY" \
http://127.0.0.1:8891/api/v1/cache/hard/exclusions{
"exclusions": ["api.example.com", "*.example.net/checkout"]
}/api/v1/cache/hard/exclusionsAuth requiredAPPENDS the given patterns to the hard cache exclusion list rather than replacing it.
| Parameter | Type | Required | Description |
|---|---|---|---|
exclusions | array | Yes | Domains or address masks that must not be cached. |
{ "exclusions": ["api.example.com"] }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{
"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.
/api/v1/cache/hard/exclusionsAuth requiredRemoves the LISTED patterns from the hard cache exclusion list. A body is required.
| Parameter | Type | Required | Description |
|---|---|---|---|
exclusions | array | Yes | What to remove from the list. |
{ "exclusions": ["api.example.com"] }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{
"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.
/api/v1/cache/exclusionsAuth requiredThe same for the ordinary cache: the exclusion list.
curl -H "X-API-Key: YOUR_API_KEY" \
http://127.0.0.1:8891/api/v1/cache/exclusions{
"exclusions": ["login.example.com"]
}/api/v1/cache/exclusionsAuth requiredAPPENDS patterns to the ordinary cache exclusion list.
| Parameter | Type | Required | Description |
|---|---|---|---|
exclusions | array | Yes | Domains or address masks that must not be cached. |
{ "exclusions": ["login.example.com"] }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{
"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.
/api/v1/cache/exclusionsAuth requiredRemoves the listed patterns from the ordinary cache exclusion list. A body is required.
| Parameter | Type | Required | Description |
|---|---|---|---|
exclusions | array | Yes | What to remove from the list. |
{ "exclusions": ["login.example.com"] }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{
"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.
/api/v1/port/{port}/traffic_logAuth requiredReports whether the log is on, where the file is, and how many records it holds.
curl -H "X-API-Key: YOUR_API_KEY" \
http://127.0.0.1:8891/api/v1/port/20134/traffic_log{
"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.
/api/v1/port/{port}/traffic_logAuth requiredTurns the log on or off on a live port. Enabling creates the file if it is not there yet.
| Parameter | Type | Required | Description |
|---|---|---|---|
enabled | bool | Yes | true starts writing; false closes the file and stops. |
{ "enabled": true }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{
"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.
/api/v1/port/{port}/traffic_log/downloadAuth requiredReturns the whole log file — one JSON line per request (NDJSON).
curl -H "X-API-Key: YOUR_API_KEY" -o traffic.jsonl \
http://127.0.0.1:8891/api/v1/port/20134/traffic_log/download{"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.
/api/v1/port/{port}/traffic_logAuth requiredEmpties the log file, leaving logging switched on.
curl -X DELETE -H "X-API-Key: YOUR_API_KEY" \
http://127.0.0.1:8891/api/v1/port/20134/traffic_log{
"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.
/api/v1/system/interceptAuth requiredWhether 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.
curl -H "X-API-Key: YOUR_API_KEY" \
http://127.0.0.1:8891/api/v1/system/intercept{
"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.
/api/v1/system/processesAuth requiredThe list of applications from which per-process interception targets are chosen.
curl -H "X-API-Key: YOUR_API_KEY" \
http://127.0.0.1:8891/api/v1/system/processes[
{ "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.
/api/v1/system/leak-auditAuth requiredStarts an observation session: it watches whether the chosen program goes around the interception.
| Parameter | Type | Required | Description |
|---|---|---|---|
exe_paths | array | Yes | Paths to the executables to watch. 🔴 The field is called exe_paths, not paths. |
policy | string | Yes | observe only watches; block also cuts off what went around. |
{ "exe_paths": ["C:\\Program Files\\App\\app.exe"], "policy": "observe" }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{
"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.
/api/v1/system/leak-auditAuth requiredThe state of the live session: the verdict, the aggregates, the honesty limits and a fresh tail of events.
curl -H "X-API-Key: YOUR_API_KEY" \
http://127.0.0.1:8891/api/v1/system/leak-audit{
"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.
/api/v1/system/leak-audit/reportAuth requiredThe report without the event feed — this is what you export. With no live session it returns the last finished one.
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.
/api/v1/system/leak-auditAuth requiredStops the session and returns the final report in the same shape as GET.
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.
/api/v1/port/{port}/testAuth requiredThe full configuration test of a port: egress leaks, the through-port fingerprint and UDP support.
curl -X POST -H "X-API-Key: YOUR_API_KEY" \
http://127.0.0.1:8891/api/v1/port/20134/test{
"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.
/api/v1/port/{port}/leakcheckAuth requiredThe egress leak check alone: it compares whose resolver answers and whose IPv6 is visible.
curl -X POST -H "X-API-Key: YOUR_API_KEY" \
http://127.0.0.1:8891/api/v1/port/20134/leakcheck{
"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.
/api/v1/port/{port}/generateAuth requiredBuilds a profile for the requested browser and version and puts it on the port.
| Parameter | Type | Required | Description |
|---|---|---|---|
browser | string | Yes | chrome, firefox, safari or edge. |
version | int | No | The browser version; 0 is the default one. |
os | string | No | windows, macos, linux, ios or android; empty means windows. |
save | bool | No | Save the profile in the database for reuse. |
{ "browser": "chrome", "version": 152, "os": "windows", "save": true }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.