> ## Documentation Index
> Fetch the complete documentation index at: https://docs.gravixlayer.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Web Services

> Expose an HTTP server inside a runtime over HTTPS on *.service.gravixlayer.ai

**Web services** give you a public HTTPS URL for an HTTP server running inside a runtime — a FastAPI app, Streamlit UI, Jupyter, or API docs — without tunnels or local port forwarding.

Each **runtime + port** gets its own hostname on `*.service.gravixlayer.ai`. Open it in a browser, call it from code or CI, or share a short-lived link with a teammate.

```mermaid theme={"theme":{"light":"catppuccin-latte","dark":"catppuccin-mocha"}}
%%{init: {'theme':'base','themeVariables':{'primaryColor':'#EEF2FF','primaryTextColor':'#1E1B4B','primaryBorderColor':'#5B4CDB','lineColor':'#6366F1','secondaryColor':'#ECFDF5','secondaryBorderColor':'#00AD69','tertiaryColor':'#F5F3FF'}}}%%
flowchart LR
  Client[Browser / SDK / curl] -->|HTTPS| Edge["*.service.gravixlayer.ai"]
  Edge -->|auth + route| Guest[HTTP server in runtime]
```

<Note>
  The guest process must already be listening on the port. Opening a web service only publishes a route — it does not start your server.
</Note>

## When to use it

| Scenario                                     | How                                                                                  |
| -------------------------------------------- | ------------------------------------------------------------------------------------ |
| Preview a web app (Streamlit, Jupyter, etc.) | Open a service, then use `browser_url`                                               |
| Call an API from code or CI                  | SDK helpers, or `curl` with the service token                                        |
| Share a live demo                            | Send `browser_url` (private) or a public URL if you intentionally open public access |

## Defaults and access model

| Setting          | Default                                      | Notes                                                              |
| ---------------- | -------------------------------------------- | ------------------------------------------------------------------ |
| Access           | **Private**                                  | Requires a service token (header or browser cookie)                |
| TTL              | **3600 s** (1 hour)                          | Min 60 s, max 86400 s (24 hours)                                   |
| Hostname         | `{port}-{runtime_id}.service.gravixlayer.ai` | Stable for that runtime + port while active                        |
| Token on refresh | Omitted                                      | Token is returned **once** on first open (or after `rotate_token`) |

**Private (default)**

* Programmatic clients send `X-Gravix-Web-Service-Token: <token>`.
* Browsers use `browser_url`, which hits `/_ws/auth` once and sets an auth cookie for later page loads and API calls from that origin.
* Auth headers and cookies are stripped before traffic is proxied into the guest — your app never sees the Gravix service token.

**Public (`is_public=true` / `--public`)**

* No token required. Anyone with the URL can reach the guest port until expiry or revoke.
* Use only for intentionally open demos. Prefer private for APIs and anything with data.

The runtime must be **running**. Expired services are removed automatically; call open again (or rotate) for a fresh link.

## Quick start: FastAPI

Start a small API in the runtime, open a private web service, then create and list items.

Guest egress is deny-by-default, so the examples create a temporary `allow_all` network policy so `pip install` can reach PyPI. Prefer a narrower [allowlist](/documentation/agentruntime/getting-started/network-policies) in production.

