API reference

BlankTrail Proxy exposes a local HTTP API so you can open ports, configure identities, and automate everything the dashboard does. This page covers the base URL, authentication and conventions.

Base URL

All endpoints live under the /api/v1 prefix on the same host and port as the dashboard:

http://127.0.0.1:8891/api/v1

The API and dashboard share one origin, so calls from your own scripts and tools work out of the box.

Authentication

The API is deny-by-default: almost every endpoint requires authentication. There are two ways to authenticate:

  • API key — send it in the X-API-Key header. Best for scripts and automation.
  • Session cookie — obtained by signing in; used by the browser dashboard.

Find and rotate your API key in the dashboard's Settings dialog. Send it on every request:

curl -H "X-API-Key: YOUR_API_KEY" http://127.0.0.1:8891/api/v1/status
ImportantThe API key grants full control of your ports. Keep it secret, and restrict network access to the dashboard port (bind to localhost or use an SSH tunnel on servers).

Conventions

  • Requests and responses are JSON. Send Content-Type: application/json on requests with a body.
  • Configuration is always sent in the JSON body, never in query strings.
  • The API is versioned under /api/v1. Breaking changes would move to a new version prefix.

Status codes

The error body is always the same: an object with an error field and English text; some refusals carry a code field next to it — a machine-readable reason. The texts are given in the endpoint descriptions: they are how one cause of a 400 is told from another. The exception is 405: the router returns it as a plain line, not JSON.

CodeMeaning
200 OKSuccess.
202 AcceptedAccepted and running in the background — this is how starting an update answers.
400 Bad RequestBad input: the body does not parse, a field is missing, a value is not allowed.
401 UnauthorizedThe API key was not sent or is wrong. Check the header name: X-API-Key.
403 ForbiddenRefused not by the key but by the state: the license is inactive (the subscription ended, or there has been no connection to the authorization server for 24 hours), or the feature requires a Pro plan.
404 Not FoundIt does not exist: the path is not registered, the port is not open, the item is not found.
405 Method Not AllowedThe path exists but the method is wrong — a PUT to a read-only endpoint, say. This code comes from the router itself, not from a handler.
409 ConflictThe state is in the way: the port is already taken, the name already exists, the gateway is in use.
429 Too Many RequestsToo often — resending a bug report, for instance. Slow down and retry.
500 Internal Server ErrorAn unexpected error inside the application.
501 Not ImplementedThe component is not built or not wired in this build — this is how dashboard sign-in answers when authentication is not configured.
502 Bad GatewayA call to the outside failed: the Cabinet is unreachable, or the visit to the address did not happen.
503 Service UnavailableNothing to answer with right now: no free port, the service is not up, the debug hooks are not wired.
NoteDo not confuse 403 and 401: 401 is about the key, 403 about the right. A client that has worked for months gets 403 on POST /api/v1/system/leak-audit, POST /api/v1/upstream/test, POST /api/v1/scraper/probe, POST /api/v1/ovpn/ping and POST /api/v1/gateway/subs/{name}/refresh after a day without a connection to the authorization server — and the key is correct all along.

Reference by topic

  • Ports & traffic — open, close, list and configure ports; test an upstream.
  • Profiles & routing — identities, presets, domain rules and gateways.
  • Port Pool — open and manage pools of ports from a proxy list.
  • License & access — license status, authentication, and the CA certificate.

Service endpoints

These endpoints are not part of a scenario but stand next to it: check that the application is alive, raise the log detail while investigating, ask about an update, set the shared routing rule.

GET/api/v1/log_levelAuth required

The current log level.

Example (curl)
curl -H "X-API-Key: YOUR_API_KEY" \
  http://127.0.0.1:8891/api/v1/log_level
Response
{
  "level": "info"
}
PUT/api/v1/log_levelAuth required

Raises the detail while investigating: debug writes noticeably more and lasts until a restart.

ParameterTypeRequiredDescription
levelstringYesdebug, info, warn or error.
Request body
{ "level": "debug" }
Example (curl)
curl -X PUT -H "X-API-Key: YOUR_API_KEY" -H "Content-Type: application/json" \
  -d '{"level":"debug"}' \
  http://127.0.0.1:8891/api/v1/log_level
Response
{
  "level": "debug"
}
  • 400 “level must be one of: debug, info, warn, error” for any other value.
GET/api/v1/update/statusAuth required

Which version is installed and whether a new one exists: available is empty while there is no update; state shows what the updater is doing right now.

Example (curl)
curl -H "X-API-Key: YOUR_API_KEY" \
  http://127.0.0.1:8891/api/v1/update/status
Response
{
  "current": "1.3.2",
  "available": "",
  "state": "idle",
  "last_error": "",
  "download_url": ""
}
  • 404 “self-update not available” in a build without self-update.
POST/api/v1/updateAuth required

Installs the update /update/status reported. The application restarts itself.

Example (curl)
curl -X POST -H "X-API-Key: YOUR_API_KEY" \
  http://127.0.0.1:8891/api/v1/update
