# Routes API

5 operations under /api/v1. Errors are RFC 9457 problem+json with a stable `code` field. The complete schemas are in https://nebulactrl.com/api/v1/openapi.json.

## POST /environments/{environmentId}/routes

Create a route service (staged until deployed)

A route service has no container: it publishes an external upstream (http), or exposes a tcp/udp port of a server (optionally for a managed service). Needs `projects:write` and `proxy:write`. The server is required. 422 codes: `invalid_config`, `invalid_listen_port`, `upstream_denied` (the server's upstream policy), `invalid_upstream`, `agent_too_old`. 409 `port_in_use` when another route holds the listen port, `domain_conflict` for a taken hostname.

Authentication: session cookie or `Authorization: Bearer nct_…` token.

Parameters:
- `Idempotency-Key` (header, string): Client-chosen key (<=255 chars). A retry with the same key replays the original response for 24 hours.
- `environmentId` (path, string, required)

Request body (application/json, required): CreateRouteInputBody
  - `allowCidrs` (string[]): tcp routes: client networks allowed; UDP cannot be restricted
  - `denyCidrs` (string[]): tcp routes: client networks refused first, even when allowCidrs admits them; UDP cannot be restricted
  - `domains` (DomainInput[] | null): Hostnames of an http route (staged; refine each with PATCH /domains/{id})
  - `healthPath` (string): Enables a proxy health check on this path
  - `insecureSkipVerify` (boolean): https upstreams: do not verify the upstream certificate
  - `listenPort` (integer (int64)): tcp/udp routes: host port on the server (1024-65535)
  - `name` (string, required)
  - `openToInternet` (boolean): tcp/udp routes: required when allowCidrs is empty
  - `passHost` (boolean): false rewrites the Host header to the upstream's (default true)
  - `protocol` (enum: http | tcp | udp, required)
  - `proxyProtocol` (enum: 0 | 1 | 2): tcp routes: PROXY protocol version sent to the upstream
  - `scheme` (enum: http | https): http routes: scheme used towards the upstream
  - `serverId` (string, required): The server that publishes the route (required)
  - `serverName` (string): https upstreams: TLS server name
  - `upstreams` (UpstreamInput[]): 1-20 upstreams
  - `verifySkip` (boolean): Skip the post-deploy HTTP verification (upstreams that legitimately answer 5xx)

Response 201: Created (ServiceView)
  - `appliedGeneration` (integer (int64), required)
  - `config` (Config, required)
  - `createdAt` (string (date-time), required)
  - `currentDeploymentId` (string, required)
  - `currentRelease` (ReleaseRef, required)
  - `desiredState` (enum: running | stopped, required)
  - `domains` (DomainView[] | null, required)
  - `environmentId` (string, required)
  - `generation` (integer (int64), required)
  - `git` (GitRef, required)
  - `gitTokenSet` (boolean, required)
  - `hasStagedChanges` (boolean, required)
  - `id` (string, required)
  - `kind` (enum: app | worker | database | route | stack, required)
  - `latestDeployment` (DeploymentRef, required)
  - `maintenance` (MaintenanceView, required)
  - `name` (string, required)
  - `networks` (NetworkRef[] | null, required)
  - `observed` (ObservedView, required)
  - `observedAt` (string (date-time) | null, required)
  - `pendingChangeCounts` (PendingChangeCounts, required)
  - `privateHost` (string, required)
  - `projectId` (string, required)
  - `registryAuthSet` (boolean, required)
  - `scaling` (ScalingView, required)
  - `server` (ServerRef, required)
  - `serverId` (string, required)
  - `slug` (string, required)
  - `sourceType` (enum: image | git | external | compose, required)
  - `stagedKind` (string | null, required): new | edited | null
  - `stale` (boolean, required)
  - `tailnet` (TailnetView, required)
  - `updatedAt` (string (date-time), required)
  - `volumes` (VolumeRef[] | null, required)
Errors: 401 Unauthorized; 403 Forbidden; 404 Not Found; 409 Conflict (`port_in_use`); 422 Unprocessable Entity; 500 Internal Server Error

## GET /routes/{routeId}

Get a route service

Authentication: session cookie or `Authorization: Bearer nct_…` token.

Parameters:
- `routeId` (path, string, required)

Response 200: OK (ServiceView)
  - `appliedGeneration` (integer (int64), required)
  - `config` (Config, required)
  - `createdAt` (string (date-time), required)
  - `currentDeploymentId` (string, required)
  - `currentRelease` (ReleaseRef, required)
  - `desiredState` (enum: running | stopped, required)
  - `domains` (DomainView[] | null, required)
  - `environmentId` (string, required)
  - `generation` (integer (int64), required)
  - `git` (GitRef, required)
  - `gitTokenSet` (boolean, required)
  - `hasStagedChanges` (boolean, required)
  - `id` (string, required)
  - `kind` (enum: app | worker | database | route | stack, required)
  - `latestDeployment` (DeploymentRef, required)
  - `maintenance` (MaintenanceView, required)
  - `name` (string, required)
  - `networks` (NetworkRef[] | null, required)
  - `observed` (ObservedView, required)
  - `observedAt` (string (date-time) | null, required)
  - `pendingChangeCounts` (PendingChangeCounts, required)
  - `privateHost` (string, required)
  - `projectId` (string, required)
  - `registryAuthSet` (boolean, required)
  - `scaling` (ScalingView, required)
  - `server` (ServerRef, required)
  - `serverId` (string, required)
  - `slug` (string, required)
  - `sourceType` (enum: image | git | external | compose, required)
  - `stagedKind` (string | null, required): new | edited | null
  - `stale` (boolean, required)
  - `tailnet` (TailnetView, required)
  - `updatedAt` (string (date-time), required)
  - `volumes` (VolumeRef[] | null, required)
Errors: 401 Unauthorized; 403 Forbidden; 404 Not Found; 422 Unprocessable Entity; 500 Internal Server Error

## PATCH /routes/{routeId}

Update a route service (staged until deployed)

Requires `If-Match: "<generation>"`. The protocol and server cannot be changed. Same 4xx codes as create; 412 `generation_conflict` for a stale generation.

Authentication: session cookie or `Authorization: Bearer nct_…` token.

Parameters:
- `routeId` (path, string, required)
- `If-Match` (header, string): The ETag (generation) from the last GET, e.g. "3". Required.

Request body (application/json, required): UpdateRouteInputBody
  - `allowCidrs` (string[]): tcp routes: client networks allowed; UDP cannot be restricted
  - `denyCidrs` (string[]): tcp routes: client networks refused first, even when allowCidrs admits them; UDP cannot be restricted
  - `healthPath` (string): Enables a proxy health check on this path
  - `insecureSkipVerify` (boolean): https upstreams: do not verify the upstream certificate
  - `listenPort` (integer (int64)): tcp/udp routes: host port on the server (1024-65535)
  - `name` (string)
  - `openToInternet` (boolean): tcp/udp routes: required when allowCidrs is empty
  - `passHost` (boolean): false rewrites the Host header to the upstream's (default true)
  - `proxyProtocol` (enum: 0 | 1 | 2): tcp routes: PROXY protocol version sent to the upstream
  - `scheme` (enum: http | https): http routes: scheme used towards the upstream
  - `serverName` (string): https upstreams: TLS server name
  - `upstreams` (UpstreamInput[]): 1-20 upstreams
  - `verifySkip` (boolean): Skip the post-deploy HTTP verification (upstreams that legitimately answer 5xx)

Response 200: OK (ServiceView)
  - `appliedGeneration` (integer (int64), required)
  - `config` (Config, required)
  - `createdAt` (string (date-time), required)
  - `currentDeploymentId` (string, required)
  - `currentRelease` (ReleaseRef, required)
  - `desiredState` (enum: running | stopped, required)
  - `domains` (DomainView[] | null, required)
  - `environmentId` (string, required)
  - `generation` (integer (int64), required)
  - `git` (GitRef, required)
  - `gitTokenSet` (boolean, required)
  - `hasStagedChanges` (boolean, required)
  - `id` (string, required)
  - `kind` (enum: app | worker | database | route | stack, required)
  - `latestDeployment` (DeploymentRef, required)
  - `maintenance` (MaintenanceView, required)
  - `name` (string, required)
  - `networks` (NetworkRef[] | null, required)
  - `observed` (ObservedView, required)
  - `observedAt` (string (date-time) | null, required)
  - `pendingChangeCounts` (PendingChangeCounts, required)
  - `privateHost` (string, required)
  - `projectId` (string, required)
  - `registryAuthSet` (boolean, required)
  - `scaling` (ScalingView, required)
  - `server` (ServerRef, required)
  - `serverId` (string, required)
  - `slug` (string, required)
  - `sourceType` (enum: image | git | external | compose, required)
  - `stagedKind` (string | null, required): new | edited | null
  - `stale` (boolean, required)
  - `tailnet` (TailnetView, required)
  - `updatedAt` (string (date-time), required)
  - `volumes` (VolumeRef[] | null, required)
Errors: 401 Unauthorized; 403 Forbidden; 404 Not Found; 409 Conflict; 412 Precondition Failed (`generation_conflict`); 422 Unprocessable Entity; 428 Precondition Required; 500 Internal Server Error

## DELETE /routes/{routeId}

Delete a route service

Frees the listen port and removes the route from its server's table (a sync is queued atomically). `DELETE /services/{id}` does the same.

Authentication: session cookie or `Authorization: Bearer nct_…` token.

Parameters:
- `routeId` (path, string, required)

Response 204: No Content
Errors: 401 Unauthorized; 403 Forbidden; 404 Not Found; 409 Conflict; 422 Unprocessable Entity; 500 Internal Server Error

## GET /servers/{serverId}/ports

List the tcp/udp ports route services hold on a server

Includes staged (not yet deployed) routes. Holder names of projects the caller cannot see are omitted.

Authentication: session cookie or `Authorization: Bearer nct_…` token.

Parameters:
- `serverId` (path, string, required)

Response 200: OK (PortsOutputBody)
  - `items` (PortView[] | null, required)
Errors: 401 Unauthorized; 403 Forbidden; 404 Not Found; 422 Unprocessable Entity; 500 Internal Server Error
