API — License, access & certificate

Check license status, manage authentication, read overall system status, and download the root certificate over the API.

License

GET/api/v1/license/statusAuth required

Returns whether the license is active, along with your plan, port-pool entitlement (pool) and Challenge Breaker process limits. It does not carry a port limit: the binding ceiling is the max_ports field of GET /api/v1/status.

Example (curl)
curl -H "X-API-Key: YOUR_API_KEY" http://127.0.0.1:8891/api/v1/license/status
Response
{
  "activated": true,
  "email": "demo@blanktrail.pro",
  "plan": "pro",
  "period_end": "2027-02-15T00:00:00Z",
  "pool": true,
  "js_solver_max_procs": 4,
  "js_solver_procs": 4,
  "js_solver_live_procs": 1
}
  • The label and allowed_domains fields are sent ONLY for a service (promo) tariff, which is why the example above has neither: label is a suffix for the plan name, rendered in the dashboard as “Pro (WB)”; allowed_domains is the list of domains that tariff is restricted to, shown under the plan name as “Restricted to: …”.
  • 🔴 allowed_domains is a rule, not a hint: while the list is non-empty, the port routes ONLY to those domains. A bare entry covers the domain itself and all of its subdomains; an entry prefixed with “=” matches that exact name only; a bare IP address NEVER matches. An empty list (the field absent) means no restriction.
  • 🔴 The refusal comes from the PROXY PORT itself, not from this API: HTTP CONNECT and plain HTTP get 403 Forbidden with an empty body — no JSON, no error field; SOCKS5 gets reply code 0x02 “connection not allowed by ruleset”; UDP ASSOCIATE is not granted at all under such a tariff. The upstream proxy is not at fault — changing the egress will not help; check the address against the list.
  • Challenge Breaker does not run for a host outside the list. The API handles, the port test and the rest of the dashboard behave as usual.

Authentication

The password here is the dashboard's, not the API's: API requests are authenticated by the key in the X-API-Key header and need no session cookie. These endpoints serve the interface and first-setup scripts.

POST/api/v1/auth/loginNo auth

Signs in to the dashboard with a password; sets a session cookie.

ParameterTypeRequiredDescription
passwordstringYesThe dashboard password.
Request body
{ "password": "your-dashboard-password" }
Example (curl)
curl -X POST -H "Content-Type: application/json" -c cookies.txt \
  -d '{"password":"your-dashboard-password"}' \
  http://127.0.0.1:8891/api/v1/auth/login
Response
{
  "status": "ok"
}
  • 401 “invalid password”; 429 “too many attempts — try again later” after a run of failures from one address.
  • 501 “auth not configured” if dashboard sign-in is not configured in this build.
POST/api/v1/auth/logoutAuth required

Clears the dashboard session cookie.

Example (curl)
curl -X POST -H "X-API-Key: YOUR_API_KEY" \
  http://127.0.0.1:8891/api/v1/auth/logout
Response
{
  "status": "ok"
}
GET/api/v1/auth/statusAuth required

The state of sign-in and password: whether you are signed in, whether a password is set, and whether it is still the initial one.

Example (curl)
curl -H "X-API-Key: YOUR_API_KEY" \
  http://127.0.0.1:8891/api/v1/auth/status
Response
{
  "authenticated": true,
  "password_is_initial": false,
  "password_set": true
}
  • password_is_initial=true means the password is still the one issued at install; that is a reason to demand a change.
POST/api/v1/auth/passwordAuth required

Changes the dashboard password. The current password is required.

ParameterTypeRequiredDescription
current_passwordstringYesThe current password. 🔴 The field is called current_password, not current.
new_passwordstringYesThe new password, at least 8 characters. 🔴 The field is called new_password, not new.
Request body
{ "current_password": "old-password", "new_password": "new-password" }
Example (curl)
curl -X POST -H "X-API-Key: YOUR_API_KEY" -H "Content-Type: application/json" \
  -d '{"current_password":"old-password","new_password":"new-password"}' \
  http://127.0.0.1:8891/api/v1/auth/password
Response
{
  "status": "ok"
}
  • 🔴 A body with the names current and new decodes into two empty strings, and the endpoint answers 401 “current password is incorrect” — as if the password were wrong rather than the field name.
  • 400 “password must be at least 8 characters” for a new password that is too short.
  • 501 “auth not configured” if dashboard sign-in is not configured in this build.
