KeyBind Server's Web API
KeyBind's HTTP server lets programs on the same PC execute KeyBind Tasks, collect their immediate results, inspect generated reports and edit the saved definitions represented by report controls. The interactive HotKeys, Triggers, Sentinels and Schedules reports use these same endpoints.
The server binds to 127.0.0.1 only. Its default origin is http://127.0.0.1:7566. It is a local HTTP interface, not a LAN or cloud service. Tasks run through KeyBind's existing command processing and have the same effects as Tasks run inside KeyBind.
This guide describes KeyBind 1.20.105 and KeyBind server 0.2.1. Examples use port 7566. If your port differs, replace it throughout, including in any Origin header. Tokens, pending request IDs, generations and control IDs shown here are invented examples, not values from your installation.
Click shaded code blocks to select them for copying. HTTP, Python, PowerShell, JavaScript and AutoIt examples belong in their respective tools. Only examples identified as KeyBind Tasks belong in a Task input. Opening this document sends no API requests.
Capabilities and intended uses
- Execute a Task containing one command or a chain joined with
**. Commands can read data, launch applications, type text, speak, manipulate windows and change saved configuration. - Collect the initial call's return value, AutoIt value type,
@errorand@extendedas JSON. - Generate current HotKeys, Triggers, Sentinels and Schedules reports and serve their CSS, JavaScript and icon.
- Enable, disable or delete registered report items; save their Tasks; change HotKey activators; edit supported Trigger and Sentinel settings; choose Sentinel targets; and link or unlink File Sentinel Tasks.
- Read the native Task Editor's command examples, @Macros, Sentinel examples and enabled Task History.
- Request user-approved session or persistent credentials for a local client, and revoke access through Options.
- Read or change the server's running keep-alive interval.
There is no separate REST collection of every definition. Report HTML exposes registered control IDs. A client that already knows an item's name or HotKey can instead send existing HotKeyControl(), TriggerControl(), SentinelControl() or ScheduleControl() commands through api/task. Their complete command syntax is in KeyBind User Commands.
Starting and keeping the server available
- Open Config Editor > Options > Server and turn Enabled on. The tray KeyBind Server menu also has the server master switch.
- For clients that should connect whenever KeyBind runs, enable Always on. It starts the server at KeyBind launch and forces permanent keep-alive.
- For on-demand operation, run
Report(HotKeys),Report(Triggers),Report(Sentinels)orReport(Schedules). Opening a control report starts or renews the server. - Run
AppUsage()to open the served App Usage page; like the control reports, it starts or renews the server.
A saved token cannot start KeyBind or wake a stopped server. If KeyBind has exited, the server is disabled or its countdown expired, keep the saved token and reconnect when the server is available. Connection failure alone does not invalidate a persistent token.
AppUsage() creates a local HTML report and reads local monthly JavaScript data. Although KeyBind serves this page through its built-in server, the page will work fine when loaded locally in your browser, even when KeyBind isn't running. Of course, your usage stats won't update if KeyBind isn't running!
AppUsage() is served through the loopback server like the other reports: running AppUsage() starts or renews the server, the browser receives a token-protected URL, and its monthly AppUsage.YYYY-MM.js files are served as registered report assets. With the server disabled the same command opens the generated file directly.
Status reads do not renew the server countdown. Opening/regenerating reports, accepting queued actions, returning their results and changing keep-alive renew it. Queued, ready and running actions defer expiry. Authorization requests and authorization polling do not renew it; Always on is useful during first-time approval.
Origin, protected base and token types
The origin is scheme, host and port without a path. With token checking enabled, add /TOKEN/ to form the protected base, then append a page name or api/ROUTE. For port 80, KeyBind omits :80 from its generated origin and expected Host.
Origin: http://127.0.0.1:7566
Protected base: http://127.0.0.1:7566/TOKEN/
API base: http://127.0.0.1:7566/TOKEN/api/
Server status: http://127.0.0.1:7566/TOKEN/api/status
Execute Task: http://127.0.0.1:7566/TOKEN/api/task
Report: http://127.0.0.1:7566/TOKEN/HotKeys.html
Auth request: http://127.0.0.1:7566/api/auth/request
Auth poll: http://127.0.0.1:7566/api/auth/status?id=PENDING_ID
Replace TOKEN and PENDING_ID with real 32-character lowercase hexadecimal values. The API token belongs in the URL path, not an Authorization header, cookie or general query parameter. Only the two authorization routes shown above omit the protected prefix while token checking is on.
Accepted app tokens have the same server capabilities as the report token, including full Task execution and saved-definition edits. There are no read-only tokens, per-command scopes, separate client queues or private result stores. The token authorizes; the label identifies an entry for display and revocation.
Authorize a native local client
This flow works with both Security token and Origin checking enabled. Its bootstrap routes are unprefixed exceptions to token checking. Exact Host checking applies when enabled, but bootstrap requests do not require an Origin header.
Step 1: request user approval
Send POST to /api/auth/request at the origin. The body is raw UTF-8 text: LABEL, or LABEL|REASON. It is not JSON and not form data. Do not send app=... unless those characters are intended as part of the literal label.
backup-script|Run my local backup Tasks
curl.exe --noproxy 127.0.0.1 --request POST --header "Content-Type: text/plain;charset=UTF-8" --data-binary "backup-script|Run my local backup Tasks" "http://127.0.0.1:7566/api/auth/request"
- The first pipe separates the label from the optional reason, so a label cannot itself contain a pipe. The rest of the body is the reason.
- The label is trimmed, must be nonempty and must be at most 64 characters after trimming. The reason is trimmed and capped at 256 characters. The complete body is capped at 512 UTF-8 bytes.
- CR, LF and NUL are forbidden anywhere in the body.
- KeyBind converts the label into a lowercase safe INI key using its existing preference-name rules. Spaces and punctuation such as
=[]().,-;#become underscores, with path-unsafe characters processed by the existing cleanup.Backup Script,backup_scriptandBACKUP_SCRIPTidentify the same normalized label. - The raw name and reason are explanatory text. KeyBind does not verify the caller's executable path, signature, publisher, PID or script identity.
An accepted request returns HTTP 202 with a secret pending ID:
{"id":"0123456789abcdef0123456789abcdef","state":"pending"}
Keep the pending ID private: possession allows collection of the approved token during its retrieval window. KeyBind queues at most four undecided requests, otherwise returning HTTP 429, and displays one approval window at a time in request order.
Step 2: choose the authorization lifetime
The approval window states that access grants full Task execution power. An existing label never retrieves its saved token or approves a new request automatically. Knowing the label grants nothing. If persistence fails, KeyBind reports an error rather than returning a new live persistent token.
An undecided request expires 120 seconds after submission. A displayed approval window closes when its request expires; a late approval is rejected.
Step 3: poll the decision
GET /api/auth/status?id=ID without a token prefix, using the exact returned ID. Poll every 250 to 1000 milliseconds. A label is not accepted instead of the ID.
curl.exe --noproxy 127.0.0.1 "http://127.0.0.1:7566/api/auth/status?id=0123456789abcdef0123456789abcdef"
{"state":"approved","token":"fedcba9876543210fedcba9876543210"}
Approved, denied and error outcomes remain available for 120 seconds from the decision. Polling is repeatable: it returns the same approved token during that window, allowing recovery from a lost response. A poll does not consume the outcome or renew its retention time. Each approved collection checks that the token is still live, so revocation/replacement prevents collection of a dead token.
Expiry of the pending ID does not expire an issued app token. Stopping/restarting the server clears pending requests and retained outcomes. Saved tokens stay saved; session tokens remain live while the same KeyBind process runs.
Step 4: reconnect without another prompt
On later client launches, read your saved token and call /TOKEN/api/status, followed by other protected routes. No bootstrap request, pending-ID polling or new approval is required. If the client loses its token, deliberately request authorization again. The old token remains live until revoked or replaced.
Do not automatically request new approval for connection failures. The server may simply be unavailable. HTTP 403 with Invalid report token means the supplied credential was not accepted; a connection refusal alone does not.
App access and revocation
Open Config Editor > Options > Server > App access.... The list shows normalized labels marked saved, session or saved+session. Select a label and click Revoke selected.
- Revocation removes its saved token and every session token under that label. Subsequent authentication fails. It is not a cancellation mechanism for an already executing Task or a queued continuation.
- For a saved token, KeyBind writes an empty INI value before deleting live access. If that write fails, it keeps authorization and reports failure.
- Session-only revocation requires no INI write.
- Persistent tokens are plaintext hex in KeyBind.ini under
[Authorised Apps]. Protect both that file and the client's own saved token. The label is not a secret. - Manual INI edits take effect on next launch, when App access opens or when ConfigEditor closes. Until reload, an emptied saved token may remain authorized in memory.
[Authorised Apps]
backup_script=fedcba9876543210fedcba9876543210
revoked_client=
This is a format example, not a suggested real token. There is no HTTP token-list, remote approval or api/auth/revoke endpoint. Normal approval and revocation are user operations in KeyBind.
Secure curl.exe examples
These use an approved app token with both security switches on. Replace the fictitious token with your client's real one. curl.exe avoids the historical PowerShell curl alias. An app token does not need a POST Origin header.
curl.exe --noproxy 127.0.0.1 "http://127.0.0.1:7566/fedcba9876543210fedcba9876543210/api/status"
curl.exe --noproxy 127.0.0.1 --request POST --header "Content-Type: text/plain;charset=UTF-8" --data-binary "Speak(Hello from my local client.)" "http://127.0.0.1:7566/fedcba9876543210fedcba9876543210/api/task"
The POST returns HTTP 202 with the accepted action ID. Use that ID below; do not assume it is 1.
curl.exe --noproxy 127.0.0.1 "http://127.0.0.1:7566/fedcba9876543210fedcba9876543210/api/task/1"
For longer Task text, use --data-binary @task.txt with a UTF-8 file without BOM. curl calculates the request's byte Content-Length. Do not include a trailing newline when building an api/save payload, whose original and replacement Tasks must be single-line.
Using the report token
A generated report URL contains its report token. Copy the protected base through the slash after that token. With Origin checking on, native POSTs using the report token need the exact server Origin. GET does not. The credential survives KeyBind restarts. Replace a revoked credential by opening the report again. External apps cannot request the reserved KeyBind Server label.
curl.exe --noproxy 127.0.0.1 --request POST --header "Origin: http://127.0.0.1:7566" --header "Content-Type: text/plain;charset=UTF-8" --data-binary "Speak(Using the report token.)" "http://127.0.0.1:7566/0123456789abcdef0123456789abcdef/api/task"
Live report and Options notifications
GET api/events opens a persistent text/event-stream response using HTTP chunked framing. The same URL token and GET security checks apply. At most seven of the server's eight client slots are event streams, reserving one for ordinary requests; an additional stream receives HTTP 503. Four report tabs plus Options normally use five slots, leaving three free. The extra stream capacity permits temporary overlap during reload. Connections do not renew the keep-alive countdown and close when the server stops or their token is revoked.
The first named event, ready, contains the normal status fields plus an integer revision. A named changed event includes those fields, a changes object in the Options changes format, and a reports array containing affected names: HotKeys, Triggers, Sentinels or Schedules. All connected clients receive the same update. Oversized changes use reset:true instead of inline option values; fetch current Options data in that case. A named status event updates the shared keep-alive state without advancing the definition revision. Reconnecting receives current status and revision rather than replaying historical events.
Windows directory notifications trigger configuration comparisons after a 100-ms settling delay. Idle notification checks do not repeatedly read the ini files. SSE comment messages every 15 seconds keep an idle connection active without extending KeyBind's server lifetime. Slow clients with an unsent event disconnect and can reload current data after reconnecting. Ordinary requests retain their 30-second client timeout; event streams are exempt. Native directory notification failures are recorded in the server log; manual report refresh remains available.
const updates = new EventSource(new URL("events", apiBase));
updates.addEventListener("changed", event => {
const update = JSON.parse(event.data);
// Fetch affected reports once, or apply update.changes to Options controls.
});
Complete Python client
Save this as KeyBind_client.py. It uses only Python's standard library. Run with --authorize only when deliberately arranging a new approval. Normal launches read KeyBind-client-token.txt beside the script and reuse the token. The example does not print that secret. Keep the directory private to the intended user.
Each HTTP operation has a ten-second client timeout; Task-result collection has a 120-second deadline. These are example client choices, not KeyBind execution limits. The code submits each Task once and does not resend a potentially executed action after a transport timeout.
#!/usr/bin/env python
"""Authorize once, save a token, then call the local KeyBind API."""
import argparse
import json
import sys
import time
import urllib.error
import urllib.request
from pathlib import Path
parser = argparse.ArgumentParser()
parser.add_argument("--origin", default="http://127.0.0.1:7566")
parser.add_argument("--authorize", action="store_true")
parser.add_argument("--label", default="python-client")
parser.add_argument("--reason", default="Run my local KeyBind Tasks")
parser.add_argument("--task", help="Task text; omitted means read status")
args = parser.parse_args()
origin = args.origin.rstrip("/")
token_file = Path(__file__).with_name("KeyBind-client-token.txt")
token = ""
# This connection is loopback-only; bypass configured HTTP proxies.
http = urllib.request.build_opener(urllib.request.ProxyHandler({}))
def request(route, body=None, bootstrap=False):
"""Send once; never automatically repeat a state-changing POST."""
prefix = "/" if bootstrap else f"/{token}/"
payload = None if body is None else body.encode("utf-8")
headers = {} if payload is None else {
"Content-Type": "text/plain;charset=UTF-8"
}
req = urllib.request.Request(origin + prefix + route,
data=payload, headers=headers)
with http.open(req, timeout=10) as response:
return json.load(response)
def authorize():
"""Collect an explicitly requested approval and save the token."""
if "|" in args.label:
raise ValueError("A label cannot contain a pipe")
pending = request("api/auth/request",
args.label + "|" + args.reason, bootstrap=True)
print("Choose Allow once or Always allow in KeyBind.")
deadline = time.monotonic() + 125
while time.monotonic() < deadline:
result = request("api/auth/status?id=" + pending["id"],
bootstrap=True)
state = result["state"]
if state == "approved":
value = result["token"]
token_file.write_text(value + "\n", encoding="utf-8", newline="\n")
print("Token saved to", token_file)
return value
if state != "pending":
raise RuntimeError(result.get("message", state))
time.sleep(0.5)
raise TimeoutError("Authorization decision was not collected")
def execute(task):
"""Queue once and poll the returned ID for the initial-call result."""
result = request("api/task", task)
task_id = result["id"]
print("Accepted Task", task_id)
deadline = time.monotonic() + 120
while result["state"] in ("queued", "ready", "running"):
if time.monotonic() >= deadline:
raise TimeoutError(
f"Task {task_id}: result not collected; do not resend blindly")
time.sleep(0.25)
result = request(f"api/task/{task_id}")
return result
def main():
global token
if args.authorize:
token = authorize()
elif token_file.exists():
token = token_file.read_text(encoding="utf-8").strip()
else:
raise RuntimeError("No saved token; run once with --authorize")
if len(token) != 32 or any(c not in "0123456789abcdef" for c in token):
raise ValueError("Saved token must be 32 lowercase hex characters")
result = execute(args.task) if args.task else request("api/status")
print(json.dumps(result, ensure_ascii=False, indent=2))
if args.task and (result.get("error", 0) != 0
or result["state"] == "skipped"):
return 1
return 0
if __name__ == "__main__":
try:
sys.exit(main())
except urllib.error.HTTPError as error:
print(f"HTTP {error.code}: "
+ error.read().decode("utf-8", "replace"), file=sys.stderr)
sys.exit(1)
except (OSError, ValueError, RuntimeError, TimeoutError) as error:
print(error, file=sys.stderr)
sys.exit(1)
except KeyboardInterrupt:
print("Interrupted. An accepted Task may still run.", file=sys.stderr)
sys.exit(130)
python KeyBind_client.py --authorize --label "backup-script"
python KeyBind_client.py
python KeyBind_client.py --task "Speak(Build finished.)"
python KeyBind_client.py --task "HotKeyControl(get|^!+#{F8}|field=enabled)"
The last example requires that example HotKey to exist. False can be a legitimate read result, so the script checks the numeric error separately rather than treating every false value as failure. A token received through Allow once is saved by this example too, but KeyBind will stop accepting that session token after it exits. Choose Always allow for restart persistence.
PowerShell client
This first block requests approval and saves the token under LOCALAPPDATA in a client-owned directory. It does not edit KeyBind.ini. For persistent authorization choose Always allow in KeyBind.
$origin = 'http://127.0.0.1:7566'
$body = [Text.Encoding]::UTF8.GetBytes('powershell-client|Run my local Tasks')
$pending = Invoke-RestMethod -Method Post -Uri "$origin/api/auth/request" `
-ContentType 'text/plain;charset=UTF-8' -Body $body -TimeoutSec 10 -ErrorAction Stop
$deadline = [DateTime]::UtcNow.AddSeconds(125)
do {
Start-Sleep -Milliseconds 500
if ([DateTime]::UtcNow -ge $deadline) { throw 'Approval was not collected' }
$reply = Invoke-RestMethod -Uri "$origin/api/auth/status?id=$($pending.id)" `
-TimeoutSec 10 -ErrorAction Stop
} while ($reply.state -eq 'pending')
if ($reply.state -ne 'approved') { throw "$($reply.state): $($reply.message)" }
$clientDir = Join-Path $env:LOCALAPPDATA 'KeyBindClient'
[IO.Directory]::CreateDirectory($clientDir) | Out-Null
$tokenPath = Join-Path $clientDir 'powershell.token'
[IO.File]::WriteAllText($tokenPath, $reply.token, [Text.UTF8Encoding]::new($false))
"Token saved to $tokenPath"
Subsequent runs load the saved token without requesting approval:
$origin = 'http://127.0.0.1:7566'
$tokenPath = Join-Path $env:LOCALAPPDATA 'KeyBindClient\powershell.token'
$token = [IO.File]::ReadAllText($tokenPath).Trim()
$base = "$origin/$token/api/"
Invoke-RestMethod -Uri ($base + 'status') -TimeoutSec 10 -ErrorAction Stop
$body = [Text.Encoding]::UTF8.GetBytes('Speak(Hello from PowerShell.)')
$result = Invoke-RestMethod -Method Post -Uri ($base + 'task') `
-ContentType 'text/plain;charset=UTF-8' -Body $body -TimeoutSec 10 -ErrorAction Stop
$taskId = $result.id
$deadline = [DateTime]::UtcNow.AddSeconds(120)
while ($result.state -in 'queued','ready','running') {
if ([DateTime]::UtcNow -ge $deadline) { throw "Task $taskId result not collected" }
Start-Sleep -Milliseconds 250
$result = Invoke-RestMethod -Uri ($base + "task/$taskId") `
-TimeoutSec 10 -ErrorAction Stop
}
$result
if ($result.error -ne 0) {
throw "KeyBind error $($result.error), extended $($result.extended)"
}
For a report token, add -Headers @{Origin=$origin} to protected POSTs while Origin checking is enabled. The two bootstrap URLs always remain unprefixed.
AutoIt using WinHTTP
This short native example uses Windows' WinHTTP COM object and a previously approved app token. It submits one Task and writes the acceptance JSON to stdout. It does not parse/poll the returned ID: use your program's existing JSON parser and GET api/task/ID as in the other clients.
local const $HTTP_ACCEPTED = 202
local $origin = "http://127.0.0.1:7566"
local $token = "fedcba9876543210fedcba9876543210"
local $http = ObjCreate("WinHttp.WinHttpRequest.5.1")
if not IsObj($http) then exit 1
$http.SetTimeouts(5000, 5000, 10000, 10000)
$http.Open("POST", $origin & "/" & $token & "/api/task", false)
$http.SetRequestHeader("Content-Type", "text/plain;charset=UTF-8")
$http.Send("Speak(Hello from an AutoIt client.)")
ConsoleWrite("HTTP " & $http.Status & @LF & $http.ResponseText & @LF)
if $http.Status <> $HTTP_ACCEPTED then exit 1
A production client should use its existing COM error handling for connection failures. WinHTTP supplies Host and Content-Length. Add Origin only when required by the credential/configuration.
JavaScript inside a live report
Live reports expose KeyBindServer.request(action, body) for a protected POST and KeyBindServer.status() for a GET status read. Action is relative to api/: use task, not api/task. The POST helper returns acceptance JSON; it does not itself poll until completion.
KeyBindServer.defineSource(id) reads a definition sample as raw text through the same authenticated transport. It performs GET api/define-source/ID, returns a Promise for the source string and rejects on an unavailable sample or transport error. It does not tokenize, highlight or calculate markers; those operations belong to the Options page.
// Run in a live report page, for example its browser console.
async function testTask(task) {
let result = await KeyBindServer.request('task', task);
const id = result.id;
while (['queued', 'ready', 'running'].includes(result.state)) {
await new Promise(resolve => setTimeout(resolve, 250));
const response = await fetch(new URL('api/task/' + id, location.href),
{cache: 'no-store'});
const body = await response.json();
if (!response.ok) throw new Error(body.error || 'Result unavailable');
result = body;
}
return result;
}
console.log(await KeyBindServer.status());
console.log(await testTask('Speak(Hello from the report page.)'));
Do not use KeyBindServer.request('status') or request('task/1') for reads: request always uses POST. Use the status method or same-origin GET fetch. Direct calls to the public helper also do not reproduce every UI confirmation, busy state or result display performed by the page's button handlers.
An unrelated page cannot normally read this API while Origin checking is enabled: no cross-origin permission is returned and preflights are refused. The native app-token Origin exemption does not turn off browser CORS. A browser tab's claimed label is not a verified executable identity.
Token-free and no-security setups
The security switches are independent. These examples deliberately omit tokens and need no client approval. Loopback-only binding remains in effect in every configuration.
Tokens off, Origin checking on
Turn Security token off and leave Origin checking on. URLs use /api/ directly. Host must match exactly and protected POSTs need the exact Origin. Because no token is examined, there is no app-token Origin exemption in this mode.
curl.exe --noproxy 127.0.0.1 "http://127.0.0.1:7566/api/status"
curl.exe --noproxy 127.0.0.1 --request POST --header "Origin: http://127.0.0.1:7566" --header "Content-Type: text/plain;charset=UTF-8" --data-binary "Speak(Token checking is off.)" "http://127.0.0.1:7566/api/task"
Tokens off, Origin checking off
Turn both switches off. There is no credential check, exact Host check or POST Origin check. Any software able to reach the loopback server can request its exposed actions. Responses permit credentialless cross-origin reads with Access-Control-Allow-Origin: *, subject to the browser's local-network and other security policies.
These settings are under [KeyBind]. Always on is optional; this example enables it to keep the server available.
[KeyBind]
server_enabled=true
server_port=7566
server_uses_tokens=false
server_secure=false
server_loads_at_launch=true
curl.exe --noproxy 127.0.0.1 "http://127.0.0.1:7566/api/status"
curl.exe --noproxy 127.0.0.1 --request POST --data-binary "Speak(No token or Origin header.)" "http://127.0.0.1:7566/api/task"
curl.exe --noproxy 127.0.0.1 "http://127.0.0.1:7566/api/task/1"
The last URL uses example ID 1. Substitute the ID returned by your POST. No-security mode still requires correct framing, valid route-specific bodies and result polling.
$base = 'http://127.0.0.1:7566/api/'
Invoke-RestMethod ($base + 'status')
$result = Invoke-RestMethod -Method Post -Uri ($base + 'task') `
-ContentType 'text/plain;charset=UTF-8' -Body 'Speak(Hello without tokens.)'
$taskId = $result.id
while ($result.state -in 'queued','ready','running') {
Start-Sleep -Milliseconds 250
$result = Invoke-RestMethod ($base + "task/$taskId")
}
$result
import json
import time
import urllib.request
base = "http://127.0.0.1:7566/api/"
http = urllib.request.build_opener(urllib.request.ProxyHandler({}))
req = urllib.request.Request(base + "task",
data="Speak(Hello without tokens.)".encode("utf-8"),
headers={"Content-Type": "text/plain;charset=UTF-8"})
with http.open(req, timeout=10) as response:
result = json.load(response)
task_id = result["id"]
while result["state"] in ("queued", "ready", "running"):
time.sleep(0.25)
with http.open(base + "task/" + str(task_id), timeout=10) as response:
result = json.load(response)
print(json.dumps(result, indent=2))
// For a webpage whose browser permits access to the loopback server.
const response = await fetch('http://127.0.0.1:7566/api/task', {
method: 'POST',
headers: {'Content-Type': 'text/plain;charset=UTF-8'},
body: 'Speak(Hello from another page.)'
});
const accepted = await response.json();
if (!response.ok) throw new Error(accepted.error);
console.log(accepted);
// Then GET api/task/accepted.id to collect its result.
Tokens on, Origin checking off
This combination retains the /TOKEN/ requirement but removes exact Host/Origin checking and adds the same wildcard CORS response headers. App and report tokens need no POST Origin. It is not token-free operation: unknown or missing tokens are still rejected.
With Origin checking off, OPTIONS preflight is accepted only for api/status, api/editor, api/options, api/mail-challenge, api/mail, api/keep-alive, api/state, api/save, api/setting, api/delete, api/task and api/task/ID. The current preflight allowlist does not include auth, HotKey edit, Sentinel edit, Schedule edit, Browse or Link. Do not assume every endpoint accepts preflight.
HTTP request and response rules
- Use HTTP/1.0 or HTTP/1.1 with GET, POST or a supported OPTIONS case. HEAD, PUT, PATCH and HTTP DELETE are not implemented; deleting a definition uses POST api/delete.
- API routes are exact and case-sensitive. Do not append a trailing slash or extra query string. The defined query exception is
/api/auth/status?id=.... Registered report-file lookup strips a query suffix. - POST bodies are raw UTF-8. Content-Type does not cause JSON or form parsing. Every protected POST requires Content-Length; libraries normally calculate it from the encoded bytes.
- Transfer-Encoding, including chunked encoding, is rejected. Duplicate header names, malformed framing, extra bytes beyond the declared body and pipelining are rejected. GET bodies are unsupported.
- The 16384-byte request cap includes headers and the request line. HTTP header separators use CRLF on the wire; that is separate from LF-only physical source and data files.
- Every response closes the HTTP connection. A reusable HTTP library must be able to open a fresh underlying connection.
- JSON responses are UTF-8 and include no-store, no-referrer, nosniff and a Content-Security-Policy restricting sources and disallowing framing.
- With Origin checking enabled, Host must equal
127.0.0.1:PORT, or127.0.0.1for port 80. Use the numeric address, not localhost, the machine name or a LAN address.
Host and Origin checks validate requests; they do not verify the executable. Native programs can supply those headers. App tokens are bearer credentials. Keep real tokens and pending IDs out of shared screenshots, request transcripts and URLs passed to others.
One shared action queue and result
Task execution and report mutations share one queue/result slot across all clients and report tabs. A 202 reply contains an incrementing ID for that process and starts in queued state. After KeyBind finishes sending and closes the acknowledgement, it becomes ready. The main event processing starts it when no other Task/hotkey work prevents execution.
{"id":17,"state":"queued"}
{"id":17,"state":"returned","error":0,"extended":0,"type":"Bool","value":"True","truncated":false}
Type is AutoIt's VarGetType result, such as Bool or String. Value is a JSON string containing its text representation, even for Boolean/numeric returns. Arrays and maps are type markers, not serialized data. Text is capped at 4096 characters and marked truncated:true when cut.
Error and extended are numeric AutoIt execution codes. False, zero and empty text have command-specific meanings; they are not universal failure indicators. For report mutations also require saved, deleted, updated or enabled confirmation as appropriate.
New actions receive 409 while the slot is queued, ready or running. Once an action is returned, waiting or skipped, another accepted action may replace its result. GET api/task/ID keeps only the latest ID, not history. Other clients can overwrite the result before collection, so coordinate clients if every result matters.
The server shares KeyBind's event processing. Socket servicing uses nonblocking readiness checks and a five-millisecond soft budget, but a Task can invoke synchronous work or a desktop chooser that delays HTTP servicing. Read polling may be retried. Repeating a POST after uncertain delivery can execute the same action twice. Poll the known accepted ID, or verify the effect before deliberately resubmitting.
Endpoint summary
BASE below is http://127.0.0.1:7566/TOKEN/ with tokens on, or http://127.0.0.1:7566/ with tokens off. Routes shown after BASE are concatenated directly, with no intervening space.
Server status and keep-alive
GET api/status does not execute work or renew the timeout. A timed response looks like:
{"minutes":5,"remaining_ms":284100,"always_on":false,"generation":"1234:example","mail_fields_editable":true,"mail_password_editable":true}
Minutes is the selected interval; zero is permanent. Remaining_ms is a nonnegative countdown or -1 when permanent. Always_on reflects the Always on option, so manually selecting permanent keep-alive can produce minutes:0 with always_on:false. Generation identifies the running KeyBind process; compare it as opaque text to detect a restart between successful requests. Mail_fields_editable is true when Origin checking is enabled; mail_password_editable additionally requires token protection. Plaintext non-password mail fields remain available regardless of mail_fields_editable.
The Options page checks status while visible, including with permanent keep-alive. Without interaction, successive checks wait 5, 10, 20, 40 and then 60 seconds, with later waits capped at 60 seconds. A pointer press or keypress resets the next check to 5 seconds. Hidden tabs make no periodic requests; returning to a tab resets the delay and requests status immediately, unless access has been revoked. Checks pause during a pending Options action and do not overlap. Failed connections retry with the same backoff; POST actions are never automatically repeated.
A transport failure displays KeyBind server unavailable. Reconnecting... and disables live controls. Successful recovery displays Reconnected to KeyBind. for five seconds. A changed generation displays KeyBind restarted. Reconnected. and reloads option values and installed fonts. A restart shorter than the current check interval is detected by generation even if no request failed. Revoked or invalid tokens stop automatic retries and require a fresh Options() URL. Other report pages retain their own request schedules; these idle intervals describe the Options page.
POST api/keep-alive accepts exactly 1, 2, 5, 10, 15, 30, 60 or 0. The body is minutes, not an INI string such as 5m. Always on allows only 0; other values receive 400 until Always on is disabled. This route changes the running interval without saving server_keep_alive in the INI.
curl.exe --noproxy 127.0.0.1 --request POST --data-binary "0" "http://127.0.0.1:7566/fedcba9876543210fedcba9876543210/api/keep-alive"
Task execution and named control commands
POST api/task accepts nonblank text, no NUL, and at most 4096 UTF-8 bytes. Unlike saved-Task editing, it does not explicitly reject CR/LF; clients should normally send the same single-line text used in a Task input. The body is not shell syntax or JSON. A chain uses KeyBind's ** separator.
Each line below is a separate Task example. Read/edit examples require the named definition to exist. New/edit operations save configuration through their existing control handlers.
Speak(Build finished.)
Speak(First announcement.)**Speak(Second announcement.)
HotKeyControl(get|^!+#{F8}|field=enabled)
HotKeyControl(disable|^!+#{F8})
HotKeyControl(enable|^!+#{F8})
HotKeyControl(new|^!+#{F6}|scope=Notepad++|enabled=false|task=Speak(Hello!))
SentinelControl(get|Example_Folder|group=Pictures|field=extensions)
TriggerControl(get|Copied greeting|kind=clipboard|field=pattern)
Named commands are useful when a client knows an item's name/HotKey and should not scrape report IDs. The specialized routes below reproduce report controls and therefore need registered IDs. A compound Task's return is the result supplied by normal Task execution, not a JSON array of every intermediate command.
Task effects occur on the KeyBind PC under its process context. Commands can open programs, display dialogs, manipulate files or type into the foreground window. Collect the result and interpret the command's return contract instead of treating HTTP acceptance as proof of its effect.
Generation and registered control IDs
G means the current report generation string. C means a positive registered control ID. Obtain both from generated HTML; neither is a token, and clients must not invent or persist them as permanent definition identities.
Locate a report card's data-control and data-generation attributes. State buttons carry the item control. Delete/Edit buttons and Task forms supply generation. Some forms, particularly Sentinel group Tasks, have their own data-control; other forms use the card's state control. A group Task ID can differ from its card-level enable/delete ID.
// Inspect a live report without changing a definition.
const card = [...document.querySelectorAll('.sentinel-card')]
.find(item => item.querySelector('h2').textContent === 'Ctrl+Alt+Shift+Win+F8');
if (!card) throw new Error('Example card is absent');
const state = card.querySelector('.report-state-toggle');
const form = card.querySelector('.server-task');
const generation = form?.dataset.generation ||
card.querySelector('.report-delete').dataset.generation;
const taskControl = form?.dataset.control || state.dataset.control;
console.log({generation, itemControl: state.dataset.control, taskControl});
Control IDs belong to the running registry. After KeyBind restarts, read current HTML and derive current IDs. Save uses the form/group Task control; Delete and state changes use the intended item control.
The current dispatcher explicitly compares G on api/save, api/setting, api/delete and api/hotkey. A mismatch gives HTTP 409. Api/sentinel, api/schedule, api/browse and api/link require a generation-shaped first field but currently do not compare its value; send current G anyway and do not rely on stale values working. Api/state has no generation field.
Enable or disable an item
POST api/state body is exactly C|1 to enable or C|0 to disable, without G. The registered control resolves to a saved HotKey, Trigger, Sentinel or Schedule. Its normal validation applies before saving.
38|0
{"id":18,"state":"returned","error":0,"extended":0,"type":"Bool","value":"True","truncated":false,"enabled":false}
The ID is illustrative. Enabling can fail for incomplete input, conflicts or Windows registration refusal. Require the returned enabled confirmation; acceptance alone does not prove that the saved state changed.
Delete a saved definition
POST api/delete body is G|C. Poll its action ID and require deleted:true. The server uses the existing control's deletion operation. There is no additional HTTP-side confirmation dialog: the report asks before sending; another client must implement whatever confirmation its workflow requires.
CURRENT_GENERATION|38
The ID selects the particular saved definition represented by that card. Another HotKey executing the same Task is not automatically the same definition. On confirmed success the report removes matching cards and updates counts; its overlay shows progress while waiting and keeps the card on failure.
Save a Task with original-text checking
POST api/save body is G|C|L|ORIGINALREPLACEMENT. L counts ORIGINAL's UTF-16 code units, matching AutoIt StringLen and JavaScript string length. There is no separator between ORIGINAL and REPLACEMENT; L defines their boundary. Pipes inside either Task remain ordinary payload.
The currently saved Task must exactly match ORIGINAL, including case. Replacement must be nonblank, single-line, NUL-free and at most 4096 UTF-8 bytes. Complete save body: at most 12288 UTF-8 bytes, also subject to the entire request cap with headers.
CURRENT_GENERATION|38|10|Speak(Old)Speak(New)
Speak(Old) is ten UTF-16 code units. This replaces that exact Task with Speak(New). Success returns saved:true in the collected result. A changed original is terminal execution error 6 with a message, not necessarily HTTP 409 on the initial POST.
def utf16_length(text):
return len(text.encode("utf-16-le")) // 2
generation = "CURRENT_GENERATION"
control = "38"
original = "Speak(Old)"
replacement = "Speak(New)"
body = f"{generation}|{control}|{utf16_length(original)}|{original}{replacement}"
# Send body encoded as UTF-8. L is not the HTTP byte Content-Length.
Change a HotKey activator
POST api/hotkey uses G|C|L|NEW_HOTKEY. L is its UTF-16 length. Text must be nonempty, single-line, NUL-free and at most 256 UTF-8 bytes. C must represent a HotKeyControl item. Existing conflicts and registration checks apply.
CURRENT_GENERATION|38|9|^!+#{F10}
The example is Ctrl+Alt+Shift+Win+F10, nine characters in raw form. Success returns updated:true, canonical raw HotKey text, scope, friendly heading and kind. This changes an existing activator while retaining its saved enabled/disabled status. Use HotKeyControl through api/task to create a new binding.
Trigger settings
POST api/setting body is G|C|FIELD|VALUE. C must identify TriggerControl. Fields are always, exact, case_sensitive and grace; applicability depends on Trigger kind.
- Flag values are exactly 0 or 1, converted to false/true before calling the control.
- Grace is a whole number, optionally followed by s, m, h or d, case-insensitively. No suffix means seconds. Empty or 0 removes grace. Fractions and ms are not accepted here.
CURRENT_GENERATION|12|case_sensitive|1
CURRENT_GENERATION|12|grace|3s
CURRENT_GENERATION|12|grace|
Each line is a separate body. Success returns updated:true, canonical setting_value and the updated raw Trigger definition. Use returned values instead of assuming the submitted spelling is the stored representation.
Sentinel settings and target selection
POST api/sentinel uses one of these body forms. Each line is a separate example.
CURRENT_GENERATION|9|target|@Desktop\Inbox
CURRENT_GENERATION|9|mask|*.txt
CURRENT_GENERATION|9|ignore|temporary-pattern
CURRENT_GENERATION|9|extensions|Pictures|jpg,png,webp
Target maps to file or folder according to the Sentinel's saved kind and must pass the control's existence/type checks. Mask, ignore and extensions use Sentinel validation/canonicalization. Empty mask/extensions are rejected. Values must contain no CR/LF/NUL and be no more than 12288 UTF-8 bytes, subject to the entire request cap.
For extensions, GROUP is its extension-group name and C identifies the Sentinel item. Success returns updated:true and canonical setting_value. Edit individual/group Task text with api/save and the corresponding Task control ID.
Schedule settings
POST api/schedule body is G|C|FIELD|VALUE. C must identify ScheduleControl. Fields are time and date.
CURRENT_GENERATION|9|time|9:00am
CURRENT_GENERATION|9|date|2026/10/25
Time uses H:MMam/pm, e.g. 9:00am. Date uses YYYY/M/D and must be a real calendar date. Values pass ScheduleControl edit validation, so Custom minute/hour schedules recompute their stored start from the submitted time and date. Success returns updated:true and the canonical saved setting_value. Use returned values instead of assuming the submitted spelling is the stored representation.
POST api/browse takes G|C and opens a desktop file/folder chooser on the KeyBind PC. This is not a browser upload. The collected result includes browsed:true and a tokenized chosen path in value, or an empty value on cancellation. Choosing does not save the new target; submit it separately to api/sentinel if wanted.
Link or unlink File Sentinel Tasks
POST api/link takes G|C|OTHER_NAME to link a File Sentinel to another by cleaned name. Omit the final name or leave it empty to unlink. C identifies the first Sentinel. Both definitions must pass SentinelControl's linking checks; Folder Sentinels do not use this shared-file-Task operation.
CURRENT_GENERATION|9|Other_File
CURRENT_GENERATION|9
Success returns updated:true. Linked File Sentinels share their actual Task. Refresh the report after membership changes to see correct groups and Task controls. This is not a filesystem shortcut or an HTTP link.
Task Editor data
GET api/editor provides the native editor's lists in this shape. The example is abbreviated.
{"commands":[["Speak","Speak(Hello!)"]],"macros":["@DataDir"],"special":[],"history":["Speak(Example.)"],"history_enabled":true}
Commands and special are arrays of [name, example] pairs. Macros and history are text arrays. Special comes from Sentinel command examples. History is empty when Task History is disabled. Reading lists neither executes a Task nor renews the server timeout.
Options page data and mail settings
Tasks submitted through the HTML Options controls or its Task overlay use POST api/options-task. The body, authentication, Origin checks, mail-secret exclusions, size limit, queue acknowledgement and GET api/task/ID result collection match api/task. The server records Options origin with the queued request and uses a reserved trigger prefix during execution. The six notification functions for schedule master state, schedule creation/deletion, Sentinel changes, HotKey changes and Trigger changes suppress their desktop tooltip notices for this source, regardless of commands_notify. Wait continuations retain the same trigger and suppression. The saved notification preference is not changed; commands submitted from other reports, apps, HotKeys or ordinary Tasks retain their existing notification behaviour. Options continues to display command results through its existing page feedback.
Sounds & Alerts
The HTML Options Sounds & Alerts group contains KeyBind sounds, Volume, CountDown notification beeps, the CountDown completion Task, and ListView Notifications, Alert type, On, Off and Repeat. Alert type is a dropdown populated from $LIST_VIEW_NOTIFY_TYPES. On and Off are milliseconds; Repeat is the number of complete on/off cycles. The notification master controls whether the live ListViews issue change alerts. Each viewer also requires Live Update.
The group requests GET api/options?keys=...&sounds=1. The response adds notification_sounds from the current $notification_sounds table and alert_types from $LIST_VIEW_NOTIFY_TYPES. Each notification record has the ordinary option fields plus label and a Boolean beep. Parameter-only rows have beep=true and create a text input, playback button and Beep editor button. The browser does not contain a fixed beep key list or row count. Task-valued records have beep=false; the CountDown Complete row uses the existing Task overlay. Registered defaults replace missing or invalid beep preferences. Without sounds=1, both additional arrays are empty.
Turning KeyBind sounds off disables the notification beep inputs and their Test and Beep buttons. Volume and the completion Task remain editable. The completion Task saves through KeyBind(countdown_complete|TASK); registered beep preferences save through KeyBind(KEY|PARAMETERS). These use the existing notification validation and persistence functions. Cancel in the Beep editor does not submit its parameters. Save submits them through the ordinary Options save path and closes only after confirmed success.
The Beep editor uses the shared shaded overlay and offers Frequency, optional Duration, Volume, Wave, Pan, ADSR, Phase and Duty fields. ADSR has Attack, Decay, Sustain and Release controls. Optional checkboxes add or remove the corresponding parameter. The raw parameter input and effective-value display use the native NormalizeBeepParameters(), SetBeepParameter() and ParseToneNote() functions through the routes below. Duty is unavailable for sine; square uses 1-99 and triangle uses 0-100. The raw parser applies the same fallback, clamping, phase wrapping and duration-unit rules as the desktop editor.
POST api/beepaccepts raw Beep parameters without the Beep() wrapper. It returnstextwith earlier duplicate parameters removed, Booleanvalid, andnote. Valid note arrays contain eleven text values in this order: frequency, duration in milliseconds, volume, waveform, pan, attack, decay, sustain, release, phase and duty. Invalid syntax returns valid=false and an empty note array; it does not play or save anything.POST api/beep-parameteracceptsKEY|ENABLED|VALUE|PARAMETERS. KEY is freq, dur, vol, wave, pan, adsr, phase or duty. ENABLED is 1 to include or 0 to remove the parameter; frequency is the mandatory first slot. The response has the same shape as api/beep. Editing preserves parameter order and keeps the last duplicate value.POST api/beep-testaccepts raw parameters, validates them before queuing, and returns the ordinary 202 Task acknowledgement. Collect the result fromGET api/task/ID. Playback runs on the KeyBind PC at the configured KeyBind volume throughDoBeep()in parameter-only mode. A playback handle indicates that playback started; false indicates failure. Preview does not expand Tasks or tokens, change preferences or require the KeyBind sounds master to be on. The group disables its beep buttons when that master is off; an already open editor retains the desktop editor's direct Test behaviour.- All three POST routes require the ordinary report authentication and Origin checks. Their bodies must fit within 4096 UTF-8 bytes and contain no whitespace or control characters. Invalid request structure receives 400. Parse and edit requests neither play audio nor renew the server timeout. A Test uses the existing acknowledged Task queue and renews its timeout in the same way as other queued actions.
The Task overlay permits its Task History menu to extend beyond the dialog edge. The menu keeps its own height limit and scrollbar. Beep and App access overlays retain their dialog scrolling.
Changes delivered with Options status
The Options page polls GET api/status?changes=1. This returns the ordinary status fields plus changes. Plain GET api/status neither includes nor consumes pending changes. Neither read extends keep-alive or executes a Task.
{"minutes":0,"remaining_ms":-1,"always_on":true,"generation":"1234:example","mail_fields_editable":true,"mail_password_editable":true,"changes":{"options":[{"section":"KeyBind","key":"disabled_color","value":"A2A2A2","protected":false,"encrypted":false,"tip":"..."}],"apps":[{"key":"keybind_server","label":"KeyBind Server","kind":"allowed + persistent"}]}}
- During normal server servicing, KeyBind checks the INI file's high-resolution modification time, identity and size at most once per second. A changes request also checks immediately before collecting its payload. It reads [KeyBind], [mail] and [sounds] only when that file state changes. The first read establishes the baseline. Later differences are accumulated in memory; repeated changes to a key replace its pending value. Blacklisted keys are excluded. No notification data is saved to disk or written to logs.
- This observes saved values regardless of whether the Config Editor, a KeyBind() command, a report control or another writer saved them. Intermediate values between checks may be coalesced; this is current-state synchronisation, not an edit history. A removed key is reported with an empty value. Snapshot reads that fail or overlap another write are retried without discarding the previous snapshot.
- Only changed preference records are sent. They use the same value, protected, encrypted and tip fields as api/options. Mail decryption happens when preparing the response, under the current protection flags; passwords are never disclosed. Changes to the security switches also queue mail records so their controls can update their availability and displayed values.
- The app list is compared from the current authorisation maps, including session grants. After its displayed contents change,
changes.appscontains the replacement list with key, label and kind; it contains no tokens. An empty array clears the list. Without an app-list change, the apps property is absent. Without preference changes, options is an empty array. - Preparing a changes response consumes the accumulated payload once, for the next requesting Options page. Other status readers and api/options do not consume it. This is not a separate delivery per tab or client, and there is no acknowledgement/replay protocol. If the response is lost, refresh the page to obtain current values. Multiple Options pages should not expect every page to receive the same update.
- The page updates existing controls in groups already loaded, without refreshing the whole page or refetching unchanged groups. Groups not yet loaded obtain current values through their normal lazy request when approached. Authorised-app changes reuse the supplied list without another api/apps request. Existing unsaved browser drafts retain their precedence.
- The existing adaptive schedule remains: visible idle waits grow through 5, 10, 20, 40 and 60 seconds; interaction resets the delay, and hidden tabs pause. Connection failures use the existing reconnect schedule. A KeyBind process restart changes generation and causes the usual group reload instead of relying on the previous process's change list.
The Web group places Define start and End on one row, with matching label sizes and compact marker inputs. It has text inputs for web_search_url, speak_task_define_url, speak_task_define_start and speak_task_define_end. Each input uses the ordinary Options save and browser-draft behaviour. Fetch opens a modal source-selection dialog over a shaded backdrop. The selection view starts at the first readable body text, using the desktop viewer's FirstDefineTextOffset() calculation. HTML tags appear in muted blue; the page head, scripts, styles, comments and leading markup are omitted. Fetched scripts and markup are never executed. Cancel, Escape or a click on the shaded backdrop closes the dialog without selecting markers. A fetch already acknowledged by KeyBind continues after closing.
Clicking Fetch while editing the Define URL waits for its Options save. The browser caches the last successful source and prepared tokens for that URL and running server generation. Reopening Fetch with the same URL makes no server request. Cached data is reused only when the URL and server generation match. A successful fetch for another URL replaces it; reloading the page clears it. KeyBind separately retains its last successful raw download in memory for reuse by the desktop Config Editor and another browser page.
POST api/define-fetch takes the Define URL template containing @LookUp. It queues the download and returns 202 with the task ID. A successful collected task result contains a numeric sample ID, with no source payload. GET api/define-source/ID retrieves that sample as text/plain; charset=utf-8, or returns 404 if it has been replaced. This read does not execute a task, renew the server timeout or put the HTML in the task log. Invalid URLs return 400; download errors appear in the collected task result.
JavaScript normalizes line endings, tokenizes and filters the body source, applies highlighting and calculates both markers from the selected original offsets. It inserts source through textContent, so downloaded markup and scripts are not executed. The page head, scripts, styles, comments and leading markup are omitted from the view. The browser follows the same candidate priority and capture rules as the Config Editor and runtime DefineSpan(): preceding unique IDs, classes, then a literal opening tag, each checked against the actual selected span. Selected surrounding whitespace is ignored. Use selection saves Define start and End through the ordinary Options input path. Failed saves retain their browser drafts and display errors in the Web group's top-right status area.
The existing POST api/define-markers endpoint remains available to explicit API clients, but the browser selector does not call it. It accepts G|ID|FROM|TO using zero-based UTF-16 offsets into the cached raw HTML, with TO exclusive. It returns start/end strings, 400 for an invalid selection, or 409 for a replaced sample or server generation. None of the definition endpoints are in the cross-origin OPTIONS preflight allowlist.
Options group feedback uses the JavaScript groupMessage() function. Each posting control identifies its nearest group; progress, validation errors, API errors and successful actions appear in that group's top-right status area. A new message cancels the previous message's timer. Successful saves, voice/mail tests and app removals clear after three seconds; errors remain until replaced. Loading and security notices are tagged so later refreshes clear only their own notice. The installed-font list is replaced only by the group that requested fonts=1; other groups' empty fonts arrays do not change the choosers or font validation.
Paths & Dates contains editable text for editor, scripts_dir, date_format, info_file, debug_log, server_log, log_viewer and color_report_file. BetweenDays is omitted. Date format has a Presets dropdown populated from date_presets in its options response. The browser and Config Editor use the same DATE_FORMAT_PRESETS array. Selecting a preset replaces and saves the format through the ordinary Options path, then resets the dropdown. A pending format-input save finishes before the selected preset saves, so the first selection is retained. The dropdown remains usable during that save. Fine-grained date/time choices remain in Config Editor. The seven path buttons POST api/pick-path with setting|current text. The authenticated request accepts only the seven path keys, rejects malformed/oversized bodies, and queues ChoosePathPick() through the existing action queue. This function also serves the desktop buttons: browser requests return the tokenized selection without saving; the browser then fills and saves the input. Cancel leaves the input and saved value unchanged. Editor and Log viewer choose existing exe files, Scripts chooses an existing folder, and output-file rows permit a new filename. Paths & Dates uses one- or two-word status text, including Picker Activated, Saved, Cancelled., Save failed and Pick failed. The shared groupMessage() function accepts main and sub-status text. The sub-status appears on a second line inside square brackets at .6rem; the main status uses .875rem. Both lines fit inside the existing 1.95rem header-title height. Native path pickers show Picker Activated with [KeyBind processing suspended] until the chooser returns. Full error details appear on hover over the status text. The native chooser appears on the KeyBind PC and blocks its normal main loop until closed. The client collects the queued result without submitting another chooser request.
The Runtime & Speech group uses the recorder's existing HotKey dialog for abort_hotkey. Max history/log size, debug level, voice, speech volume/rate and idle ignores use ordinary Options saves. Its lazy options request adds voices=1; the response voices array comes from GetSpeechVoices() on the KeyBind PC, not browser speech-synthesis voices. Other requests do not enumerate speech voices and return voices:[]. The Voice combo includes Default with an empty value, which selects KeyBind's default voice.
Runtime & Speech uses two columns for Abort HotKey / Debug level, history / log size, and Volume / Rate; narrow screens use one column. Voice, Idle Ignores, Startup Task and Shutdown Task span both columns. The Voice row contains the installed-voice combo, Edit and a test arrow. Edit reveals the shared editor for voice_test_string, with Save, Cancel, choices, history and browser drafts. The default text comes from the desktop Config Editor's VOICE_TEST_STRING. Saving writes the same voice_test_string preference used by the desktop test. The arrow queues KeyBind(test_voice), which calls Speak() with the saved text, voice, volume and rate on the KeyBind PC. Speak() expands @Tokens, including @Voice; unsaved editor text is not tested. Speech starts asynchronously and a later test replaces earlier speech.
Startup Task and Shutdown Task reuse the report Task editor, including command/@Macro choices, history, drafts, Save and Cancel. Edit opens the shared toolbar; focusing the Task input also opens it. Save writes startup_task or shutdown_task through the ordinary Options save path, including an empty value to disable the Task. Changing editor controls does not implicitly save either Task. Cancel restores the last saved value.
Idle Pick opens the browser file chooser restricted to .exe files. The page reads only the selected filename, appends it to idle_ignore using a semicolon separator and saves the resulting list. It does not read or upload executable contents or send a local path. Cancelling the chooser changes nothing.
GET api/options returns fonts, an array of installed font-family names, and options, an array of saved preference records. A ?keys= query limits the reply to those comma-separated keys, &fonts=1 adds the font array and &voices=1 adds installed speech voice names; the bare request without a query enumerates all non-blacklisted keys and always includes fonts. Malformed key lists receive 400. Definitions, history, authorisation and token sections are never enumerated. This read neither executes a Task nor renews the server timeout.
Options replies also contain date_presets: the Config Editor's date/time format presets when date_format is requested, or an empty array for other filtered requests.
Each record contains section, key, value (text), protected and encrypted (booleans), and tip (Config Editor tooltip text, or its generic fallback). The reader uses the first saved occurrence of a repeated key. The flags describe storage and disclosure, not whether a web control exists.
{"fonts":["Arial","Consolas"],"options":[{"section":"KeyBind","key":"hover_sensitivity","value":"8","protected":false,"encrypted":false,"tip":"..."}]}
- Plaintext mail details are returned without requiring Origin checking. Encrypted non-password fields are decrypted only with Origin checking enabled. When disclosure is blocked or decryption fails, the value is empty and protected is true.
mail_passis never returned. Its replacement control requires both token protection and Origin checking; otherwise the input is unavailable and displays Full security required. Other encrypted mail controls require Origin checking and display Origin checking required when blocked. Blocked controls cannot be edited or saved.- Other preferences save through
POST api/taskwith aKeyBind(setting|value)command; collect its result throughGET api/task/ID. A returned Boolean false means validation or persistence failed. Open Config Editor controls update where the setting's handler provides synchronisation. - Font controls use the returned installed-family list.
inputbox_fontanddialog_fontaccept name|size|weight|attributes; size is 6-72 points, weight is an integer 100-900 and attributes use italic 2, underline 4 and strikeout 8, combined by addition (0 is normal). A-field preserves that saved component. The web controls show Size, Bold and Italic on one row for each font. Bold saves weight 700 when checked and 400 when unchecked; Italic saves attribute 2 when checked and 0 when unchecked, matching the Config Editor picker. - Color pickers send
#RRGGBB. The color handler also accepts six-digit RGB hex, AutoIt 0x-prefixed RGB or a decimal RGB integer, and saves six-digit uppercase RGB hex. CountDown transparency is 0-95 percent for the mini window. Repeated instances of an updated INI key cause the verified save to fail rather than choosing an arbitrary occurrence. server_group_orderstores comma-separated web group IDs in [KeyBind]. It is independent of the Config Editor's group order. Dragging a web group title saves through the same KeyBind() command path.- The Test Mail Settings button submits
TestMailSettings()through the Task route. A returned Boolean true means sending succeeded; false means it failed. The web invocation does not open a desktop result dialog.
POST api/mail saves one of smtp_server, mail_user, mail_pass, from_name, from_address or to_address without placing its value in the Task queue, history or diagnostic logs. Success returns HTTP 200 with updated true and the resulting encrypted flag. Mail text sent as a KeyBind() command through api/task is refused with HTTP 400.
plain|KEY|TEXTis accepted only for a non-password field whose current stored value is plaintext. It does not bypass token or Origin checks enabled for the request. An empty TEXT clears the setting.NONCE|KEY|HEXsupplies a wire-encrypted value. Obtain nonce and salt fromGET api/mail-challenge. Origin checking is required; password changes additionally require token protection. If tokens are enabled, either frame requires an authenticated caller. Challenges are bound to the request token, expire after 120 seconds, and are single-use; a newer challenge replaces the outstanding one.- Wire encryption XORs UTF-8 bytes with successive SHA-256 blocks of token + nonce + salt + an eight-digit lowercase hexadecimal block counter starting at 00000000. Without token protection the token component is empty. Wire encryption is separate from the existing mail storage encryption. An empty HEX clears the setting.
Reports, assets and static operation
At startup the server registers HotKeys.html, Triggers.html, Sentinels.html and Schedules.html with default renderers, plus Options.html with its template renderer and shared script. GET regenerates a registered report from current state before serving. For Options.html it overwrites the installed HTML with the template bundled in the running executable; merely starting the server does not create that file. The Options Refresh button reloads this URL, so it regenerates the HTML and then requests status and current option values. This does not rebuild the executable or replace existing CSS, JavaScript or icon assets. Renderer arguments can be retained from a report command, so a filtered report can represent that selection rather than an unfiltered collection.
Only registered resources are served, not arbitrary filesystem paths. KeyBind.css, KeyBind.js and KeyBind.ico are registered shared assets. Report creation checks all three, restoring missing assets without overwriting existing copies. README.txt is installed only at Server directory creation and is not one of the automatically registered shared asset URLs.
The App Usage page registers AppUsage.html with its opening filter, plus the shared assets and every valid monthly data file under their root names. GET regenerates the page with its saved filter and re-registers the data files present at refresh time, so a new month resolves without reopening the command.
With the server disabled, report commands still generate and open HTML files. They state that live controls are unavailable. Opening a file:// report does not create HTTP authorization or bypass a disabled server. Enable the server and regenerate the report for a live connection.
Task inputs support testing, explicit saving, command/@Macro choices, history and unsaved browser drafts. A draft is not a saved KeyBind definition until a save succeeds. The Delete overlay indicates an outstanding operation, and removal waits for confirmation.
Server configuration
These values are under [KeyBind] in KeyBind.ini and are exposed in Config Editor > Options > Server. Use the UI for changes that should affect the running process.
In HTML Options, Paths & Dates disables the Debug log label, input and Pick button when debug_level is 0 or off. Server log follows server_debug_level in the same way. Positive levels enable the corresponding row again, subject to the ordinary connection and busy restrictions. The Paths & Dates request includes both debug levels even if Runtime & Speech or Server has not loaded. Live debug-level changes also update these dependencies before their groups become visible; log paths are retained.
HTML Options > Server > App access requests the current app list every time the overlay opens. While the overlay is open and the browser tab is visible, Options polls every five seconds and reads the current app list when the status response has no app-list update. App additions therefore appear without reloading Options, including when the Server group has not loaded. Unchanged rows remain in place, and a response from an older list request cannot replace a newer result. Closing the overlay restores the ordinary idle polling backoff. Persistent credentials are in [Authorised Apps], a section excluded from HotKey parsing and reporting.
Authorization log messages contain outcomes without token or pending-ID bytes. Task summaries begin at level 5; levels 9 and 10 include fuller Task payloads/results. Tasks and returned data can themselves contain private information, so high-level logging can record that data even though authorization messages omit their credentials. Server logs rotate through the existing maximum-log-size setting.
Limits and error handling
HTTP failures generally contain {"error":"explanation"} with a string error. Action results have a numeric error and optional message. Authorization polling has a state and optional message. Treat these as different reply contracts.
Troubleshooting
Client implementation checklist
- Use the configured numeric loopback origin and an available server.
- Request authorization deliberately, store the returned token privately and reuse it on later launches.
- Send raw UTF-8 with correct byte Content-Length and the route's exact frame.
- Keep accepted action IDs, poll reads and inspect final results rather than treating 202 as completion.
- Do not automatically repeat a state-changing request after uncertain delivery.
- Use fresh report IDs/generations, or named control commands when those fit better.
- Distinguish server lifetime, pending authorization lifetime and credential lifetime.
- Report denial, revoked credentials, write failure and server unavailability without repeatedly prompting for approval.