Response
{
  "status": "started"
}
  • 🔴 The answer is 202 Accepted, not 200: the update has been accepted and runs in the background rather than being finished by the time the answer arrives. Wait for it by polling /update/status.
  • 409 “no update available” if there is nothing to install; 404 “self-update not available” in a build without self-update.
GET/api/v1/domain_routingAuth required

The simple routing rule: a list of domains and one egress proxy shared by them. An empty list means the rule is off. Detailed, named rules live in /domain_rules.

Example (curl)
curl -H "X-API-Key: YOUR_API_KEY" \
  http://127.0.0.1:8891/api/v1/domain_routing
Response
{
  "domains": ["example.com", "*.example.net"],
  "proxy": "socks5://203.0.113.10:1080"
}
PUT/api/v1/domain_routingAuth required

Sets the simple rule as a whole: the domains and the proxy. An empty domain list turns it off.

ParameterTypeRequiredDescription
domainsarrayYesDomains and masks of the form *.example.net.
proxystringYesThe egress proxy for those domains.
Request body
{ "domains": ["example.com"], "proxy": "socks5://203.0.113.10:1080" }
Example (curl)
curl -X PUT -H "X-API-Key: YOUR_API_KEY" -H "Content-Type: application/json" \
  -d '{"domains":["example.com"],"proxy":"socks5://203.0.113.10:1080"}' \
  http://127.0.0.1:8891/api/v1/domain_routing
Response
{
  "domains": ["example.com"],
  "proxy": "socks5://203.0.113.10:1080"
}
  • 400 with an explanation if a domain or the proxy address is malformed.
PUT/api/v1/idle_timeoutAuth required

The shared idle timeout for ports that have none of their own. 0 never closes them.

ParameterTypeRequiredDescription
secondsintYesSeconds of idling before a port closes itself; 0 never closes it.
Request body
{ "seconds": 1800 }
Example (curl)
curl -X PUT -H "X-API-Key: YOUR_API_KEY" -H "Content-Type: application/json" \
  -d '{"seconds":1800}' \
  http://127.0.0.1:8891/api/v1/idle_timeout
Response
{
  "idle_timeout_seconds": 1800
}
  • 400 “seconds must be >= 0” for a negative value. A port with its own override (PUT /api/v1/port/{port}/idle) does not take this value.

Bug report

The report gathers the logs and the state of the ports and sends them to support. It can be viewed in full beforehand — it is the same file that will go.

GET/api/v1/bug-report/statusAuth required

Whether sending reports is enabled and whether the previous run exited uncleanly — in which case the dashboard offers to send one itself.

Example (curl)
curl -H "X-API-Key: YOUR_API_KEY" \
  http://127.0.0.1:8891/api/v1/bug-report/status
Response
{
  "enabled": true,
  "unclean_previous_exit": false
}
  • enabled=false means sending is unavailable without an active license.
POST/api/v1/bug-report/previewAuth required

Builds the report and shows it, sending nothing.

Example (curl)
curl -X POST -H "X-API-Key: YOUR_API_KEY" \
  http://127.0.0.1:8891/api/v1/bug-report/preview
Response
{
  "report": { "version": 1, "ports": [ … ], "log": [ … ] },
  "notes_included": false
}
  • 404 “bug report not available” if report collection is not wired; 500 “failed to build report”.
POST/api/v1/bug-report/sendAuth required

Sends the report to support along with your description.

ParameterTypeRequiredDescription
descriptionstringYesWhat happened. The field is required: empty after trimming is refused.
Request body
{ "description": "port stopped returning 200 after an egress change" }
Example (curl)
curl -X POST -H "X-API-Key: YOUR_API_KEY" -H "Content-Type: application/json" \
  -d '{"description":"port stopped returning 200 after an egress change"}' \
  http://127.0.0.1:8891/api/v1/bug-report/send
Response
{
  "status": "sent",
  "bug_report_id": "br-7f3a1c9d"
}
  • 400 “description required” with no description; 403 “activate your license to send a bug report”; 404 “bug report not available”; 429 when sending too often; 502 if the support server is unreachable.
  • The body is capped at 64 KiB.
POST/api/v1/bug-report/dismissAuth required

Declines the offered report: it silences the unclean-exit notice for the rest of this run.

Example (curl)
curl -X POST -H "X-API-Key: YOUR_API_KEY" \
  http://127.0.0.1:8891/api/v1/bug-report/dismiss
Response
{
  "status": "dismissed"
}
  • The flag lives in memory: a genuinely new unclean exit sets it again on the next run.

Diagnostics

These paths are for when the application misbehaves and a support request needs numbers rather than a description. They are not a product surface: their format is set by the runtime and may change between versions.

GET/api/v1/debugAuth required

A snapshot of the process itself: goroutines, OS threads, handles, memory and uptime. This is the first thing to attach to a request if the application has become heavy.

Example (curl)
curl -H "X-API-Key: YOUR_API_KEY" \
  http://127.0.0.1:8891/api/v1/debug