POST/api/v1/auth/apikey/rotateAuth required

Issues a new API key and returns it. The previous one stops working at once.

Example (curl)
curl -X POST -H "X-API-Key: YOUR_API_KEY" \
  http://127.0.0.1:8891/api/v1/auth/apikey/rotate
Response
{
  "api_key": "a1b2c3d4e5f6a7b8c9d0"
}
  • The request goes with the OLD key and the response carries the new one — there is no other occasion to learn it.
  • 500 “could not rotate key” if the new key could not be stored; 501 if sign-in is not configured.

System status

GET/api/v1/statusAuth required

Returns overall status: version, open and maximum ports (max_ports is the binding ceiling — the lower of the configured maximum and your license limit), identity counts, uptime and more.

Example (curl)
curl -H "X-API-Key: YOUR_API_KEY" http://127.0.0.1:8891/api/v1/status
Response
{
  "version": "1.3.2",
  "commit": "a1b2c3d",
  "commit_time": "2026-09-01T10:00:00Z",
  "open_ports": 2,
  "max_ports": 1000,
  "idle_timeout_seconds": 1800,
  "ports": [
    "…"
  ],
  "profile_count": 143900,
  "profile_counts": {
    "chrome_152+windows": 41000,
    "chrome_152+macos": 22000,
    "firefox_152+windows": 17400,
    "safari_18+ios": 9800
  },
  "uptime": "3d14h22m",
  "domain_routing": {
    "domains": [],
    "proxy": ""
  },
  "domain_rules_count": 3,
  "pack_loaded": true,
  "solver_bundle_state": "ready",
  "solver_bundle_total": 1,
  "vision_bundle_state": "ready",
  "vision_bundle_total": 1,
  "font_bundle_state": "ready",
  "font_bundle_total": 1,
  "build_state": "ok",
  "device_bound": true
}
Response (fresh install: bundle still downloading, file mismatched)
{
  "pack_loaded": true,
  "solver_bundle_state": "downloading",
  "vision_bundle_state": "dormant",
  "font_bundle_state": "failed",
  "font_bundle_reason": "macos: connection refused",
  "build_state": "mismatch",
  "build_reason": "the program file does not match the published build — please reinstall",
  "device_bound": true,
  "device_rebound_at": "2026-09-01T10:00:00Z"
}
  • The first example is the HEALTHY outcome. solver_bundle_state, vision_bundle_state and font_bundle_state each carry one of four values: dormant — no download has ever started (normal for vision: the models are fetched the first time a captcha is met), downloading — in progress, ready — installed, failed — the attempt broke.
  • 🔴 Do not write a readiness check as “equals ready, everything else is broken”: a fresh install reports downloading first, and such a script cannot tell “still downloading” from “failed”.
  • Every state has a paired reason field — solver_bundle_reason, vision_bundle_reason, font_bundle_reason: a short, secret-free sentence. It is absent while the state is ready or dormant. font_bundle_reason names EVERY failed OS (macos: connection refused; windows: 404), because the state itself is the worst of the per-OS pools.
  • 🔴 vision_bundle_state ready together with a NON-EMPTY vision_bundle_reason is not health: the bundle is downloaded, but the content key could not be fetched (a licence with no solver entitlement, an outdated token, Cabinet unreachable) and reCAPTCHA silently never solves.
  • pack_loaded false means the real fingerprint recipes are not loaded; pack_reason arrives beside it and says why (not yet attempted, Cabinet unreachable). An empty reason with false means the licence carries no pack entitlement.
  • build_state compares the program FILE with the build the licensing server published. Values: ok, mismatch, unregistered. 🔴 The field may be missing from the response entirely — that means “no signal yet” and must NOT be read as ok. With mismatch and unregistered, build_reason carries the ready-to-show sentence: mismatch — the file was mangled or patched and needs a reinstall; unregistered — this build was never published by the licensing server, so reinstalling the same release will not help.
  • device_bound false means the installation is not claimed by its device key. device_rebound_at (the last re-binding time) is present only if a re-binding has happened.
GET/api/v1/healthNo auth

A simple health check that always returns OK.

Example (curl)
curl http://127.0.0.1:8891/api/v1/health
Response
{
  "status": "ok"
}

