Authentication
Key format, per-key restrictions, and what to do the moment a key leaks.
Every request carries an API key in the Authorization header:
Authorization: Bearer sk-spicy-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxThere is no other scheme. The open API surface (/api/v1/*) accepts bearer keys only — no cookies, no query parameters.
Three surfaces, three credential systems
The platform exposes three prefixes whose credential systems are entirely separate. As an API consumer you only ever touch the first, but knowing the other two exist saves you one class of debugging: a credential from the wrong surface is always a 401, never a permissions error.
| Surface | Prefix | Credential | Who uses it |
|---|---|---|---|
| Open API | /api/v1 | Authorization: Bearer sk-spicy-… | your server-side code |
| User console | /console/v1 | browser session cookie + CSRF token header | the web console |
| Operations | /admin/v1 | staff session + mandatory second factor | our operators; you never reach it |
Credentials do not cross surfaces
Each surface has its own session signing key, its own audience and its own cookie name. A console cookie sent to /api/v1, or an API key attached to a console request, fails validation outright — this is not an authorisation failure, it is a credential that cannot be verified at all.
That separation is the point: a leaked API key cannot be used to sign into your console, and a stolen browser session cannot be turned into API calls.
The surfaces differ in more than credentials:
/api/v1is rate limited per account, with one shared bucket across every endpoint — see Rate limits. It does no cross-origin checking, because server-to-server calls have noOriginto check; the key itself is the control./console/v1enforces a CORS allowlist and CSRF validation, because its credential is a cookie and browsers attach cookies automatically. A small group of paths on this surface is anonymously readable (the public model catalog) — GET only, with no write method defined at all./admin/v1sits behind an edge identity check that runs before any of our code, then an IP allowlist, role-based authorisation and full audit logging. The sign-in endpoint itself passes through those layers too.
Health checks belong to no surface
/healthz and /readyz carry neither authentication nor region checks — orchestrator probes do not come through the CDN, and a region check would make them fail forever. Neither endpoint returns business data.
What a key looks like
A fixed sk-spicy- prefix followed by 48 hexadecimal characters.
| Part | Purpose |
|---|---|
sk-spicy- | Fixed prefix, so the key is recognisable in logs, repositories and GitHub secret scanning |
| Next 8 chars | Lookup segment. It locates one row; it is not part of the check |
| Remaining 40 | The actual secret. We store only a salted, peppered hash — we cannot read it back either |
The prefix is deliberate: being scanned and notified beats finding out when someone else has been spending your balance.
A key is shown exactly once
Lose it and you create a new one. We cannot recover it — and that is the point. Recoverable would mean stored in the clear.
Never ship a key to the client
Anything in a browser or an app can be read out of it
Bundling, obfuscation, splitting the string across files — none of that is protection. Anyone with devtools or a decompiler has your key, and a leaked key runs until your balance or daily cap is gone.
When a frontend needs generation, proxy it through your own backend: the browser calls you, you call us. You get to enforce your own per-user quotas and moderation at that layer anyway.
The same applies to mobile binaries, desktop clients, public notebooks, CI logs, and anything that ends up in a curl -v dump pasted into a support ticket.
Per-key restrictions
Configure these in the console. They are enforced server-side.
| Restriction | What it buys you | Business code when hit |
|---|---|---|
| Daily spend cap | New keys ship with a low one. A misconfiguration or a leak has a ceiling | 40202 |
| Lifetime spend cap | Total a key may ever spend. Useful for keys handed to a partner | 40202 |
| IP allowlist | Only the listed CIDRs may call. Recommended for server-side integrations | 40302 |
| Model allowlist | For example, staging keys limited to cheap models | 40301 |
| Mature capability | Allowed on new keys by default; disable it per key to reject mature: true | 40305 |
The IP allowlist is matched against the true client IP as determined by our CDN, not the connecting peer — so configure it against your egress addresses.
Registered accounts currently need no additional third-party age gate to use mature-capable models, but use remains limited to consenting adults and lawful purposes. You still must follow the age, consent, real-person and minor-protection laws that apply to your business and users; default access is not a substitute for that assessment.
Rotation
- One key per environment (production / staging / local), so you can disable one without touching the others.
- Rotate without downtime: create the new key, shift traffic, confirm no calls remain on the old one, then disable it.
- Disabling takes effect immediately. There is no cache to wait out.
After a leak
Disable it now
Disable the key in the console. It is instant.
Create a replacement
Create first, disable second — the other order gives you an outage.
Check what it was used for
Work through the request log in the console for the exposure window: models you did not call, source IPs you do not recognise, spend you cannot account for.
Tighten the replacement
Give the new key an IP allowlist and a tighter daily cap. A leak that happened once will happen again.
If unexpected spend occurred, contact us with the task IDs or request_id values.
What a failure looks like
{
"code": 401,
"msg": "凭据无效或已失效",
"request_id": "req_01k3m8x9q2z4v7n5p6r8s0t1w2"
}The HTTP status is 401 as well.
A non-existent key, a malformed key, a revoked key and an expired key all return exactly the same response. That prevents probing for which key prefixes exist. So do not try to infer the cause from the message — check the key's status in the console instead.
When the account itself cannot call the API (email unverified, account suspended) you get 403 instead. Swapping keys will not help with that one.