Response
{
  "goroutines": 142,
  "threads": 21,
  "handles": 380,
  "open_ports": 2,
  "uptime": "3h14m22s",
  "memory": {
    "alloc_mb": 96,
    "total_alloc_mb": 8140,
    "sys_mb": 310,
    "heap_objects": 410000,
    "heap_inuse_mb": 120,
    "heap_released_mb": 64,
    "gc_cycles": 1830,
    "stack_inuse_mb": 6
  }
}
  • The handles field is always present but only meaningful on Windows, where it is the system's own counter; on other platforms it is always 0.

Profiling (pprof)

Eleven standard Go paths, open under the ordinary API key. They return the internals of the process rather than user data, but they sit behind the key like the rest of the API.

PathWhat it returns
/debug/pprof/The index: a list of the available profiles.
/debug/pprof/heapA heap snapshot — where to start when memory grows.
/debug/pprof/goroutineEvery goroutine with its stack — where to start when something hangs.
/debug/pprof/allocsAll allocations since start.
/debug/pprof/profileA CPU profile. 🔴 It holds the connection for 30 seconds — that is not a hang.
/debug/pprof/traceAn execution trace.
/debug/pprof/blockWhere goroutines waited on blocking operations.
/debug/pprof/mutexMutex contention.
/debug/pprof/threadcreateOS thread creation.
/debug/pprof/cmdlineThe process command line.
/debug/pprof/symbolResolving addresses to function names.

To capture the heap and the goroutines for a report: curl -H "X-API-Key: YOUR_API_KEY" -o heap.pprof http://127.0.0.1:8891/debug/pprof/heap, and the same for goroutine. Open them with go tool pprof heap.pprof.

The solver's debug signature harness

WarningThe four endpoints below exist only if the application was started with the environment variable BLANKTRAIL_SOLVER_DEBUG_API=1. The comparison is strict: a value of 0 or true does not enable them. The variable is read ONCE at start — set later, it has no effect on a running process. Without it the routes are not registered at all and a request gets 404: an absent endpoint must not announce its existence.

The harness exists for one thing: to check whether a challenge signature matches without rebuilding and shipping a pack. Before it, one such cycle cost a pack rebuild, a signature, a deployment and up to half an hour of waiting for the client to re-read the pack.

GET/api/v1/debug/signaturesAuth required

Reports whether a debug signature layer currently sits on top of the pack database, and how many entries it holds.

Example (curl)
curl -H "X-API-Key: YOUR_API_KEY" \
  http://127.0.0.1:8891/api/v1/debug/signatures
Response
{
  "debug_active": true,
  "debug_vendors": 3
}
  • 503 “solver debug hooks not wired” if the switch is set but the solver hooks are not wired in this build. This is NOT 404: the route itself exists, and telling the two states apart matters.
POST/api/v1/debug/signaturesAuth required

Installs the given signature set as a SEPARATE layer on top of the pack database — with no pack rebuild, signing or deployment.

Request body
{ "Version": 7, "Vendors": [ { "…": "…" } ], "NavHints": [ ] }
Example (curl)
curl -X POST -H "X-API-Key: YOUR_API_KEY" -H "Content-Type: application/json" \
  --data-binary @signatures.json \
  http://127.0.0.1:8891/api/v1/debug/signatures
Response
{
  "installed": 3,
  "dropped": 1
}
  • The body is a signature database in the same format as the pack's. installed is how many vendors were accepted, dropped how many of the SUBMITTED conditions were rejected.
  • The layer survives any number of pack updates and is removed only by an explicit DELETE. The install is written to the log as a warning — a forgotten layer must not stay invisible.
  • 503 “solver debug hooks not wired”; 400 “invalid JSON body”.
DELETE/api/v1/debug/signaturesAuth required

Removes the debug signature layer and returns the solver to the pack database.

Example (curl)
curl -X DELETE -H "X-API-Key: YOUR_API_KEY" \
  http://127.0.0.1:8891/api/v1/debug/signatures
Response
{
  "cleared": true
}
  • 503 “solver debug hooks not wired” if the hooks are not wired.
POST/api/v1/port/{port}/solve_probeAuth required

Takes the solver to the given address THROUGH a specific port and returns what it saw there — whether or not a signature matched.

ParameterTypeRequiredDescription
urlstringYesThe address to take the solver to.
solveboolNofalse (the default) only recognises the vendor and the nature of the page, about a second. true also forces a solve.
Request body
{ "url": "https://example.com/", "solve": true }
Example (curl)
curl -X POST -H "X-API-Key: YOUR_API_KEY" -H "Content-Type: application/json" \
  -H "Accept-Language: ru-RU,ru;q=0.9" \
  -d '{"url":"https://example.com/","solve":true}' \
  http://127.0.0.1:8891/api/v1/port/20134/solve_probe
  • 🔴 With solve=true there is deliberately NO artificial timeout: a forced solve honestly takes tens of seconds, and for some challenges up to a minute and a half. The request has not hung.
  • The Accept-Language header of THIS request is passed into the task verbatim: the launch language affects the outcome, and without it the solve would go out with the exit node's language instead of the requested one.
  • 503 “solver debug hooks not wired”; 400 “url is required”; 502 with the error text if the visit itself failed.