Exactly four paths are open without a key: this health check, POST /api/v1/auth/login, GET /crl (the revocation list is fetched by the OS certificate stack, not by a person) and GET /export/{token}/… (where the token in the address is the access). Everything else answers 401. The health check is what a monitor or a container shell uses to see that the process is alive; it says nothing about the state of the ports or the license.

The root certificate (CA)

The application opens TLS with a root certificate of its own, and the system must trust it — otherwise the browser shows a certificate error on every site. Three endpoints hand out the certificate in three shapes, and a fourth installs it.

GET/api/v1/caAuth required

Returns the root certificate as PEM text.

Example (curl)
curl -H "X-API-Key: YOUR_API_KEY" -o blanktrail-ca.pem \
  http://127.0.0.1:8891/api/v1/ca
  • 404 “CA certificate path not configured” if the certificate path is unset; 500 “failed to read CA certificate” if the file cannot be read. No PEM parsing happens here — the file is served as it is; the “not PEM” refusal belongs to the endpoints that need the certificate parsed, /ca.crt and /ca.mobileconfig.
GET/api/v1/ca.crtAuth required

The same certificate in binary DER — the format the macOS and iOS system installer understands.

Example (curl)
curl -H "X-API-Key: YOUR_API_KEY" -o blanktrail-ca.crt \
  http://127.0.0.1:8891/api/v1/ca.crt
GET/api/v1/ca.mobileconfigAuth required

An Apple .mobileconfig profile: opened on the device, it installs the certificate itself.

Example (curl)
curl -H "X-API-Key: YOUR_API_KEY" -o blanktrail.mobileconfig \
  http://127.0.0.1:8891/api/v1/ca.mobileconfig
  • On an iPhone installing the profile is not enough: the certificate must also be MARKED as trusted in Settings → General → About → Certificate Trust Settings.
POST/api/v1/ca/installAuth required

Installs the root certificate into the user's trusted-root store.

Example (curl)
curl -X POST -H "X-API-Key: YOUR_API_KEY" \
  http://127.0.0.1:8891/api/v1/ca/install
Response
{
  "status": "installed"
}
  • Windows only: on other systems it answers 501 and the certificate must be downloaded and added by hand.
  • After the install the browser must be restarted COMPLETELY — every window closed, not just a new one opened: it reads the trust store at start.
  • 500 “install failed: …” if the system store refused the certificate.

Challenge Breaker

These endpoints show what the challenge solver is busy with right now and how much of the machine it is allowed to take. They are available on any plan: without Challenge Breaker the solve counters stay at zero, but the challenges encountered are still visible — that is how you learn what your traffic ran into.

GET/api/v1/solver/statsAuth required

Counters since start: attempts and successes by challenge family, the challenges seen, and the resources taken.

Example (curl)
curl -H "X-API-Key: YOUR_API_KEY" \
  http://127.0.0.1:8891/api/v1/solver/stats
Response
{
  "families": [ { "family": "recaptcha", "attempts": 41, "solved": 38 } ],
  "total_attempts": 41,
  "total_solved": 38,
  "since_start": true,
  "resources": { "live_procs": 2, "busy_procs": 1, "cap": 4,
                 "constrained": false, "reason": "", "last_error": "",
                 "launch_failures": 0, "proc_deaths": 0, "mem_recycles": 0 },
  "seen": { "total": 47, "since_unix_ms": 1757116800000,
            "vendors": [ { "vendor": "recaptcha", "count": 41 },
                         { "vendor": "turnstile", "count": 6 } ] }
}
  • 🔴 On a plan WITHOUT Challenge Breaker only seen stays non-empty: there were no solves, but there were challenges. That is what the field is for.
GET/api/v1/solver/queueAuth required

The live queue: how many are waiting and how many are being solved right now, the queue cap, and one row per request.

Example (curl)
curl -H "X-API-Key: YOUR_API_KEY" \
  http://127.0.0.1:8891/api/v1/solver/queue
Response
{
  "queued": 2,
  "running": 1,
  "max_len": 16,
  "items": [ { "port": 20134, "host": "example.com", "vendor": "recaptcha",
               "class": "chrome-152-win", "state": "solving",
               "waiters": 1, "age_ms": 4120 } ]
}
  • A separate endpoint rather than a field in /solver/stats: the queue changes orders of magnitude faster than the counters, and the panel polls it at its own pace. There is no queue control here — reading only.
