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.
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
- The tool asks for a code:
POST /api/cli/code. No key or sign-in is needed. - The tool shows the
user_codeand opensverification_uri_completein the browser (or printsverification_urifor the user to open). - 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.
- 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.
- Meanwhile the tool polls
POST /api/cli/tokeneveryintervalseconds. After approval, the next poll returns the key. It is returned once; later polls answerinvalid_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.
curl -sS -X POST https://api.oneaiapikey.com/api/cli/code \
-H "Content-Type: application/json" \
-d '{"name": "my-tool on laptop"}'{
"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
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:
{
"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 error | What the tool should do |
|---|---|
400 authorization_pending | The user has not decided yet. Wait interval seconds and poll again. |
400 slow_down | The poll came less than 4 seconds after the previous one. Add 5 seconds to the interval, as RFC 8628 asks. |
400 access_denied | The user chose Deny. Stop. |
400 expired_token | The 10 minutes ran out. Start a new sign-in. |
400 invalid_grant | Unknown device_code, a key that was already collected, or a sign-in lost to a restart. Start again. |
400 invalid_request | The body is not valid JSON or form data. |
429 slow_down | Too 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:
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')}")Related
- Authentication and API keys: sending the key, per-key limits, rotation.
- Errors: what a revoked or expired key returns.
- Headers: every header the API reads and returns.