spicyapiDocs
Main content

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-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

There 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.

SurfacePrefixCredentialWho uses it
Open API/api/v1Authorization: Bearer sk-spicy-…your server-side code
User console/console/v1browser session cookie + CSRF token headerthe web console
Operations/admin/v1staff session + mandatory second factorour 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/v1 is 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 no Origin to check; the key itself is the control.
  • /console/v1 enforces 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/v1 sits 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.

PartPurpose
sk-spicy-Fixed prefix, so the key is recognisable in logs, repositories and GitHub secret scanning
Next 8 charsLookup segment. It locates one row; it is not part of the check
Remaining 40The 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.

RestrictionWhat it buys youBusiness code when hit
Daily spend capNew keys ship with a low one. A misconfiguration or a leak has a ceiling40202
Lifetime spend capTotal a key may ever spend. Useful for keys handed to a partner40202
IP allowlistOnly the listed CIDRs may call. Recommended for server-side integrations40302
Model allowlistFor example, staging keys limited to cheap models40301
Mature capabilityAllowed on new keys by default; disable it per key to reject mature: true40305

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.

On this page