Uplink Docs
Local API
Documentation

Local API

uplink api serves a local HTTP control API over a unix socket. It speaks the same command contract the desktop app and uplink ui use, so anything you can do in the UI a script or product can do programmatically: declare a tunnel, read back its public URL, toggle it, start or stop serving, query status. It is the building-block surface for products that mint dynamic URLs on top of Uplink.

uplink api                        # serves at ~/.uplink/api.sock until Ctrl-C
uplink api --socket /run/my.sock  # custom socket path

The server runs in the foreground; keep it available by running it under your supervisor (a systemd unit, or as a child process of your product). It works alongside everything else — the CLI, desktop app, and a running agent share the same on-disk state, and changes made through the API reconcile onto live tunnels automatically.

Why a unix socket

The socket file is created owner-only (mode 0600), so filesystem permissions are the authentication: any process that can open it already runs as your user and could drive the CLI anyway. There is no port to firewall, no token to manage, and nothing a browser or another local user can reach. Every language speaks HTTP over a unix socket:

curl --unix-socket ~/.uplink/api.sock \
  -H 'content-type: application/json' \
  -d '{}' \
  http://localhost/api/invoke/list_apps

The URL’s host is ignored — the socket is the address. Treat socket access as full control of the device’s tunnels: don’t loosen the file mode, and don’t place the socket in a world-writable directory.

Wire contract

Every call is POST /api/invoke/<command> with a JSON object body holding the command’s named arguments (camelCase). Success is 200 with the command’s JSON result; failure is 400 with a structured body:

{ "code": "login-required", "message": "sign in to continue" }

code is stable (error for generic failures); match on it rather than the message text. Requests must carry a JSON content-type.

Serving a dynamic URL, end to end

Sign the device in once (interactively with uplink login, or headlessly with uplink login --device-token-file <path>), then:

API='curl -s --unix-socket ~/.uplink/api.sock -H content-type:application/json'

# 1. Who am I / which account owns new tunnels?
$API -d '{}' http://localhost/api/invoke/account_status
# → { "userId": …, "accounts": [{ "id": "acc_…", … }], "selectedAccountId": "acc_…", … }

# 2. Declare a tunnel for local port 3000. The response is the updated app
#    table — including the tunnel's public url.
$API -d '{"spec":{"accountId":"acc_…","name":"preview-42","port":3000}}' \
  http://localhost/api/invoke/upsert_app
# → [ { "name": "preview-42", "port": 3000, "url": "https://preview-42-….app.….uplink.computer", … } ]

# 3. Make sure the device is serving (starts the agent if needed; idempotent).
$API -d '{}' http://localhost/api/invoke/start_serving

# 4. Later: tear the URL down again.
$API -d '{"name":"preview-42"}' http://localhost/api/invoke/remove_app

While serving is running, the agent watches the app table, so step 2’s change also applies on its own within about a second — start_serving just makes it immediate and guarantees the agent is up.

Commands

The commands below are the stable automation surface. The API dispatches the full command set the desktop app uses, but shapes outside this table follow the product UIs and may change between releases.

CommandArgumentsReturns
account_status{}The logged-in profile (accounts, selectedAccountId, deviceSlug, …), or null when logged out
list_apps{}Every declared tunnel: name, port, url, host, public, enabled, edges, sharedWith, hasAuthToken, …
upsert_app{"spec": {…}}Creates or updates one tunnel; returns the updated app table
remove_app{"name": "…"}Removes the tunnel
rename_app{"name": "…", "newName": "…"}Renames it (the public host changes with it); returns the updated table
set_sharing{"app": "…", "email": "…", "grant": true}Grants/revokes an email on a gated tunnel
start_serving{}Ensures the agent is running and serving the declared table
stop_serving{}Stops serving everything on this device
serving_status{}{ "running": bool, "edges": [{ "edge", "phase", "apps", … }] }phase is connecting / connected / reconnecting / failed

The upsert_app spec

accountId, name, and port are required. Everything else is optional, and an omitted field keeps the existing tunnel’s value (new tunnels get the defaults), so a partial update never resets what you don’t mention:

FieldMeaning
publictrue opens the tunnel to everyone; default is gated to the owning account
authSets an access token (stored hashed); "" clears it; omitted keeps it
sharedWithEmails granted access to a gated tunnel
edgesPin to specific edge ids; omitted picks the nearest region for new tunnels
allowedIpsSource IP/CIDR allowlist
rateLimit, bandwidthCapMbps, ruleIdsRequest budget, bandwidth cap (0 = uncapped), attached WAF rules
enabledPer-tunnel on/off toggle
inspectRequest capture: off, metadata, or full
ephemeralRemove the tunnel when serving stops

Alternatively: declarative files

If your integration is write-mostly, you may not need the API for mutations at all: tunnels declared in uplink.yaml are watched while serving, so editing a config file and letting the agent reconcile is equally valid — the API’s list_apps/serving_status remain useful for reading back URLs and health either way.

Search across 15 pages.
↑↓ navigate openEsc close