PortModels
Log in

Device flow

Authorize a CLI or headless app with a short user code and polling — no redirect required.

For CLI tools and similar — no redirect at all. The app starts a session, shows the user a short code, and polls until they approve it on another device. Public clients only.

1. Start a device session#

bash
curl -X POST https://api.portmodels.com/connect/device/authorize \
  -H "Content-Type: application/json" \
  -d '{"client_id": "acme/chatbot", "scope": "models.run kv.read"}'
json
{
  "device_code": "pmdc_...",
  "user_code": "ABCD2345",
  "verification_uri": "https://app.portmodels.com/device",
  "verification_uri_complete": "https://app.portmodels.com/device?user_code=ABCD2345",
  "expires_in": 600,
  "interval": 5
}

2. Show the user the code#

Print user_code and verification_uri, or open verification_uri_complete directly if you can. The session expires after expires_in seconds.

text
To connect, visit https://app.portmodels.com/device
and enter the code:  ABCD2345

3. Poll for the token#

Poll no faster than interval seconds:

bash
curl -X POST https://api.portmodels.com/connect/device/token \
  -H "Content-Type: application/json" \
  -d '{"client_id": "acme/chatbot", "device_code": "pmdc_..."}'

While waiting, this returns 400 with:

ErrorMeaningWhat to do
authorization_pendingThe user hasn't approved yetKeep polling
slow_downYou polled before interval elapsedBack off, then continue
access_deniedThe user declinedStop; tell the user
expired_tokenThe device code expiredStop; start a new session

Once approved you get 200 with the same token shape as /connect/token. The device login receives its own token and does not invalidate another active login for the same app and user. The user can therefore keep a desktop, CLI, or browser session connected at the same time.

Polling loop#

python
import time, requests

BASE = "https://api.portmodels.com"

start = requests.post(f"{BASE}/connect/device/authorize",
                      json={"client_id": "acme/chatbot", "scope": "models.run"}).json()

print(f"Visit {start['verification_uri']} and enter: {start['user_code']}")

interval = start["interval"]
deadline = time.time() + start["expires_in"]

while time.time() < deadline:
    time.sleep(interval)
    r = requests.post(f"{BASE}/connect/device/token",
                      json={"client_id": "acme/chatbot", "device_code": start["device_code"]})
    if r.status_code == 200:
        token = r.json()["access_token"]
        break
    error = r.json().get("error")
    if error == "slow_down":
        interval += 5
    elif error != "authorization_pending":
        raise SystemExit(f"Connection failed: {error}")
else:
    raise SystemExit("Code expired before it was approved")

Tip

Store the resulting token in the OS keychain rather than a plaintext dotfile. It grants everything the user approved until they disconnect your app.