Skip to main content

API protocol

APIServer carries explicitly registered node operations over an authenticated, reliable byte stream. This page defines the shared wire format. The APIServer node reference owns gateway settings and permission configuration; each target node's reference owns its operation names, request bodies, responses, and state-change guarantees.

For a runnable client, start with the Python client example. For account commands, use the administration CLI. They share the standard-library transport module ww_api.py; ww_api_cli.py shares connection options, bounded token loading and uncertain-write advice. The transport module remains independent of argparse; AuthenticationServer body/schema validation stays in ww_admin.py. Copying the generic example requires its sibling client/ directory too. It writes decoded result bytes directly to stdout. For node implementation, see Implementing node API operations.

Transport and framing​

Supported chains deliver an ordered reliable stream:

TcpListener -> APIServer
TcpListener -> HttpServer -> APIServer
UdpListener -> TcpOverUdpServer -> APIServer

APIServer opens no socket and adds no transport encryption. Remote access needs an authenticated encrypted transport with peer identity verification. API tokens do not prevent unauthenticated resource consumption; restrict reachability or authenticate peers in the transport. Input bounds do not eliminate connection floods. Direct UdpListener -> APIServer is unsupported: a length prefix does not repair datagram loss or reordering. TcpOverUdpServer reconstructs the reliable stream before APIServer reads it. See the transport examples for complete HTTP and reliable-UDP arrangements.

Every request and response has the same framing:

4-byte unsigned big-endian length | exactly length UTF-8 JSON bytes

The length excludes the prefix and must be nonzero and within the configured envelope limit. There is no newline or terminating NUL. A socket read, tunnel payload callback, HTTP chunk, or UDP packet is not a frame boundary: a prefix or body can span reads, and one read can contain several frames.

An HTTP composition carries these framed bytes in the HTTP body. The caller dechunks the response before reading the API prefix and uses that length to recognize a complete reply, without waiting for HTTP EOF or its final chunk. HttpServer handles its configured method and path. APIServer receives only body bytes; this interface adds no REST routing, header authentication, or per-operation HTTP status mapping.

Request envelope​

{
"id": "request-42",
"auth": {"client": "monitor", "token": "<configured token>"},
"selector": {"config": "edge-europe", "node": "auth-client"},
"operation": "status",
"content_type": "application/json",
"body_encoding": "utf8",
"body": "{\"include_accounting\":true}"
}

All shown fields are required except selector.config.

FieldMeaning
idNonempty correlation string, at most 64 UTF-8 bytes. It is echoed in the response and does not deduplicate requests.
auth.clientConfigured API client name, at most 256 UTF-8 bytes.
auth.tokenThe client's 32-byte static token, encoded as exactly 64 hexadecimal characters.
selector.configOptional exact logical configuration name, at most 256 UTF-8 bytes.
selector.nodeExact public top-level node name, at most 256 UTF-8 bytes.
operationExact registered operation name, at most 64 UTF-8 bytes.
content_typeContent type of the decoded node body, at most 128 UTF-8 bytes; it must match the operation's declared request type.
body_encodingutf8 or base64.
bodyA JSON string containing the encoded node request, even when that request is itself JSON.

Metadata names are case-sensitive. The outer parser rejects unknown or duplicate keys, wrong types, malformed UTF-8, trailing non-whitespace input, and decoded NUL in metadata or UTF-8 bodies. It bounds the envelope's nesting and values before allocating a JSON tree. These outer-envelope rules do not define the target node's body schema. Runtime lexical rejections return fixed protocol errors without configuration-parser stderr diagnostics; startup/configuration parsing retains its diagnostics.

Authentication, target selection, and permission​

Every envelope authenticates independently. An API token is separate from a traffic user's password and from AuthenticationClient's upstream producer credentials. Unknown client names and incorrect tokens both return unauthorized. Authentication happens before target lookup.

