API — Profiles, presets & routing

List and import identities, manage presets and domain routing rules, and register the gateways your ports route through.

Profiles

GET/api/v1/profilesAuth required

Lists stored identities, with optional paging and a browser filter.

ParameterTypeRequiredDescription
limitint (query)NoPage size: default 100, maximum 1000. A larger value is NOT rejected — it is silently clamped to 1000; the page size actually used comes back in the response's limit field. To pull the whole database, page through it with offset and check against total.
offsetint (query)NoPage offset (default 0).
browserstring (query)NoFilter by browser, e.g. "chrome".
Example (curl)
curl -H "X-API-Key: YOUR_API_KEY" "http://127.0.0.1:8891/api/v1/profiles?browser=chrome&limit=50"
Response
{
  "profiles": [
    { "id": 8412, "name": "chrome_152_windows", "browser": "chrome",
      "version": "152", "user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) …",
      "created_at": "2026-08-20T12:00:00Z" }
  ],
  "total": 143900,
  "limit": 50,
  "offset": 0
}

total is how many profiles match the filter IN FULL, not how many this page returned. 400 “browser must be one of: chrome, firefox, safari, edge” for an unknown browser.

GET/api/v1/countsAuth required

Returns the identity breakdown by browser+version+OS combination. The endpoint does not group by browser family.

Example (curl)
curl -H "X-API-Key: YOUR_API_KEY" http://127.0.0.1:8891/api/v1/counts
Response
{
  "chrome_152+windows": 41000,
  "chrome_152+macos": 22000,
  "firefox_152+windows": 17400,
  "safari_18+ios": 9800
}

Keys are shaped "browser_version+os": the browser in LOWER case (chrome, firefox, safari, edge), an underscore, the version, a plus sign and the OS (windows, macos, linux, android, ios). A profile with no recorded OS yields a key like "chrome_152+" — an empty tail after the plus. A bare "Chrome" key never appears: sum the values yourself for a per-family total. These are the same counters returned in the profile_counts field of GET /api/v1/status, and they sum to profile_count from the same response.

A port's identity

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

Rotates a port to a different identity within its current filters.

Example (curl)
curl -H "X-API-Key: YOUR_API_KEY" -X POST http://127.0.0.1:8891/api/v1/port/20134/rotate
Example (curl)
curl -X POST -H "X-API-Key: YOUR_API_KEY" \
  http://127.0.0.1:8891/api/v1/port/20134/rotate
Response
{
  "name": "firefox_152_macos",
  "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:152.0) …",
  "browser": "firefox",
  "os": "macos"
}

500 “no profile available” if the engine has nothing to issue: the profile database is empty or filtered down to nothing. The new identity applies from the next connection — open keep-alive connections are not dropped.

GET/api/v1/port/{port}/profile/viewAuth required

Returns the full identity a port is currently presenting.

Example (curl)
curl -H "X-API-Key: YOUR_API_KEY" http://127.0.0.1:8891/api/v1/port/20134/profile/view
Example (curl)
curl -H "X-API-Key: YOUR_API_KEY" \
  http://127.0.0.1:8891/api/v1/port/20134/profile/view
Response
{
  "name": "chrome_152_windows",
  "browser": "chrome",
  "os": "windows",
  "version": 152,
  "user_agent": "Mozilla/5.0 …",
  "tls": {
    "available": true,
    "cipher_suites": ["TLS_AES_128_GCM_SHA256", "…"],
    "extensions": ["server_name", "…"],
    "curves": ["X25519MLKEM768", "…"],
    "alpn": ["h2", "http/1.1"],
    "ja3": "771,4865-4866-…",
    "ja3_hash": "cd08e31494f9531f560d64c695473da9",
    "ja3_note": "…"
  },
  "h2": { "settings": ["HEADER_TABLE_SIZE=65536", "…"] },
  "spec_source": "spec"
}

spec_source says WHERE the shape came from: preset — a prepared set, spec — built from a specification, factory — a fallback build, unavailable — the shape could not be built, and then tls.available=false. That is the answer to “why does the port look different from what I asked for”.

Presets

