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

# Admin Keys

> The mak_ credential a script uses to manage your organization — permissions, scopes, expiry, rotation, and revocation.

An admin key (`mak_...`) lets a script manage your organization — creating API keys, reading resolved limits, configuring alerts — without a person signing in. It is deliberately **not** the key you already have.

|                                  | `rsk_...`                  | `mak_...`                      |
| -------------------------------- | -------------------------- | ------------------------------ |
| Calls models, bills your balance | Yes                        | No                             |
| Manages keys, limits, alerts     | No                         | Yes                            |
| Expiry                           | Lives until you suspend it | Always expires — up to 90 days |

The separation is enforced twice server-side, by credential prefix and by separate storage. A leaked inference key cannot mint more keys, and a leaked admin key cannot spend your balance.

<Note>
  Admin keys are minted from the dashboard by a signed-in person — **Dashboard → Admin Keys**. An admin key cannot mint another admin key, by design.
</Note>

***

## What an admin key can reach

A key carries an explicit permission set. Anything not granted is a `403`, and anything not in this table is out of reach for **every** admin key:

| Permission     | Reaches                                                                     |
| -------------- | --------------------------------------------------------------------------- |
| `keys:read`    | `GET /v1/keys`, `GET /v1/keys/filter-options`, `GET /v1/keys/{id}/limits`   |
| `keys:write`   | `POST /v1/keys`, `PATCH /v1/keys/{id}`, `DELETE /v1/keys/{id}`              |
| `limits:read`  | `GET /v1/routing-policy-templates`, `GET /v1/routing-policy-templates/{id}` |
| `limits:write` | `PATCH` / `DELETE /v1/routing-policy-templates/{id}`                        |
| `alerts:read`  | `GET /alerts`                                                               |
| `alerts:write` | Alert channels and policies under `/alerts`                                 |
| `org:read`     | `GET /v1/orgs/current/permissions/catalogue`                                |

<Warning>
  Not reachable with an admin key, at any permission: **inference** (`/v1/chat/completions` and every other model call), **billing and payments**, **org governance** (invites, roles, teams, ownership), **usage and audit reads** — those take an `rsk_` key — and **minting or rotating admin keys**.

  Creating a routing-policy template is also dashboard-only; an admin key can read and edit templates but not create the first one.
</Warning>

## Scope

Scope decides *whose* rows a permission reaches:

| Scope  | Reaches                          |
| ------ | -------------------------------- |
| `self` | Only rows the key itself owns    |
| `team` | One team — `team_id` is required |
| `org`  | The whole organization           |

<Note>
  `alerts:*`, `limits:*`, and `org:read` act on the organization itself, so a narrower scope would select the same rows while reading as a restriction. They can only be granted at scope `org` — asking for them at `self` or `team` is a `422`. Only `keys:read` and `keys:write` are meaningful below org scope.
</Note>

You can grant only what you already hold: a permission you lack is a `403`, and so is a scope that reaches further than yours. A team admin can mint a `team`-scoped key for their own team, not for another.

***

## Creating one

Create one in **Dashboard → Admin Keys**. The form maps onto `POST /v1/admin-keys`, which authenticates the signed-in person — there is no credential that can mint an admin key, so this step is always a human one:

```json theme={null}
{
  "name": "ci-key-provisioner",
  "permissions": ["keys:read", "keys:write"],
  "scope": "org",
  "expires_at": "2026-11-01T00:00:00Z"
}
```

| Field         | Notes                                                                                                                                       |
| ------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
| `name`        | Required, non-blank. Shown in listings and on the audit trail                                                                               |
| `permissions` | At least one, from the table above                                                                                                          |
| `scope`       | `self`, `team`, or `org`                                                                                                                    |
| `team_id`     | Required when `scope` is `team`, and rejected otherwise                                                                                     |
| `expires_at`  | **Required** — must carry a UTC offset, be in the future, and be at most **90 days** out. There is no default and no non-expiring admin key |

<Warning>
  The plaintext key is shown **once**, in the `key` field of the response. Every later read gives you `masked_key` (`mak_*****7f3a`) instead — only a hash is stored. Copy it straight into your secret store.
</Warning>

## Using it

The same `Authorization` header your API key uses:

```bash theme={null}
curl https://api.meshapi.ai/v1/keys \
  -H "Authorization: Bearer $MESH_ADMIN_KEY"
```

Only the management routes above accept it, and they accept nothing else on that plane.

***

## What the keys it creates belong to

An inference key minted by an admin key is owned by the **admin key's lineage**, not by a person. That has three consequences worth knowing before you automate anything:

* It lands in the admin key's own organization. Sending a different `org_id` is a `403`; sending a different `team_id` works only if the key's scope reaches that team, and a `self`-scoped key can only create in the org's default team.
* It bills the organization's shared balance, like any other org key — see [Organizations & Teams](/docs/getting-started/organizations).
* It survives rotation of the admin key that created it, because the lineage is what owns it. Replacing an admin key by minting a fresh one instead does **not** carry those keys over.

## Rotating

Rotate from **Dashboard → Admin Keys** (`POST /v1/admin-keys/{key_id}/rotate`, optionally with `{"grace_hours": 24}`). Like minting, it is a signed-in action — a key cannot rotate itself.

Rotation mints a successor and shortens the predecessor to a grace window — **both keys work during the overlap**, which is what lets you redeploy without a gap. `grace_hours` defaults to 24 and is capped at 7 days.

The successor inherits the predecessor's name, permissions, scope, and team. It also inherits its **lifetime**, not the 90-day ceiling: rotating a deliberately short-lived key does not quietly turn it into a long-lived one. Pass `expires_at` to set a different one, subject to the same 90-day rule.

## Revoking

Revoke from **Dashboard → Admin Keys** (`DELETE /v1/admin-keys/{key_id}`, optionally with a `reason`).

Revocation is **immediate and has no grace window** — that is the difference from rotation. `reason` is optional and is recorded on the key and in the audit trail.

## Listing

`GET /v1/admin-keys` and `GET /v1/admin-keys/{key_id}` return the masked key, permissions, scope, expiry, `last_used_at`, and who created it — never the plaintext. What you see depends on your own scope: org-wide access lists every key in the org, team access lists your team's keys plus your own, and anything narrower lists only the keys you created yourself.

***

## Errors

| Status | When                                                                                                                                                  |
| ------ | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
| `401`  | The key is revoked, expired, or unknown — a dead credential reads differently to an operator than a permissions failure                               |
| `403`  | Permission not granted, scope too narrow, another organization, or no admin authority over the named team                                             |
| `404`  | The key exists but your scope cannot see it                                                                                                           |
| `409`  | Already revoked, or a stored grant that can no longer be reproduced on rotation — mint a replacement instead                                          |
| `422`  | `expires_at` missing an offset, in the past, or past 90 days; blank `name`; `team_id` without `scope: "team"`; an org-wide permission below org scope |

## Related

* [Authentication](/docs/getting-started/authentication) — `rsk_` vs `mak_`, and using a key to make requests
* [API Keys](/docs/getting-started/api-keys) — the management API this credential is for
* [Rate Limits & Spend Caps](/docs/getting-started/rate-limits) — resolving limits across tiers
* [Organizations & Teams](/docs/getting-started/organizations) — shared billing, teams, and what stays a dashboard action
