API — Profiles, presets & routing
List and import identities, manage presets and domain routing rules, and register the gateways your ports route through.
Profiles
/api/v1/profilesAuth requiredLists stored identities, with optional paging and a browser filter.
| Parameter | Type | Required | Description |
|---|---|---|---|
limit | int (query) | No | Page 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. |
offset | int (query) | No | Page offset (default 0). |
browser | string (query) | No | Filter by browser, e.g. "chrome". |
curl -H "X-API-Key: YOUR_API_KEY" "http://127.0.0.1:8891/api/v1/profiles?browser=chrome&limit=50"{
"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.
/api/v1/countsAuth requiredReturns the identity breakdown by browser+version+OS combination. The endpoint does not group by browser family.
curl -H "X-API-Key: YOUR_API_KEY" http://127.0.0.1:8891/api/v1/counts{
"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
/api/v1/port/{port}/rotateAuth requiredRotates a port to a different identity within its current filters.
curl -H "X-API-Key: YOUR_API_KEY" -X POST http://127.0.0.1:8891/api/v1/port/20134/rotatecurl -X POST -H "X-API-Key: YOUR_API_KEY" \
http://127.0.0.1:8891/api/v1/port/20134/rotate
{
"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.
/api/v1/port/{port}/profile/viewAuth requiredReturns the full identity a port is currently presenting.
curl -H "X-API-Key: YOUR_API_KEY" http://127.0.0.1:8891/api/v1/port/20134/profile/viewcurl -H "X-API-Key: YOUR_API_KEY" \
http://127.0.0.1:8891/api/v1/port/20134/profile/view
{
"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.
/api/v1/presetsAuth requiredThe tree of saved presets and folders.
curl -H "X-API-Key: YOUR_API_KEY" \
http://127.0.0.1:8891/api/v1/presets{
"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 }
]
}/api/v1/presetsAuth requiredSaves the configurations of open ports as a preset at the given path.
| Parameter | Type | Required | Description |
|---|---|---|---|
path | string | Yes | The preset path in the tree, e.g. parsing/marketplaces. |
ports | array | No | The port numbers to save. Empty saves every open port. |
{ "path": "parsing/marketplaces", "ports": [20134, 20135] }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{
"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.
/api/v1/presets/loadAuth requiredRaises the ports from a preset.
| Parameter | Type | Required | Description |
|---|---|---|---|
path | string | Yes | The preset path. |
mode | string | Yes | replace or merge. Anything else is a 400. |
{ "path": "parsing/marketplaces", "mode": "merge" }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{
"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”.
/api/v1/presetsAuth requiredDeletes a preset.
| Parameter | Type | Required | Description |
|---|---|---|---|
path | string | Yes | The preset path. |
{ "path": "parsing/marketplaces" }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{
"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.
/api/v1/domain_rulesAuth requiredReturns the ordered list of rules.
curl -H "X-API-Key: YOUR_API_KEY" \
http://127.0.0.1:8891/api/v1/domain_rules{
"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.
/api/v1/domain_rulesAuth requiredReplaces the whole rule list — send every rule, not one.
| Parameter | Type | Required | Description |
|---|---|---|---|
rules | array | Yes | The rules in the order they apply. |
rules[].name | string | Yes | The rule name — how a person recognises it. |
rules[].enabled | bool | Yes | A disabled rule is skipped during matching. |
rules[].matchers | array | Yes | The domains; a leading “=” makes the match exact. |
rules[].upstream | string | No | The egress for those domains. |
rules[].chain_proxy | string | No | The first link of the chain. |
rules[].upstream_gateway | string | No | A saved gateway name instead of an egress address. |
rules[].chain_gateway | string | No | A gateway name for the first link. |
rules[].spoof | string | No | inherit takes the identity from the port; custom uses its own from spoof_cfg; off does not spoof. |
rules[].spoof_cfg | object | No | The own identity for spoof=custom: mode, browser, os, profile, spoof_headers, h2_spoofing. |
{ "rules": [ { "name": "catalogue", "enabled": true,
"matchers": ["example.com"],
"upstream": "socks5://203.0.113.10:1080",
"spoof": "inherit" } ] }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{
"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.
/api/v1/ovpnAuth requiredThe list of saved gateways, the state of their tunnels, and whether the backend works at all.
curl -H "X-API-Key: YOUR_API_KEY" \
http://127.0.0.1:8891/api/v1/ovpn{
"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.
/api/v1/ovpnAuth requiredAdds 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.
| Parameter | Type | Required | Description |
|---|---|---|---|
name | string | Yes | The name by which ports will see the gateway. |
content | string | Yes | The whole .ovpn or WireGuard file text, or a vless://, trojan://, ss://, vmess:// or hysteria2:// (hy2://) link. |
kind | string | No | One 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. |
via | string | No | The name of another gateway this one reaches the outside through. |
{ "name": "eu-demo", "kind": "vless",
"content": "vless://…@203.0.113.77:443?…" }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{
"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.
/api/v1/ovpn/{name}Auth requiredEdits an existing gateway: its content, its via link, or both.
| Parameter | Type | Required | Description |
|---|---|---|---|
content | string | No | The new configuration text. An empty string keeps the old one. |
via | string | null | No | null keeps it as is; an empty string clears the link; otherwise a gateway name. |
{ "via": "us-demo" }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{
"status": "updated",
"name": "eu-demo",
"kind": "vless"
}- 404 “config "…" not found”; 400 with the code via_cycle for a closed chain.
/api/v1/ovpn/{name}Auth requiredDeletes a gateway.
curl -X DELETE -H "X-API-Key: YOUR_API_KEY" \
http://127.0.0.1:8891/api/v1/ovpn/eu-demo{
"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.
/api/v1/gateway/openvpn-statusAuth requiredWhether OpenVPN is installed on this machine and, if not, how to install it on this particular system.
curl -H "X-API-Key: YOUR_API_KEY" \
http://127.0.0.1:8891/api/v1/gateway/openvpn-status{
"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.
/api/v1/ovpn/pingAuth requiredMeasures the response time of every saved gateway and returns a snapshot of the results.
curl -X POST -H "X-API-Key: YOUR_API_KEY" \
http://127.0.0.1:8891/api/v1/ovpn/ping{
"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.
/api/v1/gateway/subsAuth requiredLists the subscriptions: how many servers each yielded, which gateways belong to it, when it was refreshed and why the last refresh failed.
curl -H "X-API-Key: YOUR_API_KEY" \
http://127.0.0.1:8891/api/v1/gateway/subs{
"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.
/api/v1/gateway/subsAuth requiredCreates 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.
| Parameter | Type | Required | Description |
|---|---|---|---|
name | string | Yes | The subscription name: Latin letters, digits, dot, hyphen and underscore, up to 64 characters. |
source | string | No | The subscription link. Required on create; omit it on edit to keep the previous one. |
enabled | bool | No | Whether to refresh the subscription on a schedule. |
interval_min | int | No | The refresh period in minutes. |
{ "name": "demo-sub", "source": "https://example.com/sub/demo",
"enabled": true, "interval_min": 60 }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{
"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.
/api/v1/gateway/subs/{name}/refreshAuth requiredRefreshes one subscription immediately, without waiting for the schedule.
curl -X POST -H "X-API-Key: YOUR_API_KEY" \
http://127.0.0.1:8891/api/v1/gateway/subs/demo-sub/refresh{
"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": []
}
}{
"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.
/api/v1/gateway/subs/{name}Auth requiredDeletes a subscription together with the gateways it created — except the ones still referenced.
curl -X DELETE -H "X-API-Key: YOUR_API_KEY" \
http://127.0.0.1:8891/api/v1/gateway/subs/demo-sub{
"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.
/api/v1/presets/folderAuth requiredCreates a folder.
| Parameter | Type | Required | Description |
|---|---|---|---|
path | string | Yes | The folder path in the tree. |
{ "path": "parsing" }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{
"status": "created",
"path": "parsing"
}- 400 “path is required”; 400 with an explanation if the path is malformed.
/api/v1/presets/folderAuth requiredDeletes a folder with everything inside it.
| Parameter | Type | Required | Description |
|---|---|---|---|
path | string | Yes | The folder path. |
{ "path": "parsing" }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{
"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.
/api/v1/presets/moveAuth requiredMoves a preset or a folder elsewhere in the tree.
| Parameter | Type | Required | Description |
|---|---|---|---|
from | string | Yes | What to move. |
to | string | Yes | Where to. |
{ "from": "daily-crawl", "to": "parsing/daily-crawl" }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{
"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.
/api/v1/fingerprint/parseAuth requiredParses 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.
{ "tls": { "ja3": "771,4865-4866-…", "ja4": "t13d1516h2_…" },
"http2": { "akamai_fingerprint": "1:65536;…" },
"user_agent": "Mozilla/5.0 …" }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{
"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.
/api/v1/profiles/importAuth requiredSaves ONE fingerprint into the profile database under a name — after which it can be chosen as mode=specific with that name.
| Parameter | Type | Required | Description |
|---|---|---|---|
name | string | Yes | The name to save it under. |
source_json | string | No | The captured fingerprint as a STRING, as it came from the fingerprinting service. |
custom_tls | object | No | An already-parsed fingerprint — what /fingerprint/parse returned. |
{ "name": "chrome_152_captured",
"source_json": "{\"tls\": … }" }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{
"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.
/api/v1/scraper/tasks/{id}/profile/viewAuth requiredShows the profile a port-pool task runs under: the same fingerprint composition that goes out on the wire.
curl -H "X-API-Key: YOUR_API_KEY" \
http://127.0.0.1:8891/api/v1/scraper/tasks/TASK_ID/profile/view{
"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.