| 1 | --- |
| 2 | title: API Reference |
| 3 | description: Portal relay API contract, endpoint groups, auth, and shared response rules. |
| 4 | --- |
| 5 | |
| 6 | # API Reference |
| 7 | |
| 8 | Portal relay exposes one control-plane API. Frontends, SDK clients, peer relays, |
| 9 | and operators all talk to this API, but each group has a small owned surface. |
| 10 | Local agent endpoints under `/agent/*` are not part of the relay API. |
| 11 | |
| 12 | ## JSON Envelope |
| 13 | |
| 14 | Matched JSON control endpoints return this envelope: |
| 15 | |
| 16 | ```json |
| 17 | { |
| 18 | "ok": true, |
| 19 | "data": {} |
| 20 | } |
| 21 | ``` |
| 22 | |
| 23 | Error responses use the same envelope with `error` instead of `data`: |
| 24 | |
| 25 | ```json |
| 26 | { |
| 27 | "ok": false, |
| 28 | "error": { |
| 29 | "code": "unauthorized", |
| 30 | "message": "unauthorized" |
| 31 | } |
| 32 | } |
| 33 | ``` |
| 34 | |
| 35 | `data` is omitted on error. `error` is omitted on success. |
| 36 | |
| 37 | The envelope does not apply to streaming or delegated endpoints: |
| 38 | |
| 39 | | Path | Format | |
| 40 | |------|--------| |
| 41 | | `/sdk/connect` | HTTP/1.1 connection hijack | |
| 42 | | `/v1/sign` | keyless TLS signer protocol | |
| 43 | | `/api/x402/*` | relay-owned x402 facilitator response | |
| 44 | | `/api/install.sh`, `/api/install.ps1`, `/api/install/bin/*` | script or binary bytes | |
| 45 | |
| 46 | Unknown routes may be handled by the frontend/proxy layer or return a normal |
| 47 | HTTP 404 outside the envelope. |
| 48 | |
| 49 | ## Auth Schemes |
| 50 | |
| 51 | | Name | Used by | How it is sent | |
| 52 | |------|---------|----------------| |
| 53 | | None | public and challenge endpoints | no credential | |
| 54 | | Admin bearer | admin API | `Authorization: Bearer <access_token>` | |
| 55 | | Lease token header | tunnel stream and keyless signer | `X-Portal-Access-Token: <access_token>` | |
| 56 | | Lease token body | lease renew/unregister | JSON field `access_token` | |
| 57 | | Signed descriptor | relay discovery announce | signed `RelayDescriptor` body | |
| 58 | | Signed hop route | relay overlay route | signed `HopRoute` body | |
| 59 | |
| 60 | Admin auth and SDK lease auth issue different tokens and are not |
| 61 | interchangeable. SDK lease registration uses SIWE; relay admin access uses the |
| 62 | configured admin token. |
| 63 | |
| 64 | ## Endpoint Groups |
| 65 | |
| 66 | ### Public |
| 67 | |
| 68 | | Method | Path | Auth | Response | |
| 69 | |--------|------|------|----------| |
| 70 | | `GET` | `/` | None | service identity | |
| 71 | | `GET` | `/api/healthz` | None | `{ "status": "ok" }` | |
| 72 | | `GET` | `/api/state` | None | `PublicStateResponse` | |
| 73 | | `GET`/`HEAD` | `/api/install.sh`, `/api/install.ps1` | None | install script | |
| 74 | | `GET`/`HEAD` | `/api/install/bin/{slug}` | None | install binary or redirect | |
| 75 | |
| 76 | ### Frontend Presentation API |
| 77 | |
| 78 | These paths are served by the TypeScript API service when the static frontend stack is |
| 79 | enabled. They live under `/ui/` and are derived from relay APIs plus frontend-owned |
| 80 | presentation state. |
| 81 | |
| 82 | | Method | Path | Auth | Response | |
| 83 | |--------|------|------|----------| |
| 84 | | `GET` | `/ui/state` | None | `PublicStateResponse` plus `landing_page_enabled` | |
| 85 | | `GET` | `/ui/service/status?hostname=...` | None | `ServiceStatusResponse` | |
| 86 | | `GET` | `/ui/policy/state` | Admin bearer | `PolicyStateResponse` plus `landing_page_enabled` in `policy` | |
| 87 | | `GET`/`POST` | `/ui/policy` | Admin bearer | `PolicySettings` plus `landing_page_enabled` | |
| 88 | | `POST` | `/ui/policy/leases`, `/ui/policy/ips` | Admin bearer | relay policy update response | |
| 89 | | `GET` | `/ui/thumbnail/{hostname}` | None | generated image | |
| 90 | |
| 91 | ### SDK |
| 92 | |
| 93 | | Method | Path | Auth | Body | Response | |
| 94 | |--------|------|------|------|----------| |
| 95 | | `GET` | `/sdk/domain` | None | none | `DomainResponse` | |
| 96 | | `POST` | `/sdk/register/challenge` | None | `RegisterChallengeRequest` | `RegisterChallengeResponse` | |
| 97 | | `POST` | `/sdk/register` | SIWE signature body | `RegisterRequest` | `RegisterResponse` | |
| 98 | | `POST` | `/sdk/renew` | lease token body | `RenewRequest` | `RenewResponse` | |
| 99 | | `POST` | `/sdk/unregister` | lease token body | `UnregisterRequest` | `{}` | |
| 100 | | `GET` | `/sdk/connect` | lease token header | none | hijacked stream | |
| 101 | |
| 102 | `/sdk/hop` is a relay-to-relay overlay route endpoint. It is not used by normal |
| 103 | SDK clients. |
| 104 | |
| 105 | ### Admin |
| 106 | |
| 107 | | Method | Path | Auth | Body | Response | |
| 108 | |--------|------|------|------|----------| |
| 109 | | `POST` | `/api/admin/auth/login` | None | `AdminAuthLoginRequest` | `AdminAuthLoginResponse` | |
| 110 | | `GET` | `/api/admin/auth/status` | Optional admin bearer | none | `AdminAuthStatusResponse` | |
| 111 | | `POST` | `/api/admin/auth/logout` | Admin bearer | none | `{}` | |
| 112 | |
| 113 | `/admin` itself is a frontend route, not a relay API endpoint. |
| 114 | |
| 115 | ### Payments |
| 116 | |
| 117 | Relay `/api/x402/*` endpoints are optional relay-owned control-plane |
| 118 | facilitator endpoints. Enable them with `X402_ENABLED=true` when a relay |
| 119 | operator wants to reserve support for relay resources such as future tunnel |
| 120 | registration fees, lease renewal fees, raw TCP/UDP port allocation, or premium |
| 121 | capacity. They are served by the embedded `gosuda/x402-facilitator` handler and |
| 122 | do not use the Portal JSON envelope. Portal selects Sui mainnet by default and |
| 123 | Sui testnet when `X402_TESTNET=true`. Portal accepts only USDC gasless |
| 124 | stablecoin address-balance payments. `X402_PAY_TO` is the relay-owned payment |
| 125 | recipient. |
| 126 | |
| 127 | Relay x402 settings do not affect tunnel paid routes. Tunnel payment recipients |
| 128 | and payment networks are local tunnel configuration and are not part of the |
| 129 | relay lease API. |
| 130 | |
| 131 | Paid routed HTTP tunnels additionally expose `/x402/prepare` and |
| 132 | `/x402/client.js` on the public tunnel origin. Those are tunnel-owned helper |
| 133 | endpoints for app frontends, not relay API routes, and they do not use the |
| 134 | `/api` prefix. Tunnel paid routes use Sui mainnet by default and Sui testnet |
| 135 | when the tunnel is exposed with `--x402-testnet` or configured with |
| 136 | `x402_testnet = true`. `/x402/client.js` is browser-only; native clients call |
| 137 | `/x402/prepare` directly and send `X-PAYMENT` on the protected request. |
| 138 | |
| 139 | | Method | Path | Auth | Body | Response | |
| 140 | |--------|------|------|------|----------| |
| 141 | | `GET` | `/api/x402/supported` | None | none | x402 supported kinds | |
| 142 | | `POST` | `/api/x402/verify` | None | x402 verify request | x402 verify response | |
| 143 | | `POST` | `/api/x402/settle` | None | x402 settle request | x402 settle response | |
| 144 | |
| 145 | ### Policy |
| 146 | |
| 147 | | Method | Path | Auth | Body | Response | |
| 148 | |--------|------|------|------|----------| |
| 149 | | `GET` | `/api/policy` | Admin bearer | none | `PolicySettings` | |
| 150 | | `POST` | `/api/policy` | Admin bearer | `PolicySettings` | `PolicySettings` | |
| 151 | | `GET` | `/api/policy/state` | Admin bearer | none | `PolicyStateResponse` | |
| 152 | | `POST` | `/api/policy/leases` | Admin bearer | `LeasePolicyUpdate` | `{}` | |
| 153 | | `POST` | `/api/policy/ips` | Admin bearer | `IPPolicyUpdate` | `{}` | |
| 154 | |
| 155 | ### Relay |
| 156 | |
| 157 | | Method | Path | Auth | Response | |
| 158 | |--------|------|------|----------| |
| 159 | | `GET` | `/discovery` | None | `DiscoveryResponse` | |
| 160 | | `POST` | `/discovery/announce` | Signed descriptor | `DiscoveryAnnounceResponse` | |
| 161 | | `POST` | `/v1/sign` | Lease token header | keyless signer response | |
| 162 | |
| 163 | ## Shared Types |
| 164 | |
| 165 | Timestamps are JSON-encoded Go `time.Time` values. |
| 166 | |
| 167 | `Identity`: |
| 168 | |
| 169 | | Field | Type | Notes | |
| 170 | |-------|------|-------| |
| 171 | | `name` | `string` | DNS label used by the lease | |
| 172 | | `address` | `string` | Ethereum address | |
| 173 | |
| 174 | `LeaseMetadata`: |
| 175 | |
| 176 | | Field | Type | Notes | |
| 177 | |-------|------|-------| |
| 178 | | `description` | `string` | optional | |
| 179 | | `owner` | `string` | optional | |
| 180 | | `thumbnail` | `string` | optional URL or data value | |
| 181 | | `tags` | `string[]` | optional | |
| 182 | | `hide` | `boolean` | hidden leases are omitted from the public state | |
| 183 | |
| 184 | `Lease`: |
| 185 | |
| 186 | | Field | Type | |
| 187 | |-------|------| |
| 188 | | `name` | `string` | |
| 189 | | `expires_at`, `first_seen_at`, `last_seen_at` | `string` | |
| 190 | | `hostname` | `string` | |
| 191 | | `udp_enabled`, `tcp_enabled` | `boolean` | |
| 192 | | `tcp_addr` | `string` | |
| 193 | | `metadata` | `LeaseMetadata` | |
| 194 | | `ready` | `number` | |
| 195 | |
| 196 | `PolicyLease` extends `Lease` with: |
| 197 | |
| 198 | | Field | Type | |
| 199 | |-------|------| |
| 200 | | `identity_key`, `address` | `string` | |
| 201 | | `bps` | `number` | |
| 202 | | `client_ip`, `reported_ip` | `string` | |
| 203 | | `is_approved`, `is_banned`, `is_denied`, `is_ip_banned` | `boolean` | |
| 204 | |
| 205 | `ServiceStatusResponse`: |
| 206 | |
| 207 | | Field | Type | |
| 208 | |-------|------| |
| 209 | | `hostname` | `string` | |
| 210 | | `registered` | `boolean` | |
| 211 | | `service_alive` | `boolean` | |
| 212 | |
| 213 | `PolicyStateResponse`: |
| 214 | |
| 215 | | Field | Type | |
| 216 | |-------|------| |
| 217 | | `policy` | `PolicySettings` | |
| 218 | | `leases` | `PolicyLease[]` | |
| 219 | |
| 220 | `PolicyPortSettings`: |
| 221 | |
| 222 | | Field | Type | Notes | |
| 223 | |-------|------|-------| |
| 224 | | `enabled` | `boolean` | enables the transport | |
| 225 | | `max_leases` | `number` | `0` means unlimited | |
| 226 | |
| 227 | `PolicySettings`: |
| 228 | |
| 229 | | Field | Type | |
| 230 | |-------|------| |
| 231 | | `approval_mode` | `"auto"` or `"manual"` | |
| 232 | | `udp` | `PolicyPortSettings` | |
| 233 | | `tcp_port` | `PolicyPortSettings` | |
| 234 | |
| 235 | `LeasePolicyUpdate`: |
| 236 | |
| 237 | | Field | Type | Notes | |
| 238 | |-------|------|-------| |
| 239 | | `identity_key` | `string` | normalized `name:address` key | |
| 240 | | `is_banned` | `boolean` | optional | |
| 241 | | `is_approved` | `boolean` | optional | |
| 242 | | `is_denied` | `boolean` | optional; `true` also revokes approval | |
| 243 | | `bps` | `number` | optional; `0` removes the limit | |
| 244 | |
| 245 | `IPPolicyUpdate`: |
| 246 | |
| 247 | | Field | Type | |
| 248 | |-------|------| |
| 249 | | `ip` | `string` | |
| 250 | | `is_banned` | `boolean` | |
| 251 | |
| 252 | ## Common Errors |
| 253 | |
| 254 | | Code | Meaning | |
| 255 | |------|---------| |
| 256 | | `invalid_json` | request body is not valid JSON | |
| 257 | | `invalid_request` | request shape or value is invalid | |
| 258 | | `method_not_allowed` | endpoint does not accept the method | |
| 259 | | `unauthorized` | credential is missing, expired, or invalid | |
| 260 | | `feature_unavailable` | feature is disabled or not configured | |
| 261 | | `rate_limited` | request was throttled | |
| 262 | | `hostname_conflict` | lease hostname is already registered | |
| 263 | | `lease_not_found` | lease token or identity has no active lease | |
| 264 | | `lease_rejected` | lease is not currently allowed to route | |
| 265 | | `ip_banned` | source or reported IP is banned | |
| 266 | | `invalid_address` | address path or body value is invalid | |
| 267 | | `invalid_ip` | IP path value is invalid | |
| 268 | | `invalid_mode` | approval mode is not `auto` or `manual` | |
| 269 | | `http11_only` | endpoint requires HTTP/1.1 | |
| 270 | | `hijack_unsupported`, `hijack_failed` | reverse stream setup failed | |
| 271 | | `udp_disabled`, `udp_capacity_exceeded`, `udp_port_exhausted` | UDP lease cannot be allocated | |
| 272 | | `tcp_port_disabled`, `tcp_port_capacity_exceeded`, `tcp_port_exhausted` | TCP port lease cannot be allocated | |
| 273 | | `transport_mismatch` | request does not match the active lease transport | |
| 274 | | `internal` | unexpected server failure | |