Files
2026-10-06 18:54:03 +08:00

94 KiB
Raw Permalink Blame History

ProxyCat API Reference

This document is the sole authoritative reference for ProxyCat's public HTTP APIs, covering two API sets:

  • Panel API (/api/*) — the panel's own API, served by app.py.
  • Pool API (/api/pool/*) — a Flask blueprint registered by modules/proxypool_api.py.

The two APIs share one port and one authentication scheme, but their error response conventions differ (the panel mostly returns HTTP 200 + status: "error", while the pool uses a real status code plus error_code). Both conventions are spelled out in this document; read 1. General Conventions before making any call.

Older versions kept the Pool API's field details in modules/proxypool/API.md; that file has since been merged into this document and now holds nothing but a pointer to this page.

Table of Contents

Group Navigation

Group Endpoints Description
1. General Conventions — Addresses, authentication, the two error conventions, parameter parsing rules, general limits
2. Panel API 33 The panel's own /api/*: status, configuration, logs, access records
3. Pool API 38 The pool's /api/pool/*: fetching, validation, tasks and database maintenance

Quick lookup by task:

I want to… Go here
See which proxies the service is currently using Get Runtime Status
Change the configuration Save Configuration
Find out why a request failed Query Access Records
See which upstream a domain goes through and how its success rate looks Query Domain Statistics
Grab a working proxy for another program Get One Proxy
Trigger a fetch by hand Run a Plugin Immediately
Check how a long-running task is doing Query Task Status

1. General Conventions

Service Address and Ports

  • Default address: http://localhost:5001; the port comes from [Server] web_port in config/config.ini.
  • The panel and the pool have no separate processes or ports; both run inside the ProxyCat process and start and stop with it.
  • The proxy listening port (default 1080) is a separate port: it only accepts HTTP and SOCKS5 proxy requests and serves no HTTP API at all.

Authentication

Except for the endpoints explicitly marked "no authentication" below, every API call must carry the token:

GET http://localhost:5001/api/status?token=<your-token>

The token lives in [Server] token in config/config.ini. Validation reads only the query-string parameter token; it does not read request headers or cookies.

An empty token lets everything through, i.e. authentication is off. Anyone who can reach the panel port can then read and write the configuration (including upstream credentials), view access records, and start or stop the service. Configure it this way only for local use; a token is mandatory if you expose the port to a network.

The config/config.ini shipped in the repository has a non-empty token, so using a fresh clone as-is gets you a 401. See Quick Deployment in the README.

Only 4 API endpoints skip authentication (/ and /static/<path> also skip the token check, see Appendix B):

Method Path
GET /api/version
GET /api/ads
POST /api/ads/dismiss
POST /api/ads/reopen

A failed authentication always returns HTTP 401:

{
  "status": "error",
  "message": "Invalid access token"
}

Two Sets of Error Conventions

This is the spot in this document most likely to trip you up: the two APIs express errors differently and must be treated separately.

Situation Panel /api/* Pool /api/pool/*
Success 200 + status: "success"; a few read-only endpoints return data directly, with no status field 200 + data; status appears only on errors
Business failure 200 + status: "error" + message A real status code (4xx/5xx) + status: "error" + message + error_code
Parameter error 400, returned only by POST /api/config 400 + error_code: "invalid_parameter"
Not authenticated 401 + status: "error" 401 + status: "error" (the same hook)
Pool not running / pool too slow Not applicable 503 pool_unavailable / 504 pool_timeout

Two hard rules follow:

  1. Always check the HTTP status code and the status field together. On the panel side, looking at the status code alone misses business failures, and looking at status alone misses authentication and pool-side errors.
  2. Programmatic branching may rely only on error_code (pool side) or the HTTP status code plus endpoint-specific flags (panel side) — never on message. The message text follows the current UI language ([Server] language), so the same error returns different text under different languages.

Parameter Parsing Rules (Pool API Only)

The pool's adapter layer parses parameters from the handler function's signature, under these rules:

Rule Description
Query parameters are a whitelist Only query parameters that appear in the handler signature are parsed; unknown parameters are silently ignored. A misspelled parameter name raises no error — you just get the unfiltered, full result
Type coercion Driven by type annotations; only bool / int / float are recognized, everything else passes through as a string
Empty string An empty string for a non-string parameter (e.g. ?status=) is the same as omitting that parameter
Request body Must be a JSON object; a missing required field returns 400 invalid_parameter, with the field names listed in message
Path parameters Such as {id} in /api/pool/proxies/{id}/favorite; parsed by the route itself

General Limits and Truncation

Location Limit / Default Description
limit of GET /api/logs Clamped to 1-2000, default 200 Out-of-range values are silently clamped, with no error
lines of GET /api/logs/file Clamped to 1-5000, default 500 Same as above
limit of GET /api/logs/records Clamped to 1-500, default 100 Same as above; since defaults to "1 hour ago"
Access record export At most 50000 records per call The truncation flag exists only in the JSON metadata (truncated, true once the count reaches the limit); the CSV carries no such flag
Pool page_size / limit / export All bound by the 50000 cap When limit is present, page is forced to 1, equivalent to "take the first limit rows"
GET /api/version Reads a cache, makes no request The check is done at process start and by a background task every 24 hours
POST /api/version/check Blocks the current web thread until the check finishes (up to ~15 s) Concurrent calls merge into the one in flight; resets the 24-hour timer

Blocking and Concurrency

The panel is served by waitress with 16 threads. On every pool call, the current web thread stays occupied until the pool returns a result, up to 60 seconds (the HEAVY tier). So 16 concurrent large batch operations will drag the panel into unresponsiveness. When calling heavy endpoints, keep concurrency in check and give your client a long enough timeout.

The pool's three internal timeout tiers (they surface in error codes):

Tier Time limit Used for
QUERY 10 seconds Queries, statistics, status
MUTATION 20 seconds Start/stop, single-record edits
HEAVY 60 seconds Import, export, database optimization, manual plugin runs, batch validation / deletion

2. Panel API

Pages and Static Assets

Open the Panel (GET /web)

Authentication: a token is required.

GET /web returns the panel's single page. The token check is the same as for the other APIs, so with a token set, visiting http://127.0.0.1:5001/ directly ends in a 401 after the redirect. The correct address is:

http://127.0.0.1:5001/?token=<your-token>

GET / itself performs no check; it simply carries ?token= through unchanged when redirecting to /web:

Request Response
GET /?token=abc 302 → /web?token=abc
GET / (no token) 302 → /web

GET /static/<path> serves the front-end assets under web/static/. Requests carrying the ?v=<version> query parameter return Cache-Control: public, max-age=31536000, immutable (every static reference in the page carries it, so after a release the parameter changes and browsers naturally fetch the new files); requests without it return Cache-Control: no-cache, revalidating against the origin on every load.

Runtime Status

Get Runtime Status (GET /api/status)

Takes no request parameters. Returns a live snapshot of the outbound proxy service.

{
  "current_proxy": "http://1.2.3.4:8080",
  "mode": "cycle",
  "port": 1080,
  "interval": 300,
  "time_left": 123.4,
  "switch_countdown_mode": "time",
  "total_proxies": 10,
  "active_proxies": [
    { "url": "http://1.2.3.4:8080", "inflight": 3, "active": 2, "capacity": 0, "failures": 0, "suspect": false, "failure_ratio": 0.0, "last_used": 12.5 }
  ],
  "proxy_source_mode": "local",
  "exit_count": 5,
  "elastic_active": true,
  "auth_required": false,
  "display_level": 1,
  "service_status": "running",
  "language": "cn",
  "request_interval": 0,
  "pool_running": true,
  "source_error": null
}
Field Description
current_proxy The upstream exit assigned most recently (masked) — not "the current proxy": with several exits, requests are spread across the whole group by load; when a local source has no usable exit this is the localized "none" text, and for non-local sources it is an empty string in that case. See active_proxies for the state of the whole group
mode The run mode. The same key means two different things depending on the source: the local source asks "who to pick" — cycle (down the list in order, ignoring load) / loadbalance (whichever is under the least pressure goes first); the API and pool sources ask "when to rotate" — continuous (continuous rotation) / request (on-request rotation). The value is normalized per source; after a source switch, a value that does not belong to the new group falls back to that group's default
port The actually bound proxy port (taken from the listening socket); falls back to the configured value when the service is not running
interval The time-based rotation interval (seconds)
time_left How much longer the soonest-expiring exit has left; the unit follows switch_countdown_mode; 0 when the roster is empty. Whether exits are replaced by requests or proactively by the background depends on mode: under continuous (continuous rotation) the background replaces on every tick, so the number keeps counting down while idle; under request (on-request rotation) and the local source, exits that expire while idle are removed and the pool may be empty
switch_countdown_mode The countdown measure: time (seconds, by each exit's own lifetime; reported when both interval and request_interval are set — seconds can draw a progress bar, and the remaining request count is given per exit by requests_left) / request_count (only request_interval is set; reports how many requests remain) / per_request (neither is set; restocks only when exits are missing or invalid). none is never returned anymore: lifetime applies to every source and all four rotation modes
total_proxies The number of exits currently in use. The target is exit_count ([Server] exit_count); the number falls below the target when an exit is judged invalid or has not been replaced yet, and rises above it during peak auto-expansion (when elastic_active is also true)
active_proxies Live state of each upstream exit: url (masked), active — the number of connections currently using this exit (requests in flight plus tunnels that recently relayed data; this is the "in use" figure shown by the panel and the CLI), inflight — connections still holding a slot (idle tunnels included, always ≥ active), capacity — the per-exit active connection allowance (0 means unlimited; idle tunnels don't count), failures — consecutive failures, suspect — whether recent failures dominate (such exits give way first but remain usable), failure_ratio — the recent failure share, expires_in — remaining lifetime in seconds, requests_left — requests left to serve (both are null when that measure is not enabled), last_used / added_at / expires_at / requests_served (time.monotonic() values and counters, for comparison within the same process only)
proxy_source_mode Proxy source: local / api / pool
exit_count The exit pool's target capacity ([Server] exit_count); read it together with total_proxies. When rotation happens depends on the mode: under continuous rotation the pool holds this many even while idle; under request-driven rotation and the local source, exits that expire while idle are removed rather than replaced and the pool may drain, until the next request fetches a fresh set to fill it
elastic_active Whether the exit count is above the target: a burst or peak auto-expansion is in progress, or the expansion has stopped but the surplus exits have not yet reached the end of their lifetime (each is retired one by one only after serving out its full lifetime)
auth_required Whether the outbound proxy requires username / password authentication
display_level Console verbosity ([Server] display_level)
service_status running / stopped
language The current UI language
request_interval The threshold for switching by request count (an assignment count, not the number of requests served successfully). It takes effect together with interval; whichever threshold is reached first triggers the rotation
pool_running Whether the proxy pool is running
source_error The reason and time of the most recent failure to fetch exits from the source ({"message": ..., "at": ...}); null when nothing has failed

Configuration Management

Read Configuration (GET /api/config)

Returns every key and value of the [Server] and [Pool] sections.

{
  "status": "success",
  "server": { "port": "1080", "web_port": "5001", "mode": "cycle" },
  "pool": { "validator.timeout_seconds": "10" }
}
  • The token key is stripped out and never appears in the response — the panel has no need to echo it back, keeping it out of the page source, browser cache and frontend logs.
  • [Users] and [api_credentials] are not returned here; each has its own endpoint.
  • Boolean values are normalized to "true" / "false": yes / on / 1 in config.ini all count as true at runtime, but the UI recognizes only "true", and without normalization the checkbox would show as unchecked — a single casual save by the user would switch the option off.

Save Configuration (POST /api/config)

Either section in the request body may be omitted, but at least one must be non-empty:

{
  "server": { "mode": "loadbalance", "interval": "600" },
  "pool": { "validator.timeout_seconds": "15" }
}

Key names are validated against a whitelist; unknown keys and invalid values are rejected with HTTP 400.

token is also a valid server key: the panel provides no editor for it, but this API accepts it — so to change the token you can either call POST /api/config (effective immediately) or edit config.ini directly and restart the process.

{
  "status": "success",
  "port_changed": false,
  "web_port_changed": false,
  "service_status": "running",
  "applied": { "proxy": true, "pool": true }
}
Field Description
port_changed Whether the proxy listening port changed. Hot-reloading the configuration does not rebind ports by itself; the panel restarts the outbound proxy service based on this flag (restart of POST /api/service) to move to the new port. When using the command-line entry point, the process must be restarted instead
web_port_changed Whether the panel port changed. The entire process must be restarted for it to take effect; the current panel still runs on the old port
applied.proxy Whether the new configuration was pushed to the outbound proxy service
applied.pool Whether the new configuration was pushed to the proxy pool

An entry of false under applied means the configuration was written to disk but the push failed; the response itself is still success. In that case, go by applied to judge whether the change really took effect.

On a validation failure (HTTP 400), besides message the response carries details, naming the offending key and the reason rendered in the current UI language; the panel uses it to attach the hint to the specific input box:

{
  "status": "error",
  "message": "Invalid option port (current value: 70000): must not be greater than 65535",
  "details": { "key": "port", "reason": "must not be greater than 65535" }
}

details is present only when both key and reason_key are set (validation exceptions for [Server] and [Pool] all carry these two attributes); other exceptions have only message. message is always retained, so older clients are unaffected.

When saving the configuration, if the current proxy source is pool, no external pool address (pool_remote_url) is configured and the pool is not running, the pool service is started automatically along the way.

Language

Switch UI Language (POST /api/language)

The language is a server-side setting, not a browser setting: it is written back to [Server] language in config.ini and switches the wording of the whole process (including the panel, the console banner, API message fields, and the headers and status columns of exported files).

{ "language": "en" }
Field Type Required Values
language string No cn / en; defaults to cn
{ "status": "success", "language": "en" }

A value outside cn / en still returns HTTP 200; judge by status:

{ "status": "error", "message": "Unsupported language" }

This endpoint follows the panel-side convention that business failures return 200 — do not go by the status code alone.

Local Proxies and Lists

Read / Write the Local Proxy List (GET/POST /api/proxies)

GET returns the contents of the proxy list file under config/.

{ "proxies": ["http://1.2.3.4:8080", "socks5://5.6.7.8:1080"] }

A read failure returns an error, not an empty list — an empty list would make the UI show "no proxies at all", and a single casual save by the user would wipe the proxy file:

{ "status": "error", "message": "Failed to load proxy file: ..." }

POST overwrites the whole file and hot-loads the outbound service immediately (without re-checking availability):

{ "proxies": ["http://1.2.3.4:8080"] }

proxies must be an array (a string is torn into lines character by character, and an object writes only its key names — either way the file is overwritten with garbage), otherwise:

{ "status": "error", "message": "The proxy list must be an array" }

The write may also fail when the file is held by another process (common on Windows):

{ "status": "error", "message": "..." }

Check Proxy Availability (GET /api/check_proxies)

Parameter Required Default Description
test_url No https://www.baidu.com Target URL for the liveness check

Checks the source list entry by entry, with the concurrency taken from [Server] check_concurrency:

  • local source: every entry in proxy_file — the active roster is deliberately not checked, because an exit that has been disabled by failures disappears from the roster, and those are exactly the ones that most need a re-check.
  • api / pool sources: there is no readable local list, so the exits currently in use are checked.
{
  "status": "success",
  "valid_proxies": ["http://1.2.3.4:8080"],
  "checked": 3,
  "total": 1,
  "message": "Proxy check completed, valid proxies: 1"
}
Field Description
valid_proxies Addresses that passed the check
checked How many were actually checked this time (the number of source-list entries)
total The number that passed the check

The check only reports its findings and does not change the active roster — to put newly validated exits into service, simply wait for the next batch refresh.

The check takes longer as the proxy count grows, so the request may wait a while (internal timeout 30 seconds).

Read / Write the IP Whitelist / Blacklist (GET/POST /api/ip_lists)

GET:

{ "whitelist": ["127.0.0.1"], "blacklist": ["1.2.3.4"] }

POST:

{ "type": "whitelist", "list": ["127.0.0.1"] }
Field Description
type whitelist or blacklist; any other value returns an error
list The complete list; written as a full overwrite

After the file is written, the outbound service is hot-loaded immediately — no restart needed. Both lists validate the client source IP (the endpoint connecting to the proxy port): the decision is made first as each new connection is established, and a client that fails it gets a 403 directly plus a logged "Unauthorized IP attempt" — the connection is not forwarded. When a client matches both lists, the priority is set by [Server] ip_auth_priority (whitelist allows / blacklist blocks).

Read / Write the Bypass List (GET/POST /api/bypass_whitelist)

GET:

{ "list": ["192.168.0.0/16"] }

POST:

{ "list": ["192.168.0.0/16"] }

Target addresses on the list are not forwarded through any upstream; they are connected to directly from this host (taking no part in exit scheduling). This is a different matter from the client-source-IP whitelist / blacklist above: adding a target to this list does not skip the client-IP whitelist / blacklist check.

Service Control

Start / Stop the Outbound Proxy Service (POST /api/service)

{ "action": "start" }
action Description
start Starts the service if it is not running; returns success directly if it is already running
stop Stops the service
restart Stops first, then starts
{ "status": "success", "message": "Service started successfully", "service_status": "running" }

start / stop responses carry service_status (values running / stopped); a successful restart does not carry that field.

This endpoint is serialized and has timeouts: a start waits for the old thread to exit (up to 10 seconds) and then for the service to be ready (up to 5 seconds); a stop waits up to 10 seconds. On timeout it returns status as error and will not silently fall back to another port. Callers should therefore allow a sufficiently long request timeout.

When the port is in use, it returns an error and does not start the service:

{
  "status": "error",
  "message": "Port 1080 is already in use by another program, please choose a different port and retry",
  "service_status": "stopped"
}

Manually Switch the Upstream Proxy (GET /api/switch_proxy)

Discards the existing standby entries and calls the source once immediately: if the pool is not full it is topped up; when exit lifetime is enabled (interval or request_interval non-zero), a full pool has its soonest-expiring exit replaced (when only request_interval is filled in, exits have no time-based lifetime, so what actually gets replaced is the first entry in the roster). This is not "switching to the next IP" (all exits are already in use at the same time), nor a wholesale replacement. The response has four forms:

Switch succeeded

{
  "status": "success",
  "current_proxy": "http://1.2.3.4:8080",
  "active_proxies": [
    { "url": "http://1.2.3.4:8080", "inflight": 0, "active": 0, "capacity": 0, "failures": 0, "suspect": false, "failure_ratio": 0.0, "last_used": 0.0 }
  ],
  "total_proxies": 1,
  "message": "Upstream exit batch refreshed"
}
Field Description
current_proxy The current exit (the most recently handed-out exit, masked); when it is replaced, falls back to the first entry in the roster
active_proxies A snapshot of the current roster; field meanings are the same as /api/status
total_proxies Number of exits in the current roster

"Success" is judged by whether the roster is non-empty after the refresh — if it was already full and only one exit was rotated this time, that still counts as success.

In the cooldown period (too soon after the last switch; no switching within [Server] switch_cooldown)

{
  "status": "error",
  "cooldown": true,
  "cooldown_remaining": 12,
  "message": "..."
}

A switch is already in progress

{ "status": "error", "switching": true, "message": "..." }

Switch failed

{ "status": "error", "current_proxy": "...", "message": "..." }

Callers should use the cooldown / switching fields to tell "not switching right now" apart from "genuinely failed"; both are status: error.

Logs

By purpose, logs fall into four categories: access (proxy access), proxy (proxy lifecycle), main (main program) and pool (proxy pool). Of these, access / proxy / main have corresponding log files, while pool only goes into the in-memory ring buffer and error.log; its own files live under modules/proxypool/logs/ and are not browsed through this group of endpoints.

Query In-Memory Logs (GET /api/logs)

Parameter Required Default Description
category No all all / access / proxy / main / pool; any other value returns an error
start No 0 Number of entries to skip backwards from the newest one
limit No 200 Number of entries to return, clamped to 1-2000
level No ALL Filter by level
search No empty Keyword filter
{
  "logs": [
    {
      "seq": 8821,
      "time": "2026-09-30 12:00:00",
      "level": "INFO",
      "category": "main",
      "message": "..."
    }
  ],
  "total": 1234,
  "status": "success"
}
  • seq is a globally incrementing sequence number, unique across categories; when several categories are merged into a single response, it restores their true order. Entries are listed in ascending chronological order.
  • total is the total after filtering, not the number of entries returned this time.
  • The semantics of start are the opposite of ordinary pagination: it is an offset counted back from the newest entry, and together with limit it is used to "page back through older logs".

Log Statistics (GET /api/logs/stats)

{
  "status": "success",
  "stats": {
    "total": 170,
    "levels": { "INFO": 150, "WARNING": 20 },
    "categories": { "main": 100, "access": 50, "proxy": 20, "pool": 0 },
    "capacity": { "main": 20000, "access": 5000, "proxy": 20000, "pool": 20000 },
    "first_time": "2026-09-30 10:00:00",
    "last_time": "2026-09-30 12:00:00"
  }
}

total is the combined count of the four rings, and capacity is each ring's capacity (the access ring is smaller). first_time / last_time are absent when the buffers are empty.

List Log Files (GET /api/logs/files)

{
  "status": "success",
  "files": [
    { "name": "main.log", "category": "main", "size": 20480,
      "modified": "2026-10-02 15:30:00", "current": true, "rotated": false }
  ]
}
Field Description
name File name (the panel uses it to call /api/logs/file)
category main / proxy / access / error. error is the aggregated file for ERROR and above, and must not be passed as the category parameter of GET /api/logs
size / modified Size in bytes / last modified time
current Whether this is the one currently being written; rotated is its opposite (a rotated historical copy)

Only whitelisted files under the logs/ directory are listed (rotated copies included); the proxy pool's own proxy_pool.log is not included.

Read a Log File (GET /api/logs/file)

Parameter Required Default Description
name Yes empty File name; must be a name from the whitelist
lines No 500 Number of trailing lines to read, clamped to 1-5000
{ "status": "success", "name": "main.log", "lines": ["..."], "truncated": true }

truncated of true means the file's line count is no fewer than the requested count (it is also true when they are exactly equal), and only the trailing part was returned. A file name that is not on the whitelist returns an error (validated against the whitelist rather than by path concatenation, ruling out directory traversal).

Export Logs (GET /api/logs/export)

Parameter Required Description
file No When a file name is specified, downloads that file as an attachment; when omitted, exports the in-memory logs
category No Takes effect when file is omitted; same as GET /api/logs
level / search No As above

When file is omitted, a plain-text attachment named proxycat_export.log is returned.

Clear Logs (POST /api/logs/clear)

{ "category": "all" }

Clears the in-memory ring buffer of the given category and truncates the corresponding log file. When category is all, everything is cleared.

{ "status": "success", "message": "Logs cleared" }

This deletes log content on disk and cannot be undone.

Access Records

Records every proxy request individually. What sets it apart from Logs is that it is built for querying and statistics: stored in columns, filterable and exportable.

Query Access Records (GET /api/logs/records)

Parameter Required Default Description
since No 1 hour ago Start time; accepts YYYY-MM-DD, YYYY-MM-DD HH:MM, YYYY-MM-DD HH:MM:SS, and the T separator as well
until No No limit End time; when given at minute or date granularity it is padded to the last second of that moment
outcome No Any success / failure / aborted; any other value returns an error
upstream No Any Filter by upstream proxy
host No Any Filter by target host
search No Empty Keyword filter
limit No 100 Clamped to 1-500
offset No 0 Pagination offset
order No desc Time sort direction
{
  "status": "success",
  "enabled": true,
  "range": { "since": "2026-09-30 11:00:00", "until": "" },
  "outcome_counts": { "success": 100, "failure": 3, "aborted": 1 },
  "total": 104,
  "export_limit": 50000,
  "records": [
    {
      "id": 12345,
      "ts": "2026-09-30 12:00:00",
      "kind": "http",
      "method": "GET",
      "client": "127.0.0.1/-",
      "client_ip": "127.0.0.1",
      "client_user": "-",
      "host": "example.com",
      "port": 443,
      "target": "example.com:443",
      "upstream": "http://1.2.3.4:8080",
      "upstream_label": "http://1.2.3.4:8080",
      "real_ip": "203.0.113.7",
      "outcome": "success",
      "status_code": 200,
      "elapsed_ms": 123,
      "reason": ""
    }
  ]
}
  • id is the auto-increment primary key; client (client_ip/client_user) and target (host:port) are derived fields — they merely join the two adjacent columns into a form the UI can use directly, and come from the same source as client_ip / host.
  • When upstream holds a special value, upstream_label supplies a readable label: direct (direct) and unknown upstream (unknown) each have their own value.
  • On failure (outcome=failure) status_code is always empty: rows that failed only after the tunnel was established do not appear here carrying the 200 from connection setup (such rows were once stored in the database; reads normalize them uniformly).
  • real_ip is the egress IP this exit actually uses (the one the target site sees); for a gateway-type exit its address is a different thing. It is a display field: for the pool source it comes from the verification performed when the pool accepts the proxy, for the api source it is produced by a background probe when [Server] access_records_real_ip_probe is on, and for the local source it is always an empty string. When probing fails or the setting is off it is "" and the UI renders --; direct requests are also "". It does not participate in exit scheduling, lifetime or failure decisions, and historical records in an older database are likewise "".
  • The response returns three persistence-status fields alongside records: pending (buffered rows not yet flushed), dropped (rows discarded because the buffer filled up), oldest_ts (timestamp of the oldest row in the database; an empty string means none). A non-zero dropped means flushing cannot keep up and records are being lost.

Export Access Records (GET /api/logs/records/export)

The filter parameters are the same as for the query endpoint, plus one more:

Parameter Required Default Description
format No csv csv or json; any other value returns an error
  • CSV: carries a UTF-8 BOM so Excel opens it without mojibake; the header row is rendered in the current UI language.
  • JSON: UTF-8, with exported_at, the filter conditions, count and truncated metadata.

Both formats are capped by export_limit (50000); the truncation flag exists only in JSON's metadata (truncated, true once the number of rows fetched reaches 50000) — CSV has just the body and no such flag. The response is an attachment download, with a filename like access_records_20260930_120000.csv.

List Filter Options (GET /api/logs/records/options)

Returns the upstream proxy and target host values that appear in the current records, for the frontend to render dropdowns.

{ "status": "success", "upstreams": [{ "value": "http://1.2.3.4:8080", "label": "..." }] }

Clear Access Records (POST /api/logs/records/clear)

{ "status": "success", "removed": 1234, "message": "Cleared 1234 access records" }

Irreversible.

Domain Statistics

Accumulates success and failure counts per "target domain × upstream proxy". The difference from Access Records: this is an aggregate count while access records are per-request detail; clearing access records therefore leaves these totals intact, and vice versa.

Query Domain Statistics (GET /api/logs/domains)

Parameter Required Default Description
upstream No Empty When given, drills down to the domain detail for that upstream; when omitted, aggregates by upstream
sort No last_seen Sort field
order No desc Sort direction
search No Empty Keyword filter
limit No 100 Clamped to 1-1000
offset No 0 Pagination offset

upstream omitted — aggregate by upstream:

{
  "status": "success",
  "enabled": true,
  "summary": { "pairs": 42, "hosts": 12, "upstreams": 5, "success": 3400, "failure": 56 },
  "mode": "proxies",
  "proxies": [
    {
      "upstream": "http://1.2.3.4:8080",
      "upstream_label": "http://1.2.3.4:8080",
      "host_count": 7,
      "success": 100,
      "failure": 2,
      "total": 102,
      "rate": 98,
      "first_seen": "2026-09-30 10:00:00",
      "last_seen": "2026-09-30 12:00:00",
      "last_error": ""
    }
  ],
  "total": 1
}

upstream given — drill down to domains:

{
  "status": "success",
  "enabled": true,
  "summary": { "pairs": 42, "hosts": 12, "upstreams": 5, "success": 3400, "failure": 56 },
  "mode": "domains",
  "upstream": "http://1.2.3.4:8080",
  "upstream_label": "http://1.2.3.4:8080",
  "domains": [
    {
      "host": "example.com",
      "upstream": "http://1.2.3.4:8080",
      "success": 50,
      "failure": 1,
      "first_seen": "2026-09-30 10:00:00",
      "last_seen": "2026-09-30 12:00:00",
      "last_status": 200,
      "last_error": ""
    }
  ],
  "total": 1
}
  • In summary, pairs is the number of "domain × upstream" combinations; rate is the success rate as a rounded percentage.
  • Use the returned mode field to tell the two shapes apart — don't guess.
  • enabled being false means the domain statistics feature is off ([Server] domain_stats_enabled), in which case there is no data to query.

Clear Domain Statistics (POST /api/logs/domains/clear)

{ "status": "success", "removed": 42, "message": "Cleared access statistics for 42 domains" }

Users and API Credentials

Read / Write Outbound Proxy Accounts (GET/POST /api/users)

GET:

{ "status": "success", "users": { "neko": "123456" } }

A failed read returns status: "error" with users: null, not an empty object — an empty object makes the UI show "no users at all", and the next time an administrator adds a user and saves, that empty table overwrites the original accounts, which amounts to turning outbound authentication off. The response contains plaintext passwords.

POST replaces the whole [Users] section:

{ "users": { "neko": "123456" } }

An empty object turns outbound authentication off (when the account table is empty, auth_required is false).

{ "status": "success", "message": "Users saved successfully" }

Manage Upstream API Credentials (GET/POST /api/api_credentials)

Stores multiple credential sets for the upstream proxy-fetch endpoint and designates the one currently in effect. When you switch the active set with set_active, or when the set you delete is the active one, the [Server] api_proxy_url / proxy_username / proxy_password keys are rewritten in step, so with the proxy source set to api the new credentials take effect immediately; save does not sync those three keys — it only updates the credential collection and the active-set marker. For newly saved values to actually be used by proxy sourcing, call set_active on them once more (or let the automatic switch triggered by deleting the active set) write the three keys back into [Server].

GET:

{
  "status": "success",
  "sets": [{ "name": "闪臣", "url": "https://...", "username": "", "password": "" }],
  "active_credential": "闪臣"
}

The response contains plaintext credentials.

POST:

{ "action": "save", "name": "闪臣", "url": "https://...", "username": "", "password": "" }
action Required fields Description
save name, url, username, password Updates the entry by name if it exists, otherwise adds one; an empty name returns an error. If there is no active set yet, or the active set is this very entry, it is made the active set
delete name Deletes by name; if the entry deleted is the active set, the first remaining entry is made active and its three keys are written back to [Server] in step (with no entries left, the three keys are cleared) — without the switch, the running service would keep using the deleted endpoint
set_active name Switches the active set and writes its url/username/password into [Server]; if name is not in the credential collection it returns an error and never writes a "nothing selected" intermediate state

save and delete return the updated sets and active_credential; set_active returns active_credential and the active entry's credential but does not return sets. An unknown action and an empty name return an error ({ "status": "error", "message": "..." }), as does set_active when its name is not in the credential collection. Passing a name that does not exist to save adds a new entry; delete does not check whether the name exists — deleting a credential name that does not exist returns success all the same (sets returned unchanged).

Version Check and Ads

Except for POST /api/version/check, the endpoints in this section require no authentication; even when a token is configured, you do not need to send it.

Check Version (GET /api/version)

Returns the result of the most recent version check. This endpoint only reads a cache and never makes an external request — calling it never incurs a network wait.

{
  "status": "success",
  "is_latest": true,
  "current_version": "ProxyCat-V3.0.0",
  "latest_version": "ProxyCat-V3.0.0"
}
Field Description
is_latest Whether the remote version is no newer than the current version; recomputed against the current CURRENT_VERSION on every read
current_version The version of this process
latest_version The remote version fetched by the most recent check

The check cadence is up to the process itself, not triggered by this endpoint:

  • Once at process startup, then once every 24 hours.
  • A restart within 24 hours does not re-check: the time of the last check is written to logs/version_check.json and read back after a restart to be reused. Both entry points (app.py and ProxyCat.py) follow this cadence.
  • The time is recorded whether the check succeeds or fails, so at most one external request is made every 24 hours; when the network is down, the "every panel open waits out one timeout" situation no longer happens.
  • The check runs on a background thread: it does not block startup, and it does not occupy a web worker thread.
  • When you need the answer right now, use POST /api/version/check below.

The version comes from the GitHub repository's release history. By default three addresses are requested concurrently and the first one to return a usable version number wins:

Address Notes
github.com/…/releases.atom The official release feed; no authentication, no rate limit
gh-proxy.com/https://api.github.com/… A third-party mirror with better reachability from mainland China; it sees your request IP and may return a cached, older version
api.github.com/repos/…/releases The official API; 60 requests per hour per IP when unauthenticated

Concurrent rather than sequential retries: a blocked or timing-out address does not slow the whole thing down — a usable address usually answers within a second, and when all three fail the call returns after roughly 8 seconds. Requests keep httpx's default behaviour of honouring the HTTP(S)_PROXY environment variables.

When the version_check_url config option is non-empty, that address is the only source and the three built-ins are not requested. The version is taken as the ProxyCat[-_]v?digits.digits[.digits] prefix of a tag, so suffixes such as -测试版V5 are dropped.

Check Version Now (POST /api/version/check)

Requires authentication — unlike the read-only cached GET above, this endpoint really does make an external request.

No parameters, no request body. The current web thread waits until the check finishes (up to about 15 seconds). Only one check runs at a time — concurrent calls wait for the one in flight instead of each firing their own round of requests.

{
  "status": "success",
  "current_version": "ProxyCat-V3.0.0",
  "latest_version": "ProxyCat-V3.0.0",
  "is_latest": true,
  "source": "github.com",
  "checked_at": "2026-10-06 18:39:42"
}
Field Description
status The outcome of this check — the key difference from the GET
source Hostname of the winning address; present on success only
checked_at The time of this check
latest_version / is_latest Present whenever a successful version is held in memory
message Why this check failed; present on failure only

When this check fails but an earlier one succeeded, status is error and carries a message, while still including the previous successful latest_version. That is exactly how it differs from the GET: the GET reports "last known state", this endpoint reports "did this one succeed". Both write the time of the check into logs/version_check.json, so a manual check also resets the 24-hour timer.

When there is neither a successful result nor a failure record (the first check after startup has not finished yet), it returns:

{
  "status": "error",
  "message": "Version check has not completed yet"
}

When the check fails (no network, remote page structure changed) it returns status error with a message explaining the cause; but if a check has succeeded before, it keeps returning the last successful result — the panel is not a monitoring tool, and showing the last known version state is more useful than showing an error. The failure itself is written to the logs.

After upgrading to a new version, a re-check happens immediately even if less than 24 hours have passed since the last one: the state file stores "the current version at the time of the check", and a mismatch with the version in the process counts as stale.

Get Ads (GET /api/ads)

Reads every *.json in the config/ads/ directory and returns the entries whose enabled is not false. Files that fail to parse are skipped silently.

{
  "status": "success",
  "ads": [
    {
      "id": "example",
      "title": "...",
      "body": "...",
      "image_url": "",
      "link_url": "",
      "link_text": "",
      "display_time": 15
    }
  ],
  "total": 1,
  "dismissed": false
}

dismissed is the in-process dismissed state; it is lost on restart.

Dismiss / Reopen Ads (POST /api/ads/dismiss, POST /api/ads/reopen)

Both endpoints take no parameters and require no authentication; the response body is { "status": "success", "dismissed": <boolean> }, where dismiss sets true and reopen sets false:

{ "status": "success", "dismissed": true }

3. Pool API

They all live under the /api/pool prefix, and authentication is the same as the panel's (the same before_request hook).

Pool API Conventions

Success Responses Have No Uniform Envelope

The adapter layer serializes the handler's return value straight into the response body, so:

  • GET /api/pool/get returns a JSON array (not {"status": ..., "data": [...]}).
  • GET /api/pool/count returns {"count": 12}, with no status field.
  • GET /api/pool/random?format=text and the export endpoints return plain text or an attachment, not JSON.
  • Only some endpoints carry their own status and message in the return value (such as the lifecycle endpoints); plugin operations return success and message, with no status field.

To tell success apart, read the HTTP status code and the response content itself; do not assume a status field is always present.

Error Responses

{
  "status": "error",
  "message": "Error description",
  "error_code": "invalid_parameter"
}
error_code HTTP Meaning
invalid_parameter 400 Invalid request parameters (including missing required fields and out-of-range values)
api_error 4xx / 5xx A business error, such as a proxy not existing or a spec mismatch. The HTTP status code comes with the error itself (e.g. plugin operation failure and backup restore failure are 500, geolocation resolution service not enabled is 503) — do not hard-code 4xx branches
pool_running 409 The pool is running and this operation is not allowed (only backup restore returns this)
pool_unavailable 503 The pool is not running, or is restarting
pool_timeout 504 The pool response timed out
queue_full 503 The write queue is full and the request was rejected (backpressure)
internal_error 500 Internal server error

Two exceptions to be aware of:

  • When POST /api/pool/start|stop|restart fails, it returns 500 with no error_code, only status / message / is_running.
  • message follows the UI language and must not be used for programmatic decisions.

When the Pool Is Not Running

While the pool has not started, apart from the status query, the lifecycle endpoints, backup listing/restore and /schema, every other endpoint returns 503 pool_unavailable. Callers should first query GET /api/pool/status to confirm is_running.

Task Model

Every long-running operation (batch validation, import, fetching, geolocation update/recompute) returns a task_id immediately; the actual execution is in the background, and progress is polled by task_id.

  • Task state is in-process memory state: all of it is lost when the process restarts.
  • Completed/failed tasks are retained for 1 hour (TTL) and are evicted after that; the status table holds at most 200 entries.
  • The status returned at creation (queued / running) is only a snapshot of the creation moment and does not represent the execution result; entries in the task table are written straight as running from creation, and no entry is ever in the queued state.
  • The states that actually appear in the task table: running / completed / failed / cancelled (queued appears only in some creation responses — filtering tasks by it will never match an entry).

No Endpoint for Reporting Usage Feedback

"Usage feedback" is what the outbound proxy reports back to the pool inside the process during real forwarding (see the Proxy Pool Mechanics section in Features); there is no HTTP endpoint for reporting a usage outcome, so do not go looking for one.

Pool Status and Lifecycle

Get Pool Status (GET /api/pool/status)

{
  "status": "success",
  "is_running": true,
  "last_error": null,
  "stats": {
    "total_proxies": 120,
    "valid_proxies": 86,
    "invalid_proxies": 34,
    "anonymity_fallback_proxies": 5,
    "protocol_distribution": { "http": 60, "socks5": 26 },
    "region_distribution": { "中国": 40, "美国": 12 }
  }
}
Field Description
is_running Whether the pool is running
last_error Text of the most recent error; null when there is none
stats Global statistics; null while the pool is not running. A failure of the statistics themselves does not stop this endpoint from returning success — stats is simply null
anonymity_fallback_proxies Number of proxies whose anonymity was judged as the fallback tier
region_distribution Only the 10 regions with the most proxies

Get Pool Config Field Schema (GET /api/pool/schema)

Returns the field groups, types, descriptions and value ranges of the [Pool] section; the panel's "Pool Settings" form is rendered from it. The wording follows the current UI language.

{
  "status": "success",
  "groups": [
    {
      "title": "Database",
      "sub": "Where data lives and how it is backed up",
      "id": "database",
      "fields": [
        { "key": "database.path", "doc": "File holding all pool data (SQLite)", "type": "text",
          "title": "Database file path", "hint": "Relative paths start from the proxypool directory; changes need a pool restart" },
        { "key": "database.pool_max_readers", "doc": "...", "type": "int", "low": 1, "high": 64,
          "title": "Max read connections", "tier": "advanced" },
        { "key": "database.backup_interval_hours", "doc": "...", "type": "int", "low": 1,
          "title": "Backup interval", "unit": "h", "tier": "advanced", "visible_when": "database.backup_enabled=true" }
      ]
    }
  ]
}
Field Description
type Field type, one of bool / int / float / list / text (string fields are always text; there is no str)
doc Full description, word for word the comment in config.ini
title Short title, used directly as the form field label
hint One-line hint under the control; whether it appears depends on whether the field has one
unit Unit of measure, rendered by the panel to the right of the input box. The unit is not folded into title, and the key is omitted when the language pack has no value for it
tier When "advanced", the field goes into the "Advanced" collapsible section
visible_when Declarative show/hide rule key=value1|value2;key2!=value (a semicolon means "and", a pipe means "or"). A field carrying this rule is shown inline and never collapsed. Hiding only affects display; the field is still collected and saved as usual
sub (group level) Group subtitle, shown to the right of the card title

low / high appear only for options that have a value range. This endpoint does not return current values — for those, use the pool field of GET /api/config.

Start the Pool (POST /api/pool/start)

No parameters.

{ "status": "success", "message": "Pool started successfully", "is_running": true }

Stop the Pool (POST /api/pool/stop)

No parameters. Waits for the write queue to drain, so it can take several seconds.

{ "status": "success", "message": "Pool stopped successfully", "is_running": false }

Restart the Pool (POST /api/pool/restart)

No parameters. Stops and then starts; the stop and the start each wait up to 30 seconds.

On failure (the same for all three lifecycle endpoints):

{ "status": "error", "message": "...", "is_running": false }

The HTTP status code is 500, and no error_code is included.

Proxy Queries and Single-Proxy Operations

Query the Proxy List (GET /api/pool/get)

Returns a JSON array whose elements are proxy objects.

Parameter Type Default Values and meaning
protocol string any http / https / socks5; anything else returns 400
region string any Fuzzy region match (matches both the Chinese and the English column); a leading - means exclude, e.g. -United States excludes proxies whose region contains the United States
min_delay int any Minimum delay (milliseconds); a negative value is an error
max_delay int any Maximum delay (milliseconds); a negative value is an error, and so is reversing the order against min_delay
status string any valid / invalid; anything else returns 400
source string any Source plugin name
sort_by string delay_ms See the whitelist below
sort_order string asc asc / desc
limit int any Return only the first N rows; when set, page is forced to 1. Cap: 50000
page int 1 Page number, must be ≥ 1
page_size int 50 Rows per page, 1 to 50000
format string json json or text; text returns plain text with one proxy address per line
ip string any Fuzzy match on IP
is_favorite bool any Favorited only / unfavorited only
anonymity_level string any transparent / anonymous / elite / unverified
min_health_score float any Minimum health score; a negative value is an error
supports_https bool any Only proxies that do / do not support HTTPS

sort_by whitelist: delay_ms, protocol, region, validated_at, created_at, health_score, anonymity_level, success_count, total_checks, avg_delay_ms, failure_count, is_favorite, supports_https, success_rate, region_en.

A misspelled sort_by raises no error: a value outside the whitelist falls back to delay_ms silently, and a sort_order other than asc / desc falls back to asc silently. When the sort order looks wrong, check the spelling of the parameter names first.

Unknown query parameters are silently ignored (see Parameter Parsing Rules). A misspelled parameter name raises no error; you simply get unfiltered results.

Get One Proxy (GET /api/pool/random)

Returns one random proxy that matches the filters. The filter parameters are the same as for /get (there is no sort_by / sort_order / limit / page / page_size), plus:

Parameter Type Default Description
format string json json returns the full proxy object; text returns only one line, protocol://ip:port
{
  "id": 42,
  "proxy_url": "http://1.2.3.4:8080",
  "protocol": "http",
  "ip": "1.2.3.4",
  "port": 8080,
  "delay_ms": 320.5,
  "health_score": 87.3,
  "anonymity_level": "elite",
  "region": "中国 广东 深圳"
}

(The example above is abridged; for the full field list see Appendix C.)

In its plain-text form, a format=text response body is exactly one line:

http://1.2.3.4:8080

Errors and edge cases:

  • No proxy matches the filters → 404 api_error.
  • format is neither json nor text → 400 api_error.

The pick is not strictly uniform: the implementation counts the rows, picks a page at random (100 rows per page), then picks one row on that page — the granularity is the page. With a small pool the odds differ slightly from row to row, but that has no practical effect on use.

This endpoint's format=text is exactly the response shape pool_remote_url expects — another ProxyCat instance's /api/pool/random?format=text&token=... can serve as an external pool address.

Count Proxies (GET /api/pool/count)

The filter parameters are the same as for /get (minus sorting and pagination).

{ "count": 86 }

Export Proxies (GET /api/pool/export)

The filter parameters are the same as for /get (minus sorting and pagination), plus:

Parameter Type Default Description
format string txt txt / json / csv; anything else returns 400
  • txt: one proxy address per line, as an attachment named proxies_YYYYMMDD_HHMMSS.txt.
  • json: an attachment with the fields protocol, ip, port, username, password, url, region, region_en, delay_ms, is_valid, real_ip, source_plugin, validated_at.
  • csv: an attachment with a UTF-8 BOM; the number and order of header columns are a public contract (Protocol, IP, Port, Username, Password, Proxy URL, Region, Delay (ms), Status, Exit IP, Source, Validated at), and the wording of the header and status columns follows the UI language. The English region column is deliberately left out of the export.

A single export is capped at 50000 rows.

Errors and edge cases: no proxy matches the filters → 404 api_error.

Import Proxies (POST /api/pool/import)

Request body:

{ "proxies": ["http://1.2.3.4:8080", "socks5://user:[email protected]:1080"] }

Import goes through the same pipeline as plugin fetching: a quick port screen first, then a full validation, and only proxies that pass validation are saved, with their source always recorded as manual_import. This endpoint is in the HEAVY tier.

The response has three possible outcomes; callers must read message and proxy_count together, not success alone:

Case Response
Accepted { "success": true, "message": "Validating N proxies in the background; only working ones are saved", "proxy_count": N, "task_id": "..." }
All failed to parse { "success": false, "message": "...", "proxy_count": 0 }
All already exist { "success": true, "message": "All proxies already exist, none imported", "proxy_count": 0 }
  • An empty list → 400 invalid_parameter.
  • "Already exists" is decided by looking each entry up in the database by ip:port; the deduplication cache is not consulted.
  • Once accepted, poll progress with the returned task_id.

Toggle Favorite Status (POST /api/pool/proxies/{id}/favorite)

No request body.

{
  "success": true,
  "is_favorite": true,
  "message": "Proxy added to favorites"
}

This is a toggle, not a setter — calling it again flips back and forth between the two states, and there is no idempotent parameter. If the proxy does not exist → 404 api_error. Favorite status represents a user choice; writes from fetching and validation never overwrite it.

Statistics and Host Information

Get Pool Statistics (GET /api/pool/stats)

{
  "total_proxies": 120,
  "valid_proxies": 86,
  "invalid_proxies": 34,
  "anonymity_fallback_proxies": 5,
  "protocol_distribution": { "http": 60, "socks5": 26 },
  "region_distribution": { "中国": 40, "美国": 12 }
}

The response has no status field; it is the statistics object itself (structurally identical to stats inside /status).

List Sources (GET /api/pool/sources)

{ "sources": ["geonode_plugin", "github_proxy_plugin", "manual_import"] }

Returns every source plugin name that has appeared in the database, including the pseudo-plugin name manual_import used for manual imports.

Get This Host's Public Egress IP (GET /api/pool/host-ip)

Returns this host's public egress IP, the baseline for deciding whether a proxy is transparent.

{
  "host_public_ip": "203.0.113.7",
  "host_public_ipv4": "203.0.113.7",
  "host_public_ipv6": "240e:37a:2563:cb00:9424:56ad:5734:d8",
  "source": "detected",
  "check_anonymity": true
}
Field Description
host_public_ip The single-value form of this host's egress IP, IPv4 preferred; IPv6 is given only when there is no IPv4. null when it cannot be determined, which is a normal state rather than a fault
host_public_ipv4 The egress IP of that family; null when it cannot be determined
host_public_ipv6 The egress IP of that family; null when it cannot be determined
source Where the value comes from (configured set manually / detected looked up automatically); an empty string when it cannot be determined
check_anonymity Whether anonymity detection is enabled

A dual-stack host has one egress IP per family, and both families take part in the anonymity comparison, which is why they are reported per family here.

The baseline is taken first from [Pool] validator.host_public_ip (one or two comma-separated values, at most one IPv4 and one IPv6); when it is left empty the program performs the lookup itself — the lookup pins the address family and queries each family once, rather than taking the first endpoint that answers, which would otherwise let the family obtained drift with endpoint availability. At most once every 30 minutes.

Plugin Management

List Plugins (GET /api/pool/plugins)

[
  {
    "name": "geonode_plugin",
    "enabled": true,
    "interval_minutes": 60,
    "last_run": "2026-09-30T11:00:00",
    "next_run": "2026-09-30T12:00:00",
    "last_error": null,
    "is_loaded": true,
    "test_url": "",
    "reval_enabled": true,
    "reval_interval_minutes": 0,
    "skip_validation": false
  }
]
Field Description
name The plugin name, equal to the plugin file's file name without its extension
enabled Whether the plugin takes part in automatic fetch scheduling
interval_minutes Fetch interval in minutes
last_run / next_run An ISO 8601 string or null
last_error The most recent error; a built-in message key is rendered in the current language, while an exception text raised by the plugin itself is passed through as-is
is_loaded Whether the module loaded successfully. The response only includes plugins that loaded successfully, so this is always true; a plugin that failed to load does not appear in the list (the error is only recorded in the pool's log)
test_url The liveness test URL used only by this plugin; an empty string means it inherits the global [Server] test_url
reval_enabled Whether the plugin takes part in automatic revalidation
reval_interval_minutes Revalidation interval; 0 means inherit the global value
skip_validation Whether to skip validation before storing (high risk, see below)

The response is a JSON array with no status field.

Enable / Disable a Plugin (POST /api/pool/plugins/{name}/enable, POST /api/pool/plugins/{name}/disable)

No request body.

{ "success": true, "message": "Plugin geonode_plugin enabled" }

On failure it returns 500 api_error.

Set the Fetch Interval (PUT /api/pool/plugins/{name}/interval)

{ "minutes": 30 }

minutes must be ≥ 1, otherwise 400 invalid_parameter.

{ "success": true, "message": "Plugin geonode_plugin interval set to 30 minutes" }

Set the Plugin Test URL (PUT /api/pool/plugins/{name}/test-url)

{ "test_url": "https://example.com" }

An empty or all-whitespace test_url means inheriting the global [Server] test_url:

{ "success": true, "message": "Test URL of plugin geonode_plugin restored to inherit the global URL" }

Set the Plugin Validation Policy (PUT /api/pool/plugins/{name}/validation)

{ "reval_enabled": true, "reval_interval_minutes": 0, "skip_validation": false }
Field Type Default Description
reval_enabled bool true Whether the plugin takes part in automatic revalidation
reval_interval_minutes int 0 Revalidation interval in minutes; 0 means inherit the global value. A string value is also accepted; a non-integer returns 400
skip_validation bool false Skip validation before storing
{ "success": true, "message": "Validation policy of plugin geonode_plugin updated" }

skip_validation is a dangerous switch: once it is on, proxies fetched by that plugin are written to the database without validation, and the pool's quality scoring and automatic cleanup both lose their meaning. It is only available for real, loaded plugins; for the pseudo-plugin name manual_import it returns 404 — otherwise turning it on once would make every manual import afterward skip validation.

Run a Plugin Immediately (POST /api/pool/plugins/{name}/run)

No request body. The call returns as soon as a background task is created; the fetch itself is bounded by [Pool] plugins.execution_timeout_seconds.

{
  "task_id": "8f14e45f-ea6b-4c1e-9c1a-0f3b2d5e7a91",
  "status": "running",
  "message": "Fetch task created for plugin geonode_plugin"
}

status is only a snapshot from the moment of creation; poll task_id for progress and results. Plugin not loaded → 404 api_error.

Hot-Reload a Plugin (POST /api/pool/plugins/{name}/reload)

No request body. After editing a plugin's source code you do not need to restart the process.

An expected failure returns 200 with success: false; only an unexpected exception produces a 500:

{ "success": true, "message": "Plugin geonode_plugin reloaded successfully" }
{ "success": false, "message": "Failed to reload plugin geonode_plugin (it may be running or the file does not exist)" }

A missing plugin file, a plugin that is currently running, or a load that returns empty all take the success: false path.

Batch Operations

Every endpoint in this section returns task_id immediately (except the two delete endpoints); poll progress under Task Management.

Revalidate Valid Proxies (POST /api/pool/validate/all-valid)

No parameters. Runs one round of revalidation over all proxies currently with is_valid = true.

{ "task_id": "...", "status": "queued", "message": "Validation task created" }

Revalidate Invalid Proxies (POST /api/pool/validate/all-invalid)

No parameters. Runs one round of revalidation over all proxies currently with is_valid = false.

This is the entry point for immediately revalidating proxies that usage feedback marked invalid: when a proxy is marked invalid, total_checks is reset to zero along with it (the log records it as "added to the next revalidation candidate round"), and every round of auto revalidation adds proxies with total_checks = 0 back into the candidates (not subject to auto_revalidation.only_valid_proxies, though still scheduled by each proxy's own revalidation interval), so these proxies are revalidated even without this endpoint.

{ "task_id": "...", "status": "queued", "message": "Validation task created" }

Full Check and Region Backfill (POST /api/pool/update-geo/all)

No parameters. Runs a complete assessment of every proxy (latency, egress IP, anonymity, protocol support) and backfills regions along the way. Much heavier than routine revalidation.

{ "task_id": "...", "status": "queued", "message": "Full proxy check task created" }

Delete All Invalid Proxies (DELETE /api/pool/delete/invalid)

No parameters. Deletion is irreversible.

{ "success": true, "message": "Deleted 34 invalid proxies", "deleted_count": 34 }

With no invalid proxies it returns success all the same, with deleted_count 0. This endpoint is in the HEAVY tier.

Batch Validate Specified Proxies (POST /api/pool/proxies/batch/validate)

{ "proxy_ids": [1, 2, 3], "mode": "liveness" }
Field Type Required Default Description
proxy_ids int[] Yes — List of proxy ids; an empty list returns 400
mode string No auto auto / liveness / full; any other value returns 400
{ "task_id": "...", "status": "running", "message": "Validating 3 proxies" }
  • Non-existent ids are silently ignored; a 404 api_error is returned only when none of them exist.
  • Three modes: liveness only checks reachability; full runs a complete assessment; auto picks one automatically from the information already held for each proxy.

Batch Delete Specified Proxies (DELETE /api/pool/proxies/batch/delete)

Note: this is a DELETE request, but the parameters go in the request body.

{ "proxy_ids": [1, 2, 3] }
{ "success": true, "message": "Deleted 3 proxies", "deleted_count": 3 }

An empty proxy_ids returns 400; non-existent ids are silently ignored, and deleted_count reports the rows actually deleted.

Task Management

List Tasks (GET /api/pool/tasks)

Parameter Type Default Description
status string Unrestricted Filter by status: running / completed / failed / cancelled (the task table has no queued, so passing it matches nothing)
{
  "tasks": [
    { "task_id": "8f14e45f-...", "status": "running", "progress": 40, "total": 120, "message": "..." }
  ],
  "total": 1
}

Query Task Status (GET /api/pool/tasks/{task_id})

The response is the task entry itself, with no outer status field (the entry carries its own status for the task state).

{
  "status": "running",
  "message": "Validating proxies...",
  "stage": "validate",
  "progress": 40,
  "total": 120,
  "stage_done": 40,
  "stage_total": 120,
  "rate": 12.5,
  "eta_seconds": 6,
  "started_at": "2026-09-30T12:00:00",
  "valid_count": 22,
  "invalid_count": 10
}

Fields vary by task type: only import / fetch tasks carry stage and the full set of counters; batch validation and full check tasks additionally have inconclusive_count. A task that does not exist or has been evicted by its TTL → 404 api_error.

Task entries are in-memory state: after a process restart, no historical task can be queried.

Cancel a Task (POST /api/pool/tasks/{task_id}/cancel)

No request body.

{ "success": true, "message": "Task cancelled" }

When the task has already finished it returns success: false, and this is not an error:

{ "success": false, "message": "The task has already finished, nothing to cancel" }

A non-existent task id → 404 api_error.

Geolocation Maintenance

Geolocation resolution uses an offline database (GeoLite2 and the Chunzhen library) and issues no external lookup requests. All three endpoints in this section are maintenance actions.

Query Offline Geolocation Database Status (GET /api/pool/geo/status)

{
  "available": true,
  "files": [ { "filename": "GeoLite2-City.mmdb", "exists": true,
               "size": 65383808, "modified_at": "2026-10-02 12:00:00" } ],
  "pending_recompute": 42
}

modified_at is null when the file does not exist.

Field Description
available Whether the offline database is usable (usable only when both data files are present)
files Status of each data file
pending_recompute Number of records whose region is pending recomputation; when it cannot be determined, it returns 0 rather than an error

Update the Offline Geolocation Database (POST /api/pool/geo/update)

No parameters. Downloads roughly 100 MB of data files, run as a background task.

{ "task_id": "...", "status": "queued", "message": "Geo database update task created" }
  • The two files total around 105 MB (GeoLite2 about 65 MB, the Chunzhen library about 40 MB).
  • Disk space is not checked; if space runs short, the download fails partway through.
  • A failure on one file does not block the other; failures are reported faithfully in the task result's message.

Recompute Regions (POST /api/pool/geo/recompute)

No parameters. Recomputes the regions of existing records from the offline database.

{ "task_id": "...", "status": "queued", "message": "Geo recompute task created" }

Errors and edge cases:

  • Only records whose country_code is empty are recomputed (that is, egress IPs never resolved through the offline database). Records already resolved are not recomputed; there is no dedicated immediate-refresh operation after updating the data files, but the full check from POST /api/pool/update-geo/all hands each egress IP back to the resolver and can write the new region back indirectly (not effective immediately, being subject to the in-process query cache and the time it takes).
  • When the geolocation resolution service is not enabled it returns 503 api_error directly and creates no task.

Database and Backups

List Backups (GET /api/pool/backups)

{
  "backups": [
    { "filename": "proxies_backup_20260930_030000.db", "size": 1048576, "created_at": "2026-09-30 03:00:00" }
  ]
}

Backups are created by a scheduled loop controlled by [Pool] database.backup_enabled; this endpoint only lists existing backups. When the backup feature is not enabled, it returns 404 api_error. The backup listing and restore endpoints in this group are pure file operations and also work while the pool is stopped.

Restore from a Backup (POST /api/pool/backups/restore)

{ "filename": "proxies_backup_20260930_030000.db" }
{ "success": true, "message": "Backup restored successfully; please restart the proxy pool to load the restored data" }

Semantic contract (all three points matter):

  1. Returns 409 pool_running while the pool is running — overwriting an open database file would corrupt it, so the pool must be stopped first. This is the only endpoint that returns pool_running.
  2. Restore only overwrites the database file; it does not check the backup for version compatibility with the current database.
  3. After a successful restore, the pool must be restarted before the new data is loaded. The endpoint itself does not restart the pool; the panel's "Database Maintenance" page restarts it automatically once the restore completes (it stops the pool first, then starts it back in its previous state), while clients calling the API directly must call POST /api/pool/start themselves.

Missing filename → 400 invalid_parameter.

Database Statistics (GET /api/pool/database/stats)

{ "stats": { "total_proxies": 120, "valid_proxies": 96, "invalid_proxies": 24,
             "db_size_mb": 1.05 } }

Requires the pool to be running. Returns 404 api_error when the maintenance feature is not enabled.

Optimize the Database (POST /api/pool/database/optimize)

No parameters. Runs ANALYZE + REINDEX + VACUUM; it is a HEAVY-tier operation and VACUUM holds an exclusive database lock for a long time, so other database operations are blocked while it runs.

{ "success": true, "message": "Database optimization complete" }

Pool Error Codes

See Pool API Conventions → Error Responses. For the summary table, see Appendix A.


4. End-to-End Examples

Python

import requests

HOST = "http://localhost:5001"
TOKEN = "YOUR_TOKEN"
PARAMS = {"token": TOKEN}

# Panel: view runtime status
status = requests.get(f"{HOST}/api/status", params=PARAMS).json()
print(status["current_proxy"], status["proxy_source_mode"])

# Panel: manually switch the upstream proxy once
switched = requests.get(f"{HOST}/api/switch_proxy", params=PARAMS).json()
if switched["status"] != "success":
    # A cooldown and a genuine failure both come back as error; tell them apart by the field
    print("In cooldown" if switched.get("cooldown") else switched.get("message"))

# Panel: query failed requests from the past hour
records = requests.get(
    f"{HOST}/api/logs/records",
    params={**PARAMS, "outcome": "failure", "limit": 50},
).json()
for row in records["records"]:
    print(row["ts"], row["host"], row["upstream_label"], row["reason"])

# Pool: get an available proxy (plain text)
proxy = requests.get(
    f"{HOST}/api/pool/random",
    params={**PARAMS, "status": "valid", "format": "text"},
).text.strip()
print(proxy)   # for example http://1.2.3.4:8080

# Pool: create a batch validation task and poll it
task = requests.post(
    f"{HOST}/api/pool/proxies/batch/validate",
    params=PARAMS,
    json={"proxy_ids": [1, 2, 3], "mode": "liveness"},
).json()
progress = requests.get(
    f"{HOST}/api/pool/tasks/{task['task_id']}", params=PARAMS
).json()
print(progress["status"], progress.get("progress"), progress.get("total"))

cURL

# Panel: runtime status
curl "http://localhost:5001/api/status?token=YOUR_TOKEN"

# Panel: save configuration (change the rotation mode)
curl -X POST "http://localhost:5001/api/config?token=YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"server": {"mode": "loadbalance"}}'

# Panel: restart the outbound proxy service
curl -X POST "http://localhost:5001/api/service?token=YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"action": "restart"}'

# Panel: switch the UI language
curl -X POST "http://localhost:5001/api/language?token=YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"language": "en"}'

# Panel: export the past hour of access records as CSV
curl -OJ "http://localhost:5001/api/logs/records/export?token=YOUR_TOKEN&format=csv"

# Pool: filtered query
curl "http://localhost:5001/api/pool/get?protocol=http&status=valid&limit=10&token=YOUR_TOKEN"

# Pool: get an available proxy (plain text)
curl "http://localhost:5001/api/pool/random?status=valid&format=text&token=YOUR_TOKEN"

# Pool: run a plugin manually
curl -X POST "http://localhost:5001/api/pool/plugins/geonode_plugin/run?token=YOUR_TOKEN"

# Pool: batch import
curl -X POST "http://localhost:5001/api/pool/import?token=YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"proxies": ["http://1.2.3.4:8080"]}'

5. Notes

  1. Authentication only compares a single token in the query string; there is no CSRF protection and caller permissions are not distinguished: anyone who can reach the panel can change the configuration (including downstream credentials). Do not expose the panel directly to the public internet.
  2. Two sets of error conventions coexist: on the panel side most business failures are 200 + status: "error", while on the pool side they are real 4xx/5xx + error_code. Callers must check both.
  3. message varies with the UI language; use it for display only, never for program logic.
  4. Runtime state lives in process memory (current proxy, exit roster, task table, ads dismissal state) and resets when the process restarts; access records and domain statistics live in SQLite and survive a restart.
  5. Changing web_port requires restarting the whole process; changing port requires restarting the outbound service (when you save through POST /api/config the panel restarts the outbound service automatically; manual edits and the command-line entry point require you to restart it yourself). Every other configuration item takes effect immediately once saved, with a few exceptions: the access-record buffer access_records_buffer_size, for example, is rebuilt at the new capacity only the next time the proxy service starts. Changes to [Server] made by hand-editing config.ini are also hot-reloaded automatically (both entry points poll the configuration file), but port rebinding does not happen automatically; the [Pool] section is hot-reloaded by the pool itself.
  6. Most batch operations return a task_id (the two delete endpoints return success / deleted_count directly); poll progress with GET /api/pool/tasks/{task_id}. Tasks routinely run for tens of minutes and can be aborted at any time with POST /api/pool/tasks/{task_id}/cancel. Note that the download in the offline geolocation database update task runs on a thread that cannot be cancelled — cancelling only writes out the status, and the download still runs to completion. The task table is in-memory with a 1-hour TTL and cannot be queried after the process restarts.
  7. The Pool API's query parameters are a signature-based whitelist: a misspelled parameter name is silently ignored, and the endpoint returns unfiltered results instead of an error.
  8. When you change an endpoint or field under /api/pool/*, this document must be updated in step — the adaptation layer is a hand-written Flask blueprint and cannot generate an OpenAPI description automatically the way FastAPI does.

Appendix A: Error Code Reference

Panel API Error Conventions

Case HTTP Response Body
Success 200 {"status": "success", ...}
Success (some read-only endpoints) 200 Returns the data directly, with no status field
Authentication failure 401 {"status": "error", "message": "..."}
Configuration validation failed (POST /api/config only) 400 {"status": "error", "message": "...", "details": {"key": "...", "reason": "..."}}
Other business failures 200 {"status": "error", "message": "..."}

Pool API Error Conventions

error_code HTTP Meaning
invalid_parameter 400 Invalid request parameters
api_error 4xx / 5xx Business error (proxy not found, no matching results, spec mismatch, plugin operation failure, restore failure, etc.); the status code comes from the ApiError itself
pool_running 409 The pool is running, so this operation is not allowed (POST /api/pool/backups/restore only)
pool_unavailable 503 The pool is not running or is restarting
pool_timeout 504 The pool timed out while responding
queue_full 503 The write queue is full
internal_error 500 Internal server error
(none) 500 Pool start/stop failure: no error_code when POST /api/pool/start|stop|restart fails

Appendix B: Endpoint Index

Page Routes (Non-api)

Method Path Auth Description
GET / No Redirects to /web with the token
GET /web Yes Panel single page
GET /static/<path> No Frontend static assets

Panel API (34)

Method Path Description
GET /api/status Runtime status
GET /api/config Read [Server] / [Pool] configuration
POST /api/config Save configuration
POST /api/language Switch the UI language
GET /api/proxies Read the local proxy list
POST /api/proxies Write the local proxy list
GET /api/check_proxies Check availability of the proxies in the source list (for the local source, all entries in ip.txt)
GET /api/ip_lists Read the IP whitelist / blacklist
POST /api/ip_lists Write the IP whitelist / blacklist
GET /api/bypass_whitelist Read the bypass list
POST /api/bypass_whitelist Write the bypass list
POST /api/service Start / stop / restart the outbound proxy service
GET /api/switch_proxy Refresh the exit pool: top it up when under capacity, replace the exit closest to expiry when full
GET /api/logs Query in-memory logs
GET /api/logs/stats Log statistics
GET /api/logs/files List log files
GET /api/logs/file Read a log file
GET /api/logs/export Export logs
POST /api/logs/clear Clear logs
GET /api/logs/records Query access records
GET /api/logs/records/export Export access records
GET /api/logs/records/options Filter options for access records
POST /api/logs/records/clear Clear access records
GET /api/logs/domains Query domain statistics
POST /api/logs/domains/clear Clear domain statistics
GET /api/users Read outbound proxy accounts
POST /api/users Write outbound proxy accounts
GET /api/api_credentials Read upstream API credentials
POST /api/api_credentials Manage upstream API credentials
GET /api/version Version check result (read-only cache, no authentication)
POST /api/version/check Trigger a version check immediately (requires authentication)
GET /api/ads Get ads (no authentication)
POST /api/ads/dismiss Dismiss ads (no authentication)
POST /api/ads/reopen Reopen ads (no authentication)

Pool API (38)

Method Path Description
GET /api/pool/status Pool status and global statistics
GET /api/pool/schema [Pool] field schema (same source as the panel form)
POST /api/pool/start Start the pool
POST /api/pool/stop Stop the pool (waits for the write queue to drain)
POST /api/pool/restart Restart the pool
GET /api/pool/get Filtered proxy list (json / text)
GET /api/pool/random Get a random proxy (json / text)
GET /api/pool/count Count proxies matching the filters
GET /api/pool/export Export proxies (txt / json / csv)
POST /api/pool/import Batch import (validate before ingesting)
POST /api/pool/proxies/{id}/favorite Toggle favorite status
GET /api/pool/stats Pool statistics
GET /api/pool/sources List of source plugin names
GET /api/pool/host-ip This host's public egress IP
GET /api/pool/plugins Plugin list and status
POST /api/pool/plugins/{name}/enable Enable a plugin
POST /api/pool/plugins/{name}/disable Disable a plugin
PUT /api/pool/plugins/{name}/interval Set the fetch interval
PUT /api/pool/plugins/{name}/test-url Set the plugin's dedicated test URL
PUT /api/pool/plugins/{name}/validation Set the plugin validation policy
POST /api/pool/plugins/{name}/run Run a plugin immediately
POST /api/pool/plugins/{name}/reload Hot-reload a plugin
POST /api/pool/validate/all-valid Revalidate all valid proxies
POST /api/pool/validate/all-invalid Revalidate all invalid proxies
POST /api/pool/update-geo/all Full check and region backfill
DELETE /api/pool/delete/invalid Delete all invalid proxies
POST /api/pool/proxies/batch/validate Batch validate specified proxies
DELETE /api/pool/proxies/batch/delete Batch delete specified proxies (parameters in the request body)
GET /api/pool/tasks List tasks
GET /api/pool/tasks/{task_id} Query task status
POST /api/pool/tasks/{task_id}/cancel Cancel a task
GET /api/pool/geo/status Offline geolocation database status
POST /api/pool/geo/update Update the offline geolocation database (about 100 MB)
POST /api/pool/geo/recompute Recompute regions for existing records
GET /api/pool/backups List backups
POST /api/pool/backups/restore Restore from a backup (the pool must be stopped first)
GET /api/pool/database/stats Database statistics
POST /api/pool/database/optimize Optimize the database (ANALYZE / REINDEX / VACUUM)

Appendix C: Field Dictionary

Proxy Object

The object returned by GET /api/pool/get and GET /api/pool/random (format=json).

Field Type Description
id int Primary key
proxy_url string Full proxy address; when the username and password are both present, it takes the form protocol://user:password@ip:port
protocol string http / https / socks5
ip string Entry IP
port int Port
username / password string | null Proxy authentication credentials
real_ip string | null Egress IP (the IP the peer sees when requests go through the proxy)
exit_ip_differs bool Whether the entry IP differs from the egress IP. Supplied in both the query and random responses (the two share one serialization); the export paths deliberately leave it out
region string Full Chinese region name, e.g. 「中国 广东 深圳」; 「未知」 ("unknown") when the lookup finds nothing
country / province / city string Chinese region levels
region_en / country_en / province_en / city_en string | null English region levels
delay_ms float | null Most recently measured delay (milliseconds)
validated_at string | null Most recent validation time (ISO 8601)
source_plugin string Source plugin name; manual_import for manual imports
is_favorite bool Whether the proxy is favorited (a user choice that fetching and validation do not overwrite)
is_valid bool Whether the proxy is currently usable; the list uses it to badge invalid rows as 「失效」 ("invalid"). Exports use their own status column
health_score float Health score, 0-100, 1 decimal place
anonymity_level string transparent / anonymous / elite / unverified
anonymity_fallback bool Whether the anonymity level was judged to be the anonymity fallback
supports_https / supports_http bool Whether the corresponding protocol is supported
quality_assessed_at string | null Most recent quality assessment time (ISO 8601)
success_rate float Validation success rate, 0-1, 3 decimal places
avg_delay_ms float | null Historical average delay (milliseconds)
total_checks int Cumulative validation count; missing values fall back to 0
probe_success_rate float Probe success rate, 0-1, 3 decimal places
probe_total_count int Cumulative probe count; missing values fall back to 0

Semantics of empty region values: the Chinese and English sets are given as separate columns, and null means not looked up yet (not 「未知」). When an English column is missing, the front end falls back to the Chinese column; identical values in the two columns mean that level has no corresponding translation. Do not write 「未知」 into these columns — 「未知」 is the value the Chinese columns take when they have been queried but nothing was found.

Task Object

The response of GET /api/pool/tasks/{task_id}, and also an element of the GET /api/pool/tasks list.

Field Type Description
task_id string Task id; it appears only on the elements of the GET /api/pool/tasks list (injected per item by the list) and is absent from the single-task response
status string running / completed / failed / cancelled (queued appears only in some creation responses, never in the task table)
message string Human-readable description of the current stage, following the UI language
progress / total int Completed / total
started_at / completed_at string Start and end times (ISO 8601); completed_at is absent until the task finishes
stage string prescreen / validate / ingest / done (done is written on completion), present only on import and fetch tasks
stage_done / stage_total int Progress within the current stage; same availability as above
in_flight int Number of items being processed concurrently in the current stage; same availability as above
rate float Processing rate (items/second)
eta_seconds int | null Estimated seconds remaining
valid_count / invalid_count / inconclusive_count int Counts by validation result category
prescreened_out int Number of items eliminated by the port prescreen
incomplete_count / failed_count int Incomplete / failed counts
results object Map from data file name to result text (已更新 / 失败: ..., in the Chinese UI), present only in the completed state of an offline geolocation database update task

The set of fields varies with task type and endpoint; the table above is the union across all of them; relying only on the four fields status, progress, total, and message is the safest.

Backup Object

An element of the GET /api/pool/backups list.

Field Type Description
filename string Backup file name, used as the parameter of POST /api/pool/backups/restore
size int File size (bytes)
created_at string Creation time, always formatted as YYYY-MM-DD HH:MM:SS