<CodeGroup>
  ```python Python theme={"theme":{"light":"catppuccin-latte","dark":"catppuccin-mocha"}}
  import time
  import uuid
  from gravixlayer import GravixLayer

  client = GravixLayer()
  policy = client.network_policies.create(
      name=f"web-service-allow-all-{uuid.uuid4().hex[:8]}",
      egress_mode="allow_all",
      description="Temporary egress for web-service example",
  )

  rt = client.runtime.create(
      template="base-small",
      timeout=600,
      network_policy_ids=[policy.id],
  )

  APP = "/home/user/app"
  rt.run_cmd(command=f"mkdir -p {APP}")
  rt.file.write(
      f"{APP}/main.py",
      """
  from fastapi import FastAPI
  app = FastAPI()
  items = []

  @app.get("/items")
  def list_items():
      return items

  @app.post("/items")
  def create_item(item: dict):
      items.append(item)
      return item
  """.strip()
      + "\n",
  )

  rt.run_cmd(command="pip install fastapi uvicorn", timeout=180)
  rt.run_cmd(
      command=(
          f"nohup env PYTHONPATH={APP} "
          "python -m uvicorn main:app --host 0.0.0.0 --port 8000 "
          "> /tmp/uvicorn.log 2>&1 &"
      ),
      working_dir=APP,
  )

  for _ in range(30):
      ready = rt.run_cmd(
          command="python -c \"import socket; s=socket.create_connection(('127.0.0.1',8000),2); s.close()\""
      )
      if ready.exit_code == 0:
          break
      time.sleep(1)

  with rt.service(port=8000, expires_in_seconds=3600) as svc:
      print("web_url:    ", svc.web_url)
      print("browser_url:", svc.browser_url)
      if svc.token:
          print("token:      ", svc.token[:8] + "…")

      svc.post("/items", json={"name": "widget", "price": 9.99}).raise_for_status()
      svc.post("/items", json={"name": "gadget", "price": 24.99}).raise_for_status()
      print(svc.get("/items").json())

  rt.kill()
  client.network_policies.delete(policy.id)
  ```

  ```bash CLI theme={"theme":{"light":"catppuccin-latte","dark":"catppuccin-mocha"}}
  # Requires: GRAVIXLAYER_API_KEY, jq

  POLICY_ID=$(gravixlayer network-policy create \
    "web-service-allow-all-$(openssl rand -hex 4)" \
    --egress-mode allow_all \
    --description "Temporary egress for web-service example" \
    --output json | jq -r '.id')

  RT=$(gravixlayer runtime create \
    -t base-small \
    --timeout 600 \
    --network-policy "$POLICY_ID" \
    --wait \
    --output json | jq -r '.runtime_id')

  gravixlayer runtime exec "$RT" mkdir -p /home/user/app

  cat <<'EOF' | gravixlayer runtime files write "$RT" /home/user/app/main.py
  from fastapi import FastAPI
  app = FastAPI()
  items = []

  @app.get("/items")
  def list_items():
      return items

  @app.post("/items")
  def create_item(item: dict):
      items.append(item)
      return item
  EOF

  gravixlayer runtime exec "$RT" --timeout 180 pip install fastapi uvicorn
  gravixlayer runtime exec "$RT" --workdir /home/user/app \
    bash -lc 'nohup env PYTHONPATH=/home/user/app python -m uvicorn main:app --host 0.0.0.0 --port 8000 > /tmp/uvicorn.log 2>&1 &'

  for i in $(seq 1 30); do
    if gravixlayer runtime exec "$RT" \
      bash -lc "python -c \"import socket; s=socket.create_connection(('127.0.0.1',8000),2); s.close()\"" \
      >/dev/null 2>&1; then
      break
    fi
    sleep 1
  done

  SVC=$(gravixlayer runtime service web-url "$RT" 8000 --expires-in 3600 --output json)
  WEB_URL=$(echo "$SVC" | jq -r '.url // .web_url')
  TOKEN=$(echo "$SVC" | jq -r '.token // empty')

  curl -sS -X POST "$WEB_URL/items" \
    -H "Content-Type: application/json" \
    -H "X-Gravix-Web-Service-Token: $TOKEN" \
    -d '{"name":"widget","price":9.99}'
  echo
  curl -sS -X POST "$WEB_URL/items" \
    -H "Content-Type: application/json" \
    -H "X-Gravix-Web-Service-Token: $TOKEN" \
    -d '{"name":"gadget","price":24.99}'
  echo
  curl -sS "$WEB_URL/items" \
    -H "X-Gravix-Web-Service-Token: $TOKEN"
  echo

  gravixlayer runtime kill "$RT" -y
  gravixlayer network-policy delete "$POLICY_ID" -y
  ```

  ```bash cURL theme={"theme":{"light":"catppuccin-latte","dark":"catppuccin-mocha"}}
  # Requires: GRAVIXLAYER_API_KEY, jq
  API="https://api.gravixlayer.ai/v1"
  AUTH=(-H "Authorization: Bearer $GRAVIXLAYER_API_KEY" -H "Content-Type: application/json")

  POLICY_ID=$(curl -sS -X POST "$API/network-policies" "${AUTH[@]}" \
    -d "{
      \"name\": \"web-service-allow-all-$(openssl rand -hex 4)\",
      \"egress_mode\": \"allow_all\",
      \"description\": \"Temporary egress for web-service example\"
    }" | jq -r '.id')

  RT=$(curl -sS -X POST "$API/agents/runtime" "${AUTH[@]}" \
    -d "{
      \"template\": \"base-small\",
      \"cloud\": \"azure\",
      \"region\": \"eastus2\",
      \"timeout\": 600,
      \"network_policy_ids\": [\"$POLICY_ID\"]
    }" | jq -r '.runtime_id')

  # Wait until running
  for i in $(seq 1 60); do
    STATUS=$(curl -sS "$API/agents/runtime/$RT" \
      -H "Authorization: Bearer $GRAVIXLAYER_API_KEY" | jq -r '.status')
    [ "$STATUS" = "running" ] && break
    sleep 2
  done

  curl -sS -X POST "$API/agents/runtime/$RT/commands/run" "${AUTH[@]}" \
    -d '{"command":"mkdir","args":["-p","/home/user/app"],"timeout":30000}' >/dev/null

  curl -sS -X POST "$API/agents/runtime/$RT/files/write" "${AUTH[@]}" \
    -d "$(jq -n --arg content "$(cat <<'PY'
  from fastapi import FastAPI
  app = FastAPI()
  items = []

  @app.get("/items")
  def list_items():
      return items

  @app.post("/items")
  def create_item(item: dict):
      items.append(item)
      return item
  PY
  )" '{path:"/home/user/app/main.py", content:$content}')"

  curl -sS -X POST "$API/agents/runtime/$RT/commands/run" "${AUTH[@]}" \
    -d '{"command":"pip install fastapi uvicorn","timeout":180000}' >/dev/null

  curl -sS -X POST "$API/agents/runtime/$RT/commands/run" "${AUTH[@]}" \
    -d '{
      "command": "bash",
      "args": ["-lc", "nohup env PYTHONPATH=/home/user/app python -m uvicorn main:app --host 0.0.0.0 --port 8000 > /tmp/uvicorn.log 2>&1 &"],
      "working_dir": "/home/user/app",
      "timeout": 30000
    }' >/dev/null

  for i in $(seq 1 30); do
    CODE=$(curl -sS -X POST "$API/agents/runtime/$RT/commands/run" "${AUTH[@]}" \
      -d '{"command":"python","args":["-c","import socket; s=socket.create_connection((\"127.0.0.1\",8000),2); s.close()"],"timeout":10000}' \
      | jq -r '.exit_code')
    [ "$CODE" = "0" ] && break
    sleep 1
  done

  SVC=$(curl -sS -X POST "$API/agents/runtime/$RT/services" "${AUTH[@]}" \
    -d '{"port": 8000, "expires_in_seconds": 3600, "is_public": false}')
  WEB_URL=$(echo "$SVC" | jq -r '.url // .web_url')
  TOKEN=$(echo "$SVC" | jq -r '.token // empty')

  curl -sS -X POST "$WEB_URL/items" \
    -H "Content-Type: application/json" \
    -H "X-Gravix-Web-Service-Token: $TOKEN" \
    -d '{"name":"widget","price":9.99}'
  echo
  curl -sS -X POST "$WEB_URL/items" \
    -H "Content-Type: application/json" \
    -H "X-Gravix-Web-Service-Token: $TOKEN" \
    -d '{"name":"gadget","price":24.99}'
  echo
  curl -sS "$WEB_URL/items" \
    -H "X-Gravix-Web-Service-Token: $TOKEN"
  echo

  curl -sS -X DELETE "$API/agents/runtime/$RT" \
    -H "Authorization: Bearer $GRAVIXLAYER_API_KEY" >/dev/null
  curl -sS -X DELETE "$API/network-policies/$POLICY_ID" \
    -H "Authorization: Bearer $GRAVIXLAYER_API_KEY" >/dev/null
  ```
