Device Authorization

OAuth 2.0 device authorization flow (RFC 8628) for signing in from CLIs, IDEs, and other devices without a browser on the same machine.

The belt and infsh CLIs use this flow by default when you run belt login or infsh login in a terminal. You can also call the endpoints directly — or point any off-the-shelf OAuth 2.0 client library at our standard endpoints (see Standard OAuth endpoints).

No API key is required to start or poll a device authorization. Approving a request requires a logged-in workspace session in the browser.

Authentication · Magic link sign-in · CLI setup


Credential kinds

When you start device authorization, an optional token_kind field selects what the first successful poll returns after browser approval:

token_kindReturned fieldBehavior
sessionsession_tokenRevocable CLI session (30-day expiry). Acts as the signed-in user, supports X-Team-ID team switching, and appears in Settings → Sessions. Send as Authorization: Bearer … on API calls.
(omit or api_key)api_keyLegacy 30-day device-scoped API key (CLI, source: device-auth). Still returned for older CLIs that send no request body.

New CLIs should request token_kind: "session". The API key path remains for backward compatibility until older CLIs are retired.


Flow overview

mermaid
1sequenceDiagram2    participant CLI as CLI or client3    participant API as api.inference.sh4    participant Browser as Browser (logged in)56    CLI->>API: POST /device/auth {"token_kind":"session"}7    API-->>CLI: user_code, device_code, approve_url, interval8    CLI->>Browser: Open approve_url (optional)9    Browser->>API: POST /device/auth/approve (session cookie)10    loop Every interval seconds11        CLI->>API: GET /device/auth/poll/{device_code}12        API-->>CLI: status pending | approved | denied | expired13    end14    Note over API,CLI: First approved poll mints and returns credential (one-shot)15    API-->>CLI: session_token or api_key
  1. Your client calls POST /device/auth and receives a short user code and a secret device code.
  2. The user opens approve_url in a browser (or enters the user code at the workspace device-auth page) and approves while signed in. Approval records the authorization only — no credential is created or stored yet.
  3. Your client polls GET /device/auth/poll/{device_code} every interval seconds until the status is approved, denied, or expired.
  4. On the first poll where status is approved, the API mints and returns a credential (one-shot delivery):
    • session_token when the flow was started with token_kind: "session" — a revocable session you send as Authorization: Bearer …. Supports X-Team-ID like a web session. The session records the polling device's IP and user agent (not the approving browser's).
    • api_key when no token_kind was sent (legacy) — a 30-day CLI key named CLI with source: device-auth.

Browser approval only records authorization (user, team, scopes). The credential is minted at poll time and is not stored in the challenge record. A second poll for the same device_code returns status: expired even after a successful delivery.

Codes expire after 300 seconds (5 minutes), including approved-but-never-collected authorizations. The recommended poll interval is 3 seconds.


Standard OAuth endpoints (RFC 8628)

For third-party clients we recommend the standards-compliant endpoints — any OAuth 2.0 library that supports the device authorization grant works unmodified. Endpoints are discoverable via /.well-known/oauth-authorization-server (RFC 8414).

Start: POST /oauth/device_authorization

Authentication: None. Always mints a session token.

bash
1curl -X POST https://api.inference.sh/oauth/device_authorization
json
1{2  "device_code": "368bcb…",3  "user_code": "4XQA-HTM9",4  "verification_uri": "https://app.inference.sh/auth/device",5  "verification_uri_complete": "https://app.inference.sh/auth/device?code=4XQA-HTM9",6  "expires_in": 300,7  "interval": 38}

Exchange: POST /oauth/token

Poll the token endpoint with the device code grant:

bash
1curl -X POST https://api.inference.sh/oauth/token \2  -d "grant_type=urn:ietf:params:oauth:grant-type:device_code" \3  -d "device_code=DEVICE_CODE_HERE"

While pending, the endpoint returns standard RFC 8628 errors:

ErrorMeaning
authorization_pendingUser has not approved yet — keep polling
access_deniedUser denied the request
expired_tokenCode expired, or the credential was already delivered (one-shot)
invalid_grantUnknown device code

On approval:

json
1{2  "access_token": "0zdzfx…",3  "token_type": "Bearer",4  "team_id": "team_abc123"5}

team_id is an extension member identifying the team the user selected at approval. Send the token as Authorization: Bearer … on subsequent requests.

Revoke a credential (RFC 7009)

