
## Set the application port

`PATCH /v1/infra/servers/:id/port`

Sets the TCP port to which the tunnel agent proxies incoming HTTPS requests from the subdomain. By default the tunnel goes to `:3000` — this is the platform standard, and most applications should listen on exactly that. This endpoint is needed in rare cases: the application works on a different port for historical reasons, or you want `0` (auto-detect) so the agent finds the listening port itself. Ports 1–1023 are forbidden — these are system ports (SSH, HTTP, HTTPS, SQL), and using them would create a risk of redirecting traffic to service processes.

## Parameters

| Parameter | In | Type | Required | Description |
|----------|---|-----|:-----:|----------|
| `id` | path | string (UUID) | yes | BLACKHOLE server ID, `status: running`, `blackholeStatus: CONNECTED` |

## Request fields (body)

| Field | Type | Required | Description |
|------|-----|:-----:|----------|
| `port` | number | **yes** | `0` (auto-detect) or `1024–65535`. Ports 1–1023 are rejected with `PORT_RESTRICTED` |

## Examples

### curl — personal key

```bash
# Set port 8080
curl -X PATCH https://vibecode.bitrix24.com/v1/infra/servers/SERVER_ID/port \
  -H "X-Api-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"port": 8080}'

# Automatic detection
curl -X PATCH https://vibecode.bitrix24.com/v1/infra/servers/SERVER_ID/port \
  -H "X-Api-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"port": 0}'
```

### curl — OAuth application

```bash
curl -X PATCH https://vibecode.bitrix24.com/v1/infra/servers/SERVER_ID/port \
  -H "X-Api-Key: YOUR_APP_KEY" \
  -H "Authorization: Bearer USER_SESSION_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"port": 8080}'
```

### JavaScript — personal key

```javascript
const res = await fetch(
  `https://vibecode.bitrix24.com/v1/infra/servers/${serverId}/port`,
  {
    method: 'PATCH',
    headers: {
      'X-Api-Key': 'YOUR_API_KEY',
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ port: 8080 }),
  }
)
const { data } = await res.json()
console.log(`Application port: ${data.port}`)
```

### JavaScript — OAuth application

```javascript
await fetch(
  `https://vibecode.bitrix24.com/v1/infra/servers/${serverId}/port`,
  {
    method: 'PATCH',
    headers: {
      'X-Api-Key': 'YOUR_APP_KEY',
      'Authorization': 'Bearer USER_SESSION_TOKEN',
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ port: 0 }),
  }
)
```

## Response fields

| Field | Type | Description |
|------|-----|----------|
| `success` | boolean | Always `true` on success |
| `data.port` | number | The final port. With `port: 0` it returns `3000` (the default value on auto-detect) |
| `data.mode` | string | `manual` (a specific port was set) or `auto` (`port: 0`) |
| `data.verified` | boolean | `true` — the platform confirmed (via probe) that the agent is already proxying to this port. `false` — the port was saved but routing isn't confirmed yet (see `data.warning`) |
| `data.warning` | string | Present only when `verified: false`: routing isn't confirmed and the app may briefly (up to ~30s) serve the Black Hole page until the auto-detector converges |

## Response example

```json
{
  "success": true,
  "data": { "port": 8080, "mode": "manual", "verified": true }
}
```

## Error response example

400 — a system port is forbidden:

```json
{
  "success": false,
  "error": {
    "code": "PORT_RESTRICTED",
    "message": "System ports (1-1023) are not allowed. Use port >= 1024 or 0 for auto-detect"
  }
}
```

409 — the agent rejected the port change (fixed-port mode / outdated binary):

```json
{
  "success": false,
  "error": {
    "code": "PORT_NOT_APPLIED",
    "message": "The server agent runs in fixed-port mode and cannot change its port at runtime. Repair the server (POST /v1/infra/servers/:id/repair) to switch the agent to port auto-detection, then retry.",
    "agentError": "NO_SCANNER"
  }
}
```

## Errors

| HTTP | Code | Description |
|------|-----|----------|
| 400 | `VALIDATION_ERROR` | `port` is not a number, outside the 0–65535 range |
| 400 | `PORT_RESTRICTED` | A port in the 1–1023 range (system ports) |
| 401 | `MISSING_API_KEY` | The `X-Api-Key` header was not provided |
| 401 | `INVALID_API_KEY` | Invalid or expired API key |
| 404 | `NOT_FOUND` | The server is not `BLACKHOLE + RUNNING`, was deleted, or belongs to another API key |
| 409 | `SERVER_NOT_READY` | The tunnel agent is not in the `CONNECTED` status |
| 409 | `PORT_NOT_APPLIED` | The agent rejected the port change. `agentError: NO_SCANNER` — the server is in fixed-port mode (see below); a different `agentError` — the agent is outdated. Both are resolved by [`/repair`](/docs/infra/lifecycle/repair) |
| 429 | `RATE_LIMITED` | The platform's overall request limit was exceeded |
| 502 | `GATEWAY_ERROR` | The Gateway could not deliver the command to the agent |

Full list of common API errors — [Errors](/docs/errors).

## Known specifics

- **System ports (1–1023) are forbidden for a reason.** SSH (22), HTTP (80), HTTPS (443), DNS (53), PostgreSQL (5432), MySQL (3306) — opening a tunnel on them would create a risk of redirecting external traffic to service processes inside the virtual machine.
- **How `port: 0` works.** The agent scans `/proc/net/tcp` and `/proc/net/tcp6`, finds the process listening on an application port (≥1024), and proxies there. Useful for applications that pick a port dynamically at startup.
- **The value is preserved across agent restarts.** The platform writes `localPort` to the server's database — after [`/repair`](/docs/infra/lifecycle/repair) or a reboot the agent will use the same value. On a `/deploy` with an explicit `port` the value is overwritten.
- **`port: 0` is returned in the response as `3000`.** This is not an error: "on auto-detect we return 3000 if nothing suitable was found". Check the actual listening port via [`/exec`](./exec.md) with the command `ss -tlnp`.
- **The platform confirms the port change (`data.verified`).** After `set_port` the platform probes the agent and checks the tunnel actually proxies to the requested port. `verified: true` — routing is ready. `verified: false` + `data.warning` — the port was saved but the agent hasn't converged yet (the auto-detector picks it up within ~30s); if the app stays unavailable longer, run [`/repair`](/docs/infra/lifecycle/repair). `409 PORT_NOT_APPLIED` — the agent did not accept the change at all: with `agentError: NO_SCANNER` the server runs in fixed-port mode (run your app on the current port, or `/repair` to switch to auto-detection); with a different `agentError` the agent is outdated (`/repair` updates it).

## See also

- [Full deploy](./deploy.md) — the `port` field in `/deploy` also configures the tunnel.
- [Tunnel metrics](./metrics.md) — whether there are HTTP connections and `lastRequestAt`.
- [Run a command](./exec.md) — `ss -tlnp` to check the listening ports.
