PORTAL v2

API Reference

This page provides a complete reference for the Portal relay server HTTP API. Control-plane endpoints are served over the relay API HTTPS listener; tenant TLS is handled separately by the SDK using the relay’s keyless signing endpoint.

Response Envelope

Every API response uses a consistent JSON envelope:

{
  "ok": true,
  "data": { ... },
  "error": null
}
FieldTypeDescription
okbooleantrue if the request succeeded
dataTResponse payload (omitted on error)
errorobject \| nullError details (omitted on success)

Error responses include a structured error object:

{
  "ok": false,
  "error": {
    "code": "unauthorized",
    "message": "unauthorized"
  }
}

Authentication

Portal uses two separate authentication mechanisms depending on the caller.

SDK Authentication (SIWE Challenge/Response)

SDK clients authenticate using Sign-In with Ethereum (SIWE):

  1. POST a challenge request to /sdk/register/challenge with your identity
  2. Sign the returned SIWE message with your Ethereum private key
  3. POST the signed message to /sdk/register to receive a JWT access token
  4. Include the access token in subsequent requests via the X-Portal-Access-Token header or in the JSON request body

Admin Authentication (Wallet Session)

Admin clients authenticate with a wallet signature:

  1. POST to /admin/auth/challenge with { "address": "<wallet-address>" }
  2. Sign the returned SIWE message with the wallet
  3. POST the signed message to /admin/auth/login
  4. The server sets a portal_admin session cookie (HttpOnly, Secure, SameSite=Strict)
  5. Include the cookie in subsequent admin requests
  6. Sessions expire after 24 hours

