Skip to main content

Quick path

  1. Enter or target a Box. In integrations, use the command endpoint to run the same setup inside the Box:
box ssh bx_f7k2q9hd
npm run dev -- --host 0.0.0.0 --port 3000
curl -sS -X POST "$BOX_API_BASE/boxes/bx_f7k2q9hd/commands" \
  -H "Authorization: Bearer $BOX_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"command":"npm run dev -- --host 0.0.0.0 --port 3000","cwd":"my-repo"}'
await box.command({
  boxId: "bx_f7k2q9hd",
  commandRequest: { command: "npm run dev -- --host 0.0.0.0 --port 3000", cwd: "my-repo" },
});
box.command("bx_f7k2q9hd", CommandRequest(
    command="npm run dev -- --host 0.0.0.0 --port 3000",
    cwd="my-repo",
))
  1. Start your app on an explicit port.
Your app must bind to 0.0.0.0. If it only listens on localhost or 127.0.0.1, the hosted HTTPS URL will not be able to reach it.
  1. Inside the Box, run host to start hosting your app. Programmatic integrations can execute the same command in the Box:
host 3000 --title "App preview"
curl -sS -X POST "$BOX_API_BASE/boxes/bx_f7k2q9hd/commands" \
  -H "Authorization: Bearer $BOX_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"command":"host 3000 --title \"App preview\""}'
await box.command({
  boxId: "bx_f7k2q9hd",
  commandRequest: { command: 'host 3000 --title "App preview"' },
});
box.command("bx_f7k2q9hd", CommandRequest(command='host 3000 --title "App preview"'))
  1. Open the public HTTPS URL printed by host:
https://<box-subdomain>-3000.on.ascii.dev
If the port is protected, the usable URL includes a _token query parameter:
https://<box-subdomain>-3000.on.ascii.dev?_token=<access-token>
A fresh host <port> URL is protected by default and includes _token. Pass --public only when you want to clear the access token and return an ungated URL.
If you want the process to survive after your SSH command exits, start it as a detached process before hosting the port.

How HTTPS hosting works

When you run host <port>, the Box asks the Ascii backend to create a stable public route for that Box and port. The backend registers a subdomain using the Box’s current machine address and the port you asked to expose. The generated hostname is based on the Box subdomain plus the port:
https://<box-subdomain>-<port>.on.ascii.dev
Ascii terminates TLS for on.ascii.dev, then proxies each request to the target Box over the exposed port. Your application still runs inside the Box; the public HTTPS route sits in front of it. This is why your server must listen on 0.0.0.0: the route connects to the Box from outside the application process, not through your app’s local loopback interface. The Box authenticates to the Ascii backend with its machine token, and the backend performs the route registration. This keeps routing credentials out of the Box environment while still letting the host CLI create, list, and remove routes.

Host CLI reference

The host CLI runs inside a Box and exposes services from that Box on public HTTPS URLs.
The service you expose must listen on 0.0.0.0, not only on localhost or 127.0.0.1.

host <port>

Expose a running service on a stable HTTPS URL:
host 3000
host 3000 --title "Login preview"
host 3000 --private
host 3000 --public
curl -sS -X POST "$BOX_API_BASE/boxes/bx_f7k2q9hd/commands" \
  -H "Authorization: Bearer $BOX_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"command":"host 3000 --title \"Login preview\""}'
await box.command({ boxId: "bx_f7k2q9hd", commandRequest: { command: "host 3000" } });
await box.command({ boxId: "bx_f7k2q9hd", commandRequest: { command: 'host 3000 --title "Login preview"' } });
await box.command({ boxId: "bx_f7k2q9hd", commandRequest: { command: "host 3000 --private" } });
await box.command({ boxId: "bx_f7k2q9hd", commandRequest: { command: "host 3000 --public" } });
box.command("bx_f7k2q9hd", CommandRequest(command="host 3000"))
box.command("bx_f7k2q9hd", CommandRequest(command='host 3000 --title "Login preview"'))
box.command("bx_f7k2q9hd", CommandRequest(command="host 3000 --private"))
box.command("bx_f7k2q9hd", CommandRequest(command="host 3000 --public"))
The command opens the firewall for that port, registers an HTTPS subdomain, and prints the URL. Calling it again for the same port returns the same URL. A Box can host up to 50 ports. If you only need raw access without HTTPS or a subdomain, you can instead open the port yourself with ufw and use http://<box-ip>:<port> directly.
OptionDescription
--title <title>Set the display title for the hosted port.
--privateRequire the generated _token query parameter to access the URL.
--publicClear any saved access token and return a URL that does not require _token.
By default, host <port> creates a protected URL with an _token query parameter. That protection is sticky: hosting the same port again returns a URL with the same _token query parameter unless you pass --public.

host list

Show hosted ports for the current Box:
host list
curl -sS -X POST "$BOX_API_BASE/boxes/bx_f7k2q9hd/commands" \
  -H "Authorization: Bearer $BOX_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"command":"host list"}'
await box.command({ boxId: "bx_f7k2q9hd", commandRequest: { command: "host list" } });
box.command("bx_f7k2q9hd", CommandRequest(command="host list"))
Protected ports are shown as (gated). host list does not print the access token. Use host url <port> to print the full URL with _token=....

host url <port>

Wait until the HTTPS URL is ready, then print it:
host url 3001
host url 3001 --public
curl -sS -X POST "$BOX_API_BASE/boxes/bx_f7k2q9hd/commands" \
  -H "Authorization: Bearer $BOX_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"command":"host url 3001"}'
await box.command({ boxId: "bx_f7k2q9hd", commandRequest: { command: "host url 3001" } });
await box.command({ boxId: "bx_f7k2q9hd", commandRequest: { command: "host url 3001 --public" } });
box.command("bx_f7k2q9hd", CommandRequest(command="host url 3001"))
box.command("bx_f7k2q9hd", CommandRequest(command="host url 3001 --public"))
For a protected port, this prints the full token-gated URL:
https://<box-subdomain>-3001.on.ascii.dev?_token=<access-token>
This is useful when one service needs another service’s public URL:
BACKEND_URL=$(host url 3001)
BACKEND_URL=$(curl -sS -X POST "$BOX_API_BASE/boxes/bx_f7k2q9hd/commands" \
  -H "Authorization: Bearer $BOX_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"command":"host url 3001"}' | jq -r '.stdout')
const result = await box.command({ boxId: "bx_f7k2q9hd", commandRequest: { command: "host url 3001" } });
const backendUrl = result.stdout.trim();
result = box.command("bx_f7k2q9hd", CommandRequest(command="host url 3001"))
backend_url = result.stdout.strip()

host hide <port>

Take down the public URL:
host hide 3000
curl -sS -X POST "$BOX_API_BASE/boxes/bx_f7k2q9hd/commands" \
  -H "Authorization: Bearer $BOX_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"command":"host hide 3000"}'
await box.command({ boxId: "bx_f7k2q9hd", commandRequest: { command: "host hide 3000" } });
box.command("bx_f7k2q9hd", CommandRequest(command="host hide 3000"))
This closes public access and unregisters the HTTPS route. It does not stop the local server process. Stop the server separately when you are done. Access tokens are preserved, so hosting the same port again keeps existing protected links valid. To get an ungated URL after a port has become protected, run host <port> --public.