Get your API key
Pick the right kind of key, mint a signed-in key with device login, verify it with GET /api/v1/me, and know when it expires.
Every authenticated request sends a cxy_live_ user key, and a user key is one
of two kinds. A signed-in credential comes from device login and belongs in a
person's own tool, such as a CLI or a desktop app. A developer API key is
created in the Cruxy Console and belongs on a server.
Two kinds of key
| Signed-in credential | Developer API key | |
|---|---|---|
| How you get it | Device login, or native OAuth login, whichever client runs the flow. | Created in the Cruxy Console. |
| Use it for | A person's own tool, such as a CLI or a desktop app. | A server, and anything that serves people other than the key's owner. |
| Memory and rules | On by default. | Off unless a request opts in. |
| Expires | At expires_at, 90 days after issue. Use does not renew it. | Never. |
| Rotate or revoke | Not by its holder. | In the Console. |
| Spend limits | A monthly spend cap. | The Console can limit it to chosen models and a monthly spend cap. |
| Limits bucket | subscription | apikey |
A signed-in credential on a server would recall its owner's memory into other
people's turns, store what they say in that memory, and apply the owner's
organization rules, unless every request sends
memory: {"recall": false, "capture": false} and rules: false. A headless
key, billed from prepaid credit, has memory and rules off by default, like a
developer API key. To see which kind a key is, read bucket on
GET /api/v1/limits; see Rate limits and budgets.
Device login
Cruxy issues signed-in credentials through an RFC 8628 device-authorization flow: your client asks for a code, a person approves it in a browser, and your client polls until the key is minted. The browser and the client can be different machines.
The two device-login endpoints take no key and no Authorization header: their
job is to issue one. Both are rate-limited and return 429 with
rate_limited if you call them too often.
Request a device code
POST /api/v1/device/codewith no body.curl -X POST https://api.cruxy.ai/api/v1/device/codeconst res = await fetch('https://api.cruxy.ai/api/v1/device/code', { method: 'POST', }) if (!res.ok) throw new Error(`device code request failed: ${res.status}`) const device = await res.json() console.log(`Open ${device.verification_uri_complete} to approve.`) console.log(`Confirm this code on that page: ${device.user_code}`){ "device_code": "cxy_dev_QK3f...redacted", "user_code": "WDJB-MJHT", "verification_uri": "https://cruxy.ai/device", "verification_uri_complete": "https://cruxy.ai/device?user_code=WDJB-MJHT", "expires_in": 600, "interval": 5 }Field Meaning device_codeThe code your client polls with. The person approving never needs it. user_codeThe short code the person confirms in the browser. verification_uriThe approval page. The person types user_codethere.verification_uri_completeThe approval page with user_codealready filled in.expires_inSeconds until device_codeexpires.intervalSeconds to wait between polls. Read
verification_uri,expires_in, andintervalfrom each response rather than hardcoding the example values.Approve in the browser
Send the person to
verification_uri_complete, or toverification_uriwith theuser_codeto type. They approve the request beforeexpires_inseconds pass. If they decline, polling returnsaccess_denied.Poll for the key
POST /api/v1/device/tokenwith thedevice_code, waitingintervalseconds between attempts. Polling faster than the limiter allows returns429withrate_limited; wait theRetry-Afterseconds, then resume atinterval.# DEVICE_CODE holds the device_code from step 1. curl -X POST https://api.cruxy.ai/api/v1/device/token \ -H "Content-Type: application/json" \ -d "{\"device_code\": \"$DEVICE_CODE\"}"// `device` is the response from step 1. const sleep = (seconds: number) => new Promise((r) => setTimeout(r, seconds * 1000)) async function pollForKey(device: { device_code: string; interval: number; expires_in: number }) { const deadline = Date.now() + device.expires_in * 1000 while (Date.now() < deadline) { await sleep(device.interval) const res = await fetch('https://api.cruxy.ai/api/v1/device/token', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ device_code: device.device_code }), }) if (res.ok) return res.json() if (res.status === 429) { // Polling faster than the limiter allows: honour Retry-After, then resume. await sleep(Number(res.headers.get('Retry-After')) || device.interval) continue } const { code, reason } = await res.json() if (code === 'authorization_pending') continue throw new Error(`device login stopped: ${code}${reason ? ` (${reason})` : ''}`) } throw new Error('device_code expired before approval; start a new device login') } const key = await pollForKey(device) // Store key.access_token in your secrets manager now. Log the id, never the key. console.log(`Minted key ${key.key_id} for ${key.org.name}, expires ${key.expires_at}`)Until the flow resolves, the endpoint returns
400with an OAuth-style body carryingcode,error, andrequest_id:{ "code": "authorization_pending", "error": "authorization_pending", "request_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890" }Only
authorization_pendingmeans keep polling:codeMeaning What to do authorization_pendingNot approved yet. Wait intervalseconds and poll again.access_deniedThe person declined. Stop. expired_tokenThe device_codeexpired.Start a new device login. invalid_grantThe device_codeis not valid.Start a new device login. A refusal can also carry
reasonanderror_description. The onereasontoday isorg_membership_revoked: the login was approved for an organization the account no longer belongs to. A new login succeeds, but it may bind to a different organization, so tell the person instead of retrying silently. Branch onreason;error_descriptionis for people and is not meant to be parsed.On approval the endpoint returns
200with the key:{ "access_token": "cxy_live_...redacted", "token_type": "bearer", "key_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "user_id": "6f9c1e2a-1b2c-4d5e-8f90-1a2b3c4d5e6f", "expires_at": "2026-10-20T12:00:00.000Z", "org": { "id": "8c1a5b3e-2d4f-4a6b-9c8d-0e1f2a3b4c5d", "name": "Acme Corp", "role": "member" } }
What the token response tells you
| Field | Meaning |
|---|---|
access_token | Your API key. Send it as Authorization: Bearer <key>. |
token_type | bearer. |
key_id | The key's id. Safe to log. |
user_id | The account the key belongs to. |
expires_at | When the key stops working, 90 days after issue. Record it with the key; see Expiry and revocation. |
org | The one organization this key is bound to: its id, name, and your role in it (owner, admin, or member). |
A key is bound to exactly one organization, fixed when it is issued. That
organization decides which shared memory, rules, vaults, and conversations the
key can reach. A login is not always bound to the same organization as your
previous one, so show org.name to the person after every login, and read
org.role rather than inferring authority from anything else.
Key format
| Prefix | What it is |
|---|---|
cxy_live_ | A user key, either kind: what you send as a Bearer token. |
cxy_svc_ | A service token, minted by an operator. It is not a user key, and an endpoint that expects one rejects the other with 401. |
Treat everything after the prefix as opaque.
Verify the key
GET /api/v1/me echoes the identity behind a key. Call it once after you store
a new key to confirm it works end to end.
curl https://api.cruxy.ai/api/v1/me \
-H "Authorization: Bearer $CRUXY_API_KEY"const res = await fetch('https://api.cruxy.ai/api/v1/me', {
headers: { Authorization: `Bearer ${process.env.CRUXY_API_KEY}` },
})
if (!res.ok) throw new Error(`key check failed: ${res.status}`)
const me = await res.json()
console.log(`key ${me.api_key_id} belongs to user ${me.user_id}`){
"api_key_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"user_id": "6f9c1e2a-1b2c-4d5e-8f90-1a2b3c4d5e6f"
}A 200 means the key works. A 401 unauthorized means it is missing,
malformed, unknown, expired, or revoked: the same answer for each. Check the
Authorization header, then the key itself, then its expires_at.
Expiry and revocation
- A signed-in credential stops working at
expires_at, 90 days after issue. The time is fixed when the key is issued: use does not extend it, and there is no refresh grant. Prompt for a new device login before it passes. - A developer API key does not expire. Rotate or revoke it in the Console.
- You cannot revoke a signed-in credential yourself today. Its holder can
neither rotate nor revoke it, so it ends only at
expires_at, when its organization membership ends, or when Cruxy support revokes it. If one leaks, ask Cruxy support to revoke it. - An expired, revoked, or unknown key is
401unauthorized, withWWW-Authenticate: Bearer. A key whose owner's account is suspended is403account_suspended. - Revocation applies from the next request, since nothing is cached. A stream already running is not cut.
What a key is allowed to do
A key can carry restrictions. A request outside them is refused:
| Restriction | Refusal |
|---|---|
| Allowed scopes: the endpoint families the key may call, and whether it may use memory, rules, or files | 403 key_scope_forbidden, naming required_scope when a request field asked for a family the key lacks |
| Model allowlist: the tiers the key may call | 403 model_not_allowed |
| Read-only: the key may not perform billable operations | 403 key_read_only |
| Monthly spend cap | 429 key_spend_cap_exceeded |
A 403 never clears by waiting; re-scope the key or issue a new one. For a
developer API key, the Console sets the model allowlist and the monthly spend
cap.
Memory recall, memory capture, and your organization's standing rules are on by
default for a signed-in credential, which includes every key device login
mints, and off by default for a developer API key. To get the same behavior from
any key, set memory and rules on each chat request; see
Memory and Standing rules.
Keep the key safe
- Treat it like a password. Anyone who holds it can call the API as you.
- Keep it on the server. Never ship a key to a browser. Put a server route between the browser and the gateway that adds the key; see Streaming chat in the browser.
- Read it from the environment. Keep it in an environment variable or a
secrets manager, never in source control. Every sample on this site reads
CRUXY_API_KEY. - Use a developer API key on a server. A signed-in credential carries its owner's memory and rules into every request, expires, and cannot be revoked by its holder.
- Store it the moment it is issued. The token endpoint returns it once; if you lose it, run device login again for a new key.
- Log the
key_id, never the key.key_idandexpires_atidentify a key and tell you when to replace it. - Use one key per environment. Separate keys for local, staging, and
production keep each
key_idmeaningful in your logs. - Issue the narrowest key a service needs. A key whose allowed scopes omit memory can never read or write anyone's memory through chat.
- Send it only to
https://api.cruxy.ai.
Next steps
- Authentication: send the key on every request.
- Quickstart: make your first chat call.
- Errors and retries: handle
401,403, and every other error code.