A preset is a saved SET of ports together with their settings, filed in folders. It raises a ready-made working arrangement in one request.

GET/api/v1/presetsAuth required

The tree of saved presets and folders.

Example (curl)
curl -H "X-API-Key: YOUR_API_KEY" \
  http://127.0.0.1:8891/api/v1/presets
Response
{
  "name": "",
  "path": "",
  "is_folder": true,
  "children": [
    { "name": "parsing", "path": "parsing", "is_folder": true,
      "children": [ { "name": "marketplaces", "path": "parsing/marketplaces",
                      "is_folder": false } ] },
    { "name": "daily-crawl", "path": "daily-crawl", "is_folder": false }
  ]
}
POST/api/v1/presetsAuth required

Saves the configurations of open ports as a preset at the given path.

ParameterTypeRequiredDescription
pathstringYesThe preset path in the tree, e.g. parsing/marketplaces.
portsarrayNoThe port numbers to save. Empty saves every open port.
Request body
{ "path": "parsing/marketplaces", "ports": [20134, 20135] }
Example (curl)
curl -X POST -H "X-API-Key: YOUR_API_KEY" -H "Content-Type: application/json" \
  -d '{"path":"parsing/marketplaces","ports":[20134,20135]}' \
  http://127.0.0.1:8891/api/v1/presets
Response
{
  "status": "saved",
  "path": "parsing/marketplaces"
}
  • 400 “path is required”; 400 “port N is not open” if the list names a closed port; 400 “invalid path …”, “invalid path segment …” or “path escapes root” if the path leads outside the preset store. 403 comes back only when the file system itself denies permission.
POST/api/v1/presets/loadAuth required

Raises the ports from a preset.

ParameterTypeRequiredDescription
pathstringYesThe preset path.
modestringYesreplace or merge. Anything else is a 400.
Request body
{ "path": "parsing/marketplaces", "mode": "merge" }
Example (curl)
curl -X POST -H "X-API-Key: YOUR_API_KEY" -H "Content-Type: application/json" \
  -d '{"path":"parsing/marketplaces","mode":"merge"}' \
  http://127.0.0.1:8891/api/v1/presets/load
Response
{
  "results": [
    { "port": 20134, "status": "opened" },
    { "port": 20135, "status": "failed", "error": "port 20135 is already in use" }
  ]
}
  • 🔴 mode=replace CLOSES EVERY open port before raising the preset's ports — the traffic going through them is cut. mode=merge closes only the ports whose numbers the preset names.
  • The response is a per-port outcome: a preset may have been written long ago and some ports may fail to open. A port whose saved config combines mode=random with filters is refused here just as it would be on an ordinary open.
  • 400 “path is required”, 400 “mode must be replace or merge”, 404 “preset not found”.
DELETE/api/v1/presetsAuth required

Deletes a preset.

ParameterTypeRequiredDescription
pathstringYesThe preset path.
Request body
{ "path": "parsing/marketplaces" }
Example (curl)
curl -X DELETE -H "X-API-Key: YOUR_API_KEY" -H "Content-Type: application/json" \
  -d '{"path":"parsing/marketplaces"}' \
  http://127.0.0.1:8891/api/v1/presets
Response
{
  "status": "deleted"
}
  • 400 “path is required”; 404 “preset not found”. A body is required.

Domain routing rules

The rules split the traffic of ONE port by domain: which domain goes where, and under which identity. The rules are ordered, and the first enabled one whose matchers hit the hostname wins.

GET/api/v1/domain_rulesAuth required

Returns the ordered list of rules.

Example (curl)
curl -H "X-API-Key: YOUR_API_KEY" \
  http://127.0.0.1:8891/api/v1/domain_rules
Response
{
  "rules": [
    {
      "id": "r1",
      "name": "catalogue",
      "enabled": true,
      "matchers": ["example.com", "=shop.example.net"],
      "upstream": "socks5://203.0.113.10:1080",
      "chain_proxy": "",
      "upstream_gateway": "",
      "chain_gateway": "",
      "spoof": "inherit",
      "spoof_cfg": { "mode": "", "browser": "", "os": "",
                     "spoof_headers": null, "h2_spoofing": null }
    }
  ]
}
  • A matcher with no prefix hits the domain AND all its subdomains; a matcher starting with “=” hits the domain alone.
