API Keys
Define route-access scopes in Inspire and manage actual tokens through the CLI.
The apiKeys block defines which machine-access scopes your app supports. Routes use those scope names through auth: apiKey(scope), and the platform handles the actual token lifecycle.
The key idea is that API keys should be explicit capabilities, not just long-lived passwords. A read key, an import key, and an admin key should mean different things in the schema.
Basic Shape
apiKeys {
scopes: {
read: @system("api_read") {
grant read on Project
},
write: @system("api_write") {
grant read, write on Project
},
}
}Each scope names a machine capability. Routes can then require that capability directly.
routes("/v1", auth: apiKey(read)) {
GET /projects -> listProjectsRoute
}
routes("/v1", auth: apiKey(write)) {
POST /projects -> createProjectRoute
}What The Block Does
The apiKeys block is not where you mint or rotate tokens. It defines:
- which scope names exist
- what each scope is allowed to do
- which routes may use those scopes
Everything about token creation, listing, rotation, and revocation happens through the CLI or through server code using the SDK.
Scope Design
Good scope design is narrow and task-shaped.
apiKeys {
scopes: {
read: @system("api_read") {
grant read on Project
grant read on Task
},
importer: @system("api_importer") {
grant read on Project
grant read, write on Task
},
admin: @system("api_admin") {
grant read, write, delete on Project
grant read, write, delete on Task
},
}
}In practice:
readis for reporting, sync, and external dashboards- task-shaped scopes like
importerare for one integration workflow adminshould be rare
If one key can do everything, you lose most of the benefit of declaring scopes at all.
Lifecycle: CLI
Use the CLI when you need to create or rotate a key directly.
kizaki api-keys create --scope read --name "Mobile app"
kizaki api-keys create --scope importer --name "CRM sync" --expires 90d
kizaki api-keys list
kizaki api-keys list --scope read
kizaki api-keys revoke <id>
kizaki api-keys rotate <id> --grace-period 24hTwo details matter:
- the plaintext token is shown only on create and rotate
rotatecan keep the old token alive briefly so integrations have time to switch
Lifecycle: Server Code
If your app lets customers create their own integration keys, do that through normal server functions.
import { getContext } from "@kizaki/sdk";
/** @expose */
export async function createReportingKey(name: string) {
const { sdk } = getContext();
return sdk.apiKeys.create({
scope: "read",
name,
expiresIn: 60 * 60 * 24 * 90,
metadata: { createdFrom: "settings" },
});
}
/** @expose */
export async function rotateReportingKey(keyId: string) {
const { sdk } = getContext();
return sdk.apiKeys.rotate(keyId, {
gracePeriod: 60 * 60 * 24,
});
}
/** @expose */
export async function listIntegrationKeys() {
const { sdk } = getContext();
return sdk.apiKeys.list();
}That keeps key-management permissions inside the same application workflows you already expose to your own UI.
What Callers Get Back
On create or rotate, the returned object includes the plaintext token once.
const key = await sdk.apiKeys.create({
scope: "read",
name: "Data warehouse sync",
});
key.id;
key.scope;
key.name;
key.token; // plaintext, shown once
key.createdAt;
key.expiresAt;On list, callers only get metadata.
const keys = await sdk.apiKeys.list({ scope: "read" });
// Each item includes metadata only.
// No token values are returned from list.That is the right security posture for machine credentials: show the secret once, then treat it as write-only.
API Keys Vs kizaki keys
These are different systems.
api-keysare app-level credentials for your declared HTTP routeskeysare platform or CI credentials for working with Kizaki itself
If the credential is meant to call your app's routes, it belongs in apiKeys, not in kizaki keys.
Recommended Pattern
- keep scopes narrow and named after real capabilities
- prefer several small scopes over one broad integration key
- treat
admin-style keys as exceptional - rotate keys with a grace window when integrations cannot switch instantly
- manage customer-facing keys through your own exposed functions
Related docs: