Hoody Proxy
Section titled “Hoody Proxy”The Hoody Proxy makes every container capability reachable over HTTPS. Web servers are only part of it: terminals, displays, files, databases, browsers, and scripts all flow through the same proxy, each with its own URL.
API endpoints summary
Section titled “API endpoints summary”The Hoody Proxy is infrastructure that runs on your server, not an API you call directly. You configure it through the Hoody API:
Proxy configuration:
- Proxy Aliases - Create custom domains (
my-app.{serverName}.containers.hoody.com) - Proxy Permissions (Project) - Project-level access control
- Proxy Permissions (Container) - Container-level overrides
Related infrastructure:
- Container Network - Configure proxy/VPN routing
- Container Firewall - Network-level rules
- Container Operations - Start/stop containers
The proxy handles TLS, routing, and IP preservation on its own; you configure aliases and permissions through the API.
What the Hoody Proxy is
Section titled “What the Hoody Proxy is”The Hoody Proxy runs as a container on your bare metal server, the same machine that hosts your containers.
What it does:
- Routes all traffic to container services (terminal, display, files, exec, sqlite, browser, agent, code, curl, notifications, daemon, cron, pipe, notes, watch, run, proxy logs, tunnel, egress, and the optional bot)
- Terminates TLS for every connection
- Preserves real client IPs, so no
X-Forwarded-Forheaders are needed - Generates URLs for all services
- Enforces authentication and permissions
- Handles HTTP/1.1, HTTP/2, and WebSocket, plus HTTP/3 where it is enabled
What it runs on: a dedicated container on your server with access to:
- The host network stack (for IP preservation)
- Container networking (for service routing)
- TLS certificate management
- Permission configuration files
URLs for every capability
Section titled “URLs for every capability”The proxy generates a URL for each of these:
- display-X — Your desktop in a browser
- http-X — Any HTTP server is proxified
- https-X — Any HTTPS server is proxified
- ssh-22 — Native SSH, works with any clients
- terminal-X — Your shell with multiplayer
- files-X — Your filesystem as HTTP
- exec-X — Any script becomes an API
- browser-X — Automated browser for AI & humans
- curl-X — Complex APIs made simple
- sqlite-X — Instant databases everywhere
- daemon-X — Long-running services
- notify-X — Native notifications from the web
- code-X — VSCode in your browser
- agent-X — AI with full system control
This covers more than websites. Terminal sessions, desktop environments, file systems, and databases each get a generated URL of their own.
The URL pattern
Section titled “The URL pattern”A new container immediately has URLs for all of its services:
https://{projectId}-{containerId}-{service}-{instance}.{serverName}.containers.hoody.com └────┬────┘ └────┬─────┘ └───┬───┘ └───┬───┘ └─────┬─────┘ Project Container Service Instance Your Server (24-char) (24-char) Name Number LocationExample for one container:
Terminal: https://67e89abc123def456789abcd-890abcdef12345678901cdef-terminal-1.node-us.containers.hoody.comDisplay: https://67e89abc123def456789abcd-890abcdef12345678901cdef-display-1.node-us.containers.hoody.comFiles: https://67e89abc123def456789abcd-890abcdef12345678901cdef-files-1.node-us.containers.hoody.comSQLite: https://67e89abc123def456789abcd-890abcdef12345678901cdef-sqlite-1.node-us.containers.hoody.comExec: https://67e89abc123def456789abcd-890abcdef12345678901cdef-exec-1.node-us.containers.hoody.comBrowser: https://67e89abc123def456789abcd-890abcdef12345678901cdef-browser-1.node-us.containers.hoody.comAgent: https://67e89abc123def456789abcd-890abcdef12345678901cdef-agent-1.node-us.containers.hoody.comCode: https://67e89abc123def456789abcd-890abcdef12345678901cdef-code-1.node-us.containers.hoody.comcURL: https://67e89abc123def456789abcd-890abcdef12345678901cdef-curl-1.node-us.containers.hoody.comNotify: https://67e89abc123def456789abcd-890abcdef12345678901cdef-n-1.node-us.containers.hoody.comDaemon: https://67e89abc123def456789abcd-890abcdef12345678901cdef-daemon-1.node-us.containers.hoody.comCron: https://67e89abc123def456789abcd-890abcdef12345678901cdef-cron-1.node-us.containers.hoody.comPipe: https://67e89abc123def456789abcd-890abcdef12345678901cdef-pipe-1.node-us.containers.hoody.comNotes: https://67e89abc123def456789abcd-890abcdef12345678901cdef-notes-1.node-us.containers.hoody.comWatch: https://67e89abc123def456789abcd-890abcdef12345678901cdef-watch-1.node-us.containers.hoody.comRun: https://67e89abc123def456789abcd-890abcdef12345678901cdef-run-1.node-us.containers.hoody.comYou do not need to configure DNS. The proxy makes the services accessible on its own.
How routing works
Section titled “How routing works”Request flow for every container service:
User/AI/Device ↓POST https://67e89abc123def456789abcd-890abcdef12345678901cdef-terminal-1.node-us.containers.hoody.com/api/v1/terminal/execute ↓Hoody Proxy Container (on your server) ├─ Terminates TLS (port 443) ├─ Parses URL: projectId, containerId, service=terminal, instance=1 ├─ Validates authentication (if permissions configured) ├─ Preserves real client IP └─ Routes to internal service ↓Container's terminal service (internal port 76) ↓Response (through proxy, back to client)External clients see HTTPS on port 443, and the Hoody Proxy maps each service URL to its internal port.
Reserved ports and HTTP access
Section titled “Reserved ports and HTTP access”When enabled, the Hoody Kit runs its HTTP services on 0.0.0.0 at reserved ports inside each container’s own network namespace. These are in-container listeners with their own port space, not public ports, and they are never shared between containers. Two things follow: which ports are free for your apps, and how you reach the Kit.
Start your apps at port 80
Section titled “Start your apps at port 80”Every fixed core-service port is below 80 (the highest is 78), so 80 is the recommended starting point for your own HTTP apps. Ports 80 to 499 are always clear of the Kit.
Above 500, a few services and their dynamic pools come into play. Hoody assigns pool ports as you start browser, display, or code instances, so a port that is unused right now can be claimed later. If you run your app on a higher port, check it against the tables below.
Core Kit services, one fixed port each, all below 80:
| Port | Service | Port | Service |
|---|---|---|---|
4 | hoody-curl | 59 | hoody-egress |
5 | hoody-sqlite | 60 | hoody-cron |
40 | hoody-bot | 61 | hoody-watch |
44 | hoody-run | 74 | hoody-terminal (CLI) |
45 | hoody-agent | 75 | hoody-exec |
46 | hoody-notes | 76 | hoody-terminal |
48 | hoody-pipe | 77 | hoody-daemon |
50 | hoody-tunnel | 78 | hoody-files |
55 | hoody-workspaces |
hoody-bot is the chat front door. See Hoody Bot. Treat port 40 as reserved for it.
Higher ports & dynamic pools:
| Service | Fixed port | Dynamic pool |
|---|---|---|
hoody-code | 3971 | instances from 60000 |
hoody-display | 3998 | sessions from 4000 |
hoody-notifications | 3999 | None |
hoody-browser | 23333 | HTTP instances from 30000 |
hoody-browser also numbers its X displays from 501 (browser-1 shows on display-501). Those are display numbers, not TCP ports.
Reach Kit services through the proxy
Section titled “Reach Kit services through the proxy”Kit services listen only on their in-container ports, and you can’t reach them there directly. A request to localhost:<port> (or the container’s own address) from inside the container fails: most services answer 403, and hoody-files closes the connection. Kit services accept only requests that arrive through the Hoody Proxy.
Keep the two traffic directions separate:
- Expose your app → listen on
0.0.0.0:{port}, then reach it athttps://{projectId}-{containerId}-http-{port}.{serverName}.containers.hoody.comor through a proxy alias. - Reach a Kit service → use its service URL (e.g.
https://{projectId}-{containerId}-files-1.{serverName}.containers.hoody.com), an alias, or thehoodyCLI, never its raw port.
Stop or disable Kit services
Section titled “Stop or disable Kit services”Use hoody-daemon to stop or disable any Kit service it supervises, which is the supported and reversible way, or terminate its processes directly. The daemon itself is not one of its programs: it runs as the systemd unit hoody-daemon.service, which restarts it if its process ends, so stop it with systemctl over SSH.
Access web servers with the http program
Section titled “Access web servers with the http program”When you run a web server in a container, reach it through the http program:
Example: Apache2 on port 80
# Install Apache2 in containerapt-get install apache2
# Apache runs on port 80 by default# Access via http programhttps://67e89abc123def456789abcd-890abcdef12345678901cdef-http-80.node-us.containers.hoody.comExample: Node.js API on port 3000
# Your Node.js appapp.listen(3000, () => { console.log('API running on port 3000');});
# Access via http programhttps://67e89abc123def456789abcd-890abcdef12345678901cdef-http-3000.node-us.containers.hoody.comExample: Multiple web services in one container
# Frontend on port 3000# Backend on port 5000# Admin panel on port 8000
# Access each separatelyhttps://67e89abc123def456789abcd-890abcdef12345678901cdef-http-3000.node-us.containers.hoody.com (frontend)https://67e89abc123def456789abcd-890abcdef12345678901cdef-http-5000.node-us.containers.hoody.com (backend)https://67e89abc123def456789abcd-890abcdef12345678901cdef-http-8000.node-us.containers.hoody.com (admin)http-{port} in the URL routes to your service on that port.
Externally everything is HTTPS on port 443, and the proxy translates that to your internal port.
Always-on HTTPS
Section titled “Always-on HTTPS”Every container URL is served over HTTPS, and the proxy manages the certificates for you.
Automatic TLS
Section titled “Automatic TLS”- Wildcard certificates (
*.{serverName}.containers.hoody.com) protect all service URLs - Automatic provisioning - certificates managed by Hoody
- Automatic renewal - no manual intervention
- TLS 1.2+ - modern encryption standards
- HTTPS enforcement - HTTP automatically redirects to HTTPS
Private endpoints
Section titled “Private endpoints”Your container URLs never appear in public Certificate Transparency logs.
Standard Let’s Encrypt certificates are public: anyone can see what domains you’re hosting. Hoody uses per-server wildcard certificates (*.{serverName}.containers.hoody.com), so your specific container URLs remain private.
Unguessable URLs, not a permission boundary:
- 24-character hex container IDs = 2^96 possible combinations
- Even knowing your project ID, guessing a container ID: trillions of years at 1B attempts/second
The default proxy permission for a newly created container is allow, so the URL is the only access gate until you add proxy permissions. Treat the URL like a bearer credential: anyone it leaks to (browser history, chat logs, referer headers, screenshots) can hit the container directly. For real access control, set proxy permissions instead of relying on URL secrecy.
Real client IPs
Section titled “Real client IPs”Applications see the real client IP address directly in remoteAddr, with no special configuration and no header parsing.
How IP preservation works
Section titled “How IP preservation works”Most proxies replace the client IP with the proxy’s own, so applications have to parse X-Forwarded-For headers. That breaks:
- Legacy applications (don’t know about proxy headers)
- Standard firewalls (can’t filter by real IP)
- Analytics tools (see proxy IP instead of user IP)
Hoody’s proxy preserves the original client connection information end to end. When traffic reaches your container, the container sees the real client IP as if there were no proxy at all.
This holds for application code, firewall rules, and older apps with no proxy awareness:
// Any application, any languageapp.get('/', (req, res) => { const clientIP = req.connection.remoteAddress; console.log(`Request from: ${clientIP}`); // Shows: 203.0.113.50 (real client) // NOT: 10.0.0.1 (proxy IP)});# Use iptables with real client IPsiptables -A INPUT -s 203.0.113.0/24 -j ACCEPTiptables -A INPUT -s 198.51.100.0/24 -j DROP
# Works perfectly - sees real IPs// Old PHP code (no proxy awareness)<?php$client_ip = $_SERVER['REMOTE_ADDR'];// Works correctly - real client IP?>None of this needs configuration, code changes, or proxy headers.
Protocol support
Section titled “Protocol support”The proxy handles HTTP/1.1, HTTP/2, and WebSocket without configuration:
HTTP/2 and HTTP/3
Section titled “HTTP/2 and HTTP/3”- HTTP/1.1 - Universal compatibility
- HTTP/2 - Multiplexing for faster connections
- HTTP/3 (QUIC) - Low-latency over UDP, available where the server supports it
- Automatic negotiation - Client gets best protocol it supports
HTTP/3 is available on wildcard container hostnames where the server supports it. Custom domains serve HTTP/1.1 and HTTP/2 only.
WebSocket
Section titled “WebSocket”- WebSocket - For terminal sessions, real-time updates, live displays
- Bi-directional - Full-duplex communication
- Multiplayer support - Multiple WebSocket connections to same service
Hoody Terminal uses WebSocket through the proxy for real-time command execution. Multiple users can connect to the same terminal URL, each with their own WebSocket, which is what makes terminals multiplayer by default.
Protocol limitations
Section titled “Protocol limitations”Service instances
Section titled “Service instances”A container can run multiple instances of each service:
One container, multiple service instances:
https://67e89abc123def456789abcd-890abcdef12345678901cdef-terminal-1.node-us.containers.hoody.comhttps://67e89abc123def456789abcd-890abcdef12345678901cdef-terminal-2.node-us.containers.hoody.comhttps://67e89abc123def456789abcd-890abcdef12345678901cdef-terminal-3.node-us.containers.hoody.com
https://67e89abc123def456789abcd-890abcdef12345678901cdef-display-1.node-us.containers.hoody.comhttps://67e89abc123def456789abcd-890abcdef12345678901cdef-display-2.node-us.containers.hoody.com
https://67e89abc123def456789abcd-890abcdef12345678901cdef-sqlite-1.node-us.containers.hoody.comhttps://67e89abc123def456789abcd-890abcdef12345678901cdef-sqlite-2.node-us.containers.hoody.comEach instance is a separate session: terminal-1 is a different shell from terminal-2, and display-1 is a separate X display from display-2. SQLite is the exception: sqlite-1 and sqlite-2 reach the same service, and the db query parameter selects the database. Instances share the container and are not an access boundary between each other.
The proxy routes to the correct instance based on the instance number in the URL. For SQLite the number does not change where the request goes.
Authentication and permissions
Section titled “Authentication and permissions”The proxy enforces access control for container services.
Open access by default
Section titled “Open access by default”By default, anyone with the URL can reach the service. The URL contains:
- 24-char project ID (2^96 possibilities)
- 24-char container ID (2^96 possibilities)
That makes it practically unguessable. But anyone the URL reaches (proxies, CI logs, browser history, screenshots, referer headers) can hit the container directly, so treat the URL like a bearer credential rather than a real access gate. For anything you wouldn’t paste into a public channel, add explicit proxy permissions before exposing it.
Configurable permissions
Section titled “Configurable permissions”Add authentication when you need it:
$TOKENin the HTTP examples below refers to a Hoody API bearer token. Obtain and export one withexport TOKEN="$(hoody login --print-token)", or create an automation token per the authentication guide.
# Whole-document `permissions replace` does not work from the CLI today — its# --groups/--permissions values are transmitted as strings (and the container# variant never sends the required body `container` field), so the API rejects# them. Build the document with the granular commands below, or send it in one# call with the SDK/HTTP tabs.
# Project-level permissions (apply to all containers)hoody projects proxy groups ip set --project $PROJECT_ID \ --group-name <name> --range 203.0.113.0/24 --if-match file:v<N>hoody projects proxy groups permissions set --project $PROJECT_ID \ --group-name <name> --program terminal --access true --if-match file:v<N>hoody projects proxy default set --project $PROJECT_ID --default deny --if-match file:v<N>
# Container-level permissions (override project settings)hoody containers proxy groups ip set -c $CONTAINER_ID \ --group-name <name> --range 0.0.0.0/0 --if-match file:v<N>hoody containers proxy groups permissions set -c $CONTAINER_ID \ --group-name <name> --program http --access 8080 --if-match file:v<N>hoody containers proxy default set -c $CONTAINER_ID --default deny --if-match file:v<N>
# Each write bumps file_version. Before every project write, re-read it with:# hoody projects proxy permissions get --project $PROJECT_ID -o json# Before every container write, re-read it with:# hoody containers proxy permissions get -c $CONTAINER_ID -o json// Project-level permissionsawait client.api.proxy.projectPermissions.set(PROJECT_ID, config, { ifMatch: 'file:v<N>' });
// Container-level permissions (override project)await client.api.proxy.containerPermissions.set(CONTAINER_ID, config, { ifMatch: 'file:v<N>' });# The If-Match ETag (file:v<N>) comes from a prior GET of the permissions document.
# Project-level permissions (apply to all containers)curl -X PUT "https://api.hoody.com/api/v1/projects/$PROJECT_ID/proxy/permissions" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -H "If-Match: file:v<N>" \ -d '{...}'
# Container-level permissions (override project settings)curl -X PUT "https://api.hoody.com/api/v1/containers/$CONTAINER_ID/proxy/permissions" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -H "If-Match: file:v<N>" \ -d '{...}'One request, one link
cURL runs inside your container and can wrap any HTTP request into a single GET URL. The call stops being something you need a client for and becomes something you can paste into a browser, send in a chat, bookmark, schedule with cron, or drop into a no-code tool.
Nothing is installed on the machine that opens it. The link does carry whatever credentials the call needs, so treat it as you would treat those credentials.
Slashes, colons and braces pass through as they are. The one character you must
encode is an & inside a value, which happens when the wrapped URL
carries its own query string. Left raw it ends the value early, and the rest is
read as cURL's own parameters, so you get a 200 on a request you did
not make.
How the wrapping works Chaining calls into one link Turning a link into a shortcut
Replaces the whole permissions document at project or container scope. Fetch the current document first: its file:v<N> ETag goes in If-Match, and its JSON body replaces {...}, or the call is rejected.
# Project-level
https://PROJECT_ID-CONTAINER_ID-curl-1.SERVER.containers.hoody.com/api/v1/curl/request?url=https://api.hoody.com/api/v1/projects/PROJECT_ID/proxy/permissions&method=PUT&bearer_token=TOKEN&header=If-Match:%20file:v<N>&header=Content-Type:%20application/json&data={...}&response=transparent
# Container-level
https://PROJECT_ID-CONTAINER_ID-curl-1.SERVER.containers.hoody.com/api/v1/curl/request?url=https://api.hoody.com/api/v1/containers/CONTAINER_ID/proxy/permissions&method=PUT&bearer_token=TOKEN&header=If-Match:%20file:v<N>&header=Content-Type:%20application/json&data={...}&response=transparent The link carries a credential and executes with it, so it is as sensitive as the credential itself — and it passes through the cURL service's request log on the way, not just the target's. Share it only where you would share the secret, and prefer a delegated token with minimal permissions and an expiry: see API tokens.
Authentication methods:
- JWT - Token-based with claims validation
- Password - Username/password via HTTP Basic Auth
- IP-based - Allow/deny by IP address or CIDR range
- Token - Exact token validation from a configured header, cookie, or query parameter
Program-specific permissions: Each group maps program names to access rules. Boolean rules allow or deny a program; network programs can additionally be limited to particular instance or port numbers, arrays, ranges, or *.
See: Proxy Permissions → for complete configuration.
Service discovery
Section titled “Service discovery”There are two ways to find out which URLs a container has.
Construct URLs from the pattern
Section titled “Construct URLs from the pattern”// You know: projectId, containerId, serverNameconst projectId = "67e89abc123def456789abcd";const containerId = "890abcdef12345678901cdef";const serverName = "node-us";
// Construct any service URLconst terminalUrl = `https://${projectId}-${containerId}-terminal-1.${serverName}.containers.hoody.com`;const displayUrl = `https://${projectId}-${containerId}-display-1.${serverName}.containers.hoody.com`;const sqliteUrl = `https://${projectId}-${containerId}-sqlite-1.${serverName}.containers.hoody.com`;Query the API
Section titled “Query the API”# Get container details with live service informationhoody containers get $CONTAINER_ID --runtime trueconst container = await client.api.containers.get(CONTAINER_ID, { runtime: 'true' });console.log(container.data.runtime_info);curl "https://api.hoody.com/api/v1/containers/$CONTAINER_ID?runtime=true" \ -H "Authorization: Bearer $TOKEN"One request, one link
cURL runs inside your container and can wrap any HTTP request into a single GET URL. The call stops being something you need a client for and becomes something you can paste into a browser, send in a chat, bookmark, schedule with cron, or drop into a no-code tool.
Nothing is installed on the machine that opens it. The link does carry whatever credentials the call needs, so treat it as you would treat those credentials.
Slashes, colons and braces pass through as they are. The one character you must
encode is an & inside a value, which happens when the wrapped URL
carries its own query string. Left raw it ends the value early, and the rest is
read as cURL's own parameters, so you get a 200 on a request you did
not make.
How the wrapping works Chaining calls into one link Turning a link into a shortcut
Fetches the container’s live service information, including which service instances are currently running.
https://PROJECT_ID-CONTAINER_ID-curl-1.SERVER.containers.hoody.com/api/v1/curl/request?url=https://api.hoody.com/api/v1/containers/CONTAINER_ID?runtime=true&method=GET&bearer_token=TOKEN&response=transparent The link carries a credential and executes with it, so it is as sensitive as the credential itself — and it passes through the cURL service's request log on the way, not just the target's. Share it only where you would share the secret, and prefer a delegated token with minimal permissions and an expiry: see API tokens.
The response includes live service information:
{ "data": { "id": "890abcdef12345678901cdef", "project_id": "67e89abc123def456789abcd", "server_name": "node-us", "runtime_info": { "terminals": [ { "id": "1", "display": 1, "username": "user" } ], "displays": [ { "display": 1, "pid": 12345, "connected_clients": 2 } ], "services": [ { "name": "hoody-sqlite", "status": "running", "pid": 23456 } ] } }}From that, construct URLs for the services that are running.
The Hoody API exception
Section titled “The Hoody API exception”The Hoody API does not use the container URL pattern:
Hoody API: https://api.hoody.com (platform management, no project/container prefix)
Container: https://{project}-{container}-terminal-1.{server}.containers.hoody.com (container service, includes project/container IDs)Why they differ:
- Hoody API manages the platform (create containers, configure networks, manage billing)
- Container services run inside containers (execute commands, access files, query databases)
Proxy capabilities
Section titled “Proxy capabilities”Custom aliases
Section titled “Custom aliases”Create memorable URLs for production:
# Create a memorable alias instead of cryptographic URLshoody proxy aliases create --container-id $CONTAINER_ID --alias my-api --program http --port 3000const alias = await client.api.proxy.aliases.create({ container_id: CONTAINER_ID, alias: 'my-api', program: 'http', port: 3000});console.log(alias.data.url);// https://my-api.node-us.containers.hoody.comcurl -X POST "https://api.hoody.com/api/v1/proxy/aliases" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "container_id": "'$CONTAINER_ID'", "alias": "my-api", "program": "http", "port": 3000 }'One request, one link
cURL runs inside your container and can wrap any HTTP request into a single GET URL. The call stops being something you need a client for and becomes something you can paste into a browser, send in a chat, bookmark, schedule with cron, or drop into a no-code tool.
Nothing is installed on the machine that opens it. The link does carry whatever credentials the call needs, so treat it as you would treat those credentials.
Slashes, colons and braces pass through as they are. The one character you must
encode is an & inside a value, which happens when the wrapped URL
carries its own query string. Left raw it ends the value early, and the rest is
read as cURL's own parameters, so you get a 200 on a request you did
not make.
How the wrapping works Chaining calls into one link Turning a link into a shortcut
Creates the alias my-api.{serverName}.containers.hoody.com, routed to the container’s HTTP service on port 3000.
https://PROJECT_ID-CONTAINER_ID-curl-1.SERVER.containers.hoody.com/api/v1/curl/request?url=https://api.hoody.com/api/v1/proxy/aliases&method=POST&bearer_token=TOKEN&json={"container_id":"CONTAINER_ID","alias":"my-api","program":"http","port":3000}&response=transparent The link carries a credential and executes with it, so it is as sensitive as the credential itself — and it passes through the cURL service's request log on the way, not just the target's. Share it only where you would share the secret, and prefer a delegated token with minimal permissions and an expiry: see API tokens.
The alias my-api.{serverName}.containers.hoody.com routes to your container’s HTTP service.
See: Proxy Aliases →
Custom domains
Section titled “Custom domains”Point your own domain at container services:
# Create alias as CNAME targethoody proxy aliases create --container-id $CONTAINER_ID --alias my-app --program http --port 3000
# Then point your domain via DNS# api.mycompany.com CNAME my-app.node-us.containers.hoody.com# SSL certificate provisioned automatically// Create alias as CNAME targetconst alias = await client.api.proxy.aliases.create({ container_id: CONTAINER_ID, alias: 'my-app', program: 'http', port: 3000});
// Then point your domain via DNS:// api.mycompany.com CNAME my-app.node-us.containers.hoody.com// SSL certificate provisioned automatically# Create alias as CNAME targetcurl -X POST "https://api.hoody.com/api/v1/proxy/aliases" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{"container_id": "'$CONTAINER_ID'", "alias": "my-app", "program": "http", "port": 3000}'
# Then point your domain via DNS# api.mycompany.com CNAME my-app.node-us.containers.hoody.com# SSL certificate provisioned automaticallyOne request, one link
cURL runs inside your container and can wrap any HTTP request into a single GET URL. The call stops being something you need a client for and becomes something you can paste into a browser, send in a chat, bookmark, schedule with cron, or drop into a no-code tool.
Nothing is installed on the machine that opens it. The link does carry whatever credentials the call needs, so treat it as you would treat those credentials.
Slashes, colons and braces pass through as they are. The one character you must
encode is an & inside a value, which happens when the wrapped URL
carries its own query string. Left raw it ends the value early, and the rest is
read as cURL's own parameters, so you get a 200 on a request you did
not make.
How the wrapping works Chaining calls into one link Turning a link into a shortcut
Creates the my-app alias as a CNAME target. Pointing your own domain at it via DNS and provisioning its TLS certificate both happen afterward, outside this call.
https://PROJECT_ID-CONTAINER_ID-curl-1.SERVER.containers.hoody.com/api/v1/curl/request?url=https://api.hoody.com/api/v1/proxy/aliases&method=POST&bearer_token=TOKEN&json={"container_id":"CONTAINER_ID","alias":"my-app","program":"http","port":3000}&response=transparent The link carries a credential and executes with it, so it is as sensitive as the credential itself — and it passes through the cURL service's request log on the way, not just the target's. Share it only where you would share the secret, and prefer a delegated token with minimal permissions and an expiry: see API tokens.
See: Connect a Domain →
Landing path for the root request
Section titled “Landing path for the root request”Give an alias a landing path, so a bare root request lands somewhere other than /:
# Create alias with target path routinghoody proxy aliases create --container-id $CONTAINER_ID \ --alias my-app --program http --port 3000 \ --target-path /api/v1 --allow-path-overrideconst alias = await client.api.proxy.aliases.create({ container_id: CONTAINER_ID, alias: 'my-app', program: 'http', port: 3000, target_path: '/api/v1', allow_path_override: true});curl -X POST "https://api.hoody.com/api/v1/proxy/aliases" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "container_id": "'$CONTAINER_ID'", "alias": "my-app", "program": "http", "port": 3000, "target_path": "/api/v1", "allow_path_override": true }'One request, one link
cURL runs inside your container and can wrap any HTTP request into a single GET URL. The call stops being something you need a client for and becomes something you can paste into a browser, send in a chat, bookmark, schedule with cron, or drop into a no-code tool.
Nothing is installed on the machine that opens it. The link does carry whatever credentials the call needs, so treat it as you would treat those credentials.
Slashes, colons and braces pass through as they are. The one character you must
encode is an & inside a value, which happens when the wrapped URL
carries its own query string. Left raw it ends the value early, and the rest is
read as cURL's own parameters, so you get a 200 on a request you did
not make.
How the wrapping works Chaining calls into one link Turning a link into a shortcut
Creates an alias whose bare root request lands on /api/v1 in the container’s HTTP service on port 3000. Every other request path is forwarded to the container unchanged.
https://PROJECT_ID-CONTAINER_ID-curl-1.SERVER.containers.hoody.com/api/v1/curl/request?url=https://api.hoody.com/api/v1/proxy/aliases&method=POST&bearer_token=TOKEN&json={"container_id":"CONTAINER_ID","alias":"my-app","program":"http","port":3000,"target_path":"/api/v1","allow_path_override":true}&response=transparent The link carries a credential and executes with it, so it is as sensitive as the credential itself — and it passes through the cURL service's request log on the way, not just the target's. Share it only where you would share the secret, and prefer a delegated token with minimal permissions and an expiry: see API tokens.
Routing:
my-app.node-us.containers.hoody.com/→ Container’s/api/v1(thetarget_path)my-app.node-us.containers.hoody.com/users→ Container’s/usersmy-app.node-us.containers.hoody.com/api/v1/posts→ Container’s/api/v1/posts
target_path is never used as a prefix. With allow_path_override: true (the default, used here) every non-root path is forwarded exactly as sent. With false the alias serves only the root, or target_path itself: any other path, sub-paths and assets included, answers 404 ALIAS_PATH_PINNED, and a parameter written in target_path cannot be overridden by the visitor. See Path override.
Multi-level permissions
Section titled “Multi-level permissions”Configure who can access what:
Project level (applies to all containers):
# Set project-level proxy permissions (IP-restricted).# `projects proxy permissions set` cannot express this today, so build the# document one field at a time, re-reading file_version between commands with# `hoody projects proxy permissions get --project $PROJECT_ID -o json`hoody projects proxy groups ip set --project $PROJECT_ID \ --group-name developers --range 203.0.113.0/24 --if-match file:v<N>hoody projects proxy groups permissions set --project $PROJECT_ID \ --group-name developers --program terminal --access true --if-match file:v<N>hoody projects proxy groups permissions set --project $PROJECT_ID \ --group-name developers --program files --access true --if-match file:v<N>hoody projects proxy default set --project $PROJECT_ID --default deny --if-match file:v<N>await client.api.proxy.projectPermissions.set(PROJECT_ID, { project: PROJECT_ID, groups: { developers: { type: 'ip', range: '203.0.113.0/24' } }, permissions: { developers: { terminal: true, files: true } }, default: 'deny'}, { ifMatch: 'file:v<N>' });# The If-Match ETag (file:v<N>) comes from a prior GET of the permissions document.curl -X PUT "https://api.hoody.com/api/v1/projects/$PROJECT_ID/proxy/permissions" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -H "If-Match: file:v<N>" \ -d '{ "project": "'$PROJECT_ID'", "groups": { "developers": { "type": "ip", "range": "203.0.113.0/24" } }, "permissions": { "developers": { "terminal": true, "files": true } }, "default": "deny" }'One request, one link
cURL runs inside your container and can wrap any HTTP request into a single GET URL. The call stops being something you need a client for and becomes something you can paste into a browser, send in a chat, bookmark, schedule with cron, or drop into a no-code tool.
Nothing is installed on the machine that opens it. The link does carry whatever credentials the call needs, so treat it as you would treat those credentials.
Slashes, colons and braces pass through as they are. The one character you must
encode is an & inside a value, which happens when the wrapped URL
carries its own query string. Left raw it ends the value early, and the rest is
read as cURL's own parameters, so you get a 200 on a request you did
not make.
How the wrapping works Chaining calls into one link Turning a link into a shortcut
Replaces the project’s whole permissions document with this IP-restricted policy. Fetch the current document first and put its ETag in place of file:v<N>, or the call is rejected.
https://PROJECT_ID-CONTAINER_ID-curl-1.SERVER.containers.hoody.com/api/v1/curl/request?url=https://api.hoody.com/api/v1/projects/PROJECT_ID/proxy/permissions&method=PUT&bearer_token=TOKEN&header=If-Match:%20file:v<N>&json={"project":"PROJECT_ID","groups":{"developers":{"type":"ip","range":"203.0.113.0/24"}},"permissions":{"developers":{"terminal":true,"files":true}},"default":"deny"}&response=transparent The link carries a credential and executes with it, so it is as sensitive as the credential itself — and it passes through the cURL service's request log on the way, not just the target's. Share it only where you would share the secret, and prefer a delegated token with minimal permissions and an expiry: see API tokens.
Container level (evaluated together with project):
# Override permissions for a specific container (public HTTP only).# `containers proxy permissions set` cannot express this today, so build the# document one field at a time, re-reading file_version between commands with# `hoody containers proxy permissions get -c $CONTAINER_ID -o json`hoody containers proxy groups ip set -c $CONTAINER_ID \ --group-name public --range 0.0.0.0/0 --if-match file:v<N>hoody containers proxy groups permissions set -c $CONTAINER_ID \ --group-name public --program http --access '[8080]' --if-match file:v<N>hoody containers proxy groups permissions set -c $CONTAINER_ID \ --group-name public --program files --access false --if-match file:v<N>hoody containers proxy default set -c $CONTAINER_ID --default deny --if-match file:v<N>await client.api.proxy.containerPermissions.set(CONTAINER_ID, { project: PROJECT_ID, container: CONTAINER_ID, groups: { public: { type: 'ip', range: '0.0.0.0/0' } }, permissions: { public: { http: [8080], files: false } }, default: 'deny'}, { ifMatch: 'file:v<N>' });# The If-Match ETag (file:v<N>) comes from a prior GET of the permissions document.curl -X PUT "https://api.hoody.com/api/v1/containers/$CONTAINER_ID/proxy/permissions" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -H "If-Match: file:v<N>" \ -d '{ "project": "'$PROJECT_ID'", "container": "'$CONTAINER_ID'", "groups": { "public": { "type": "ip", "range": "0.0.0.0/0" } }, "permissions": { "public": { "http": [8080], "files": false } }, "default": "deny" }'One request, one link
cURL runs inside your container and can wrap any HTTP request into a single GET URL. The call stops being something you need a client for and becomes something you can paste into a browser, send in a chat, bookmark, schedule with cron, or drop into a no-code tool.
Nothing is installed on the machine that opens it. The link does carry whatever credentials the call needs, so treat it as you would treat those credentials.
Slashes, colons and braces pass through as they are. The one character you must
encode is an & inside a value, which happens when the wrapped URL
carries its own query string. Left raw it ends the value early, and the rest is
read as cURL's own parameters, so you get a 200 on a request you did
not make.
How the wrapping works Chaining calls into one link Turning a link into a shortcut
Replaces the container-level document only; the project document stays in force except where entries overlap (container wins overlaps). This example opens HTTP port 8080 to everyone while denying files. Fetch the current document first and put its ETag in place of file:v<N>.
https://PROJECT_ID-CONTAINER_ID-curl-1.SERVER.containers.hoody.com/api/v1/curl/request?url=https://api.hoody.com/api/v1/containers/CONTAINER_ID/proxy/permissions&method=PUT&bearer_token=TOKEN&header=If-Match:%20file:v<N>&json={"project":"PROJECT_ID","container":"CONTAINER_ID","groups":{"public":{"type":"ip","range":"0.0.0.0/0"}},"permissions":{"public":{"http":[8080],"files":false}},"default":"deny"}&response=transparent The link carries a credential and executes with it, so it is as sensitive as the credential itself — and it passes through the cURL service's request log on the way, not just the target's. Share it only where you would share the secret, and prefer a delegated token with minimal permissions and an expiry: see API tokens.
See: Proxy Permissions → for complete authentication configuration.
What the URL model enables
Section titled “What the URL model enables”Every capability over HTTP
Section titled “Every capability over HTTP”When every capability is an HTTP endpoint, four things follow.
-
Any device can control any container
- Phone → Terminal → Execute deployment
- Tablet → Display → Access full desktop
- Smart watch → Files → Read logs
- IoT device → Agent → Trigger workflow
-
Services can call each other over HTTP
- hoody-exec calls hoody-terminal to run commands
- An exec script stores terminal results in hoody-sqlite
- hoody-agent orchestrates the rest over HTTP
- External APIs integrate via hoody-curl
-
AI agents can drive the same endpoints
- LLMs already understand HTTP
- There is no SDK to install and no custom training
- An HTTP request is the whole integration
-
Capabilities embed in other pages
- Terminals in documentation
- Desktops in dashboards
- Databases in spreadsheets
- All via
<iframe src="https://{project}-{container}-{service}.{server}.containers.hoody.com">
All of this works because the proxy gives every container capability an HTTP endpoint.
One proxy per server
Section titled “One proxy per server”Each of your bare metal servers runs its own Hoody Proxy container:
Server 1 (node-us): └─ Hoody Proxy Container ├─ Routes traffic to Server 1's containers └─ URLs: {project}-{container}-{service}.node-us.containers.hoody.com
Server 2 (node-eu): └─ Hoody Proxy Container ├─ Routes traffic to Server 2's containers └─ URLs: {project}-{container}-{service}.node-eu.containers.hoody.comContainer URLs include the server name (node-us, node-eu), so you know which proxy handles them.
Cross-server communication: Containers on different servers communicate via their public URLs (through their respective proxies). Use Realms for API isolation (token scoping and safety), not networking.
Technical details
Section titled “Technical details”Load balancing
Section titled “Load balancing”The proxy handles concurrent connections:
- Thousands of WebSocket connections per service
- HTTP keep-alive for efficiency
- Graceful degradation under load
Caching
Section titled “Caching”The proxy does not cache; it streams. Request and response bodies stream straight through without being cached. Set Cache-Control headers in your services for client-side or CDN caching, and the proxy forwards them untouched.
The proxy ignores the X-Accel-Redirect, X-Accel-Expires, X-Accel-Limit-Rate and X-Accel-Charset response headers from your services and does not pass them to the client: your service’s response reaches the client as you sent it. X-Accel-Buffering is still honoured, so X-Accel-Buffering: no keeps a stream unbuffered.
Request size limits
Section titled “Request size limits”Per-service limits are set by the individual Kit services (hoody-files, hoody-exec, hoody-sqlite, etc.); check each service’s OpenAPI spec for the exact limits. The proxy adds a ceiling of its own, and it depends on the route: requests to a container service are capped at 10 GiB before the Kit service’s own limit applies, while forward-proxy request bodies are capped at 150 MiB.
Error responses
Section titled “Error responses”When the proxy itself refuses a request (a permission denial, a container that is still provisioning, a body over the size limit, a rate limit, or a container that does not answer), it picks the response format from the request headers:
- A page load (
Sec-Fetch-Dest: document,iframeorframe) gets the HTML error page. Afetch()or XHR request (Sec-Fetch-Dest: empty,script, and similar) never gets the HTML page. - Otherwise the
Acceptheader decides betweenapplication/json,text/htmlandtext/plain, using q-values as in RFC 9110. - A tie, or a request with no
Acceptheader, is settled by a fixed preference among the tied types only: plain text, then HTML, then JSON forcurl; JSON, then HTML, then plain text for every other client. A client other thancurlthat accepts onlytext/*therefore gets HTML, andcurlwithAccept: application/json, text/htmlalso gets HTML. If none of the three types is acceptable, the answer is JSON.
The JSON body has a fixed message and a code:
{ "error": "Access denied.", "errorCode": "ACCESS_DENIED" }The plain-text form is one line, Error 403: Access denied.. An error in any of the three formats also carries:
X-Hoody-Error-Codewith the same code, when the code is upper case. The two lower-case codes,permission_backend_unavailableandmigration_pending, appear only in the body;Cache-Control: no-store;Vary: Accept, Sec-Fetch-Dest, User-Agent, added to anyVaryalready present;WWW-Authenticateon a 401 andRetry-Afteron a 503, when the refusal has one.
A cross-origin page can read the error when the refusal comes with a CORS decision for its origin (permission refusals do). X-Hoody-Error-Code is then listed in Access-Control-Expose-Headers. Errors with no CORS decision, such as a 502 from a container that is down, stay unreadable to a cross-origin page.
When no specific code applies, the code follows the status: BAD_REQUEST (400), AUTH_REQUIRED (401), ACCESS_DENIED (403), NOT_FOUND (404), REQUEST_BODY_TOO_LARGE (413), MISDIRECTED_REQUEST (421), RATE_LIMITED (429), REQUEST_HEADERS_TOO_LARGE (431, request headers over the size limit on HTTP/1.1; over HTTP/2 the request gets no response), INTERNAL_ERROR (500), BACKEND_GATEWAY_ERROR (502), SERVICE_UNAVAILABLE (503) and BACKEND_TIMEOUT (504). These codes belong to the proxy. A Kit service that refuses a request answers with its own codes, and errors your own app returns pass through unchanged.
Some failures get no formatted error. A request the proxy cannot parse (a malformed request line or a URL over the length limit) and a method it does not accept, such as TRACE, get the server’s plain default page. TLS failures, HTTP/2 protocol errors and failures inside an open tunnel close the connection without an HTTP response.
Proxy vs container services
Section titled “Proxy vs container services”The proxy and the services it routes to have different jobs:
Hoody Proxy (Infrastructure)
Location: Container on your server
Responsibilities:
- TLS termination
- URL routing
- IP preservation
- Authentication enforcement
- Protocol handling
- Certificate management
You configure: Aliases, permissions, routing rules
Container Services (Capabilities)
Location: Inside your containers
Responsibilities:
- Actual functionality
- Command execution
- File operations
- Database queries
- Display rendering
You use: The HTTP endpoints they expose
The proxy routes to services; the services provide the capabilities.
Useful questions
Section titled “Useful questions”Does the Hoody Proxy run on Hoody’s servers or mine?
Section titled “Does the Hoody Proxy run on Hoody’s servers or mine?”Your server. The Hoody Proxy runs as a container on your own bare metal. All container traffic terminates on the machine you rent, and there is no central Hoody proxy tier between your users and your containers. Platform management operations go through the Hoody API.
Why can’t I access UDP services through the proxy?
Section titled “Why can’t I access UDP services through the proxy?”The Hoody Proxy carries HTTP/1.1, HTTP/2, WebSocket (TCP), and HTTP/3 over QUIC, the one UDP-based protocol it supports. Other UDP protocols cannot route through it. For UDP applications, use Container Firewall to configure direct access, or bring an IPv4 address to the container via IPv4 Management.
Do I need to manage SSL certificates for container services?
Section titled “Do I need to manage SSL certificates for container services?”No. All *.{serverName}.containers.hoody.com URLs use wildcard certificates automatically managed by Hoody. For custom domains (via CNAME), SSL is provisioned automatically via Let’s Encrypt when your DNS points to an alias. You never touch certificates.
Can I run my own reverse proxy inside a container?
Section titled “Can I run my own reverse proxy inside a container?”Yes. You can install nginx, Caddy, Traefik, or any other proxy inside a container. The Hoody Proxy routes to your container’s exposed port, and your internal proxy handles the routing from there. A common pattern is Hoody Proxy → nginx in the container → multiple backend services.
What if I run several HTTP servers in one container?
Section titled “What if I run several HTTP servers in one container?”Access them via different ports: http-3000, http-5000, http-8000 in the URL. The Hoody Proxy routes each to the correct internal port automatically. Alternatively, use one internal nginx proxy and route through http-80.
Does the proxy add latency to my applications?
Section titled “Does the proxy add latency to my applications?”Minimal. The proxy runs on the same physical server as your containers (localhost routing). TLS termination and IP preservation add <1ms overhead, which your application’s own response time dominates.
Can clients reach containers without the proxy?
Section titled “Can clients reach containers without the proxy?”Not by default. All traffic flows through the proxy for security and observability. However, you can configure direct access via IPv4 addresses or SSH which bypass the proxy completely.
Why do I see the real client IP with no headers?
Section titled “Why do I see the real client IP with no headers?”Hoody uses custom netfilter hooks at the kernel level to preserve the original client connection information. Your container sees the real client IP in remoteAddr as if there were no proxy, so there is no X-Forwarded-For parsing to do.
Can I disable the proxy for a specific container?
Section titled “Can I disable the proxy for a specific container?”Yes. Via SDK: client.api.proxy.containerPermissions.disable(CONTAINER_ID, {}, { ifMatch: 'file:v<N>' }), or HTTP: PATCH /api/v1/containers/{id}/proxy/permissions/state with {"enable_proxy": false} and an If-Match: file:v<N> header (the ETag from a prior GET). Via CLI: hoody containers proxy disable disables the proxy and hoody containers proxy enable turns it back on (hoody projects proxy disable and hoody projects proxy enable do the same at project scope). When disabled, the proxy stops serving that container: new requests to its service URLs are answered with 403 instead of being forwarded. This is a kill-switch, not a bypass: the switch is evaluated before authentication groups, permission rules, and the default policy, so nothing can re-open access while it is off. Set it at container level: the container setting wins over the project one in both directions, and because the API persists an explicit enable_proxy: true whenever a container permissions document is saved, a project-level false will not disable a container that has its own document. The container keeps running, and access that does not traverse the proxy (for example SSH) is unaffected. Re-enable to restore group/permission enforcement.
Do I need different proxies for different containers?
Section titled “Do I need different proxies for different containers?”No. One Hoody Proxy per server handles every container on that server. The proxy routes based on the projectId-containerId pattern in URLs. Each server runs its own proxy instance, but you don’t manage multiple proxies manually.
Troubleshooting
Section titled “Troubleshooting”Container service URLs not working
Section titled “Container service URLs not working”Problem: Service URLs return connection errors or timeouts
Check container status:
Verify:
- status: “running” (not stopped or error)
- runtime_info shows services are running
Common causes:
-
Container not running:
POST Start the container (use operation=start)/api/v1/containers/{container_id}/{operation}Click "Run" to execute the request -
Service not started in container:
Terminal window # SSH into container and check# Or use terminal URL to verify service is running -
Wrong service name in URL:
Terminal window # Wrong: ...-shell-1... (no such service; the terminal service is named "terminal")# Correct: Check container's runtime_info for available services
401 and 403 errors on container URLs
Section titled “401 and 403 errors on container URLs”Problem: Cryptographic or alias URLs return authentication errors
Cause: Proxy permissions are configured, but you don’t match any group
Check permissions:
Solutions:
-
If IP-based auth: Verify you’re connecting from whitelisted IP
Terminal window curl https://ifconfig.me # Check your current IP -
If JWT/password auth: Ensure credentials are correct
-
Temporarily remove permissions for testing:
DELETE Remove container permissions (reverts to the project-level document; open only if no project document exists)/api/v1/containers/{container_id}/proxy/permissionsClick "Run" to execute the request
Proxy disabled errors
Section titled “Proxy disabled errors”Problem: All container URLs return 403, and no permission rule explains it
A disabled proxy is not distinguishable from an ordinary access denial by looking at the response: both surface as Error 403: Access denied. for CLI clients and as the standard error page in a browser. Don’t try to diagnose this from the response body. Read the switch directly, and check the container level as well as the project level.
Check proxy state:
Solution:
Re-enable proxy at project level:
Or at container level:
TLS and SSL certificate errors
Section titled “TLS and SSL certificate errors”Problem: Browser shows certificate warnings for container URLs
Expected behavior: every *.{serverName}.containers.hoody.com URL has a valid wildcard certificate, so this should not happen
If it occurs:
- Clear browser cache - Old cached certificates might cause issues
- Try different browser - Isolate if browser-specific
- Check system time - Wrong time causes certificate validation failures
- Verify URL: Ensure you’re accessing the container’s real hostname under
*.{serverName}.containers.hoody.com, not a typo
HTTP service unreachable
Section titled “HTTP service unreachable”Problem: https://{projectId}-{containerId}-http-3000.node-us.containers.hoody.com returns connection error
Solutions:
-
Verify service is running on that port in container:
Terminal window # SSH or use terminal URL to checknetstat -tlnp | grep 3000# Should show service listening on port 3000 -
Check it’s the correct port number:
Terminal window # If your app runs on 5000, use:# https://{projectId}-{containerId}-http-5000.node-us.containers.hoody.com -
Check the address the service binds to:
0.0.0.0always works. With Hoody Kit, a service bound to IPv4 loopback (127.0.0.1) is reachable too, because hoody-daemon forwards the container’s incoming IPv4 traffic to loopback. A service bound only to IPv6 loopback (::1) is not reachable.// Reachable through the proxyapp.listen(3000, '0.0.0.0');app.listen(3000, '127.0.0.1');// Not reachable (IPv6 loopback only)app.listen(3000, '::1');
Real client IP not showing
Section titled “Real client IP not showing”Problem: Application sees proxy IP instead of real client IP
Hoody preserves real IPs via netfilter hooks, so this should not happen
If it occurs:
-
Verify you’re reading
remoteAddrcorrectly:Node.js const clientIP = req.connection.remoteAddress;// or req.socket.remoteAddress -
Contact support - this indicates an infrastructure issue
What’s next
Section titled “What’s next”Proxy features:
- Create Aliases → - Clean URLs:
my-app.node-us.containers.hoody.com - Connect a Domain → - Point
api.mycompany.comto containers - Configure Permissions → - Authentication and access control
Container services:
- The Hoody Kit → - The 19 published Kit services the proxy routes to. The proxy also handles additional protocol and internal routes.
- API Reference → - Complete endpoint documentation