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

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

  1. Open Config Editor > Options > Server and turn Enabled on. The tray KeyBind Server menu also has the server master switch.
  2. For clients that should connect whenever KeyBind runs, enable Always on. It starts the server at KeyBind launch and forces permanent keep-alive.
  3. For on-demand operation, run Report(HotKeys), Report(Triggers), Report(Sentinels) or Report(Schedules). Opening a control report starts or renews the server.
  4. 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.

Credential
Obtained from
Lifetime
Persistent report token
The URL of a generated control report.
Stored as keybind_server in [Authorised Apps], shown as KeyBind Server in App access. Survives process and server restarts. Revoke invalidates open pages immediately; a later report command issues a new saved token.
Session app token
Access request approved with Allow once.
Memory only. Survives server disable/enable within the same process, but ends when KeyBind exits or the label is revoked.
Persistent app token
Access request approved with Always allow or explicit Replace.
Stored in KeyBind.ini and loaded at launch. Survives process restarts and server disable/enable until replaced, revoked or removed from the loaded authorization data.
Pending authorization ID
POST /api/auth/request response.
Short-lived secret for collecting one authorization decision. It cannot authorize ordinary API actions.

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"

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

Choice
Effect
Allow once
Create a new session token. Existing saved tokens keep working. Multiple session tokens can coexist under the same label.
Always allow
For a label without a saved token, save a new persistent token. Authorization becomes live only after the INI write succeeds.
Replace
For a label with a saved token, explicitly replace that saved token. The previous saved token immediately stops working. Separate session tokens for the label remain valid.
Deny / close the window
Issue no token for this request. Existing authorizations are not revoked.

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"
Response
Meaning
200 / state: pending
No decision yet. Continue polling until the server or your client deadline expires.
200 / state: approved, token
Use the returned token as the protected URL prefix. Save your copy if it should be available on later client launches.
200 / state: denied, message
User denial, or an approved credential that has been revoked/replaced. Stop polling and report the explanation.
200 / state: error, message
Token generation, persistence or a duplicate-label decision failed. This is not approval.
410 Gone
The pending ID is unknown or expired. It can no longer be collected.
400 Bad Request
Malformed authorization request or ID format.
{"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.

[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

Protected POST credential/configuration
Origin requirement
Report token; Origin checking on
Exact server Origin required.
App token accepted with token checking on
No Origin required. Host checking still applies when enabled.
Tokens off; Origin checking on
Exact Origin required. No app-token classification is performed.
Origin checking off
No Origin check. The token switch remains independent.
Unprefixed auth/request and auth/status
No Origin required. Host checking applies when enabled.

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.

State
Meaning
queued
Accepted; HTTP acknowledgement is still being sent/closed.
ready
Acknowledged and waiting for KeyBind to start the action.
running
The initial action call is executing. Synchronous work can delay HTTP responses.
returned
The initial call returned. Inspect error, value and operation-specific confirmation.
waiting
The initial call returned with a continuation in KeyBind's command queues, such as Wait(). This result does not track the eventual continuation.
skipped
The Task did not start through the normal execution path. It is not proof of a completed operation.
{"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.

Method and route
Request / purpose
Normal response
POST /api/auth/request
Unprefixed raw label or label|reason.
202 pending ID.
GET /api/auth/status?id=ID
Unprefixed decision/token collection.
200 pending/approved/denied/error, or 410.
GET BASE api/status[?changes=1]
Read server timeout.
200 minutes, remaining_ms, always_on, generation and mail permission flags.
POST BASE api/define-fetch
Queue a raw definition sample download from a URL template.
202 task ID; collect a small result containing sample ID. No HTML in the task result.
GET BASE api/define-source/ID
Read the cached raw sample as text/plain; charset=utf-8.
200 source text; 404 if the sample is missing or replaced. No task execution or timeout renewal.
GET BASE api/options[?keys=,...][&fonts=1]
Read saved preferences and installed font families.
200 fonts/options arrays; protected mail values omitted; 400 on a malformed key list.
GET BASE api/mail-challenge
Request a single-use mail save challenge.
200 nonce/salt; 403 without required protection.
POST BASE api/mail
plain|KEY|TEXT or NONCE|KEY|HEX.
200 updated:true/encrypted; no Task queue or history.
GET BASE api/editor
Read editor lists and history.
200 commands/macros/special/history/history_enabled.
GET BASE api/task/ID
Collect latest action result.
200 result or 404 unavailable ID.
POST BASE api/keep-alive
Raw minutes, or 0 for on.
200 server timeout.
POST BASE api/task
Raw KeyBind Task.
202; poll returned action ID.
POST BASE api/state
C|0 or C|1.
202; result confirms enabled state.
POST BASE api/delete
G|C.
202; result deleted:true.
POST BASE api/save
G|C|L|ORIGINAL immediately followed by REPLACEMENT.
202; result saved:true.
POST BASE api/hotkey
G|C|L|NEW_HOTKEY.
202; result updated/raw/scope/heading/kind.
POST BASE api/setting
G|C|FIELD|VALUE for supported Trigger settings.
202; result updated/setting_value/raw.
POST BASE api/sentinel
G|C|FIELD|VALUE or G|C|extensions|GROUP|VALUE.
202; result updated/setting_value.
POST BASE api/schedule
G|C|FIELD|VALUE for time and date.
202; result updated/setting_value.
POST BASE api/browse
G|C for a Sentinel target chooser.
202; result browsed:true and value.
POST BASE api/link
G|C|OTHER_NAME to link, G|C to unlink.
202; result updated:true.
GET BASE HotKeys.html / Triggers.html / Sentinels.html / Schedules.html / Options.html
Generate/read the registered report.
200 HTML; 404 unavailable; 500 render failure.
GET BASE AppUsage.html
Generate/read the registered App Usage report.
200 HTML; 404 unavailable; 500 render failure.
GET BASE AppUsage.YYYY-MM.js
Read one registered month of App Usage data.
200 JavaScript; 404 unavailable.
GET BASE KeyBind.css / KeyBind.js / KeyBind.ico
Read registered report assets.
200 asset or 404.

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.

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.

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.

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"}]}}

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":"..."}]}

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.

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.

INI key
Default
Meaning
server_enabled
true
Master switch. Off stops every HTTP endpoint, including authorization. Reports can still open as static files.
server_port
7566
Integer 1-65535. Changing closes the server; Always on restarts immediately, otherwise a report starts it later. Existing client URLs keep the old port until updated.
server_uses_tokens
true
Require recognized 32-hex URL tokens on protected routes. Changes close/restart the server as appropriate. Bootstrap auth stays unprefixed.
server_secure
true
Exact Host and protected POST Origin validation, with documented app-token/bootstrap exceptions. Off adds wildcard CORS and limited OPTIONS handling.
server_keep_alive
5m
Saved choice: 1m, 2m, 5m, 10m, 15m, 30m, 60m or permanently. HTTP changes do not save this preference.
server_loads_at_launch
false
Always on: start at launch when Enabled, force permanent keep-alive, lock report selector.
server_debug_level
0
Server logging 0-10; zero is off. Independent of main debug level.
server_log
@DataDir\server.log
Log path with @Tokens supported. Empty disables writes.

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

Limit
Current value
Concurrent HTTP clients
8
Client timeout
30 seconds from acceptance; synchronous Tasks can delay servicing.
Entire request
16384 bytes including request line and headers.
Task text
4096 UTF-8 bytes.
Save replacement / complete save body
4096 / 12288 UTF-8 bytes.
New HotKey text
256 UTF-8 bytes.
Returned textual value
4096 characters, with truncated flag.
Auth body / trimmed label / trimmed reason
512 UTF-8 bytes / 64 characters / 256 characters.
Undecided approvals
4
Decision deadline / outcome retention
120 seconds from request / 120 seconds from decision.
Execution/result slots
One shared action and one latest result, not one per token.
HTTP status
Meaning here
200
Read, synchronous keep-alive result, auth state or collected action result. It can contain state:denied/error or a nonzero execution error.
202
Accepted action or pending authorization; not completion.
204
Supported successful OPTIONS preflight with Origin checking off.
400
Invalid route/body/framing. Oversized api/task text also receives 400; not every size rejection uses 413.
403
Invalid Host/token/Origin or refused preflight. Missing credentials use 403, not 401.
404
Unknown protected route/resource/control or unavailable latest Task ID.
409
Busy action slot or stale generation. Changed saved-original text is instead collected execution error 6.
410
Unknown/expired authorization ID.
411
Protected POST missing Content-Length. Auth framing uses its own 400 for missing length.
413
Overall request/save/HotKey size limit, or rejected Sentinel value size/line content, depending on route.
429
Four authorization requests already await a decision.
500
Auth-request random-ID failure or registered report regeneration failure. A later approval generation/save failure is a collected state:error outcome.

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

Symptom
Fix
Connection refused / connect timeout
Check KeyBind, master Enabled, port and server expiry. A token cannot wake the server. Do not discard persistent credentials just because it is offline.
403 Invalid report token
Supply real lowercase 32-hex text as the first path segment. The saved KeyBind Server token survives process restarts; it becomes invalid when its access is revoked or its saved authorization is removed or changed. App tokens can be revoked/replaced. Literal TOKEN is rejected, not interpreted as a valid prefix.
403 Invalid Host
Use 127.0.0.1 with the exact configured port. Avoid localhost and proxies rewriting Host. Port 80 expects the numeric host without :80.
403 Invalid Origin
Process-token/token-free protected POSTs need exact Origin while checking is on. An app-token exemption applies only when token checking is on. Bootstrap routes need no Origin.
Existing label still asks for approval
Labels do not authorize. Load the saved token directly. Fresh requests never retrieve an existing token by label.
410 during authorization
The request/outcome expired or the server restarted and cleared pending data. Request again only when authorization is still intended.
Persistent approval could not be saved
The collected state:error is not approval. Correct the write problem and deliberately request another decision.
404 Unknown resource/action
Check protected base, method and exact route. Do not POST status, append a trailing slash or retain a token segment when tokens are off. Report-control endpoints need currently registered IDs.
409 action pending/running
All clients share one slot. Wait for the outstanding action and collect its result where possible instead of repeatedly submitting state-changing POSTs.
202 but result delayed
Acceptance precedes execution. KeyBind can be busy or waiting for a desktop dialog. Poll the known ID with a suitable timeout.
200 but no saved/deleted/updated confirmation
HTTP delivery succeeded, not necessarily the operation. Read state, numeric error, message and the endpoint's confirmation field.
Result ID becomes 404
Another action can replace the one retained result. The old action may already have executed; do not repeat automatically.
Save result error 6
The saved original changed. Read it again, compare it with the intended edit and submit an explicitly reconciled replacement.
Browser fetch blocked with a valid app token
Native authorization and CORS differ. Secure responses do not grant a foreign page access. With Origin checking off, preflight is still limited to the allowlist, and browser local-network policies can add restrictions.

Client implementation checklist

  1. Use the configured numeric loopback origin and an available server.
  2. Request authorization deliberately, store the returned token privately and reuse it on later launches.
  3. Send raw UTF-8 with correct byte Content-Length and the route's exact frame.
  4. Keep accepted action IDs, poll reads and inspect final results rather than treating 202 as completion.
  5. Do not automatically repeat a state-changing request after uncertain delivery.
  6. Use fresh report IDs/generations, or named control commands when those fit better.
  7. Distinguish server lifetime, pending authorization lifetime and credential lifetime.
  8. Report denial, revoked credentials, write failure and server unavailability without repeatedly prompting for approval.