PUT/api/v1/solver/procsAuth required

Sets how many solver processes run at once. It applies with no restart and survives one.

ParameterTypeRequiredDescription
procsintYesThe number of processes; 0 turns solving off.
Request body
{ "procs": 4 }
Example (curl)
curl -X PUT -H "X-API-Key: YOUR_API_KEY" -H "Content-Type: application/json" \
  -d '{"procs":4}' \
  http://127.0.0.1:8891/api/v1/solver/procs
Response
{
  "js_solver_procs": 4
}
  • 400 “procs must be >= 0”; 409 if more is asked than the license allows — the cap is visible as js_solver_max_procs in the response of GET /api/v1/license/status.
  • When no value is set, the license cap applies: allocating processes by hand is not required.

Settings and keys

GET/api/v1/settings/networkAuth required

The network settings: whether the proxy ports accept connections from the local network, whether they demand a login, and at which address the certificate revocation list is visible.

Example (curl)
curl -H "X-API-Key: YOUR_API_KEY" \
  http://127.0.0.1:8891/api/v1/settings/network
Response
{
  "allow_lan": false,
  "crl_public_host": "",
  "proxy_auth_enabled": true,
  "proxy_auth_user": "proxy"
}
  • The proxy password is never returned in the response — it can only be set.
PUT/api/v1/settings/networkAuth required

Changes the network settings. Send only what changes: a field absent from the body stays as it was.

ParameterTypeRequiredDescription
allow_lanboolNoAccept connections to the proxy ports from the local network, not only from this machine.
crl_public_hoststringNoThe address at which the certificate revocation list is visible from ANOTHER machine: host or host:port, with no scheme and no path. Loopback is refused — the address must be one this machine is reachable at from outside.
proxy_auth_enabledboolNoDemand a login and password on the proxy ports.
proxy_auth_userstringNoThe proxy login.
proxy_auth_passstringNoThe proxy password.
Request body
{ "allow_lan": true, "proxy_auth_enabled": true,
  "proxy_auth_user": "proxy", "proxy_auth_pass": "…" }
Example (curl)
curl -X PUT -H "X-API-Key: YOUR_API_KEY" -H "Content-Type: application/json" \
  -d '{"allow_lan":true}' \
  http://127.0.0.1:8891/api/v1/settings/network
Response
{
  "allow_lan": true,
  "crl_public_host": "",
  "proxy_auth_enabled": true,
  "proxy_auth_user": "proxy"
}
  • 400 “proxy_auth_user and proxy_auth_pass are required to enable proxy auth”: the check cannot be turned on without both.
  • 400 with an explanation for an invalid crl_public_host; 503 “auth store not configured”. The body is capped at 8 KiB.
GET/api/v1/auth/apikeyAuth required

Returns the API key — the one that travels in the X-API-Key header.

Example (curl)
curl -H "X-API-Key: YOUR_API_KEY" \
  http://127.0.0.1:8891/api/v1/auth/apikey
Response
{
  "api_key": "a1b2c3d4e5f6a7b8c9d0"
}
  • The response is empty when no retrievable copy of the key exists: a key stored only as a hash cannot be shown.
PUT/api/v1/auth/apikeyAuth required

Sets an API key of your own instead of the generated one. The previous one stops working at once.

ParameterTypeRequiredDescription
api_keystringYes12–128 printable ASCII characters with no spaces: the key travels in the header verbatim.
Request body
{ "api_key": "my-own-api-key-2026" }
Example (curl)
curl -X PUT -H "X-API-Key: YOUR_API_KEY" -H "Content-Type: application/json" \
  -d '{"api_key":"my-own-api-key-2026"}' \
  http://127.0.0.1:8891/api/v1/auth/apikey
Response
{
  "api_key": "my-own-api-key-2026"
}
  • 400 “api key must be 12-128 characters” or “api key must be printable ASCII without spaces”. The body is capped at 8 KiB.
GET/api/v1/settings/integration-keyAuth required

Reports whether an integration key is stored — it is what activates a license on an integrator's behalf.

Example (curl)
curl -H "X-API-Key: YOUR_API_KEY" \
  http://127.0.0.1:8891/api/v1/settings/integration-key
Response
{
  "integration_key": "",
  "set": false
}
PUT/api/v1/settings/integration-keyAuth required

