Skip to content
OfferSign up and get $10 of credit for any model in Chat and the API. Top up from $1, with no processing fees. See free models →·Some free models are at the daily cap — refills 05:30 IST; requests auto-route to a live backup. Status →
One AI API Key
DocsDevice login

Docs

Sign in a command-line tool

How a terminal tool gets an API key without copy and paste, using the device login flow (RFC 8628). Endpoints, fields, errors and a sample client.

Updated
On this page

A tool that runs in a terminal can get its own API key without asking the user to copy one from the console. The tool shows a short code, the user approves it in the browser, and the tool receives a new key. The flow follows the OAuth device authorization grant (RFC 8628), the same pattern GitHub CLI uses to sign in.

This page is for people who build tools on top of the API. There is no official command-line client yet; if you only need a key for your own code, create one on the API keys page instead.

How it works

  1. The tool asks for a code: POST /api/cli/code. No key or sign-in is needed.
  2. The tool shows the user_code and opens verification_uri_complete in the browser (or prints verification_uri for the user to open).
  3. The user signs in, checks that the code on the page matches the terminal, and chooses Approve or Deny. The page warns when the request came from a different network than the browser.
  4. On approval the console creates a key named after the tool. It has no spend limit, so the account balance applies, and it follows the account’s key expiry policy if one is set.
  5. Meanwhile the tool polls POST /api/cli/token every interval seconds. After approval, the next poll returns the key. It is returned once; later polls answer invalid_grant.

A code expires after 10 minutes. Pending sign-ins are held in memory, so a service restart can drop one; the tool then gets invalid_grant and should start again.

Start a sign-in

Send an optional name (printable ASCII; longer than 40 characters is cut, empty becomes “CLI”). It becomes the key’s name and the label on the approval page.

Terminal
curl -sS -X POST https://api.oneaiapikey.com/api/cli/code \
  -H "Content-Type: application/json" \
  -d '{"name": "my-tool on laptop"}'
200 response
{
  "device_code": "<long random string, keep it secret>",
  "user_code": "BCDF-GHJK",
  "verification_uri": "https://oneaiapikey.com/console/device",
  "verification_uri_complete": "https://oneaiapikey.com/console/device?code=BCDF-GHJK",
  "expires_in": 600,
  "interval": 5
}

The endpoint also accepts application/x-www-form-urlencoded. Keep device_code inside the tool: anyone who holds it can collect the key once the user approves.

Poll for the key

Terminal
curl -sS -X POST https://api.oneaiapikey.com/api/cli/token \
  -H "Content-Type: application/json" \
  -d '{"device_code": "<device_code>"}'

Until the user decides, the poll returns HTTP 400 with one of these errors. A finished sign-in returns 200:

200 response
{
  "access_token": "sk-…",
  "api_key": "sk-…",
  "name": "my-tool on laptop",
  "token_type": "api_key"
}

access_token and api_key hold the same key, so OAuth device-flow libraries work without changes. Use it like any other key: Authorization: Bearer sk-… on every request (see Authentication).

Status and errorWhat the tool should do
400 authorization_pendingThe user has not decided yet. Wait interval seconds and poll again.
400 slow_downThe poll came less than 4 seconds after the previous one. Add 5 seconds to the interval, as RFC 8628 asks.
400 access_deniedThe user chose Deny. Stop.
400 expired_tokenThe 10 minutes ran out. Start a new sign-in.
400 invalid_grantUnknown device_code, a key that was already collected, or a sign-in lost to a restart. Start again.
400 invalid_requestThe body is not valid JSON or form data.
429 slow_downToo many new codes from one address (10 per 15 minutes). Wait for the Retry-After seconds.

Sample client

A complete flow in Python with the standard library only:

device_login.py
import json, time, urllib.error, urllib.request

BASE = "https://api.oneaiapikey.com/api/cli"

def post(path, body):
    req = urllib.request.Request(BASE + path, json.dumps(body).encode(),
                                 {"Content-Type": "application/json"})
    try:
        with urllib.request.urlopen(req) as r:
            return r.status, json.load(r)
    except urllib.error.HTTPError as e:
        return e.code, json.load(e)

_, flow = post("/code", {"name": "my-tool"})
print(f"Open {flow['verification_uri']} and enter the code {flow['user_code']}")

interval, deadline = flow["interval"], time.time() + flow["expires_in"]
while time.time() < deadline:
    time.sleep(interval)
    status, body = post("/token", {"device_code": flow["device_code"]})
    if status == 200:
        api_key = body["api_key"]  # store it like a password, e.g. in the OS keychain
        break
    if body.get("error") == "slow_down":
        interval += 5
    elif body.get("error") != "authorization_pending":
        raise SystemExit(f"Sign-in failed: {body.get('error')}")

↑ ↓ moveEnter openEsc close