</CodeGroup>

<Tip>
  For production, replace `allow_all` with an allowlist that includes only the hosts you need (for example `pypi.org` and `files.pythonhosted.org`). See [Network Policies](/documentation/agentruntime/getting-started/network-policies).
</Tip>

## Open a service

<CodeGroup>
  ```python Python theme={"theme":{"light":"catppuccin-latte","dark":"catppuccin-mocha"}}
  # From a Runtime handle
  svc = rt.service(port=8000, expires_in_seconds=3600)

  # Or from the client
  svc = client.runtime.service(rt.runtime_id, 8000, expires_in_seconds=3600)

  print(svc.web_url)      # stable HTTPS base URL
  print(svc.browser_url)  # one-shot auth URL for browsers (private)
  print(svc.token)        # present on first open / rotate only

  # Public demo (no token)
  public = rt.service(port=8000, is_public=True, expires_in_seconds=1800)

  # Mint a new private token (invalidates the previous token and cookies)
  rotated = rt.service(port=8000, rotate_token=True)
  ```

  ```bash CLI theme={"theme":{"light":"catppuccin-latte","dark":"catppuccin-mocha"}}
  # Private (default)
  gravixlayer runtime service web-url "$RT" 8000 --expires-in 3600

  # Public
  gravixlayer runtime service web-url "$RT" 8000 --public --expires-in 1800

  # Rotate private token
  gravixlayer runtime service web-url "$RT" 8000 --rotate-token
  ```

  ```bash cURL theme={"theme":{"light":"catppuccin-latte","dark":"catppuccin-mocha"}}
  curl -X POST "https://api.gravixlayer.ai/v1/agents/runtime/$RT/services" \
    -H "Authorization: Bearer $GRAVIXLAYER_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "port": 8000,
      "expires_in_seconds": 3600,
      "is_public": false,
      "rotate_token": false
    }'
  ```