The local agent has its own loopback wallet auth endpoints under /v1/agent/auth/*. Agent wallet sessions can read /v1/agent/status; mutating agent actions require the bearer token stored in the agent state directory. See Portal Agent for the local control API.

Endpoint Summary

SDK Endpoints

MethodPathDescriptionAuth
GET/sdk/domainGet relay domain and version infoNone
POST/sdk/register/challengeRequest a SIWE challenge for registrationNone
POST/sdk/registerComplete registration with signed challengeNone
POST/sdk/renewRenew an existing lease TTLAccess Token
POST/sdk/unregisterRemove an active leaseAccess Token
GET/sdk/connectEstablish reverse tunnel connectionAccess Token

Admin Endpoints

MethodPathDescriptionAuth
POST/admin/auth/challengeRequest wallet login challengeNone
POST/admin/auth/loginComplete wallet loginNone
POST/admin/logoutEnd admin sessionSession Cookie
GET/admin/auth/statusCheck authentication statusNone
GET/admin/snapshotGet full relay state snapshotSession Cookie
POST/admin/settings/landing-pageToggle landing pageSession Cookie
POST/admin/settings/udpConfigure UDP settingsSession Cookie
POST/admin/settings/tcp-portConfigure TCP port settingsSession Cookie
POST/admin/settings/approval-modeSet approval modeSession Cookie
POST/admin/leases/{name}/{addr}/banBan a lease identitySession Cookie
DELETE/admin/leases/{name}/{addr}/banUnban a lease identitySession Cookie
POST/admin/leases/{name}/{addr}/bpsSet bandwidth limit for a leaseSession Cookie
DELETE/admin/leases/{name}/{addr}/bpsRemove bandwidth limitSession Cookie
POST/admin/leases/{name}/{addr}/approveApprove a leaseSession Cookie
DELETE/admin/leases/{name}/{addr}/approveRevoke lease approvalSession Cookie
POST/admin/leases/{name}/{addr}/denyDeny a leaseSession Cookie
DELETE/admin/leases/{name}/{addr}/denyRemove lease denialSession Cookie
POST/admin/ips/{ip}/banBan an IP addressSession Cookie
DELETE/admin/ips/{ip}/banUnban an IP addressSession Cookie

System Endpoints

MethodPathDescriptionAuth
GET/healthzHealth checkNone
GET/discoveryRelay discoveryNone
POST/discovery/announceRelay discovery self-announceSigned Descriptor
POST/v1/signKeyless TLS signingAccess Token
GET/thumbnail/{hostname}Cached thumbnail screenshotNone
GET/tunnel/statusTunnel connection statusAccess Token

System Endpoints

These endpoints are small enough to document inline.

GET /healthz

Returns relay health status.

Response:

{
  "ok": true,
  "data": {
    "status": "ok"
  }
}

GET /discovery

Returns signed relay discovery descriptors for this relay and any known peer relays. Only available when discovery is enabled in the server configuration.

Response fields:

FieldTypeDescription
protocol_versionstringProtocol version identifier
generated_atstringISO 8601 timestamp
relaysRelayDescriptor[]Signed descriptors for this relay and known peer relays

RelayDescriptor contains the signed relay contract and relay-reported telemetry:

FieldTypeDescription
addressstringRelay signing address used to verify signature
versionstringDiscovery protocol version used by this signed descriptor
issued_atstringDescriptor issue time
expires_atstringDescriptor expiry time
api_https_addrstringPublic HTTPS API base URL
wireguard_public_keystringWireGuard overlay public key, present when overlay is enabled
wireguard_portnumberPublic WireGuard UDP port on the api_https_addr host, present when overlay is enabled
supports_overlaybooleanRelay can participate in WireGuard multi-hop overlay routing
supports_udpbooleanRelay can allocate public UDP leases
supports_tcpbooleanRelay can allocate raw TCP port leases
active_connectionsnumberCurrent proxied connection count reported by the relay
tcp_bpsnumberRecent proxied TCP throughput in bytes per second
signaturestringSignature over the descriptor fields above

Relay telemetry is sampled when the descriptor is issued; use issued_at to judge freshness.

Overlay peer support is advertised by supports_overlay. When it is true, wireguard_public_key and wireguard_port are present. The WireGuard endpoint host is the api_https_addr host, and the overlay IPv4 is derived from the WireGuard public key. Relay-local observations such as recent overlay reachability are not part of the signed descriptor.

Example:

curl https://relay.example.com/discovery

POST /discovery/announce

Submits this relay’s signed descriptor to a bootstrap relay so registry-external relays can enter the discovery mesh. Relays self-announce periodically when discovery is enabled.

Auth: Signed relay descriptor

Request fields:

FieldTypeRequiredDescription
protocol_versionstringNoDiscovery protocol version
descriptorRelayDescriptorYesSigned relay descriptor

Response fields:

FieldTypeDescription
protocol_versionstringDiscovery protocol version
acceptedbooleanWhether the descriptor was accepted

POST /v1/sign

Keyless TLS signing endpoint. Used by the SDK-side tenant TLS server during the default stream handshake. Requests must include a valid lease access token in the X-Portal-Access-Token header.

Only available when the API server is configured with a TLS private key.

Returns 404 Not Found if signing is not configured.

GET /thumbnail/{hostname}

Returns a cached thumbnail screenshot for a registered tunnel hostname.

Response headers:

HeaderValue
Content-TypeImage content type (e.g. image/png)
Cache-Controlpublic, max-age=300

Returns 404 Not Found if the hostname is not registered or no thumbnail is available.

Example:

curl https://relay.example.com/thumbnail/myapp.relay.example.com

Error Codes

All error codes that may appear in the error.code field:

CodeDescription
feature_unavailableRequested feature is not available
hijack_failedHTTP connection hijack failed
hijack_unsupportedHTTP connection hijack not supported
hostname_conflictHostname already in use by another lease
http11_onlyEndpoint requires HTTP/1.1
invalid_addressInvalid Ethereum address
invalid_ipInvalid IP address format
invalid_jsonMalformed JSON request body
invalid_modeInvalid approval mode value
invalid_requestGeneral request validation failure
internalInternal server error
ip_bannedSource IP is banned
lease_not_foundNo lease found for the given identity
lease_rejectedLease is not approved for routing
method_not_allowedHTTP method not allowed for this endpoint
unauthorizedAuthentication required or token invalid
udp_port_exhaustedNo UDP ports available
udp_disabledUDP transport is disabled
udp_capacity_exceededUDP lease capacity reached
tcp_port_exhaustedNo TCP ports available
tcp_port_disabledTCP port transport is disabled
tcp_port_capacity_exceededTCP port lease capacity reached
transport_mismatchTransport type mismatch