MainSDK
Search⌘K
How-to

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.

Building for 0.3

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.
Stability levels
Updated 2026-10-28

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 tenancy and access set up (see build a multi-tenant app).
  • The permissions you want as scopes, such as orders.read, defined in access.

1. Declare the scopes

main.yaml
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

app/api/keys/route.ts
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

app/api/v1/orders/route.ts
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

app/api/keys/manage.ts
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 everywhere

5. Test it

app/api/v1/orders/route.test.ts
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. Send Authorization: Bearer <key>.
  • MAIN-APIKEY-0003: the key lacks the scope the action needs. Create a key with it.
  • MAIN-APIKEY-0007: a test key in production, or a live key elsewhere.
  • MAIN-APIKEY-0005: the caller lacks apikeys.manage, or asked for a scope they do not hold.

See the apikeys interface for every code and option.

Was this page helpful?

↑↓ moveEnter openEsc close

Not decided yet