PUT/api/v1/domain_rulesAuth required

Replaces the whole rule list — send every rule, not one.

ParameterTypeRequiredDescription
rulesarrayYesThe rules in the order they apply.
rules[].namestringYesThe rule name — how a person recognises it.
rules[].enabledboolYesA disabled rule is skipped during matching.
rules[].matchersarrayYesThe domains; a leading “=” makes the match exact.
rules[].upstreamstringNoThe egress for those domains.
rules[].chain_proxystringNoThe first link of the chain.
rules[].upstream_gatewaystringNoA saved gateway name instead of an egress address.
rules[].chain_gatewaystringNoA gateway name for the first link.
rules[].spoofstringNoinherit takes the identity from the port; custom uses its own from spoof_cfg; off does not spoof.
rules[].spoof_cfgobjectNoThe own identity for spoof=custom: mode, browser, os, profile, spoof_headers, h2_spoofing.
Request body
{ "rules": [ { "name": "catalogue", "enabled": true,
                "matchers": ["example.com"],
                "upstream": "socks5://203.0.113.10:1080",
                "spoof": "inherit" } ] }
Example (curl)
curl -X PUT -H "X-API-Key: YOUR_API_KEY" -H "Content-Type: application/json" \
  -d '{"rules":[{"name":"catalogue","enabled":true,"matchers":["example.com"],"upstream":"socks5://203.0.113.10:1080","spoof":"inherit"}]}' \
  http://127.0.0.1:8891/api/v1/domain_rules
Response
{
  "rules": [ { "id": "r1", "name": "catalogue", "enabled": true,
               "matchers": ["example.com"],
               "upstream": "socks5://203.0.113.10:1080", "spoof": "inherit" } ]
}
  • 400 with an explanation if a rule is malformed — it names a gateway that does not exist, say. The response returns the list as stored, with the assigned ids.

Gateways (OpenVPN, VLESS, VMess, Trojan, Shadowsocks, Hysteria2, WireGuard)

A gateway is a saved tunnel configuration that is raised on this machine and offers a local SOCKS5. It is then assigned to a port as the egress (upstream_gateway) or as the first link of a chain (chain_gateway) — by name, not by address.

GET/api/v1/ovpnAuth required

The list of saved gateways, the state of their tunnels, and whether the backend works at all.

Example (curl)
curl -H "X-API-Key: YOUR_API_KEY" \
  http://127.0.0.1:8891/api/v1/ovpn