selector.config names a loaded configuration's logical name; it is not a filename and cannot load a configuration. When an APIServer is configured, all loaded configurations must have nonempty unique logical names. Generated internal child nodes are not public API targets.

Omitting selector.config works only when the exact node name occurs once across all declared top-level nodes in all loaded configurations. Count nodes even if they expose no API or the caller cannot access them. Registration and permission filtering cannot make an ambiguous name unique; supply the configuration name to resolve ambiguous_target.

After resolving the target, APIServer checks the client's exact allowlist entry for that configuration, node, and operation. There are no wildcard permissions or permissions implied by a different operation. A read-only descriptor does not make an operation public. Startup validates allowlist targets and registered operations and prepares immutable descriptors reused after registry freeze. A missing exact permission returns forbidden. An old TunnelApi symbol alone exposes nothing through this gateway. read_only is descriptive metadata; it enforces neither permission, rollback nor retry safety.

Body encoding and content type​

With body_encoding: "utf8", the decoded UTF-8 bytes of body are delivered unchanged. With "base64", strict canonical base64 carries arbitrary bytes, including NUL. Base64 is an encoding, not encryption.

APIServer does not parse, normalize, or reserialize the node request. For example, the envelope above delivers the bytes {"include_accounting":true} to AuthenticationClient's status handler. A different registered operation can define a different body format and content type. Use that node's reference for the accepted schema; a valid envelope can still contain an invalid node request.

Response envelope​

A successfully dispatched operation, including an operation that returns a node-level error, uses this envelope:

{
"id": "request-42",
"error": null,
"result": {
"code": 0,
"content_type": "application/json",
"body_encoding": "utf8",
"body": "{\"can_authenticate\":true}"
}
}

The body above is illustrative, not a complete status snapshot. result.code preserves the handler's result: 0 is success and 1 is an error. The registered operation declares the result content type and encoding, which need not match the request's encoding. Decode result.body using the returned body_encoding, then interpret it according to the target node's schema. An empty handler result becomes an empty body with its declared type and encoding.

Operation references remain authoritative:

Gateway errors​

A gateway failure has result: null:

{
"id": "request-42",
"error": {"code": "ambiguous_target", "message": "Specify the configuration name."},
"result": null
}

id is null when no trustworthy bounded correlation ID was recovered. Messages are fixed and bounded; clients should branch on the error code.

CodeMeaning
invalid_requestInvalid framing or request-envelope schema.
invalid_encodingUnsupported or invalid body encoding.
unauthorizedInvalid API credentials.
forbiddenThe operation is absent from the caller's exact allowlist.
target_not_foundThe selected public node could not be resolved.
ambiguous_targetThe unqualified node name matches multiple declared nodes.
unsupported_content_typeThe request content type does not match the operation.
busyGateway or dispatch capacity could not admit the work.
timeoutAn API deadline expired.
shutting_downAPI admission has closed.
response_too_largeA valid result cannot fit the encoded response limit or representable size.
invalid_responseThe handler returned invalid UTF-8 or NUL for a declared UTF-8 result.

Shared clients also recognize generic unsupported_api and unsupported_operation codes. Current APIServer does not emit them: startup resolves each permitted descriptor, and requests absent from the allowlist receive forbidden.

Gateway busy and shutting_down are definite pre-dispatch refusals: the handler was not invoked. Python create/update/password clients report these refusals without automatic retry. A node result containing the same code is a separate operation outcome; apply that operation's commit contract.

A gateway error and a node error are different layers. For example, an AuthenticationServer conflict is inside result.body with result.code: 1, while a gateway timeout is in error and has no node result. Delivery failure after dispatch can hide an already committed mutation; it does not turn that mutation into a rollback. invalid_response is a post-dispatch failure, so a mutation may already have committed. Invalid handler bytes are neither echoed nor logged. Base64 operations may return arbitrary bytes.

Ordering, connection closure, and limits​

Each connection executes one operation at a time. Complete pipelined requests wait in FIFO order, and replies retain that order. Separate connections can execute concurrently; operation-specific locks and preconditions govern their shared state.

