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
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.
| Code | Meaning |
|---|---|
200 OK | Success. |
202 Accepted | Accepted and running in the background — this is how starting an update answers. |
400 Bad Request | Bad input: the body does not parse, a field is missing, a value is not allowed. |
401 Unauthorized | The API key was not sent or is wrong. Check the header name: X-API-Key. |
403 Forbidden | Refused 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 Found | It does not exist: the path is not registered, the port is not open, the item is not found. |
405 Method Not Allowed | The 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 Conflict | The state is in the way: the port is already taken, the name already exists, the gateway is in use. |
429 Too Many Requests | Too often — resending a bug report, for instance. Slow down and retry. |
500 Internal Server Error | An unexpected error inside the application. |
501 Not Implemented | The component is not built or not wired in this build — this is how dashboard sign-in answers when authentication is not configured. |
502 Bad Gateway | A call to the outside failed: the Cabinet is unreachable, or the visit to the address did not happen. |
503 Service Unavailable | Nothing to answer with right now: no free port, the service is not up, the debug hooks are not wired. |
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.
/api/v1/log_levelAuth requiredThe current log level.
curl -H "X-API-Key: YOUR_API_KEY" \
http://127.0.0.1:8891/api/v1/log_level{
"level": "info"
}/api/v1/log_levelAuth requiredRaises the detail while investigating: debug writes noticeably more and lasts until a restart.
| Parameter | Type | Required | Description |
|---|---|---|---|
level | string | Yes | debug, info, warn or error. |
{ "level": "debug" }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{
"level": "debug"
}- 400 “level must be one of: debug, info, warn, error” for any other value.
/api/v1/update/statusAuth requiredWhich 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.
curl -H "X-API-Key: YOUR_API_KEY" \
http://127.0.0.1:8891/api/v1/update/status{
"current": "1.3.2",
"available": "",
"state": "idle",
"last_error": "",
"download_url": ""
}- 404 “self-update not available” in a build without self-update.
/api/v1/updateAuth requiredInstalls the update /update/status reported. The application restarts itself.
curl -X POST -H "X-API-Key: YOUR_API_KEY" \
http://127.0.0.1:8891/api/v1/update{
"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.
/api/v1/domain_routingAuth requiredThe 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.
curl -H "X-API-Key: YOUR_API_KEY" \
http://127.0.0.1:8891/api/v1/domain_routing{
"domains": ["example.com", "*.example.net"],
"proxy": "socks5://203.0.113.10:1080"
}/api/v1/domain_routingAuth requiredSets the simple rule as a whole: the domains and the proxy. An empty domain list turns it off.
| Parameter | Type | Required | Description |
|---|---|---|---|
domains | array | Yes | Domains and masks of the form *.example.net. |
proxy | string | Yes | The egress proxy for those domains. |
{ "domains": ["example.com"], "proxy": "socks5://203.0.113.10:1080" }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{
"domains": ["example.com"],
"proxy": "socks5://203.0.113.10:1080"
}- 400 with an explanation if a domain or the proxy address is malformed.
/api/v1/idle_timeoutAuth requiredThe shared idle timeout for ports that have none of their own. 0 never closes them.
| Parameter | Type | Required | Description |
|---|---|---|---|
seconds | int | Yes | Seconds of idling before a port closes itself; 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/idle_timeout{
"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.
/api/v1/bug-report/statusAuth requiredWhether sending reports is enabled and whether the previous run exited uncleanly — in which case the dashboard offers to send one itself.
curl -H "X-API-Key: YOUR_API_KEY" \
http://127.0.0.1:8891/api/v1/bug-report/status{
"enabled": true,
"unclean_previous_exit": false
}- enabled=false means sending is unavailable without an active license.
/api/v1/bug-report/previewAuth requiredBuilds the report and shows it, sending nothing.
curl -X POST -H "X-API-Key: YOUR_API_KEY" \
http://127.0.0.1:8891/api/v1/bug-report/preview{
"report": { "version": 1, "ports": [ … ], "log": [ … ] },
"notes_included": false
}- 404 “bug report not available” if report collection is not wired; 500 “failed to build report”.
/api/v1/bug-report/sendAuth requiredSends the report to support along with your description.
| Parameter | Type | Required | Description |
|---|---|---|---|
description | string | Yes | What happened. The field is required: empty after trimming is refused. |
{ "description": "port stopped returning 200 after an egress change" }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{
"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.
/api/v1/bug-report/dismissAuth requiredDeclines the offered report: it silences the unclean-exit notice for the rest of this run.
curl -X POST -H "X-API-Key: YOUR_API_KEY" \
http://127.0.0.1:8891/api/v1/bug-report/dismiss{
"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.
/api/v1/debugAuth requiredA 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.
curl -H "X-API-Key: YOUR_API_KEY" \
http://127.0.0.1:8891/api/v1/debug{
"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.
| Path | What it returns |
|---|---|
/debug/pprof/ | The index: a list of the available profiles. |
/debug/pprof/heap | A heap snapshot — where to start when memory grows. |
/debug/pprof/goroutine | Every goroutine with its stack — where to start when something hangs. |
/debug/pprof/allocs | All allocations since start. |
/debug/pprof/profile | A CPU profile. 🔴 It holds the connection for 30 seconds — that is not a hang. |
/debug/pprof/trace | An execution trace. |
/debug/pprof/block | Where goroutines waited on blocking operations. |
/debug/pprof/mutex | Mutex contention. |
/debug/pprof/threadcreate | OS thread creation. |
/debug/pprof/cmdline | The process command line. |
/debug/pprof/symbol | Resolving 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
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.
/api/v1/debug/signaturesAuth requiredReports whether a debug signature layer currently sits on top of the pack database, and how many entries it holds.
curl -H "X-API-Key: YOUR_API_KEY" \
http://127.0.0.1:8891/api/v1/debug/signatures{
"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.
/api/v1/debug/signaturesAuth requiredInstalls the given signature set as a SEPARATE layer on top of the pack database — with no pack rebuild, signing or deployment.
{ "Version": 7, "Vendors": [ { "…": "…" } ], "NavHints": [ ] }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{
"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”.
/api/v1/debug/signaturesAuth requiredRemoves the debug signature layer and returns the solver to the pack database.
curl -X DELETE -H "X-API-Key: YOUR_API_KEY" \
http://127.0.0.1:8891/api/v1/debug/signatures{
"cleared": true
}- 503 “solver debug hooks not wired” if the hooks are not wired.
/api/v1/port/{port}/solve_probeAuth requiredTakes the solver to the given address THROUGH a specific port and returns what it saw there — whether or not a signature matched.
| Parameter | Type | Required | Description |
|---|---|---|---|
url | string | Yes | The address to take the solver to. |
solve | bool | No | false (the default) only recognises the vendor and the nature of the page, about a second. true also forces a solve. |
{ "url": "https://example.com/", "solve": true }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.