Device login (OAuth device flow)
A CLI or coding agent that has no browser of its own gets a token without
anyone pasting a secret: it asks for a code, a person approves it in the
console, and the agent's next poll receives a scoped, expiring agent token
(scb_agt_…). This is the OAuth 2.0 Device Authorization Grant
(RFC 8628). scribase login,
npx scribase login and npx scribase init implement the client side; see
Why you never need to paste a secret key.
agent control API person (console)
| POST /v1/oauth/device/code | |
|---------------------------->| device_code, user_code, URL |
|<----------------------------| |
| shows URL + SCRB-7F3K-Q2MX | |
| | /device?user_code=... (signed in) |
| POST /v1/oauth/token (poll) |<------------------------------------|
| authorization_pending | lookup, then approve or deny |
| ... | |
| POST /v1/oauth/token | |
| 200 access_token=scb_agt_… | |POST /v1/oauth/device/code
Anonymous. The body is application/x-www-form-urlencoded (as RFC 8628
specifies) or JSON.
| Field | Notes |
|---|---|
client_id |
Optional. Identifies the client, e.g. scribase-cli |
client_name |
Optional. Shown to the person approving, e.g. Claude Code on laptop (1 to 80 printable characters) |
scope |
Optional, space-separated. See below. Default project:write sql:write migrations:write functions:deploy branches:write |
Requested scopes: org:read, project:read, project:write,
project:admin (the person picks the project), project:<id>:read|write|admin
(a specific project), sql:read, sql:write, migrations:write,
functions:deploy, branches:write, keys:secret. The person can grant
less than was requested, never more.
curl -X POST "$SCRIBASE_API_URL/v1/oauth/device/code" \
-d client_id=scribase-cli \
--data-urlencode 'client_name=Claude Code' \
--data-urlencode 'scope=project:write sql:write migrations:write'Response 200:
{
"device_code": "GmRhmhcxhwAzkoEqiMEg_DnyEysNkuNhszIySk9eS",
"user_code": "SCRB-7F3K-Q2MX",
"verification_uri": "https://console.scribase.com/device",
"verification_uri_complete": "https://console.scribase.com/device?user_code=SCRB-7F3K-Q2MX",
"expires_in": 600,
"interval": 5
}User codes use only unambiguous characters (no 0/O, 1/I/L), and the
console accepts them with or without the SCRB- prefix, dashes or capitals.
Keep device_code private to the polling process; show the person only the
URL and user_code. Creation is rate limited per client address
(429 with Retry-After).
POST /v1/oauth/token
Anonymous. Poll no faster than interval seconds.
| Field | Value |
|---|---|
grant_type |
urn:ietf:params:oauth:grant-type:device_code |
device_code |
From the step above |
client_id |
Optional, the same as above |
While waiting, and on failure, the answer is 400 with an OAuth error:
error |
Meaning | What the client does |
|---|---|---|
authorization_pending |
Nobody has approved yet | Poll again after interval |
slow_down |
Polled too fast | Add 5 seconds to the interval (the response carries the new interval) |
access_denied |
The person clicked Deny | Stop |
expired_token |
Ten minutes passed | Start over |
invalid_grant |
Unknown code, or its token was already issued | Stop |
unsupported_grant_type |
Wrong grant_type |
Fix the request |
Once approved, the next poll answers 200, exactly once:
{
"access_token": "scb_agt_1p-rw-sqlw-mig-x2609260412_3f9a...",
"token_type": "Bearer",
"expires_in": 28800,
"scope": "project:web:write sql:write migrations:write",
"organization_id": "acme",
"project_id": "web",
"token_id": "7d1e..."
}The token is an ordinary agent token: bound to the
organization, limited to the approved scopes, listed, audited and revocable
under Account > Access tokens. A second poll with the same device_code
gets invalid_grant.
Console routes
These back the console's /device page. They need a signed-in console
session; agent tokens are refused.
POST /v1/oauth/device/lookup
{ "user_code": "SCRB-7F3K-Q2MX" } returns the pending request: client name,
requested scopes, the address it came from, when it was made and when it
expires. Lookups are rate limited per person, so codes cannot be guessed.
POST /v1/oauth/device/approve
| Field | Notes |
|---|---|
user_code |
The code shown by the agent |
organization_id |
You must be a member |
project_id |
A project in that organization, or * for every project |
level |
read, write or admin, at most what was requested |
capabilities |
Subset of the requested sql:read, sql:write, migrations:write, functions:deploy, branches:write, keys:secret |
ttl |
1 hour to 30 days: 1h, 8h (default), 24h, 7d, 30d |
POST /v1/oauth/device/deny
{ "user_code": "SCRB-7F3K-Q2MX" }. The agent's next poll gets
access_denied.
Every approval and denial is recorded: who, which client, from which address, which scopes, and the id of the token that was issued.