</CodeGroup>

## Call the service

The SDK handle adds the token header for private services automatically:

<CodeGroup>
  ```python Python theme={"theme":{"light":"catppuccin-latte","dark":"catppuccin-mocha"}}
  svc = rt.service(port=8000)

  resp = svc.get("/api/items")
  resp = svc.post("/api/items", json={"name": "widget"})
  resp = svc.put("/api/items/1", json={"name": "updated"})
  resp = svc.patch("/api/items/1", json={"status": "active"})
  resp = svc.delete("/api/items/1")

  # Browser: open svc.browser_url (sets cookie; no token in the page URL after auth)
  print(svc.browser_url)
  ```

  ```bash CLI theme={"theme":{"light":"catppuccin-latte","dark":"catppuccin-mocha"}}
  # After web-url, use curl with the returned token
  curl -sS "$WEB_URL/api/items" \
    -H "X-Gravix-Web-Service-Token: $TOKEN"

  curl -sS -X POST "$WEB_URL/api/items" \
    -H "Content-Type: application/json" \
    -H "X-Gravix-Web-Service-Token: $TOKEN" \
    -d '{"name":"widget"}'
  ```

  ```bash cURL theme={"theme":{"light":"catppuccin-latte","dark":"catppuccin-mocha"}}
  # Your own HTTP client — same header
  curl -sS "https://8000-$RT.service.gravixlayer.ai/api/items" \
    -H "X-Gravix-Web-Service-Token: $TOKEN"

  # Or open browser_url from the open response in a browser
  ```
