Router Admin API
REST API reference for the QueryPilot Router admin surface and its plugin endpoints.
Overview
The Router (binary querypilot) serves a REST admin API for translations, routing, configuration reload, statistics, pattern rules, and pool control, plus the Accelerator and Guard plugin endpoints.
The admin API has no bind address of its own. It listens on the [metrics] bind IP at the metrics port plus one: with the default 127.0.0.1:9090, the admin API is 127.0.0.1:9091. If the metrics port is 65535, startup fails with a port-overflow error.
The server starts when [metrics] enabled = true (full admin surface) or when an enabled plugin contributes REST routes. When only a plugin is enabled, only the plugin routes are served; enabling Guard alone does not expose the admin control plane.
The core admin routes are unauthenticated. Keep the metrics bind on loopback or a private network, and firewall both the metrics port and the derived admin port. The plugin routes support an api_key; set one whenever the bind is not loopback. See Security.
Errors return JSON with an error field. All examples below assume the default 127.0.0.1:9091.
Translations
Query translations rewrite matching queries and pin them to a pool. Translations registered through this API are ephemeral: a config reload replaces the registry with the [[translation.rules]] from the file.
POST /api/translations
Register a translation. Send concrete SQL for both the original and translated query; the Router parameterizes both and builds the index mapping. The target pool must exist in the current config.
curl -X POST http://127.0.0.1:9091/api/translations \
-H 'Content-Type: application/json' \
-d '{
"original_sql": "SELECT name FROM users WHERE id = 42",
"translated_sql": "SELECT name FROM analytics.users WHERE id = 42",
"target_pool": "analytics"
}'{
"hash": 1234567890123456789,
"normalized": "SELECT name FROM users WHERE id = $1",
"original_values": 1,
"translated_values": 1
}GET /api/translations
List all registered translations.
curl http://127.0.0.1:9091/api/translations{
"translations": [
{
"hash": 1234567890123456789,
"original_sql": "SELECT name FROM users WHERE id = $1",
"translated_pattern": "SELECT name FROM analytics.users WHERE id = $1",
"target_pool": "analytics",
"enabled": true
}
],
"total": 1,
"enabled": 1
}DELETE /api/translations/:hash
Delete a translation by fingerprint hash. Returns 204 No Content on success, 400 with an error body if the hash is unknown.
curl -X DELETE http://127.0.0.1:9091/api/translations/1234567890123456789PUT /api/translations/:hash/enable
Enable a translation. Returns 204 No Content.
curl -X PUT http://127.0.0.1:9091/api/translations/1234567890123456789/enablePUT /api/translations/:hash/disable
Disable a translation without deleting it. Returns 204 No Content.
curl -X PUT http://127.0.0.1:9091/api/translations/1234567890123456789/disableRouting
GET /api/routing/test
Dry-run the routing decision for a SQL query without executing it. Query parameters: sql (required) and in_transaction (optional bool, default false). The response includes the query's fingerprint hash, which you can use in type = "hash" pattern rules and in the pattern-rules API below.
curl 'http://127.0.0.1:9091/api/routing/test?sql=SELECT%20*%20FROM%20orders%20WHERE%20id%20%3D%201'{
"decision": "pool",
"target_pool": "primary",
"rewritten_sql": null,
"fingerprint_hash": 9876543210987654321,
"normalized_sql": "SELECT * FROM orders WHERE id = $1",
"cached": false,
"duration_ns": 12345
}decision is one of pool, translation (with rewritten_sql set), reject (with the rejection message in rewritten_sql), or pinned (the session is pinned mid-transaction).
Configuration
POST /api/config/reload
Re-read the config file from disk and apply the reloadable parts: routing rules, pattern rules, translation rules, read/write split settings, routing cache settings, and log level. Validation failures (bad regex, unknown pool) return 400 and leave the running config untouched.
curl -X POST http://127.0.0.1:9091/api/config/reload{
"success": true,
"reloaded_at": "2026-07-18T12:00:00Z",
"changes": {
"routing_rules": 2,
"pattern_rules": 1,
"translation_rules": 3,
"read_write_split_changed": false,
"cache_config_changed": false,
"log_level_changed": false
}
}GET /api/config/status
Report the config file path, load time, last reload attempt, and a summary of the active configuration. The shipped container healthcheck probes this endpoint.
curl http://127.0.0.1:9091/api/config/status{
"config_file": "/etc/sqp/querypilot.toml",
"loaded_at": "2026-07-18T11:00:00Z",
"current": {
"read_write_split": true,
"read_pool": "replica",
"write_pool": "primary",
"routing_rules_count": 2,
"pattern_rules_count": 1,
"translation_rules_count": 3,
"cache_enabled": true,
"log_level": "info"
}
}Query statistics
GET /api/stats/digests
List per-query digest statistics, ranked and filtered. Query parameters:
limit: maximum entries returned (default100)order_by: one ofcount_star,sum_rows_sent,last_seen,mean_time,query_throughput,sum_time(defaultsum_time); an unrecognized value returns an errormin_count: minimum execution count (default0)min_rows_sent: minimum total rows sent (default0)schema: exact schema filter
curl 'http://127.0.0.1:9091/api/stats/digests?order_by=count_star&limit=10'{
"digests": [
{
"digest_text": "SELECT * FROM orders WHERE id = $1",
"fingerprint_hash": 9876543210987654321,
"schema": "public",
"username": "app",
"count_star": 15230,
"sum_time_us": 4820000,
"sum_rows_sent": 15230,
"first_seen": 1752830000,
"last_seen": 1752840000
}
],
"total_unique_digests": 42,
"overflow_count": 0
}Pattern rules
Runtime pattern rules route by fingerprint hash. Rules created here are hash-matched, unowned by any plugin, and separate from the [[routing.pattern_rules]] in the config file.
POST /api/pattern-rules
Create a rule routing a fingerprint to a pool. Body fields: fingerprint_hash (required), target_pool (required), comment (optional). Returns 201 Created.
curl -X POST http://127.0.0.1:9091/api/pattern-rules \
-H 'Content-Type: application/json' \
-d '{"fingerprint_hash": 9876543210987654321, "target_pool": "replica", "comment": "hot lookup"}'{
"fingerprint_hash": 9876543210987654321,
"status": "created"
}GET /api/pattern-rules
List runtime pattern rules.
curl http://127.0.0.1:9091/api/pattern-rules{
"rules": [
{
"fingerprint_hash": 9876543210987654321,
"target_pool": "replica",
"enabled": true,
"comment": "hot lookup",
"created_at": 1752840000
}
],
"total": 1
}DELETE /api/pattern-rules/:hash
Remove a rule. Returns 204 No Content, or 404 if the hash is unknown.
curl -X DELETE http://127.0.0.1:9091/api/pattern-rules/9876543210987654321PUT /api/pattern-rules/:hash/disable
Disable a rule in place. Returns 200 OK, or 404 if the hash is unknown. There is no enable endpoint: to re-enable a disabled rule, delete it and create it again.
curl -X PUT http://127.0.0.1:9091/api/pattern-rules/9876543210987654321/disablePools
GET /api/pools
List pools and their health status.
curl http://127.0.0.1:9091/api/pools{
"pools": [
{ "name": "primary", "status": "online" },
{ "name": "replica", "status": "online" }
],
"total": 2
}PUT /api/pools/:name/status
Override a pool's status. Body: {"status": "..."} where the value is online, offline_soft (drain: no new assignments), or shunned (excluded). Returns 200 OK, or 503 if pool management is unavailable.
curl -X PUT http://127.0.0.1:9091/api/pools/replica/status \
-H 'Content-Type: application/json' \
-d '{"status": "offline_soft"}'Accelerator plugin API
Mounted at /api/v1/plugins/accelerator when [plugins.accelerator] is enabled. Mutating endpoints require the X-QueryPilot-Api-Key header when an api_key is configured, compared in constant time. When the admin API binds a non-loopback address and no api_key is set, the mutating endpoints refuse to serve entirely (403). Every mutation is recorded in the audit trail.
GET /api/v1/plugins/accelerator/candidates
List caching candidates from the query digest, with per-candidate threshold flags. The literal-bearing digest_text is redacted unless the request presents a valid key.
curl -H 'X-QueryPilot-Api-Key: change-me' \
http://127.0.0.1:9091/api/v1/plugins/accelerator/candidates[
{
"fingerprint_hash": "0x89ABCDEF01234567",
"schema": "public",
"username": "app",
"count": 15230,
"sum_time_us": 4820000,
"first_seen": 1752830000,
"last_seen": 1752840000,
"past_execution_threshold": true,
"past_warmup_window": true,
"digest_text": "SELECT * FROM orders WHERE id = 1"
}
]GET /api/v1/plugins/accelerator/rules
List routing rules owned by the Accelerator.
curl http://127.0.0.1:9091/api/v1/plugins/accelerator/rules[
{
"fingerprint_hash": "0x89ABCDEF01234567",
"target_pool": "readyset",
"state": "active",
"enabled": true,
"created_at": 1752840000
}
]state is active, warmup:until=<unix>, health_disabled, or operator_disabled.
GET /api/v1/plugins/accelerator/status
Report discovery-loop health: last tick, duration, result, last success, consecutive errors, and pause state.
curl http://127.0.0.1:9091/api/v1/plugins/accelerator/status{
"last_tick_unix": 1752840000,
"last_duration_ms": 120,
"last_result": "ok",
"last_success_unix": 1752840000,
"consecutive_errors": 0,
"paused": false
}GET /api/v1/plugins/accelerator/decisions
List recent discovery-cycle outcomes (tick time, result, duration).
curl http://127.0.0.1:9091/api/v1/plugins/accelerator/decisionsGET /api/v1/plugins/accelerator/denylist
Report how many denylist rules are loaded. Entry text is not returned; it can contain literal-bearing SQL.
curl http://127.0.0.1:9091/api/v1/plugins/accelerator/denylist{ "rule_count": 3 }GET /api/v1/plugins/accelerator/audit
Read the bounded audit trail of mutations performed through this API. Requires a valid key when one is configured.
curl -H 'X-QueryPilot-Api-Key: change-me' \
http://127.0.0.1:9091/api/v1/plugins/accelerator/auditDELETE /api/v1/plugins/accelerator/rules/:hash
Drop an Accelerator-owned rule by fingerprint hash (decimal). Returns 204 No Content; 403 if the rule exists but is not owned by the Accelerator. Mutating: key required when configured.
curl -X DELETE -H 'X-QueryPilot-Api-Key: change-me' \
http://127.0.0.1:9091/api/v1/plugins/accelerator/rules/9920397370294392167POST /api/v1/plugins/accelerator/actions/force-cache
Queue a fingerprint for caching, bypassing the execution-count and warmup thresholds. The fingerprint must exist in the current digest, be SELECT-shaped, and pass the denylist; the Readyset support check still applies when the loop processes it. Returns 202 Accepted. Mutating: key required when configured.
curl -X POST -H 'X-QueryPilot-Api-Key: change-me' \
-H 'Content-Type: application/json' \
-d '{"fingerprint_hash": 9920397370294392167}' \
http://127.0.0.1:9091/api/v1/plugins/accelerator/actions/force-cache{
"fingerprint_hash": "0x89ABCDEF01234567",
"status": "queued"
}POST /api/v1/plugins/accelerator/actions/pause
Pause the discovery loop. Mutating: key required when configured.
curl -X POST -H 'X-QueryPilot-Api-Key: change-me' \
http://127.0.0.1:9091/api/v1/plugins/accelerator/actions/pause{ "paused": true }POST /api/v1/plugins/accelerator/actions/resume
Resume a paused discovery loop. Mutating: key required when configured.
curl -X POST -H 'X-QueryPilot-Api-Key: change-me' \
http://127.0.0.1:9091/api/v1/plugins/accelerator/actions/resumeGuard plugin API
Mounted at /api/v1/plugins/guard when [plugins.guard] is enabled. Read endpoints are open; mutating endpoints (POST /policies/reload, POST /registry, DELETE /registry/:fingerprint) require the X-Guard-Api-Key header (or Authorization header) to match the configured api_key. When no api_key is set, all endpoints are open; that is a development posture only.
GET /api/v1/plugins/guard/policies
List loaded policies.
curl http://127.0.0.1:9091/api/v1/plugins/guard/policies[
{
"name": "no-unbounded-deletes",
"severity": "high",
"enabled": true,
"description": "Block DELETE without a WHERE clause"
}
]POST /api/v1/plugins/guard/policies/reload
Reload policies from a directory. Body: {"dir": "..."}, or an empty object {} to reload from the configured policy_dir. Mutating: key required when configured.
curl -X POST -H 'X-Guard-Api-Key: change-me' \
-H 'Content-Type: application/json' -d '{}' \
http://127.0.0.1:9091/api/v1/plugins/guard/policies/reload{ "loaded": 4 }GET /api/v1/plugins/guard/registry
List trusted-query registry entries.
curl http://127.0.0.1:9091/api/v1/plugins/guard/registryPOST /api/v1/plugins/guard/registry
Add a registry entry. The body is a registry entry object; on success returns {"ok": true}. Mutating: key required when configured.
curl -X POST -H 'X-Guard-Api-Key: change-me' \
-H 'Content-Type: application/json' \
-d @entry.json \
http://127.0.0.1:9091/api/v1/plugins/guard/registryGET /api/v1/plugins/guard/registry/:fingerprint
Fetch one registry entry by fingerprint (hex like 0xABC... or decimal). Returns 404 if absent.
curl http://127.0.0.1:9091/api/v1/plugins/guard/registry/0x89ABCDEF01234567DELETE /api/v1/plugins/guard/registry/:fingerprint
Remove a registry entry. Returns {"removed": true}, or 404 if absent. Mutating: key required when configured.
curl -X DELETE -H 'X-Guard-Api-Key: change-me' \
http://127.0.0.1:9091/api/v1/plugins/guard/registry/0x89ABCDEF01234567POST /api/v1/plugins/guard/test
Dry-run a decision against the live policy set without executing the query. Body: sql (required), username (optional), trust_level (optional).
curl -X POST -H 'Content-Type: application/json' \
-d '{"sql": "DELETE FROM orders", "trust_level": "verified"}' \
http://127.0.0.1:9091/api/v1/plugins/guard/test{
"decision": "block",
"policy": "no-unbounded-deletes",
"message": "DELETE without WHERE is not allowed",
"reason": "policy matched",
"latency_us": 180
}decision is one of allow, block, amend, or reroute.
GET /api/v1/plugins/guard/decisions
Read recent audit records from the in-memory ring buffer. Query parameter limit (default 100, capped at the buffer capacity).
curl 'http://127.0.0.1:9091/api/v1/plugins/guard/decisions?limit=20'GET /api/v1/plugins/guard/stats
Report registry size, policy counts, and audit health.
curl http://127.0.0.1:9091/api/v1/plugins/guard/stats{
"registry_size": 12,
"policies_loaded": 4,
"policies_invalid": 0,
"policies_compile_warnings": 0,
"audit_dropped": 0
}