> ## Documentation Index
> Fetch the complete documentation index at: https://unkey.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

> ## Agent Instructions
> Unkey is two separate products. Compute builds, deploys, and runs apps behind a gateway. API Management issues API keys, enforces rate limits, manages identities and permissions, and reports usage. Say which product a page belongs to; a reader can use either without the other.
> Every Unkey API endpoint is an HTTP POST to https://api.unkey.com/v2/{service}.{procedure} with a root key in the Authorization: Bearer header. Root keys are workspace scoped.
> Error codes have the form err:{system}:{category}:{specific} and each has a page at /errors/{system}/{category}/{specific}.
> The word environment means production or preview in Compute. Rate limiting has four meanings on this site; the glossary lists them.

# Permission queries

> Ask for the permissions a request needs when you verify a key.

Send a permission query in the `permissions` field of `keys.verifyKey` to check that a key has the permissions a request needs. A query combines permission slugs with `AND`, `OR`, and parentheses. If the key's permissions (its own plus its roles') don't satisfy it, verification fails with `code: INSUFFICIENT_PERMISSIONS` and no rate limit or credits are used.

## Grammar

```text theme={"theme":"kanagawa-wave"}
expression    -> andExpression (OR andExpression)*
andExpression -> primary (AND primary)*
primary       -> PERMISSION | "(" expression ")"
```

A `PERMISSION` is a slug made of letters, digits, `.`, `_`, `-`, `:`, `*`, and `/`. `AND` and `OR` can be any case. `AND` is evaluated before `OR`, as in SQL, and parentheses change that. The whole query can be up to 1000 characters.

| Query | Passes when the key has |
| - | - |
| `documents.read` | `documents.read` |
| `documents.read AND documents.write` | both |
| `admin OR editor` | either |
| `admin OR documents.read AND documents.write` | `admin`, or both document permissions (`AND` groups first) |
| `(admin OR editor) AND billing:view` | `billing:view` plus one of the two |

```bash theme={"theme":"kanagawa-wave"}
curl -X POST https://api.unkey.com/v2/keys.verifyKey \
  -H "Authorization: Bearer $UNKEY_ROOT_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "key": "sk_live_...", "permissions": "(admin OR editor) AND billing:view" }'
```

## Matching is exact

Each slug in the query must match one of the key's permissions exactly, including case. There are no wildcards: a key with `documents.*` does **not** pass a query for `documents.read`. A query for `documents.*` only passes for a key that has a permission literally named `documents.*`.

<Warning>
  Case matters when you query but not when you attach. If a permission is stored as `Documents.Read`, attaching `documents.read` finds it and attaches it, but a query for `documents.read` fails. Pick one casing and use it everywhere.
</Warning>

To grant a family of permissions, attach them to a role and assign the role. See [Roles and permissions](/docs/api-management/authorization/roles-and-permissions).

## Outcomes and errors

| Situation | Result |
| - | - |
| Expression is true for the key | Verification continues; `data.permissions` and `data.roles` list what the key holds. |
| Expression is false | HTTP 200, `valid: false`, `code: INSUFFICIENT_PERMISSIONS`. |
| Expression does not parse: unbalanced parentheses, empty parentheses, a dangling operator, or an illegal character | HTTP 400 [`err:user:bad_request:permissions_query_syntax_error`](/docs/errors/user/bad_request/permissions_query_syntax_error) with the position of the problem in `detail`. |
| Query is empty or longer than 1000 characters | HTTP 400. |
| No `permissions` field | No permission check; the response still lists the key's permissions and roles. |

## Checking in your own code

When the decision needs data Unkey doesn't have, verify without a query and read `data.permissions`:

```typescript authorize.ts theme={"theme":"kanagawa-wave"}
const { data } = await unkey.keys.verifyKey({ key: incomingKey });
if (!data.valid) {
  return deny(data.code);
}

const permissions = data.permissions ?? [];
const document = await db.documents.find(documentId);

const canDelete =
  permissions.includes("admin") ||
  (permissions.includes("documents.delete") && document.ownerId === data.identity?.externalId);
```

You can combine both: send a query for the part Unkey can decide, and use the returned list for the rest.
