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
PropertyValue
Prefix literalkrallo_live_ (12 characters)
Random body40 random characters appended to the prefix
Display prefixFirst 16 characters (krallo_live_ + 4 random) — safe to log
Last 4Final 4 characters — shown in the console for identification
Environmentlive

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:

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):

POST/api/developer/keys
FieldTypeRequiredDescription
appIdstringYesThe app that will own the key. You must own it (resolved across your linked identities).
scopesstring[]NoThe scopes to grant. Each must be grantable; unknown or non-grantable values (including the * wildcard) are rejected. Duplicates are collapsed.
labelstringNoA human label for the key (max 120 chars), e.g. prod-worker.
csrfstringYesCSRF 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:

LimitDefaultError when exceeded
Apps per user (DEVELOPER_API_MAX_APPS_PER_USER)10422 app_cap_reached
Live keys per app (DEVELOPER_API_MAX_KEYS_PER_APP)20422 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.

POST/api/developer/keys/rotate
FieldTypeRequiredDescription
idstringYesThe opaque id (SHA-256 hash) of the key to rotate.
csrfstringYesCSRF 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.

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.

POST/api/developer/keys/revoke
FieldTypeRequiredDescription
idstringYesThe opaque id (SHA-256 hash) of the key to revoke.
csrfstringYesCSRF 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.

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.

StatusCodeCause
404not_foundThe Developer API is disabled platform-wide (DEVELOPER_API_ENABLED off). The endpoints do not exist at all.
429rate_limitedThe pre-auth per-IP throttle tripped (before any key lookup). Honor Retry-After.
401unauthorizedNo Authorization: Bearer header, or an empty token.
401invalid_tokenThe token does not hash to any known key (wrong, malformed, or truncated secret).
401token_revokedThe key has been revoked, or a rotated key’s grace window has elapsed.
401token_expiredThe key carries an expiry that is now in the past.
403app_disabledThe owning app is missing or has been deactivated.
403owner_suspendedThe account behind the key is suspended (checked across every linked identity).
403insufficient_scopeAuthenticated, 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