Getting started
Authentication
Every request is authenticated with a bearer API key. Here is how keys are minted, rotated, revoked, and scoped.
The Authorization header
Every request to the API base at https://exe.krallo.com/api/v1 carries your key as a
bearer credential in the Authorization header:
Authorization: Bearer krallo_live_...
The scheme keyword is matched case-insensitively (Bearer, bearer) and must be
followed by a single space and the raw key. There are no query-string or cookie fallbacks on the
/api/v1 surface — this realm never reads cookies or CSRF tokens; the bearer key is the
only credential. A request that omits the header, or sends a scheme other than Bearer,
is treated as unauthenticated.
The fastest way to confirm a key works is GET /api/v1/me, which needs no scope:
curl https://exe.krallo.com/api/v1/me \
-H "Authorization: Bearer krallo_live_..."
{
"data": {
"app": { "id": "01J8Z...", "name": "My integration", "tier": "free" },
"owner_user_id": "1234567890",
"scopes": ["ai:invoke", "guilds:read"]
},
"meta": { "request_id": "3f2a..." }
}
Native /api/v1 endpoints wrap success in the frozen envelope shown above —
{ "data": …, "meta": { "request_id": … } } — and errors in
{ "error": { "code", "message", … }, "meta": { "request_id": … } }. Always log the
request_id; it is your correlation handle for support. The OpenAI-compatible
Chat completions endpoint is the one exception — it returns
raw OpenAI objects. See Errors for the full catalog.
Key anatomy and storage
A live key is a single opaque string with a fixed, greppable prefix:
krallo_live_a1b2XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX9f3d
└──────────┘└──┘ └──┘
prefix (12) 4 random ... last4
| Property | Value |
|---|---|
| Prefix literal | krallo_live_ (12 characters) |
| Random body | 40 random characters appended to the prefix |
| Display prefix | First 16 characters (krallo_live_ + 4 random) — safe to log |
| Last 4 | Final 4 characters — shown in the console for identification |
| Environment | live |
Krallo never stores the raw secret. At mint time the raw key is hashed with SHA-256 and
only the hash, the display prefix, and the last-4 are persisted. On every request your presented key
is re-hashed and looked up by that hash. This means:
- The full secret is shown exactly once, at the moment of minting. Copy it then — it is unrecoverable afterward.
- A console leak (screenshot, support session, database dump) never exposes a usable credential — only the hash and the maskable prefix/last-4.
- The opaque
idthe console uses to reference a key is its SHA-256 hash. That id cannot authenticate a request, so it is safe to pass to the rotate/revoke calls below.
Minting a key
Keys belong to an app, which is the unit that also owns your
webhook endpoints and carries your rate-limit tier. Create an app
and mint keys from the in-app Developer console at /ops/dev. Under the hood the
console calls a session-authenticated portal (cookie + CSRF, not a bearer key — you cannot
mint your first key with a key you do not yet have):
/api/developer/keys| Field | Type | Required | Description |
|---|---|---|---|
appId | string | Yes | The app that will own the key. You must own it (resolved across your linked identities). |
scopes | string[] | No | The scopes to grant. Each must be grantable; unknown or non-grantable values (including the * wildcard) are rejected. Duplicates are collapsed. |
label | string | No | A human label for the key (max 120 chars), e.g. prod-worker. |
csrf | string | Yes | CSRF token bound to your session cookie. |
A successful mint returns 201 Created with the raw secret inline, once:
{
"ok": true,
"key": {
"id": "9c1e...", // SHA-256 hash = the opaque key id
"prefix": "krallo_live_a1b2",
"last4": "9f3d",
"label": "prod-worker",
"scopes": ["ai:invoke"],
"createdAt": 1720900000000,
"lastUsedAt": null,
"expiresAt": null,
"revokedAt": null
},
"secret": "krallo_live_a1b2...9f3d" // shown exactly once
}
Portal responses (/api/developer/*) use camelCase fields and a plain
{ "ok": true, … } / { "error": "code" } shape — this is the console
bootstrap surface, distinct from the frozen data/meta envelope on the
bearer /api/v1 surface.
Limits enforced at mint time:
| Limit | Default | Error when exceeded |
|---|---|---|
Apps per user (DEVELOPER_API_MAX_APPS_PER_USER) | 10 | 422 app_cap_reached |
Live keys per app (DEVELOPER_API_MAX_KEYS_PER_APP) | 20 | 422 key_cap_reached |
| Unknown / non-grantable scope requested | — | 422 invalid_scope |
Rotating a key
Rotation issues a fresh key that inherits the old key’s label and scopes, then schedules the old key for revocation after a grace window so live integrations roll over with zero downtime — deploy the new secret, and let the old one keep working until the window closes.
/api/developer/keys/rotate| Field | Type | Required | Description |
|---|---|---|---|
id | string | Yes | The opaque id (SHA-256 hash) of the key to rotate. |
csrf | string | Yes | CSRF token bound to your session cookie. |
The response is 201 Created with the new key and its
one-time secret, in the same shape as minting. During the grace window both keys authenticate; when
the window elapses the old key begins returning token_revoked.
- Grace window:
DEVELOPER_API_KEY_ROTATE_GRACE_HOURS— default 24 hours (set to0to revoke the old key immediately on rotation). - Rotation respects the per-app key cap, but excludes the key being rotated from the live count, so repeated rotations never trip
key_cap_reachedfor the replacement. - If the owning app is disabled, rotation is refused with 403
app_disabled.
Rotate on a schedule and whenever a secret may have been exposed. Because the old key stays valid for the grace window, you can roll the secret through your fleet without a coordinated cutover.
Revoking a key
Revocation is immediate and irreversible — use it the moment a key leaks.
/api/developer/keys/revoke| Field | Type | Required | Description |
|---|---|---|---|
id | string | Yes | The opaque id (SHA-256 hash) of the key to revoke. |
csrf | string | Yes | CSRF token bound to your session cookie. |
A successful revoke returns 200 OK with { "ok": true }. From that
instant the key’s revokedAt is in the past, so the very next API request presenting it
fails with 401 token_revoked. Revoking a key that is already
revoked is a no-op.
Scopes and least privilege
Every key carries an explicit set of coarse <resource>:<action> scopes, stored
as a JSON array on the key. Each request runs a scope check after authentication: the
endpoint declares the scope it needs, and a key missing it is rejected with 403
insufficient_scope — the error body names the missing grant in a
required_scope field.
{
"error": {
"code": "insufficient_scope",
"message": "Missing required scope.",
"required_scope": "guilds:read"
},
"meta": { "request_id": "3f2a..." }
}
Grant a key only the scopes it needs — for chat that is ai:invoke; for the guild list it is
guilds:read. The * wildcard is a staff-only super-scope and can never be
requested when minting. See Scopes for the full vocabulary and which
endpoint each scope unlocks.
Linked identity resolution
A single human can hold two or three linked login identities — a Discord-first canonical id (the raw
snowflake) and a Google-first id (g-<googleId>) — because linking a second provider
only adds a resolver row; it never repoints existing data. When your key authenticates, Krallo resolves
the owning app’s canonical id into the full set of ids that belong to that human
and scopes owner-based reads across all of them.
In practice, a key sees the human behind it, not one login. GET /api/v1/guilds, for
example, returns the guilds you belong to or can manage under any of your linked identities. This
resolution is read-only and non-destructive: it unions across your ids but never merges or moves data,
and it fails closed to the single canonical id if the resolver is briefly unavailable.
The pre-authentication throttle
Before your key is even hashed or looked up, every request passes a coarse per-IP
throttle. This exists so a flood of missing, invalid, or revoked tokens cannot hammer the key-lookup
path and exhaust the database pool. It is keyed on the resolved client IP — not on a spoofable
X-Forwarded-For header — and is deliberately generous, so it only trips under a genuine
flood, never during normal multi-key use.
- Budget:
DEVELOPER_API_AUTH_MAX_PER_MIN— default 600 requests / minute / IP, evaluated on a 60-second window. - On trip it returns 429 with a
Retry-Afterheader and body{ "error": "rate_limited", "hint": "…", "retryAfterSeconds": 60 }.
This is separate from the per-key rate limit applied after authentication, which is
tier-driven (free 60, pro 600, enterprise 6000 requests/minute by default, or a per-app override) and
keyed on your key. Authenticated responses carry advisory X-RateLimit-Limit,
X-RateLimit-Remaining, and X-RateLimit-Reset headers; a per-key overage also
returns 429 with Retry-After. See Errors
for handling both.
Authentication failures
The auth gate evaluates conditions in a fixed order and returns the first that fails. Every failure below
is returned in the frozen error envelope with a stable code.
| Status | Code | Cause |
|---|---|---|
| 404 | not_found | The Developer API is disabled platform-wide (DEVELOPER_API_ENABLED off). The endpoints do not exist at all. |
| 429 | rate_limited | The pre-auth per-IP throttle tripped (before any key lookup). Honor Retry-After. |
| 401 | unauthorized | No Authorization: Bearer header, or an empty token. |
| 401 | invalid_token | The token does not hash to any known key (wrong, malformed, or truncated secret). |
| 401 | token_revoked | The key has been revoked, or a rotated key’s grace window has elapsed. |
| 401 | token_expired | The key carries an expiry that is now in the past. |
| 403 | app_disabled | The owning app is missing or has been deactivated. |
| 403 | owner_suspended | The account behind the key is suspended (checked across every linked identity). |
| 403 | insufficient_scope | Authenticated, but the key lacks the scope the endpoint requires (see required_scope). |
A valid, unrevoked, unexpired key on an active app also updates its lastUsedAt timestamp on
every successful authentication — visible in the console so you can spot idle or stale keys.
Keep your keys secret
A krallo_live_ key authenticates as the human behind it and can spend from their prepaid
balance. Treat it like a password:
- Never embed a key in browser JavaScript, a mobile app bundle, a public repository, or client-side code of any kind. Keep it server-side, behind your own backend.
- Store it in a secret manager or environment variable — never in source control.
- Grant the narrowest scopes that the integration needs.
- If a key is ever exposed, revoke it immediately (or rotate, if you need continuity), then investigate.
Next steps
- Scopes — the full grant vocabulary and what each endpoint requires.
- Chat completions — the flagship AI endpoint, authenticated with your key.
- Billing & quotas — the free daily quota and per-token metering behind a key.
- Errors — every status code and error shape in one place.