Endpoint reference
Your node serves HTTP on two listeners: a public one for client and relay traffic, and a private one for operator-only health and metrics.
This is a map, not the wire specification. A full protocol specification (exact paths, headers, request and response schemas, encryption framing, and signing formats) is coming soon; until then, the node software is the reference implementation.
The two listeners
| Listener | Config key | Default | TLS | Carries |
|---|---|---|---|---|
| Public | server.listen | 127.0.0.1:9090 (loopback; bind :9090 to serve) | Serve HTTPS directly (tls.mode: acme/manual), or terminate TLS at a single reverse proxy — never a load balancer | All client and relay traffic: inference, reserve, discovery, attestation, relay |
| Private | server.private_listen | 127.0.0.1:9091 | Never TLS-wrapped | Operator-only /healthz, /livez and /metrics |
The public listener's loopback default is safe but serves nobody. A serving node
binds all interfaces with :9090, which is dual-stack (IPv4 and IPv6);
0.0.0.0 would be IPv4-only. zs-node init writes that bind for you. The private listener is meant
to stay on loopback or a private network you control; never expose /healthz,
/livez or /metrics to the internet.
One port instead of two. Set server.private_listen to "" to colocate
/healthz, /livez and /metrics on the public port, for single-port
deployments. When you do set a port in YAML, quote it (private_listen: ":9091");
an unquoted leading colon is read as a YAML mapping marker.
Public endpoints
These are served on server.listen (default port 9090).
| Method · Path | Purpose |
|---|---|
POST /v1/responses | Sealed, streamable. The Responses-style inference route, and the only text route the web app uses. Your node decrypts the prompt, runs the model, streams the sealed reply, and returns a signed receipt. |
POST /v1/chat/completions | Sealed, streamable. OpenAI-style chat completions. zs-proxy sends a connected tool's chat-completions requests here; the web app doesn't use it. Whether your node serves it natively or translates depends on your provider (see Serving models & pricing). |
POST /v1/images/generations | Sealed. Generates images directly and returns them with a signed receipt. |
POST /v1/images/edits | Sealed (multipart). Edits a supplied image and returns the result with a signed receipt. |
GET /v1/models | Plaintext. Your live model list — what clients read to build the model picker. Always served unencrypted. |
GET /v1/models/{id} | Plaintext. Single-model lookup. |
POST /v1/zs/reserve | Sealed both ways. Two-phase admission, phase 1: returns a signed ticket (price ceiling, expiry) and the wrapped response key. The request has its own sealed content type (application/vnd.zs-reserve+json) so a relay can't read the payer's address; a plaintext request is rejected 415 bad_content_type. The 200 response is sealed too (application/vnd.zs-reserve-response+json), because its pre-signed open transaction names the payer's address. Error responses stay plaintext so the caller can see the cause. |
GET /v1/zs/details | Plaintext, unauthenticated. Your operator self-description: live catalog, capacities, protocol version, the node's build version, pricing, oracle/TEE status, a config_hash fingerprint of your policy config (see The config hash), and your current signed ephemeral encryption recipient (see Encryption & keys). Clients probe it for health and discovery and read it to build the model picker. Your NFD name isn't here; it resolves from your on-chain operator record. |
GET /v1/zs/attestation | TEE evidence bundle. Returns the node's attestation evidence when confidential mode is enabled, and 404 when TEE is disabled. See Confidential compute (TEE). |
POST /v1/zs/relay | The transport hop other operators' requests arrive through. Your node forwards a sealed payload to a named target node and streams the response back, without being able to read it. Registered only when a relay directory is available (escrow + algod configured). See Relays. |
Private endpoints
These are served on server.private_listen (default port 9091). Point your
supervisor and Prometheus scraper at this listener, not the public one. All three
routes are exempt from rate limiting.
| Method · Path | Purpose |
|---|---|
GET /healthz | Readiness probe — should traffic go here? 200 when serving, 503 {"status":"draining"} for the whole graceful shutdown, which is what pulls the node out of your load balancer. |
GET /livez | Liveness probe — is the process wedged? 200 whenever the node is responsive, including while draining. Point container liveness checks here, never at /healthz. On a node build that predates it this returns 404 — run no liveness probe at all on those, rather than falling back to /healthz. |
GET /metrics | Prometheus exposition — provider health, discovered model count, settlement state, and the rest of the node's operational metrics. |
How the public routes relate
A normal request touches the public routes in this order:
Discover — /v1/zs/details
Clients probe it in the background to discover you.
Reserve — /v1/zs/reserve
The client calls it to get a signed price ceiling and the keys for the request.
Run the model
The client calls a text route (/v1/responses, or /v1/chat/completions, which
only zs-proxy sends) or an image route (/v1/images/generations or
/v1/images/edits) to run the model.
Receipt & settlement
Your node returns a signed receipt; settlement follows on-chain (see The payment flow).
When your node relays someone else's request, that request arrives at
/v1/zs/relay instead, and you forward it to the target's reserve, inference,
or image endpoint. It stays sealed, so you never see its contents.
Sealed envelopes
The prompt-carrying routes don't accept plaintext. A sealed request carries the
content type application/vnd.zs+json; a POST to one of these routes that
isn't a proper envelope is rejected with 400 bad_envelope. Sealed responses
come back the same way, as an application/vnd.zs+json body with the response key
delivered out-of-band, and stream as sealed event: zs frames when the client
requests streaming. The discovery routes (/v1/models, /v1/models/{id},
/v1/zs/details) and the private routes are served unencrypted by design.
See Encryption & keys for how the sealing keys are generated, signed, and rotated.
Transport requirements
- HTTPS at a publicly reachable URL for the public listener. Clients and
relays both need to reach your advertised base URL from the open internet, with
TLS terminated either by your reverse proxy or by the node (
tls.mode: acme/manual). Reaching your node covers the hostname, certificate, DNS record, and on-chain base URL. - Streaming. The text routes stream; relays must forward streamed responses without buffering the whole reply.
- Current protocol version. The version you advertise in
/v1/zs/detailsmust match what clients speak, or you're flagged incompatible and skipped (see Health & compatibility).