</CodeGroup>

## List and revoke

<CodeGroup>
  ```python Python theme={"theme":{"light":"catppuccin-latte","dark":"catppuccin-mocha"}}
  services = client.runtime.service.list(rt.runtime_id)
  for s in services:
      print(s.port, s.is_public, s.expires_at, s.web_url)

  client.runtime.service.revoke(rt.runtime_id, 8000)
  ```

  ```bash CLI theme={"theme":{"light":"catppuccin-latte","dark":"catppuccin-mocha"}}
  gravixlayer runtime service list "$RT"
  gravixlayer runtime service revoke "$RT" 8000
  ```

  ```bash cURL theme={"theme":{"light":"catppuccin-latte","dark":"catppuccin-mocha"}}
  curl "https://api.gravixlayer.ai/v1/agents/runtime/$RT/services" \
    -H "Authorization: Bearer $GRAVIXLAYER_API_KEY"

  curl -X DELETE "https://api.gravixlayer.ai/v1/agents/runtime/$RT/services/8000" \
    -H "Authorization: Bearer $GRAVIXLAYER_API_KEY"
  ```
</CodeGroup>

## From the UI

On the runtime detail page, open the **Web Service** section, enter a guest port, and open or copy the link. Private links use the same token/cookie model as the API. After expiry, open again for a fresh URL.

## Security best practices

1. **Keep private by default** — only use `--public` / `is_public=True` for deliberate demos.
2. **Treat tokens like secrets** — store them in CI secrets; never commit them; prefer `browser_url` for humans so the token is not left in shared chat after auth.
3. **Short TTLs** — use the smallest `expires_in_seconds` that fits the session; revoke when done.
4. **Rotate when leaked** — `rotate_token=True` / `--rotate-token` invalidates the previous token and browser cookies.
5. **Bind guest servers carefully** — listen on `0.0.0.0` only if you intend edge access; do not expose admin UIs without auth inside the app.
6. **Egress is separate** — web services are **inbound** HTTPS to the guest. Outbound calls from the guest still follow [Network Policies](/documentation/agentruntime/getting-started/network-policies).

<Warning>
  A public web service URL is reachable by anyone who has it until it expires or you revoke it. Private tokens grant the same reachability for the TTL — share them only with intended clients.
</Warning>

## Troubleshoot

| Symptom                      | Likely cause                         | Fix                                                                    |
| ---------------------------- | ------------------------------------ | ---------------------------------------------------------------------- |
| Auth / 401 from the edge     | Missing or expired token             | Re-open the service; use `X-Gravix-Web-Service-Token` or `browser_url` |
| Connection errors to the URL | Nothing listening on that guest port | Confirm the process is up (`run_cmd` / logs) before opening            |
| Token missing on second open | Expected for private refresh         | Save the first token, or pass `rotate_token=True`                      |
| `pip install` fails          | Deny-by-default egress               | Attach a network policy for PyPI / registries                          |

## Next

<CardGroup cols={2}>
  <Card title="Network Policies" icon="shield-halved" href="/documentation/agentruntime/getting-started/network-policies">
    Control outbound destinations from the sandbox
  </Card>

  <Card title="Run commands" icon="terminal" href="/documentation/agentruntime/command-execution/run-command">
    Start servers and install packages inside the runtime
  </Card>

  <Card title="Create Runtime" icon="play" href="/documentation/agentruntime/sandbox-management/create-sandbox">
    Create a sandbox to host your HTTP app
  </Card>

  <Card title="Identity Providers" icon="key" href="/documentation/agentruntime/getting-started/identity-providers">
    Inject credentials your guest app needs
  </Card>
</CardGroup>