Malformed framing/schema and invalid authentication close the connection after at most one bounded error. Gateway timeout and invalid_encoding also close the connection. Valid authorized target, operation, and body errors may continue in FIFO order. A resource refusal can close directly when even an error cannot fit. After a response starts, transport failure or output expiry closes the connection without injecting a second frame into a partial response. If a later queued request expires behind an earlier pending response, the connection closes rather than returning its error out of order. Clients should keep pipeline depth within the configured queue and request deadline; a slow head operation does not suspend later requests' deadlines. When the outer JSON has a valid id but later schema validation fails, the error echoes that ID. Invalid or unavailable IDs produce null.

The APIServer limits table defines settings and defaults. Envelope caps exclude the four-byte prefix; memory limits also account for retained allocations, fragments, queues, jobs, and temporary working storage. A node can impose a smaller body or work limit. JSON escaping and base64 expand encoded results, so the raw node-body length alone does not establish that its response envelope fits. Declared request lengths do not allocate unread bodies: frame capacity grows for received bytes, charging old/new capacity overlap. The hard input-byte limit includes copied incomplete input; pressure watermarks subtract those copied bytes so receiving can continue after raw backlog or older queued requests drain.

Predictive Pause also holds input while older complete/active requests leave insufficient budget for the next frame. Before its header is known, the check uses max_request_envelope + 4; afterward it uses the declared complete size, counting already buffered bytes once. Completion/output drain autonomously releases the hold. An incomplete frame alone can keep receiving. Pause is advisory; late or coalesced input can still exceed a hard cap and close, and the original frame deadline remains absolute.

The limits apply together: max_jobs=64 does not guarantee 64 concurrent jobs. With current defaults roughly seven small jobs fit the shared 64 MiB domain before substantial session storage because each reserves an 8 MiB handler allowance plus other storage. Actual retained input/output can reduce that capacity. Connections that have not yet supplied valid credentials share the separate max_unauthenticated_memory ceiling (16 MiB with defaults). Verification releases that extra charge while retaining ordinary memory accounting. This limits anonymous retention without exempting any later envelope from authentication; session-count and CPU limits remain shared.

DeadlineInterval
Frame assemblyFrom first-fragment arrival, including queue residence; trickle input does not reset it.
RequestFrom complete-request admission through response publication, including FIFO and worker wait.
OutputFrom publication through transport handoff, including paused time.
IdleWhile no frame, request, or output is in progress.

All configured limits are positive finite integers; zero does not disable a protection. A disconnect or timeout abandons delivery, but a running handler retains its reservations until it finishes. The gateway cannot interrupt and undo arbitrary work that has already committed.

Writes and uncertain replies​

The request id is correlation only. Reusing it creates no replay guarantee, checkpoint, or automatic deduplication. The generic client makes one attempt and does not retry writes automatically. The Python transport bounds each socket recv request to 64 KiB while preserving the accepted frame limit and one original monotonic deadline across resolution, connection, send and all response chunks. Short reads do not renew the deadline. Early EOF, oversized prefixes, malformed replies, timeout or transport loss after sending leave mutation delivery uncertain; pre-dispatch refusals remain definite.

Consult the target operation's contract before retrying. AuthenticationServer's public mutation APIs guarantee that a returned node-operation failure did not commit that invocation. A lost, malformed, or late reply, a disconnect, or a post-dispatch gateway error, including timeout or invalid_response, leaves its outcome unknown. A success acknowledges server memory; ordinary persistence and client refresh happen separately.

After a conflict or uncertain result, reread and explicitly reconsider the requested change. Do not silently fetch a fresh edit token and replay a write. Account creation recovery keeps the original ID; public existence alone does not prove which caller created it. Public reads omit credentials, so they also cannot prove which password is stored after a lost password-change reply. See the account administration examples and each operation's reference for the appropriate recovery steps.