Device Authentication
Headless login flow for CLI tools and MCP servers using human-readable codes
Device Authentication
Device authentication lets CLI tools and MCP servers obtain API keys without browser-based OAuth redirects. It follows the OAuth 2.0 Device Authorization Grant pattern with a human-readable code.
How It Works
CLI / MCP Server Buildd Server Browser
│ │ │
├── POST /api/auth/device/code ──→ │ │
│ { clientName: "CLI" } │ │
│ │ │
│ ←── { user_code: "ABCD-1234", │ │
│ device_token: "abc...", │ │
│ verification_url: "..." } │ │
│ │ │
│ Display code to user │ │
│ "Enter ABCD-1234 at url" │ │
│ │ │
│ │ ←── User visits URL ─────── │
│ │ enters code, approves │
│ │ │
│── POST /api/auth/device/token ─→ │ │
│ (polling every 5s) │ │
│ │ │
│ ←── 428 (pending) │ │
│ ←── 428 (pending) │ │
│ ←── 200 { api_key: "bld_..." } │ │
│ │ │
│ Store key, authenticated ✓ │ │- CLI requests a code —
POST /api/auth/device/codereturns a user code (ABCD-1234format) and a device token - User approves in browser — visits the verification URL, enters the code while logged in
- CLI polls for completion —
POST /api/auth/device/tokenwith the device token, receives428until approved - API key returned — once approved, the CLI receives a
bld_xxxAPI key (one-time retrieval, then cleared from DB)
Account Level
There are three levels — see Task Access Model:
admin— full access: create tasks, manage workspaces and schedules, send agent instructionsworker— claim tasks, report progress, open and merge PRs, read/write memorytrigger— file and read only; cannot claim or execute
POST /api/auth/device/code
{ "clientName": "MCP Server", "level": "worker" }Omitting level produces an admin key. The endpoint accepts only the
literal strings "worker" and "trigger"; every other input — the field
missing, null, "Worker", "ADMIN", a typo — falls through to admin. The
device_codes.level column defaults to admin as well. Always pass level
explicitly, and pass it lowercase.
The approval flow creates or updates a named account for the user's team with the
requested level, matching on (team, account name).
Approval overwrites the level of an existing account with the same name. If
a worker-level account named Runner already exists in the team and someone
completes a device flow with clientName: "Runner" and no level, that
account's API key is rotated and it is promoted to admin. Use a distinct
clientName per credential, and always set level.
Security
- 15-minute expiry — codes expire after 15 minutes if not approved
- One-time key retrieval — the plaintext API key is cleared from the database after the CLI retrieves it
- Human-readable codes —
ABCD-1234format excludes ambiguous characters (I, O) to prevent typos - Session-gated approval — only authenticated users can approve codes
- Unauthenticated code request —
POST /api/auth/device/coderequires no auth; the security boundary is the browser approval step, not the code request
API Reference
| Method | Endpoint | Auth | Description |
|---|---|---|---|
POST | /api/auth/device/code | None | Generate user code + device token |
POST | /api/auth/device/token | None | Poll for approval (returns 428 if pending) |
POST | /api/auth/device/approve | Session | Approve a code (browser-side) |