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. Looking up a user code and approving a request require the person's own sign-in (a logged-in web app 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 by default; configurable at approval). Acts as the signed-in user, supports switching workspaces with X-Team-ID, optional API key scopes at approval, and appears in settings → personal → 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 | slow_down | 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 web app's 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, workspace, scopes). The credential is minted at poll time and is not stored in the challenge record. After a successful delivery, a later poll of the same device_code gets no credential: GET /device/auth/poll answers status: expired, and POST /oauth/token answers invalid_grant.

Codes expire after 300 seconds (5 minutes), including approved-but-never-collected authorizations. Each code starts with a poll interval of 3 seconds; polling faster raises it (see Polling and slow_down).

When the device starts the flow, the API records who started it: the client and version (from the user agent's product token, for example belt 1.22.1), the OS, the IP address, and the location from a geo lookup. The approval page shows this record, never anything the link carries.


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}

Scope on the start request

The optional scope parameter (space-separated, RFC 8628 3.1) bounds what the approval may grant:

bash
1curl -X POST https://api.inference.sh/oauth/device_authorization \2  -d "scope=apps:read apps:execute"
scopeResult
Omitted or emptyThe device asks for full access; the approver picks what to grant
Names known resource scopesThe approval grants at most these. Protocol and unknown names next to them are ignored
Names only protocol scopes (for example openid), or only unknown scopes400 invalid_scope

The approval page starts from the requested scopes. An approval that picks a scope the device did not ask for fails with 400 invalid_scope. The legacy POST /device/auth takes no scope: its devices ask for full access.

Too many starts from one IP (30 per 15 minutes, shared with POST /device/auth) return 429 temporarily_unavailable with a Retry-After header.

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"

Until the credential is delivered, the endpoint returns standard RFC 8628 errors (HTTP 400):

ErrorMeaning
authorization_pendingUser has not approved yet — keep polling
slow_downStill pending, and this poll came too soon. Add 5 seconds to your interval (see Polling and slow_down)
access_deniedUser denied the request
expired_tokenCode expired before the credential was collected
invalid_grantUnknown device code, or the credential was already delivered (one-shot)

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 workspace 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"

Authorization code flow (MCP and third-party clients)

MCP clients (Claude, Cursor, Windsurf, and others) can call https://api.inference.sh/mcp with OAuth access tokens instead of workspace API keys. The authorization server advertises:

Supported scopes are apps:read and apps:execute. A request that names no recognised scope is granted both (apps:read apps:execute), never full access. When the client registered a scope at POST /oauth/register, the grant stays within it, and a request entirely outside it fails with 400 invalid_scope. Public clients must use PKCE (code_challenge_method=S256). The server supports Client ID Metadata Documents (CIMD) (client_id_metadata_document_supported: true) and Dynamic Client Registration at POST /oauth/register.

