
## 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 it. This endpoint is needed in rare cases: the application runs 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 the agent proxies to this port (by probing the agent, and on a pinned server by reading the settings on the machine plus a probe after the restart). `false` — the port was saved but routing is not confirmed yet (see `data.warning` and "Known specifics") |
| `data.warning` | string | Present only when `verified: false`. Three states, each calling for a different action: the agent is still in auto-detect mode (the pin did not take — the scanner routes traffic), the agent accepted the port but the confirmation did not arrive (retry the request), or the port was written to a pinned server's settings but the agent has not confirmed the restart yet. In the first two cases `/repair` does not help and makes things worse; in the third it does the opposite and applies the saved port. The warning text says what to do |
| `data.pinned` | boolean | Whether the port is pinned in the agent settings on the machine. `true` — it survives a wake and a repair; `false` — it lives only in the agent's memory until the next restart |

## Response example

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

## Error response example

409 — the agent rejected the port change:

```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 an integer (fractional values are rejected), or is outside the 0–65535 range |
| 400 | `PORT_RESTRICTED` | A port in the 1–1023 range (system ports) |
| 400 | `GALAXY_APP_USE_GALAXY_ROUTE` | The server is a Galaxy app. Its container port is bound to the host and is set by the `port` field when you [upload code](./deploy.md) — see [Galaxy app](/docs/infra/galaxy) |
| 401 | `MISSING_API_KEY` | The `X-Api-Key` header was not provided |
| 401 | `INVALID_API_KEY` | The key is not recognized — no such key exists on the platform |
| 403 | `INFRA_FORBIDDEN_FOR_COWORK_KEY` | The call was made with a Cowork key — such a key works with data only and cannot perform write operations. To issue a key that can, see [Project key for deploy](/docs/cowork/deploy-key) |
| 403 | `SERVER_ROLE_FORBIDDEN` | You are on this server's development team with the Developer role, and this operation is open to the Administrator role. `error.hint` carries your role, the required threshold and the list of calls that are open to you. Role breakdown — [List servers](/docs/infra/servers/list) |
| 404 | `NOT_FOUND` | The server is not `BLACKHOLE + RUNNING`, was deleted, or belongs to another API key while you are not on its development team |
| 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). On a pinned server the same code arrives without `agentError` when nothing on the machine listens on the requested port: pinning it would leave the public URL dead, so the write is refused — the message lists the ports that are listening on the machine itself |
| 409 | `SERVER_BUSY` | A deploy, an exec or another port change is already running on this server. Retry in a few seconds |
| 429 | `RATE_LIMITED` | Rate limit exceeded: 10 requests per minute on this endpoint. The current limit is returned in the `x-ratelimit-limit` header |
| 502 | `AGENT_CONFIG_WRITE_FAILED` | Pinned servers only: the machine refused to write the agent settings. [`/repair`](/docs/infra/lifecycle/repair) is the fix — it reinstalls the agent together with its settings |
| 502 | `GATEWAY_ERROR` | The Gateway could not deliver the command to the agent |

For the full list of common API errors, see [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 reads the machine's listening sockets from `/proc/net/tcp` and `/proc/net/tcp6`, probes each port with an HTTP request and points the tunnel at the one that answered. On the first choice, the agent prefers the standard port `3000` when it responds; otherwise the previous rule applies — it takes the lowest responding port, with `80` and `443` chosen only when nothing else answered. If a neighboring HTTP process answers before the app on `3000` during startup, the scanner has a short four-observation window in which it may correct the choice to `3000` once; normal target retention applies afterward. The restriction on system ports `1–1023` applies to setting the port explicitly with this call, and does not affect auto-detection. Five ports never take part in auto-detection: `22`, `2375`, `2376`, `4243` and `9200`. The machine's service interfaces answer on those ports, and publishing such an interface on the subdomain would open it to the internet. The filter applies to auto-detection only — the agent accepts any explicitly set port from the allowed range, so keep service interfaces out of the tunnel. Auto-detect suits applications that pick a port dynamically at startup.
- **Auto-detect also considers processes that listen on `127.0.0.1` only.** For a Black Hole application, that is the normal way to start: the agent reaches the application over the local interface, while the machine is closed from the outside. The consequence — a service process brought up on the machine only for local calls is indistinguishable from the application during auto-detection and can take the tunnel. When other processes that answer over HTTP run next to your application on the machine, set the application port explicitly with this call. Then the choice does not depend on who else is listening.
- **Auto-detect holds on to the previous port while something still answers on it.** The scanner does not switch to a new port on a single observation: while a responding process is alive on the previous port, the tunnel target stays on it — so a brief stall of the application does not drop the tunnel onto a stub page. The practical consequence: if the application moved to another port and a live process stayed on the old one, auto-detection will not switch by itself. The way out is to set the port explicitly with this call, or to stop the process on the previous port. If the previous port simply became free, the switch happens on its own within about a minute. The hold does not apply to `80` and `443`: when the target sits on one of them, auto-detection switches as soon as an application port answers.
- **A pinned port survives both a wake and a repair.** The pin is stored on the machine itself — as the `local_http` line in the agent settings at `/etc/vibe-agent/config.yml`. The value `auto` means auto-detection, while a value such as `127.0.0.1:8080` means a fixed port. The platform writes that line. On a pinned server this call rewrites it. An unpinned server becomes pinned once the port is confirmed — by a successful deploy that passed its healthcheck, or by a confirmed port change through this call — if pinning is enabled for your Bitrix24 account. A pinned server answers `portPinned: true` in [`GET /v1/infra/servers/:id`](/docs/infra/servers/get), and once it is pinned both waking the server and [`/repair`](/docs/infra/lifecycle/repair) bring the agent back to the same port. On an UNPINNED regular server, repair sets auto-detection and the port is chosen again. For agents, bots and galaxy hosts, repair writes the fixed port from the database.
- **`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`).** On an UNPINNED server, after `set_port` the platform probes the agent and checks that the tunnel actually proxies to the requested port. `verified: true` — routing is ready. `verified: false` + `data.warning` — the agent accepted the command but the confirmation did not arrive. Retry the request in a few seconds. **`/repair` is not the fix here and makes things worse:** a port set by that command lives in the agent's memory, so the restart during a repair discards it and returns the agent to auto-detection. `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 application on the current port, or `/repair` to switch to auto-detection); with a different `agentError` the agent is outdated (`/repair` updates it).
- **On a PINNED server this call works differently — but not right away.** The port is applied by rewriting the agent settings and restarting the agent, and `verified: true` means the settings carry the requested port and the agent is back online. The tunnel drops for a few seconds while that happens. `port: 0` removes the pin and returns the agent to auto-detection. If the agent did not make it back in time, the response carries `verified: false` — that is not a failure: the port is already written and takes effect once the agent comes up. Distinct failures: `502 AGENT_CONFIG_WRITE_FAILED` — the machine refused to write the settings (run [`/repair`](/docs/infra/lifecycle/repair)), `502 GATEWAY_ERROR` — the command never reached the machine (retry later), `409 PORT_NOT_APPLIED` without `agentError` — nothing listens on the requested port.
- **The behavior above starts after the agent's first restart, not the moment `portPinned: true` appears.** The path is chosen by the state of the MACHINE, not by the response field: until the agent has restarted, it still runs the scanner, and the call behaves as on an unpinned server — the tunnel does not drop, and `409 PORT_NOT_APPLIED` with an `agentError` can arrive. A restart happens on a server wake, a deploy or a repair. So a handler written against `portPinned` must be ready for both modes.

## See also

- [Full deploy](./deploy.md)
- [Tunnel metrics](./metrics.md)
- [Run a command](./exec.md)
