# Ports and private URLs

> Expose a machine's ports as HTTPS URLs, keep them private with short-lived tokens, or make them public.

Any TCP port in a running machine can be served at its own HTTPS address:

```text
https://<label>-<port>.<edge domain>
```

`label` is 12 random characters given to the machine the first time it exposes a port. It never changes and is never reused, even after the machine is deleted, so a URL can never reach a different machine. VMYard's edge terminates TLS and forwards to the machine; the machine is never reachable directly.

> [!NOTE]
> In the console, a machine's **Network** tab exposes ports and issues links. The CLI has `vy ports` and the SDKs have `machine.ports`; this page shows the HTTP API. Ports need a VMYard deployment with an edge configured; without one, these routes answer `503 ingress_unavailable`. Machines on the `docker` development backend have no network and can't expose ports.

## Expose a port

```sh
curl -X POST "$VMYARD_API_URL/v1/machines/$MACHINE_ID/ports" \
  -H "Authorization: Bearer $VMYARD_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{"port": 8080}'
```

| Field | Meaning |
| --- | --- |
| `port` | 1–65535, except 22 (use an [SSH key](/docs/guides/ssh) for SSH). |
| `public` | `true` serves the URL to anyone. The default, `false`, requires a token. |
| `localhost` | `true` also reaches a service that listens only on `127.0.0.1` or `[::1]` inside the machine. |

Exposing is an [operation](/docs/concepts/operations); the port is open once it succeeds. Exposing a port that is already open replaces its `public` and `localhost` settings. A machine can have up to **16** exposed ports (`409 port_limit_reached`).

## List ports

```sh
curl "$VMYARD_API_URL/v1/machines/$MACHINE_ID/ports" \
  -H "Authorization: Bearer $VMYARD_API_KEY"
```

```json
{
  "label": "k7f2mq3x9a1b",
  "items": [
    { "port": 8080, "public": false, "localhost": false, "url": "https://k7f2mq3x9a1b-8080.on.example.com" }
  ]
}
```

The list shows ports as the machine's host last confirmed them. Any member and any API key preset can read it.

## Open a private port

Private ports need a token. Ask for one, valid for 1 to 3,600 seconds (900 by default):

```sh
curl -X POST "$VMYARD_API_URL/v1/machines/$MACHINE_ID/ports/8080/token" \
  -H "Authorization: Bearer $VMYARD_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"ttl_seconds": 900}'
```

```json
{
  "url": "https://k7f2mq3x9a1b-8080.on.example.com",
  "url_with_token": "https://k7f2mq3x9a1b-8080.on.example.com/?_token=v1.…",
  "header_token": "v1.…",
  "expires_at": "2026-10-12T06:06:01Z"
}
```

- **In a browser**, open `url_with_token`. The edge checks the token, sets a cookie scoped to that one host, and redirects to `url` so the token leaves the address bar.
- **From code**, send the token in a header:

```sh
curl https://k7f2mq3x9a1b-8080.on.example.com/health \
  -H "X-Vmyard-Token: $HEADER_TOKEN"
```

A token opens only the one machine and port it was issued for and stops working at `expires_at`. Missing credentials get `401` from the edge; invalid or expired ones get `403`. VMYard doesn't store tokens; issuing one is recorded with its port and expiry. Asking for a token for a port that isn't exposed is `404 port_not_exposed`.

## What the edge forwards

- HTTP/1.1 requests, streamed responses (server-sent events, long polls), and WebSocket upgrades.
- The edge sets `X-Forwarded-For`, `X-Forwarded-Proto`, and `X-Forwarded-Host`, and removes the token cookie and header before forwarding.
- Idle connections close after 5 minutes without traffic by default.

## Close a port

```sh
curl -X DELETE "$VMYARD_API_URL/v1/machines/$MACHINE_ID/ports/8080" \
  -H "Authorization: Bearer $VMYARD_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)"
```

Closing a port that isn't exposed makes the operation fail. Deleting a machine closes all of its ports.

## Access

Exposing, closing, and issuing tokens need a running machine and Owner, Admin, or Operator access (or a `use_existing` key). Port changes that fail emit `operation.failed`; successful ones emit no webhook.