Stores or clears the integration key. It takes effect with no restart.

ParameterTypeRequiredDescription
integration_keystringYesThe key, up to 512 characters. An empty value clears the stored one.
Request body
{ "integration_key": "…" }
Example (curl)
curl -X PUT -H "X-API-Key: YOUR_API_KEY" -H "Content-Type: application/json" \
  -d '{"integration_key":""}' \
  http://127.0.0.1:8891/api/v1/settings/integration-key
Response
{
  "ok": true
}
  • 400 “integration_key must be at most 512 characters”; 503 “auth store not configured”.

Activation from code

Activation usually happens in the dashboard, but it can be done by request too — while provisioning a machine with a script, say. The credentials go straight to the Cabinet; the application does not store them, only the signed license.

POST/api/v1/license/enrollAuth required

Starts activation with a blanktrail.com account.

ParameterTypeRequiredDescription
emailstringYesThe account email.
passwordstringYesThe account password.
Request body
{ "email": "you@example.com", "password": "…" }
Example (curl)
curl -X POST -H "X-API-Key: YOUR_API_KEY" -H "Content-Type: application/json" \
  -d '{"email":"you@example.com","password":"…"}' \
  http://127.0.0.1:8891/api/v1/license/enroll
Response
{
  "choose": false,
  "activated": true,
  "plan": "pro"
}
  • If the account holds several licenses the answer comes with choose=true, a list and a one-off ticket — the choice is made with the next endpoint.
  • 400 “email and password are required”; 502 with the status inside the body if the Cabinet is unreachable; 503 “license manager not configured”.
POST/api/v1/license/enroll/confirmAuth required

Chooses the license when the account holds several.

ParameterTypeRequiredDescription
ticketstringYesThe one-off ticket from the previous response.
license_idintYesThe id of the chosen license, from the same response.
Request body
{ "ticket": "…", "license_id": 42 }
Example (curl)
curl -X POST -H "X-API-Key: YOUR_API_KEY" -H "Content-Type: application/json" \
  -d '{"ticket":"…","license_id":42}' \
  http://127.0.0.1:8891/api/v1/license/enroll/confirm
Response
{
  "activated": true,
  "plan": "pro"
}
  • 400 “ticket and license_id are required”; 502 if the Cabinet is unreachable.
POST/api/v1/auth/onboard-passwordAuth required

Sets the dashboard password on the first screen — by the one-off bootstrap_token returned by POST /api/v1/auth/onboard-activate, not by a previous password.

ParameterTypeRequiredDescription
bootstrap_tokenstringYesThe one-off first-run token, taken from the POST /api/v1/auth/onboard-activate response. Single use, valid 15 minutes.
new_passwordstringYesThe new password, at least 8 characters.
Request body
{ "bootstrap_token": "…", "new_password": "new-password" }
Example (curl)
curl -X POST -H "X-API-Key: YOUR_API_KEY" -H "Content-Type: application/json" \
  -d '{"bootstrap_token":"…","new_password":"new-password"}' \
  http://127.0.0.1:8891/api/v1/auth/onboard-password
Response
{
  "status": "ok"
}
  • 401 “invalid or expired activation token”; 400 with the password rule if it is shorter than eight characters; 501 if sign-in is not configured.
POST/api/v1/auth/onboard-activateAuth required

Activates the license on that same first screen, without signing in to the dashboard separately.

ParameterTypeRequiredDescription
emailstringYesThe account email.
passwordstringYesThe account password.
ticketstringNoThe license-choice ticket when there are several.
license_idintNoThe id of the chosen license.
Request body
{ "email": "you@example.com", "password": "…" }
Example (curl)
curl -X POST -H "X-API-Key: YOUR_API_KEY" -H "Content-Type: application/json" \
  -d '{"email":"you@example.com","password":"…"}' \
  http://127.0.0.1:8891/api/v1/auth/onboard-activate
Response
{
  "activated": true,
  "plan": "pro",
  "bootstrap_token": "…"
}
  • bootstrap_token is returned only here, and only when activated is true. It is single-use and expires in 15 minutes; keep it and pass it to POST /api/v1/auth/onboard-password. There is no other source — the installer does not issue it.
  • 400 “email and password are required”; 429 after a run of attempts from one address; 502 if the Cabinet is unreachable; 503 “license manager not configured”.