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#
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"}'{
"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.
To connect, visit https://app.portmodels.com/device
and enter the code: ABCD23453. Poll for the token#
Poll no faster than interval seconds:
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:
| Error | Meaning | What to do |
|---|---|---|
authorization_pending | The user hasn't approved yet | Keep polling |
slow_down | You polled before interval elapsed | Back off, then continue |
access_denied | The user declined | Stop; tell the user |
expired_token | The device code expired | Stop; 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#
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.