Response
{
  "configs": [
    {
      "name": "eu-demo",
      "kind": "openvpn",
      "size": 4821,
      "remote": "203.0.113.77:1194",
      "via": "",
      "uploaded_at": "2026-08-20T12:00:00Z",
      "tunnel": { "config": "eu-demo", "status": "up", "ports": 2, "socks_addr": "127.0.0.1:41675" },
      "ping": { "ms": 38, "at": "2026-09-04T09:41:12Z" }
    }
  ],
  "available": true
}
  • available=false means the host has neither OpenVPN nor Xray; a reason comes alongside then. The via field names the gateway this one reaches the outside through — a chain can be built here too.
  • The tunnel object comes only for a gateway that has already been raised, and its state lives in the status field — up, connecting or error. There is no state field, and the endpoint does not report when the tunnel came up at all. Alongside it come ports (how many ports currently hold the tunnel), socks_addr (that tunnel's local SOCKS5) and config (the gateway name); on status=error an error field carries why the tunnel failed to come up.
POST/api/v1/ovpnAuth required

Adds a gateway. The configuration is passed as TEXT in the content field — a whole .ovpn or WireGuard configuration file, or a vless://, trojan://, ss://, vmess:// or hysteria2:// (equivalently hy2://) link; the endpoint has no separate file upload.

ParameterTypeRequiredDescription
namestringYesThe name by which ports will see the gateway.
contentstringYesThe whole .ovpn or WireGuard file text, or a vless://, trojan://, ss://, vmess:// or hysteria2:// (hy2://) link.
kindstringNoOne of openvpn, vless, trojan, shadowsocks, vmess, hysteria2, wireguard. Empty infers it from the content: from the link scheme (hy2:// counts as hysteria2), from the [Interface] and [Peer] sections for wireguard, otherwise openvpn.
viastringNoThe name of another gateway this one reaches the outside through.
Request body
{ "name": "eu-demo", "kind": "vless",
  "content": "vless://…@203.0.113.77:443?…" }
Example (curl)
curl -X POST -H "X-API-Key: YOUR_API_KEY" -H "Content-Type: application/json" \
  -d "{\"name\":\"eu-demo\",\"content\":\"$(cat eu-demo.ovpn | sed 's/\"/\\\\"/g')\"}" \
  http://127.0.0.1:8891/api/v1/ovpn
Response
{
  "status": "saved",
  "name": "eu-demo",
  "kind": "vless"
}
  • 400 “name is required”; 400 “content is required (the .ovpn text or a vless:// link)”; 400 “unknown gateway kind …” for an unknown kind.
  • 400 with the code via_cycle if the gateway chain would close on itself — the code comes next to the text so an interface can tell this cause from the others.
  • 501 “gateway config manager is not enabled” in a build without gateways.
PUT/api/v1/ovpn/{name}Auth required

Edits an existing gateway: its content, its via link, or both.

ParameterTypeRequiredDescription
contentstringNoThe new configuration text. An empty string keeps the old one.
viastring | nullNonull keeps it as is; an empty string clears the link; otherwise a gateway name.
Request body
{ "via": "us-demo" }
Example (curl)
curl -X PUT -H "X-API-Key: YOUR_API_KEY" -H "Content-Type: application/json" \
  -d '{"via":"us-demo"}' \
  http://127.0.0.1:8891/api/v1/ovpn/eu-demo
Response
{
  "status": "updated",
  "name": "eu-demo",
  "kind": "vless"
}
  • 404 “config "…" not found”; 400 with the code via_cycle for a closed chain.
DELETE/api/v1/ovpn/{name}Auth required

Deletes a gateway.

Example (curl)
curl -X DELETE -H "X-API-Key: YOUR_API_KEY" \
  http://127.0.0.1:8891/api/v1/ovpn/eu-demo
Response
{
  "status": "deleted",
  "name": "eu-demo"
}
  • 409 with the code in_use if the gateway is assigned to a live port; 400 with the code via_referenced if another gateway goes out through it; 404 if there is no such gateway.
GET/api/v1/gateway/openvpn-statusAuth required

Whether OpenVPN is installed on this machine and, if not, how to install it on this particular system.

Example (curl)
curl -H "X-API-Key: YOUR_API_KEY" \
  http://127.0.0.1:8891/api/v1/gateway/openvpn-status
Response
{
  "os": "windows",
  "available": false,
  "reason": "openvpn executable not found in PATH",
  "install": {
    "title": "Install OpenVPN Community for Windows",
    "download_url": "https://openvpn.net/community-downloads/",
    "steps": ["…"],
    "note": "…"
  }
}
  • The install field appears only with available=false. The check is capped at three seconds.
  • Inside install: title is the heading, steps are the steps, download_url is the download link (download_url, not url) and note is an optional remark. On linux and macOS a command field is also returned — a ready one-line install command, for example brew install openvpn. download_url, command and note are optional: they are omitted when empty.
POST/api/v1/ovpn/pingAuth required

Measures the response time of every saved gateway and returns a snapshot of the results.

Example (curl)
curl -X POST -H "X-API-Key: YOUR_API_KEY" \
  http://127.0.0.1:8891/api/v1/ovpn/ping
Response
{
  "results": {
    "eu-demo": { "ms": 38, "at": "2026-09-04T09:41:12Z" },
    "us-demo": { "ms": 0, "at": "2026-09-04T09:41:12Z", "error": "timeout" }
  }
}
  • Measurement also runs on a schedule, so the response may carry earlier values: every result carries the time it was taken.
  • 501 “gateway ping is not enabled”; 403 when the license is inactive — this endpoint sits behind the license.
GET/api/v1/gateway/subsAuth required

Lists the subscriptions: how many servers each yielded, which gateways belong to it, when it was refreshed and why the last refresh failed.

Example (curl)
curl -H "X-API-Key: YOUR_API_KEY" \
  http://127.0.0.1:8891/api/v1/gateway/subs
Response
{
  "subscriptions": [
    {
      "name": "demo-sub",
      "source": "https://example.com/…",
      "enabled": true,
      "interval_min": 60,
      "last_ok": "2026-09-04T08:00:00Z",
      "servers": 3,
      "gateways": ["demo-sub.eu-demo", "demo-sub.us-demo", "demo-sub.asia-demo"],
      "skipped": 0
    }
  ]
}
  • The source field comes back REDACTED: the scheme and host remain, the path is replaced with /… and the query with ?…. The subscription secret lives in the path, so this value is not a working link — do not store it as the subscription URL.
  • Subscription gateway names have the form subscription.server (for example demo-sub.DE-Germaniya); the base comes from the server's remark. A colon is not allowed in a name — the gw: prefix in the dashboard is only how a route line is rendered, never part of the name sent to the API.
  • skipped is how many of the subscription's servers were skipped as unsupported.
POST/api/v1/gateway/subsAuth required

Creates a subscription or edits an existing one by name. The subscription's servers appear as ordinary gateways and can be assigned as an egress or a first link.

ParameterTypeRequiredDescription
namestringYesThe subscription name: Latin letters, digits, dot, hyphen and underscore, up to 64 characters.
sourcestringNoThe subscription link. Required on create; omit it on edit to keep the previous one.
enabledboolNoWhether to refresh the subscription on a schedule.
interval_minintNoThe refresh period in minutes.
Request body
{ "name": "demo-sub", "source": "https://example.com/sub/demo",
  "enabled": true, "interval_min": 60 }
Example (curl)
curl -X POST -H "X-API-Key: YOUR_API_KEY" -H "Content-Type: application/json" \
  -d '{"name":"demo-sub","source":"https://example.com/sub/demo","interval_min":60}' \
  http://127.0.0.1:8891/api/v1/gateway/subs
Response
{
  "subscription": {
    "name": "demo-sub",
    "source": "https://example.com/…",
    "enabled": true,
    "interval_min": 60,
    "last_ok": "2026-09-04T08:00:00Z",
    "servers": 3,
    "gateways": ["demo-sub.eu-demo", "demo-sub.us-demo", "demo-sub.asia-demo"],
    "skipped": 0
  },
  "result": {
    "added": 3, "updated": 0, "kept": 0,
    "removed": 0, "retained": 0,
    "skipped": 0, "unsupported": []
  }
}
  • There are no top-level status or name fields: the whole record arrives under subscription, in the same shape as the list items of GET /api/v1/gateway/subs.
  • The subscription is refreshed inside this very request, for up to 60 seconds: that is why its servers appear immediately instead of after the first scheduled tick. The response may take a moment.
  • 🔴 A failed refresh is NOT a failed save: the status code is still 200, but refresh_error with the error text arrives instead of result, while the record is already on disk and will retry. A client must check refresh_error: otherwise a broken subscription reads as successfully created.
  • result is the outcome of this refresh: added, updated, kept, removed, retained, skipped and unsupported (entries shaped as scheme×count).
  • The subscription link is never returned in the clear: source comes back shortened — scheme and host, with the path and query replaced by an ellipsis.
POST/api/v1/gateway/subs/{name}/refreshAuth required

Refreshes one subscription immediately, without waiting for the schedule.

Example (curl)
curl -X POST -H "X-API-Key: YOUR_API_KEY" \
  http://127.0.0.1:8891/api/v1/gateway/subs/demo-sub/refresh
Response
{
  "subscription": {
    "name": "demo-sub",
    "source": "https://example.com/…",
    "enabled": true,
    "interval_min": 60,
    "last_ok": "2026-09-04T08:00:00Z",
    "servers": 3,
    "gateways": ["demo-sub.eu-demo", "demo-sub.us-demo", "demo-sub.asia-demo"],
    "skipped": 0
  },
  "result": {
    "added": 1, "updated": 0, "kept": 2,
    "removed": 0, "retained": 0,
    "skipped": 0, "unsupported": []
  }
}
Response when the refresh failed (still 200)
{
  "subscription": { "name": "demo-sub", "servers": 3, "…": "…" },
  "refresh_error": "refresh failure text"
}
  • There are no top-level status or name fields. The name, the server count and the gateway list live inside subscription — the same object GET /api/v1/gateway/subs returns, with source redacted down to scheme and host.
  • result is the summary of one sync pass: added, updated, kept, removed, retained, skipped, unsupported. The skipped inside result is about this pass; the skipped inside subscription is about the subscription's state.
  • A failed refresh returns 200 with a refresh_error field, not an HTTP error: the subscription record stays on disk and the schedule keeps retrying. Detect success by the presence of result and the absence of refresh_error, not by the status code.
  • 403 when the license is inactive; 404 if there is no subscription with that name; 501 “gateway subscriptions are not enabled” when the subscription scheduler is off.
DELETE/api/v1/gateway/subs/{name}Auth required

Deletes a subscription together with the gateways it created — except the ones still referenced.

Example (curl)
curl -X DELETE -H "X-API-Key: YOUR_API_KEY" \
  http://127.0.0.1:8891/api/v1/gateway/subs/demo-sub
Response
{
  "status": "deleted",
  "removed": ["demo-sub.eu-demo", "demo-sub.us-demo"],
  "retained": ["demo-sub.asia-demo"]
}
  • The response carries NO subscription name: it carries two gateway lists — removed (deleted) and retained (kept). They are the only way to learn what survived.
  • 🔴 The subscription itself is ALWAYS deleted; this endpoint has no port-busy refusal. A gateway referenced by an open port or an enabled domain-routing rule goes into retained: it stays as an ordinary gateway, but it no longer has an owning subscription and will not be refreshed. If you want the gateway to go with the subscription, free the port BEFORE deleting.
  • 404 if no subscription has that name; 501 if the gateway store is disabled.

Preset folders

Presets are filed in folders, and a folder is not decoration: a preset is loaded as a whole set of ports, and a set is easier to keep together.

POST/api/v1/presets/folderAuth required

Creates a folder.

ParameterTypeRequiredDescription
pathstringYesThe folder path in the tree.
Request body
{ "path": "parsing" }
Example (curl)
curl -X POST -H "X-API-Key: YOUR_API_KEY" -H "Content-Type: application/json" \
  -d '{"path":"parsing"}' \
  http://127.0.0.1:8891/api/v1/presets/folder
Response
{
  "status": "created",
  "path": "parsing"
}
  • 400 “path is required”; 400 with an explanation if the path is malformed.
DELETE/api/v1/presets/folderAuth required

Deletes a folder with everything inside it.

ParameterTypeRequiredDescription
pathstringYesThe folder path.
Request body
{ "path": "parsing" }
Example (curl)
curl -X DELETE -H "X-API-Key: YOUR_API_KEY" -H "Content-Type: application/json" \
  -d '{"path":"parsing"}' \
  http://127.0.0.1:8891/api/v1/presets/folder
Response
{
  "status": "deleted",
  "path": "parsing"
}
  • 400 “path is required”; 404 “folder not found”; 400 “invalid path …”, “invalid path segment …” or “path escapes root” if the path leads outside the preset store. 403 comes back only when the file system itself denies permission.
POST/api/v1/presets/moveAuth required

Moves a preset or a folder elsewhere in the tree.

ParameterTypeRequiredDescription
fromstringYesWhat to move.
tostringYesWhere to.
Request body
{ "from": "daily-crawl", "to": "parsing/daily-crawl" }
Example (curl)
curl -X POST -H "X-API-Key: YOUR_API_KEY" -H "Content-Type: application/json" \
  -d '{"from":"daily-crawl","to":"parsing/daily-crawl"}' \
  http://127.0.0.1:8891/api/v1/presets/move
Response
{
  "status": "moved",
  "from": "daily-crawl",
  "to": "parsing/daily-crawl"
}
  • 400 “from and to are required”; 404 “source not found”; 409 “destination already exists”.

Profiles and fingerprints

A fingerprint captured from a real browser can be parsed, inspected and saved under a name — so it can later be chosen like any other profile.

POST/api/v1/fingerprint/parseAuth required

Parses a captured fingerprint and shows what it is made of, saving nothing. The request body is the RAW JSON of the capture as the fingerprinting service returned it.

Request body
{ "tls": { "ja3": "771,4865-4866-…", "ja4": "t13d1516h2_…" },
  "http2": { "akamai_fingerprint": "1:65536;…" },
  "user_agent": "Mozilla/5.0 …" }
Example (curl)
curl -X POST -H "X-API-Key: YOUR_API_KEY" -H "Content-Type: application/json" \
  --data-binary @captured.json \
  http://127.0.0.1:8891/api/v1/fingerprint/parse
Response
{
  "custom_tls": { "ja3": "…", "ja4": "…", "ciphers": ["…"], "extensions": [{"name": "server_name (0)"},
                    {"name": "application_layer_protocol_negotiation (16)", "protocols": ["h2", "http/1.1"]}],
                  "alpn": ["h2", "http/1.1"], "userAgent": "Mozilla/5.0 …" },
  "summary": { "ja3_hash": "…", "ja4": "…", "user_agent": "Mozilla/5.0 …",
                "browser": "chrome", "ciphers": 16, "extensions": 15 }
}
  • custom_tls is a ready body for PUT /api/v1/port/{port}/custom_tls: parse, inspect, apply.
  • The tls.peet.ws /api/all and ChromeApi /tls formats are accepted. The body is capped at 1 MiB. 400 with an explanation if parsing failed.
  • custom_tls.extensions is a list of OBJECTS: each has a required name plus optional supported_groups, signature_algorithms, versions and protocols. It is NOT the same field as extensions in the /profile/view response, where the elements are plain strings. summary is an object too, not a string.
POST/api/v1/profiles/importAuth required

Saves ONE fingerprint into the profile database under a name — after which it can be chosen as mode=specific with that name.

ParameterTypeRequiredDescription
namestringYesThe name to save it under.
source_jsonstringNoThe captured fingerprint as a STRING, as it came from the fingerprinting service.
custom_tlsobjectNoAn already-parsed fingerprint — what /fingerprint/parse returned.
Request body
{ "name": "chrome_152_captured",
  "source_json": "{\"tls\": … }" }
Example (curl)
curl -X POST -H "X-API-Key: YOUR_API_KEY" -H "Content-Type: application/json" \
  -d '{"name":"chrome_152_captured","custom_tls":{ … }}' \
  http://127.0.0.1:8891/api/v1/profiles/import
Response
{
  "name": "chrome_152_captured",
  "summary": { "ja3_hash": "…", "ja4": "…", "user_agent": "Mozilla/5.0 …",
                "browser": "chrome", "ciphers": 16, "extensions": 15 }
}
  • One of source_json or custom_tls is required: without both the answer is 400 “custom_tls or source_json required”. With no name — 400 “name is required”.
  • 409 if a profile with that name already exists. The body is capped at 1 MiB.
GET/api/v1/scraper/tasks/{id}/profile/viewAuth required

Shows the profile a port-pool task runs under: the same fingerprint composition that goes out on the wire.

Example (curl)
curl -H "X-API-Key: YOUR_API_KEY" \
  http://127.0.0.1:8891/api/v1/scraper/tasks/TASK_ID/profile/view
Response
{
  "port": 20134,
  "profile": { "name": "chrome_152_windows", "browser": "chrome",
                "os": "windows", "tls": { "ja3_hash": "…" }, "spec_source": "spec" }
}

port names which of the pool's ports was inspected: a pool holds many, and each has its own profile. 409 “scraper task has no open ports yet” and “scraper task ports are not live”; 404 if there is no such pool.