Typical flow:

  1. GET /oauth/authorize with client_id, redirect_uri, scope, state, PKCE parameters, and optional team_id (workspace hint for the consent UI). Unauthenticated users are redirected to the web app to sign in. Signed-in users are always sent to the consent screen — the authorize endpoint never issues an authorization code directly, even if the user approved the same client before or the request includes prompt=none. Clients that stay signed in should use grant_type=refresh_token instead of repeating authorize.
  2. The API redirects to the web app consent page at /auth/consent, forwarding the authorize query string (including optional team_id). On that screen the user picks a workspace and approves scopes. When team_id names a workspace the signed-in user belongs to, the workspace switcher starts on that team; otherwise it defaults to the browser session's active workspace, or the user's first workspace. Changing the switcher before Authorize sets the team_id sent on approval — the authorization code and tokens are issued for that workspace, not silently for whichever workspace happened to be active in the app shell. Approval is recorded with POST /oauth/authorize/approve (the person's own sign-in) or denied with POST /oauth/authorize/deny.
  3. After approval, the client exchanges the authorization code at POST /oauth/token with grant_type=authorization_code and the PKCE verifier.
  4. Send the JWT access_token as Authorization: Bearer … on MCP requests (short-lived; refresh with grant_type=refresh_token). Tokens are issued for the workspace chosen at consent. An OAuth access token in the ?access_token= query parameter is refused: send it in the Authorization header only.

Token lifetimes

Every POST /oauth/token response states how long both tokens last, so a client can plan its refreshes instead of finding out by being refused:

json
1{2  "access_token": "eyJ…",3  "token_type": "Bearer",4  "expires_in": 600,5  "refresh_token": "rt_…",6  "refresh_token_expires_in": 2592000,7  "scope": "apps:read apps:execute"8}
TokenLifetimeNotes
access_token10 minutes (expires_in: 600)A JWT. Renew it with grant_type=refresh_token; a request with an expired one gets 401.
refresh_token30 days unused (refresh_token_expires_in: 2592000)Single use. Each refresh retires it and issues a new pair with a full 30-day term again.

The refresh token's term is an idle limit, not an absolute one: a client that refreshes at least once every 30 days keeps the same authorization indefinitely and never sends the user back through consent. There is no cap on how long a chain of refreshes may run. The authorization ends only when:

  • the refresh token goes unused for 30 days: the next refresh answers 400 invalid_grant ("Refresh token expired") and the client starts at GET /oauth/authorize again;
  • the user revokes the client at settings → personal → authorized apps or POST /oauth/connected-apps/revoke;
  • a refresh token is presented twice: the second request gets invalid_grant, since the first already rotated it. Store the new pair before you discard the old one.

refresh_token_expires_in is not part of RFC 6749 (which leaves refresh-token lifetime to the server and unstated); it is the field name GitHub and other providers use for the same number. A client that does not know it can ignore it.

Authentication: The person's own sign-in (browser session or belt login session): these take user:read or user:write, which only it holds. API keys and OAuth access tokens get 403 requires_sign_in.

The consent page loads client metadata before the user approves or denies:

QueryRequiredDescription
client_idYesOAuth client id (opaque dyn_… id, or HTTPS URL for CIMD)
redirect_uriNoRedirect URI from the authorize request (used for verification hints)
scopeNoSpace-separated scopes

Response (OAuthAuthorizeInfoResponse):

FieldDescription
client_nameDisplay name shown on the consent screen
client_typemetadata_document, registered, etc.
originHostname derived from the client id or redirect URI
verifiedWhether the platform treats the client as a known publisher
scopesParsed scope list with labels
redirect_hostHostname of redirect_uri when provided

Approve or deny with POST /oauth/authorize/approve and POST /oauth/authorize/deny. Both endpoints require the person's own sign-in: the browser session (cookie after magic link or SSO) or a belt login session. Both take user:write, which only that sign-in holds: API keys (personal and workspace), OAuth access tokens, and sessions bound to one place (for example an embedded workspace frame) get 403 requires_sign_in. None of them can approve an OAuth client or a device code, even for their own workspace. An approval mints credentials that renew on their own (CLI sessions, OAuth refresh tokens) or roam across workspaces (device-auth keys), so only a session may grant one.

Approve body (OAuthApproveRequest): client_id, redirect_uri, code_challenge, state, scope, and optional team_id (workspace picked on the consent form; omit to use the session's active workspace). An empty scope grants apps:read apps:execute. Requested scopes cannot exceed what the approving session holds — see Scope ceiling at approval.

Workspace choice at approval

When the approver is the person's own sign-in (the only credential that may call these routes), team_id selects the workspace for the minted OAuth tokens or device credential. The API checks membership with the same rules as X-Team-ID elsewhere: the team must exist and the signed-in person must be allowed to act as it (personal workspace, org membership, service-account rules, and so on).

team_id in approve bodyResult
OmittedUses the session's active workspace
Same as the session's active workspaceAllowed
Another workspace you belong toAllowed
A workspace you cannot act as403 forbidden

The same team_id rules apply to POST /device/auth/approve.

Scope ceiling at approval

Approvals follow the same scope ceiling as POST /apikeys: the minted credential cannot be stronger than the approver.

  • An unrestricted approving session (no scope limit on the session itself) may grant any requested scopes. Omitting scopes on device approve means full access for the new credential. An OAuth grant is never full access: an empty scope on OAuth approve grants apps:read apps:execute.
  • A scoped approving session (for example a belt login session approved with a subset of permissions) may grant only scopes it already holds. A requested scope outside that set returns 403 forbidden (This sign-in cannot grant a scope it does not have). Omitting scopes on device approve grants the approver's scopes, not full access.

Resource-level scopes on the approver satisfy action-level requests the same way as on key creation (for example an approver with apps may grant apps:read).

The API marks some OAuth clients as verified so users can distinguish first-party and partner integrations from arbitrary Dynamic Client Registration clients.

Client identityverifiedclient_name on consent
CIMD whose URL host is claude.ai, anthropic.com, cursor.com, or windsurf.comtrueCanonical partner name (Claude, Cursor, …)
CIMD hosted on inference.sh (for example https://inference.sh/oauth/clients/apple.json)trueclient_name from the metadata document (for example inference.sh for Apple)
Opaque client id from POST /oauth/register whose redirect_uri host is claude.ai, anthropic.com, cursor.com, or windsurf.comtrueCanonical partner name
Any other opaque client id from POST /oauth/registerfalseName supplied at registration
CIMD on any other hostfalseName from the fetched metadata document

Verification for first-party documents is based on the client_id URL hostname only. A redirect_uri that points at inference.sh does not verify a registered client; only the partner hosts above verify by redirect. Hostname lookalikes (for example inference.sh.example.com) are not treated as inference.sh.

First-party metadata documents are public JSON files under https://inference.sh/oauth/clients/. Example:

code
1GET https://inference.sh/oauth/clients/apple.json
json
1{2  "client_id": "https://inference.sh/oauth/clients/apple.json",3  "client_name": "inference.sh for Apple",4  "redirect_uris": ["https://inference.sh/oauth/callback/apple"],5  "grant_types": ["authorization_code", "refresh_token"],6  "response_types": ["code"],7  "token_endpoint_auth_method": "none"8}

Manage authorized OAuth clients

Authentication: The person's own sign-in (browser session or belt login session): these take user:read or user:write, which only it holds. API keys and OAuth access tokens get 403 requires_sign_in.

EndpointDescription
GET /oauth/connected-appsOAuth clients you have authorized (active refresh grants)
POST /oauth/connected-apps/revokeBody { "client_id": "…" } — revokes refresh tokens for that client

The web app lists these under settings → personal → authorized apps. Each grant is shown under the identity it was approved for: a registered client approved through a partner's callback keeps its verified badge and name in this list.


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_urlstringWeb app URL to open for one-click approval
poll_urlstringConvenience URL (web app 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 "Content-Type: application/json" \3  -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}

Too many starts from one IP (30 per 15 minutes, shared with POST /oauth/device_authorization) return 429 rate_limited with a Retry-After header.


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. slow_down is not terminal: keep polling at the new interval.

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, slow_down, approved, denied, or expired
intervalnumberPresent when status is slow_down: the seconds between polls this code now requires
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_idstringWorkspace context for the login (when approved)

Only one credential field is set per flow. While status is pending or slow_down, both are omitted. An unknown device_code answers status: expired.

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.

Polling and slow_down

Both poll endpoints follow RFC 8628 3.5. Each code holds its device to an interval, starting at 3 seconds:

  • A poll is admitted when the code's previous poll was at least the interval ago, less 1 second of leeway for network delay. Of two concurrent polls, one is admitted.
  • A poll that comes sooner is answered slow_down (status: slow_down with the new interval here, error slow_down on POST /oauth/token), and the code's interval grows by 5 seconds for that poll and every later one.
  • slow_down only replaces pending. A code that was approved, denied, or expired answers that however soon it is polled.

A client that ignores slow_down and keeps polling early gets slow_down on every poll until the code leaves pending, then gets its terminal answer. Older CLIs that treat any non-terminal status as pending keep working.

Example:

bash
1curl "https://api.inference.sh/device/auth/poll/DEVICE_CODE_HERE"
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"

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

Switching workspaces: Send X-Team-ID to act as another workspace you belong to, with the same rules as a browser session. See Authentication: workspace context.

Revocation: CLI sessions appear in settings → personal → sessions (auth_method: device-auth) with the polling device's IP and user agent. The belt and infsh CLIs send an RFC 7231 product token (belt/x.y.z or legacy inference-cli/x.y.z), which the API parses into client name and version for the session record. Revoke sessions from the web app or via the sign-in sessions API below.

Sliding lifetime

CLI session tokens use the same sliding idle window as browser sign-in cookies:

  • On each authenticated API request (every belt command that hits the API counts), if less than half of the current window remains, the server moves expires_at forward to now + 7 days.
  • The token string does not rotate. The CLI keeps sending the value stored in ~/.inferencesh/config.json (or under BELT_CONFIG_DIR); you do not need to log in again just because the server extended the session.
  • Sessions stay alive while you use them regularly; idle sessions still expire when expires_at passes.

Fixed TTL: When approval sets expires_in to a lifetime shorter than 7 days (for example a one-hour session), that expiry is intentional and is not extended on use — for bearer tokens or session cookies.


Sign-in sessions

Manage browser and CLI authentication sessions (distinct from app sessions used for stateful POST /run).

Routes are under /auth/ and require an authenticated session or bearer token.

EndpointScope
GET /auth/sessionsuser:read
DELETE /auth/sessions/{id}user:write
POST /auth/sessions/revoke-alluser:write

List sessions

GET /auth/sessions

Returns active sign-in sessions for the current user. The session making the request is marked current: true.

FieldTypeDescription
idstringSession ID
created_atstringISO timestamp
expires_atstringISO timestamp
ipstringClient IP
browserstringBrowser or client name
auth_methodstringmagic-link, device-auth, api_key, etc.
scopesstring[]Active API key scopes when auth_method is api_key
currentbooleanWhether this is the requesting session
bash
1curl https://api.inference.sh/auth/sessions \2  -H "Authorization: Bearer inf_your_session_token"

Revoke one session

DELETE /auth/sessions/{id}

Deletes a specific session. Use another valid session or token to call this endpoint.

Revoke all other sessions

POST /auth/sessions/revoke-all

Deletes every session except the one making the request.

You can also revoke bearer tokens with POST /oauth/revoke (RFC 7009).


Check user code (approval page)

GET /device/auth/code/{code}

Authentication: The person's own sign-in (browser session or belt login session). It takes user:read: API keys and OAuth access tokens get 403 requires_sign_in. Signed-out callers get 401.

Used by the web app's device-auth UI to describe a user code before approval. The answer shows where the device that started it is, so it is only for a signed-in person, and each lookup spends one code attempt. Accepts codes with or without the dash (ABCD2345 or ABCD-2345).

Only a pending code within its window is described. An unknown, used, denied, or expired code answers the same way (valid: false, status: expired), so a guessed code reveals nothing.

Response

DeviceAuthCodeInfo:

FieldTypeDescription
user_codestringNormalized code
validbooleanWhether the code can still be approved
statusstringpending or expired
expires_atstringISO timestamp (pending codes)
kindstringlogin (a device sign-in) or admin_elevation (a belt admin elevate request)
deviceobjectThe device that started the code, as recorded at the start (DeviceAuthDeviceInfo): client, client_version, os, ip, city, country, created_at
proximitystringWhere the code was started relative to this request: same_network, same_country, different, or unknown (see Approving from another network)
scopesstring[]Scopes the device asked for at the start; empty when it named none

Approve device authorization

POST /device/auth/approve

Authentication: The person's own sign-in: the browser session (cookie after SSO or magic link) or a belt login session. It takes user:write: API keys and OAuth access tokens get 403 requires_sign_in. Guests get 401.

Approval records the authorization only (signed-in user, workspace, optional scopes, and optional credential TTL). It does not return a credential, so 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 by default, or expires_in from approval).
  • Legacy (no token_kind) — CLI API key (30-day expiry by default, or expires_in from approval).

Pass an explicit scopes list to restrict what the minted credential can do. Scope limits on the minted credential are enforced like API keys. The approving session cannot grant more than it holds — see Scope ceiling at approval.

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_idstringNoWorkspace for the minted credential. Omit to use the approver's active workspace. Only the person's own sign-in (browser or belt login session) may call this endpoint; see Workspace choice at approval.
scopesstring[]NoPermission scopes for the minted credential (session or API key). Omit for full access when the approver is unrestricted; when the approver is scoped, omit to grant the approver's scopes only.
expires_innumberNoCredential lifetime in seconds (default 2592000 — 30 days). Capped at one year.
typed_codestringUnless same networkThe user code as the person typed it from the device's screen. Required unless the code's proximity is same_network; see Approving from another network.
json
1{2  "code": "ABCD-2345",3  "scopes": ["apps:execute", "billing:read"],4  "expires_in": 72005}

Response

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

Errors include invalid_code when the code is unknown or no longer pending (used, denied, or expired), and already_processed when another approval took it first; code_confirmation_required when typed_code is missing or does not match; invalid_scope when scopes names a scope the device did not ask for at the start; requires_sign_in for any credential that is not the person's own sign-in; forbidden when team_id names a workspace this sign-in may not choose or when scopes includes a permission the approver does not hold; 429 rate_limited when the attempt limit is spent.

Approving from another network

The API compares where the code was started with where the approval comes from, using the IP address and geo lookup recorded at each end:

proximityMeaning
same_networkSame IPv4 /24 or IPv6 /64
same_countryAnother network in the same country
differentAnother country
unknownAnother network, and a geo lookup could not tell the country on one side

Only same_network approves with one click. For any other proximity, the approval must carry typed_code, the code typed from the device's screen, matching the user code. The link the person opened already carries the code, so following it proves nothing about who holds the device (RFC 8628 5.4, remote phishing). The same country is not enough: a VPN exit in the victim's country puts an attacker's device there. Without a matching typed_code the API answers 400 code_confirmation_required.

On the approval page, a sign-in started away from the browser's network shows:

  • A warning: started from another network, from another country (with its location), or not confirmed to have started near you, followed by "if someone sent you this link, don't approve it."
  • A started from panel with the client and version, OS, location, IP address, and how long ago it started.
  • A field to type the code shown on the terminal. Authorize stays disabled until a complete code is entered, and a wrong code shows "that code doesn't match the one on your terminal".

On the same network the page shows the started from panel without the warning or the field.

Attempt limits

Code lookups (GET /device/auth/code/{code}) and approvals (POST /device/auth/approve) each spend one attempt from two budgets:

  • 20 per signed-in person per 15 minutes
  • 60 per IP address per 15 minutes, across accounts. An IPv6 address counts by its /64.

When either budget is spent, the API answers 429 rate_limited with a Retry-After header. The limit fails closed: if the limiter cannot answer, the attempt is refused.

Web app approval UI

When approving from app.inference.sh/auth/device, the consent form lets you pick permissions (scopes) and session expiration before authorizing.

The permissions start at the belt login preset (standard plus approvals:write). Under custom, the workspace permissions are listed by group, and your account's own permissions follow in a separate your account section, grouped the same way: api keys, user, billing (billing:write) and approvals, plus admin for staff. These are the sign-in-only scopes: a login can hold them, an API key cannot. The page reads them from account_scopes and account_groups on GET /scopes. The expiration picker defaults to 1 year and offers 1 hour through 1 year (sessions cannot be set to never expire). The selected TTL is sent as expires_in on this endpoint. Programmatic callers that omit expires_in still get the 30-day API default.


CLI usage

bash
1belt login                # device flow → session token (supports belt auth switch)2belt login --key-stdin < key.txt  # API key login — team-pinned, for CI / automation3belt auth token           # mint a 15m team-pinned API key to stdout (no newline) for scripts; --long-lived prints the stored credential4belt 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 your sessions list in settings, where it can be revoked. belt login --key-stdin (or INFERENCE_API_KEY) stores an API key: pinned to one workspace by design, so belt auth switch has no effect and will say so.

In non-interactive environments (no TTY, CI set, or BELT_NON_INTERACTIVE=1), belt login without --key prints the approval link and the user code instead of opening a browser. When the browser is on another network, the approval page asks for that code. After approving, the browser shows a new 6-digit code: run belt login --code <6-digit code from the browser> on the same machine. The link and codes expire after 5 minutes, and running belt login again starts over. For CI and scripts, pass --key-stdin or set INFERENCE_API_KEY. See CLI setup: Login. 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.