Device flow

Sign in a CLI, TV or headless device with a short code the person approves in any browser (OAuth 2.0 Device Authorization Grant, RFC 8628).

Use the device flow when your app runs somewhere a person can't easily complete a browser redirect: a command-line tool, a TV app, an SSH session, a build box. Your app asks Hypery for a code, shows it, and polls the token endpoint. The person opens https://hypery.ai/device on their phone or laptop, signs in, checks the code and the permissions, and presses Approve. Your app's next poll receives the same access_token and refresh_token as the authorization code flow.

your device
POST /api/oauth/device_authorization
your device
shows the code and URL
any browser
hypery.ai/device, sign in, Approve
your device
polls /api/oauth/token, gets tokens

At a glance

StartPOST https://hypery.ai/api/oauth/device_authorization
PollPOST https://hypery.ai/api/oauth/token with grant_type=urn:ietf:params:oauth:grant-type:device_code
Approval pagehttps://hypery.ai/device (the person must be signed in, with 2FA if enabled)
Code lifetime10 minutes; each code can be redeemed once
User code8 letters shown as XXXX-XXXX (no vowels or look-alike characters; case and dashes are ignored when typed)
Client authclient_id only (public client) or client_id + client_secret
TokensSame as the authorization code flow; refresh with grant_type=refresh_token
Opt-inOff by default; turn on Sign in from other devices in your app's settings

0. Enable it for your app

The device flow is off for every app until you turn it on: in the developer console, open your app's Settings, switch on Sign in from other devices in the Sign-in section, and save. Until then both endpoints answer unauthorized_client. Turning it off again stops new device sign-ins and any codes that haven't been redeemed yet; tokens already issued keep working until they expire or are revoked.

1. Start: request a device code

POST https://hypery.ai/api/oauth/device_authorization
Content-Type: application/x-www-form-urlencoded
 
client_id=YOUR_CLIENT_ID&scope=read%20ai%3Achat

The body may be form-encoded or JSON. Client credentials may also be sent with HTTP Basic.

FieldRequiredDescription
client_idYesOAuth client id from the registered app
client_secretNoSend it only if the secret is safe where your app runs (a server, not a distributed CLI). When you send it here, the token request must send it too.
scopeYesSpace-separated scopes. Ask only for what the device needs; every scope must be registered on the app.

Response (200):

{
  "device_code": "Wm9...long opaque value...",
  "user_code": "BCDF-GHJK",
  "verification_uri": "https://hypery.ai/device",
  "verification_uri_complete": "https://hypery.ai/device?user_code=BCDF-GHJK",
  "expires_in": 600,
  "interval": 5
}

Keep device_code secret: it is what redeems the tokens. Show the person user_code and verification_uri (or render verification_uri_complete as a QR code, so they don't have to type the code).

ErrorStatusMeaning
invalid_request400client_id missing
invalid_client401Unknown client, suspended app, or wrong client_secret
unauthorized_client400The device flow isn't enabled for this app (step 0)
invalid_scope400scope missing, or a requested scope isn't registered on the app
rate_limited429Too many device authorization requests from this IP

2. The person approves

The person opens verification_uri, signs in to Hypery if needed, and enters the code. The page shows your app's name, logo, the code and the permissions requested, with Approve and Deny. Nothing is granted until they press Approve, even when they arrive through verification_uri_complete.

Approving records your app on the person's Connected Apps list, exactly like the consent screen of the authorization code flow.

3. Poll the token endpoint

POST https://hypery.ai/api/oauth/token
Content-Type: application/x-www-form-urlencoded
 
grant_type=urn:ietf:params:oauth:grant-type:device_code&device_code=DEVICE_CODE&client_id=YOUR_CLIENT_ID

Wait interval seconds between requests. Until the person answers, the response is an error (HTTP 400, { "error": "<code>" }):

ErrorWhat to do
authorization_pendingNot answered yet. Keep polling at the current interval.
slow_downYou polled too fast. Add 5 seconds to your interval for this and all later requests.
access_deniedThe person pressed Deny, or revoked the app before you redeemed the code. Stop.
expired_tokenThe 10 minutes ran out. Stop, and start again with a new device code if the person still wants to sign in.
invalid_grantUnknown device code, a code issued to another client, or one that was already redeemed. Stop.
invalid_clientUnknown or suspended client, wrong secret, or the secret was sent at step 1 but not now (401). Stop.
unauthorized_clientThe app's device flow was turned off. Stop.

Once approved, the next poll returns the tokens (200), once:

{
  "access_token": "…",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "…",
  "scope": "read ai:chat"
}

Refresh and revoke them as described in OAuth 2.0 apps. A public client (no client_secret at step 1) refreshes without a secret too.

Example

const HUB = 'https://hypery.ai';
const form = (fields) => ({
  method: 'POST',
  headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
  body: new URLSearchParams(fields),
});
 
const start = await (
  await fetch(`${HUB}/api/oauth/device_authorization`, form({ client_id: CLIENT_ID, scope: 'read ai:chat' }))
).json();
console.log(`Open ${start.verification_uri} and enter ${start.user_code}`);
 
let interval = start.interval;
for (;;) {
  await new Promise((r) => setTimeout(r, interval * 1000));
  const res = await fetch(
    `${HUB}/api/oauth/token`,
    form({
      grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
      device_code: start.device_code,
      client_id: CLIENT_ID,
    }),
  );
  const body = await res.json();
  if (res.ok) {
    saveTokens(body); // access_token, refresh_token, expires_in, scope
    break;
  }
  if (body.error === 'authorization_pending') continue;
  if (body.error === 'slow_down') {
    interval += 5;
    continue;
  }
  throw new Error(`Sign-in failed: ${body.error}`);
}