Give your customers API keys
Issue a customer an API key with scopes, verify it on every call, rotate it without downtime and revoke it in seconds.
Status Status is where the work is: Planned (in a milestone, not started), Building (tickets in progress), Preview (usable, may change), then the stability it ships at.
Stability The promise an interface ships with.
- Stable Follows semantic versioning; breaking changes need a new major, 12 months of overlap and a codemod.
- Beta Usable in production; may change before it becomes stable.
- Experimental May change or go away; needs an explicit opt-in in main.yaml.
You will let a tenant create an API key, call your app with it, rotate it and revoke it. It takes about 20 minutes. The secret is shown once and only its SHA-256 digest is stored.
You need
- A MainSDK app with
tenancyandaccessset up (see build a multi-tenant app). - The permissions you want as scopes, such as
orders.read, defined inaccess.
1. Declare the scopes
apikeys:
prefix: acme
scopes: [orders.read, orders.write]Run npx mainsdk check. A scope no access permission defines is reported as MAIN-APIKEY-0011.
2. Let a tenant admin create a key
import { apikeys } from "mainsdk";
export async function POST() {
const { key, secret } = await apikeys.create({ name: "CI", scopes: ["orders.read"] });
return Response.json({ id: key.id, secret }); // the only time the secret is shown
}The caller needs the apikeys.manage permission and can only grant scopes they hold.
3. Verify the key on each call
import { apikeys } from "mainsdk";
export async function GET(req: Request) {
const { tenant, scopes } = await apikeys.verify(req); // reads Authorization: Bearer
return Response.json({ tenant: tenant.id, scopes });
}4. Rotate and revoke
import { apikeys } from "mainsdk";
const keyId = "key_123"; // from apikeys.list()
const { key: next, secret } = await apikeys.rotate(keyId, { overlap: "24h" }); // the old key works for 24 hours
await apikeys.revoke(keyId, { reason: "leaked in a public repo" }); // within 5 seconds everywhere5. Test it
import { createTestApp, withApiKey } from "@mainsdk/testing";
import { apikeys } from "mainsdk";
const app = createTestApp({ yaml: "schema: 1\nproject: prj_test\ntenancy:\n resolve: [apikey]\napikeys:\n prefix: acme\n scopes: [orders.read]\n" });
const tenant = await app.tenancy.create({ name: "Acme", region: "eu" });
const key = await app.apikeys.issue({ tenant: tenant.id, scopes: ["orders.read"] });
const { scopes } = await apikeys.verify(withApiKey(new Request("http://x/"), key));Check it worked
scopes is ["orders.read"], and after apikeys.revoke() the same call throws MAIN-APIKEY-0002. app.apikeys.toHaveAudited("apikey.created") passes.
Troubleshooting
MAIN-APIKEY-0001: no key or a malformed one. SendAuthorization: Bearer <key>.MAIN-APIKEY-0003: the key lacks the scope the action needs. Create a key with it.MAIN-APIKEY-0007: atestkey in production, or alivekey elsewhere.MAIN-APIKEY-0005: the caller lacksapikeys.manage, or asked for a scope they do not hold.
See the apikeys interface for every code and option.
Was this page helpful?