---
title: "Give your customers API keys"
description: "Issue a customer an API key with scopes, verify it on every call, rotate it without downtime and revoke it in seconds."
section: "guides"
status: "Building for 0.3"
spec: "apikeys"
canonical: "https://mainsdk.dev/docs/guides/give-your-customers-api-keys"
updated: "2026-10-28"
---

# 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: Building for 0.3 · Canonical: https://mainsdk.dev/docs/guides/give-your-customers-api-keys · 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](https://mainsdk.dev/docs/guides/build-a-multi-tenant-app)).
- The permissions you want as scopes, such as `orders.read`, defined in `access`.

## 1. Declare the scopes

```yaml title="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

```ts title="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

```ts title="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

```ts title="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

```ts title="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](https://mainsdk.dev/docs/interfaces/apikeys) for every code and option.