POST /oauth/revoke — possession of a token is the authority to revoke it. Works for session tokens and API keys. Unknown tokens still return 200 (per the RFC, so validity can't be probed).

bash
1curl -X POST https://api.inference.sh/oauth/revoke -d "token=TOKEN_HERE"

Start device authorization

POST /device/auth

Authentication: None.

Request (optional body)

FieldTypeRequiredDescription
token_kindstringNosession for a revocable CLI session; omit for a legacy API key
json
1{2  "token_kind": "session"3}

An empty body or omitted token_kind keeps legacy behavior (API key on the first approved poll).

Response

FieldTypeDescription
user_codestringShort code shown to the user (format XXXX-XXXX)
device_codestringSecret code used only for polling (do not display to the user)
approve_urlstringWorkspace URL to open for one-click approval
poll_urlstringConvenience URL (workspace host); clients should poll the API path below
expires_innumberSeconds until the code expires (300)
intervalnumberRecommended seconds between poll requests (3)

Example:

bash
1curl -X POST https://api.inference.sh/device/auth \2  -H "X-API-Version: 2" \3  -H "Content-Type: application/json" \4  -d '{"token_kind":"session"}'
json
1{2  "user_code": "ABCD-2345",3  "device_code": "01HXYZ…",4  "approve_url": "https://app.inference.sh/auth/device?code=ABCD-2345&utm_source=cli&utm_medium=app",5  "poll_url": "https://app.inference.sh/device/auth/poll/01HXYZ…",6  "expires_in": 300,7  "interval": 38}

Poll for approval

GET /device/auth/poll/{device_code}

Authentication: None.

Poll until status is terminal (approved, denied, or expired). Wait at least interval seconds between requests.

The credential is minted by — and delivered only to — the first poll that sees approved. Subsequent polls return expired with no credential. If you miss the response, restart the flow.

Response

FieldTypeDescription
statusstringpending, approved, denied, or expired
session_tokenstringPresent when status is approved and the flow used token_kind: "session"
api_keystringPresent when status is approved and the flow used the legacy API-key path (starts with 1nfsh-)
team_idstringTeam context for the login (when approved)

Only one credential field is set per flow. While status is pending, both are omitted.

One-shot delivery: The first poll that observes status: approved atomically mints the credential and returns it. Later polls for the same device_code return status: expired. If the user approves in the browser but the client stops polling before the code expires, the authorization is lost when the 300-second window ends.

Example:

bash
1curl "https://api.inference.sh/device/auth/poll/DEVICE_CODE_HERE" \2  -H "X-API-Version: 2"
json
1{2  "status": "approved",3  "session_token": "0zdzfx…",4  "team_id": "team_abc123"5}

Using a session token

After approval with token_kind: "session", store the session_token securely and send it on API requests:

bash
1curl https://api.inference.sh/apps/my \2  -H "Authorization: Bearer SESSION_TOKEN_HERE" \3  -H "X-API-Version: 2"

Session tokens are neither API keys (1nfsh-…) nor JWTs. The API resolves them as user sessions via the Authorization header.

Team switching: Send X-Team-ID to act as another team you belong to — same rules as a browser session. See Authentication — Team context.

Revocation: CLI sessions appear in Settings → Sessions (auth_method: device-auth) with the polling device's IP and user agent. Revoke them from the workspace or via DELETE /auth/sessions/{id} while authenticated with another session.


Check user code (approval page)

GET /device/auth/code/{code}

Authentication: None.

Used by the workspace device-auth UI to validate a user code before approval. Accepts codes with or without the dash (ABCD2345 or ABCD-2345).

Response

FieldTypeDescription
user_codestringNormalized code
validbooleanWhether the code can still be approved
statusstringpending, approved, or expired
expires_atstringISO timestamp (when known)

Approve device authorization

POST /device/auth/approve

Authentication: Workspace session (browser cookie after SSO or magic link). Guests cannot approve.

Approval records the authorization only (signed-in user, team, and optional scopes). It does not return a credential — the client must keep polling. The token_kind from initiation determines what the first successful poll mints:

  • token_kind: "session" — revocable CLI session (30-day expiry).
  • Legacy (no token_kind)CLI API key (30-day expiry, full scopes unless scopes is set).

Nothing secret is stored server-side at approval; a minted session records the polling device's own IP and user agent.

Request

FieldTypeRequiredDescription
codestringYesUser code from POST /device/auth
team_idstringNoTeam to issue the credential for (defaults to active team)
scopesstring[]NoAPI key scopes (legacy flows only); omit for full access
json
1{2  "code": "ABCD-2345"3}

Response

json
1{2  "success": true,3  "message": "Device authorized successfully"4}

Errors include invalid_code, expired, and already_processed when the code is no longer pending.


CLI usage

bash
1belt login                # device flow → session token (supports belt auth switch)2belt login --key 1nfsh-  # API key login — team-pinned, for CI / automation3belt auth token           # print stored credential to stdout (no newline) for scripts4belt me                   # shows auth method: session vs api key (team-pinned)

belt login stores a session token: it acts as you, works with belt auth switch, and shows up in the workspace sessions list where it can be revoked. belt login --key (or INFSH_API_KEY) stores an API key: team-pinned by design — belt auth switch has no effect and will say so.

In non-interactive environments (no TTY, CI set, or BELT_NON_INTERACTIVE=1), device authorization is skipped; pass --key or set INFSH_API_KEY. After login, use belt auth token in subshells: curl -H "Authorization: Bearer $(belt auth token)" ...

Session-start hook: When the belt plugin is installed for Claude Code or Codex, the SessionStart hook calls belt plugin hook session-start. If no credential is configured (including anonymous guest sessions), the hook opens a browser for device authorization, prints the approval URL and user code into session context, and polls for up to 45 seconds. A 6-hour cooldown prevents reopening the browser on every session (~/.inferencesh/hook-auth-cooldown). When already logged in, the hook prints your account email instead.

CLI setup — Login · Coding agents — SessionStart hook


we use cookies

we use cookies to ensure you get the best experience on our website. for more information on how we use cookies, please see our cookie policy.

by clicking "accept", you agree to our use of cookies.
learn more.