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

# API Keys

> Create, scope, rotate, and monitor API keys — permissions, resource scopes, per-key usage, and end-user keys

An API key authenticates every request to Mixpeek. Keys belong to your
**organization** and are created **per user**. This page covers creating keys,
restricting what they can do (permissions and scopes), rotating and revoking
them, monitoring per-key usage, and the special key types that power end-user
multi-tenancy.

For the broader authentication and tenancy model (the `Authorization` and
`X-Namespace` headers, isolation guarantees), see
[Security & Tenancy](/operations/security). For rate-limit tiers and usage
pools, see [Rate Limits & Quotas](/operations/rate-limits-quotas).

<Note>
  **Managing keys requires an admin key.** Every endpoint on this page requires
  a key with the `admin` [permission](#permissions). Your organization ships
  with a protected `admin-key` you can use to mint your first scoped keys.
</Note>

## How a key looks

When you create a key, the **plaintext secret is returned exactly once** — it is
hashed (SHA-256) at rest and can never be read back. Store it immediately in a
secrets manager or environment variable.

| Field        | Example                   | What it is                                                                                                     |
| ------------ | ------------------------- | -------------------------------------------------------------------------------------------------------------- |
| `key`        | `mxp_sk_9f3a…` (60 chars) | The plaintext secret. **Shown once, on create/rotate.** Send it as `Authorization: Bearer <key>`.              |
| `key_prefix` | `mxp_sk_9f3...`           | First 10 characters + `...`. A safe display hint so you can tell keys apart in a list — never the full secret. |
| `key_id`     | `key_a1b2c3d4e5f6g7h`     | Stable public identifier. Used for [usage lookups](#monitor-usage-per-key).                                    |
| `name`       | `backend-service`         | Human label you choose. Used to reference the key in update/rotate/revoke calls.                               |

<Warning>
  The plaintext `key` is **never retrievable after creation**. If you lose it or
  suspect exposure, [rotate](#rotate-expire-and-revoke) the key — don't try to
  recover it.
</Warning>

## Create a key

`POST /v1/organizations/users/{user_email}/api-keys` — create a key owned by the
user at `{user_email}` (use your own email to create keys for yourself).

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST "https://api.mixpeek.com/v1/organizations/users/you@acme.com/api-keys" \
    -H "Authorization: Bearer $MIXPEEK_ADMIN_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "name": "backend-service",
      "description": "Service account for the ingestion pipeline",
      "permissions": ["read", "write"]
    }'
  ```

  ```python Python theme={null}
  import os, requests

  resp = requests.post(
      "https://api.mixpeek.com/v1/organizations/users/you@acme.com/api-keys",
      headers={"Authorization": f"Bearer {os.environ['MIXPEEK_ADMIN_KEY']}"},
      json={
          "name": "backend-service",
          "description": "Service account for the ingestion pipeline",
          "permissions": ["read", "write"],
      },
  )
  new_key = resp.json()["key"]   # plaintext — store it now, it won't be shown again
  print(new_key)
  ```

  ```javascript JavaScript theme={null}
  const resp = await fetch(
    "https://api.mixpeek.com/v1/organizations/users/you@acme.com/api-keys",
    {
      method: "POST",
      headers: {
        Authorization: `Bearer ${process.env.MIXPEEK_ADMIN_KEY}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        name: "backend-service",
        description: "Service account for the ingestion pipeline",
        permissions: ["read", "write"],
      }),
    },
  );
  const { key } = await resp.json(); // plaintext — store it now
  console.log(key);
  ```
</CodeGroup>

The response is the key's metadata plus the one-time plaintext `key`:

```json theme={null}
{
  "key": "mxp_sk_9f3a2c7b1e...KUwq",
  "key_id": "key_a1b2c3d4e5f6g7h",
  "key_prefix": "mxp_sk_9f3...",
  "name": "backend-service",
  "permissions": ["read", "write"],
  "scopes": [],
  "status": "active",
  "created_at": "2026-07-07T00:00:00Z",
  "created_by": "usr_a1b2c3d4e5f6g7"
}
```

### Request fields

<ParamField body="name" type="string" required>
  Human-friendly label (1–100 chars). Used to reference the key in
  update/rotate/revoke calls. Must be unique among your active keys.
</ParamField>

<ParamField body="permissions" type="string[]" default="[read, write, delete]">
  Permission levels granted to the key. **Defaults to full `read`+`write`+`delete`
  access** — pass a narrower list explicitly when you want a restricted key. See
  [Permissions](#permissions).
</ParamField>

<ParamField body="scopes" type="ResourceScope[]" default="[] (org-wide)">
  Resource-level restrictions. **Omitting scopes grants org-wide access.** See
  [Restrict a key to specific resources](#restrict-a-key-to-specific-resources).
</ParamField>

<ParamField body="rate_limit_override" type="integer">
  Per-key requests-per-minute ceiling. Defaults to your plan limit when absent.
  See [Per-key rate limits](#per-key-rate-limits).
</ParamField>

<ParamField body="expires_at" type="string (ISO 8601)">
  UTC timestamp when the key auto-expires. Omit for a non-expiring key.
</ParamField>

<ParamField body="description" type="string">
  Optional note (≤500 chars) explaining the key's purpose.
</ParamField>

<ParamField body="principal_id" type="string">
  End-user identifier. Setting this makes the key **user-scoped** for
  document-level ACL — see [End-user keys](#end-user-keys-for-multi-tenant-apps).
</ParamField>

<ParamField body="allowed_origins" type="string[]">
  CORS allowlist of web origins (exact or wildcard-subdomain). When set, browser
  requests must send a matching `Origin` header. See [Restrict a key to browser
  origins](#restrict-a-key-to-browser-origins).
</ParamField>

## Permissions

Every key carries one or more of four permission levels. They form a strict
hierarchy — a higher level **implies** all lower ones, so you never list more
than the strongest you need.

| Permission | Implies                   | Use it for                                                        |
| ---------- | ------------------------- | ----------------------------------------------------------------- |
| `read`     | —                         | Dashboards, analytics, read-only search clients                   |
| `write`    | `read`                    | Ingestion and service accounts that create documents              |
| `delete`   | `write`, `read`           | Full data management (create + remove)                            |
| `admin`    | `delete`, `write`, `read` | Org administration — **managing keys and users requires `admin`** |

```
admin  ⊃  delete  ⊃  write  ⊃  read
```

A route declares the minimum permission it needs, and a key satisfies it if the
key's permission is at least that level. To issue a **read-only key** for an
internal dashboard, create it with `"permissions": ["read"]`:

```bash theme={null}
curl -X POST "https://api.mixpeek.com/v1/organizations/users/you@acme.com/api-keys" \
  -H "Authorization: Bearer $MIXPEEK_ADMIN_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "name": "metrics-dashboard", "permissions": ["read"] }'
```

## Restrict a key to specific resources

Permissions control *what actions* a key can take; **scopes** control *which
resources* it can take them on. A scope is a `ResourceScope`:

```json theme={null}
{
  "resource_type": "namespace",
  "resource_id": "ns_customer_acme",
  "operations": ["read_data", "execute_retriever"]
}
```

<ParamField body="resource_type" type="enum" required>
  What kind of resource the scope governs: `namespace`, `collection`, `bucket`,
  `retriever`, `cluster`, `taxonomy` (and other [resource
  types](#resource-types-and-operations)).
</ParamField>

<ParamField body="resource_id" type="string" required>
  A literal ID (`ns_production`) **or a wildcard** — `*` for all, or a prefix
  pattern like `ns_customer_*` to match every namespace for a set of tenants.
</ParamField>

<ParamField body="operations" type="string[]">
  A subset of [namespace operations](#resource-types-and-operations) the key may
  perform within the scope. Omit (or `null`) to allow any operation its
  [permissions](#permissions) already grant.
</ParamField>

Example — a key that can only **read and search** within one customer's
namespace, and nothing else:

```bash theme={null}
curl -X POST "https://api.mixpeek.com/v1/organizations/users/you@acme.com/api-keys" \
  -H "Authorization: Bearer $MIXPEEK_ADMIN_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "acme-readonly",
    "permissions": ["read"],
    "scopes": [{
      "resource_type": "namespace",
      "resource_id": "ns_customer_acme",
      "operations": ["read_data", "execute_retriever"]
    }]
  }'
```

<Warning>
  **An empty `scopes` list means org-wide access** — this is the legacy default,
  so a key created without scopes can reach every namespace and resource in your
  organization. To confine a key, you **must** provide at least one scope.
  Setting `scopes` to `[]` on an update *widens* the key back to org-wide.
</Warning>

### Resource types and operations

<AccordionGroup>
  <Accordion title="resource_type values">
    `organization`, `user`, `api_key`, `namespace`, `collection`, `bucket`,
    `retriever`, `cluster`, `taxonomy`, `storage_connection`, `alert`,
    `annotation`, `secret`, `webhook`.
  </Accordion>

  <Accordion title="operations values (within a namespace)">
    **Data:** `read_data`, `write_data`, `delete_data` ·
    **Retrieval:** `execute_retriever`, `create_retriever`, `delete_retriever` ·
    **Jobs:** `execute_job`, `cancel_job` ·
    **Clusters:** `create_cluster`, `delete_cluster`, `modify_cluster` ·
    **Infrastructure:** `modify_infrastructure`, `manage_permissions`.

    Common bundles: read-only `["read_data", "execute_retriever"]`;
    data-engineer `["read_data", "write_data", "execute_job"]`.
  </Accordion>
</AccordionGroup>

## Per-key rate limits

Set `rate_limit_override` to cap a single key's requests per minute — useful for
throttling a third-party integration below your plan's default:

```bash theme={null}
curl -X PATCH "https://api.mixpeek.com/v1/organizations/users/you@acme.com/api-keys/backend-service" \
  -H "Authorization: Bearer $MIXPEEK_ADMIN_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "rate_limit_override": 120 }'
```

The override interacts with your organization's plan tiers and usage pools,
which are documented in [Rate Limits & Quotas](/operations/rate-limits-quotas).

## Restrict a key to browser origins

Set `allowed_origins` to a CORS allowlist of web origins. When set, any
**browser** request using the key must send an `Origin` header that matches the
list — an exact origin (`https://app.example.com`) or a wildcard subdomain
(`https://*.example.com`) — otherwise the request is rejected with a `403`.
Requests that send no `Origin` header (server-side curl and the SDKs) are
unaffected. This is enforced org-wide, on every route, for any key that carries
an allowlist.

Set it on create, or change it later with `PATCH`. **An empty list clears the
restriction:**

<CodeGroup>
  ```bash Set origins theme={null}
  curl -X PATCH "https://api.mixpeek.com/v1/organizations/users/you@acme.com/api-keys/web-widget" \
    -H "Authorization: Bearer $MIXPEEK_ADMIN_KEY" \
    -H "Content-Type: application/json" \
    -d '{ "allowed_origins": ["https://app.example.com", "https://*.example.com"] }'
  ```

  ```bash Clear origins theme={null}
  curl -X PATCH "https://api.mixpeek.com/v1/organizations/users/you@acme.com/api-keys/web-widget" \
    -H "Authorization: Bearer $MIXPEEK_ADMIN_KEY" \
    -H "Content-Type: application/json" \
    -d '{ "allowed_origins": [] }'
  ```
</CodeGroup>

The allowlist carries across [rotation](#rotate-expire-and-revoke) — a rotated
key keeps its origins.

<Warning>
  `allowed_origins` is **defense-in-depth, not a standalone security boundary.**
  It only constrains requests that send an `Origin` header (i.e. browsers); a
  stolen key used from a non-browser client can omit `Origin` and bypass it.
  Pair it with least-privilege [permissions](#permissions) and
  [scopes](#restrict-a-key-to-specific-resources), and treat any key shipped to
  a browser as public.
</Warning>

## Rotate, expire, and revoke

<Steps>
  <Step title="Rotate — issue a new secret, invalidate the old one">
    `POST /v1/organizations/users/{user_email}/api-keys/{key_name}/rotate`
    returns a **new plaintext `key`** and immediately revokes the previous
    secret. The `key_id` and settings carry over; only the secret changes.

    ```bash theme={null}
    curl -X POST "https://api.mixpeek.com/v1/organizations/users/you@acme.com/api-keys/backend-service/rotate" \
      -H "Authorization: Bearer $MIXPEEK_ADMIN_KEY"
    ```
  </Step>

  <Step title="Expire — set an automatic cutoff">
    Set `expires_at` on create or via `PATCH`. Once passed, the key's status
    flips to `expired` and it stops authenticating. Expired keys cannot be
    reactivated — create or rotate instead.
  </Step>

  <Step title="Revoke — kill a key immediately">
    `DELETE /v1/organizations/users/{user_email}/api-keys/{key_name}` sets the
    key's status to `revoked`. It's a soft revoke (the record is retained for
    the audit trail), and it cannot be undone.

    ```bash theme={null}
    curl -X DELETE "https://api.mixpeek.com/v1/organizations/users/you@acme.com/api-keys/backend-service" \
      -H "Authorization: Bearer $MIXPEEK_ADMIN_KEY"
    ```
  </Step>
</Steps>

A key's `status` is always one of `active`, `revoked`, or `expired`, and
`last_used_at` records the timestamp of its most recent successful request — a
quick way to find stale keys worth revoking. List a user's keys (add
`?include_revoked=true` to see revoked ones) with:

```bash theme={null}
curl "https://api.mixpeek.com/v1/organizations/users/you@acme.com/api-keys" \
  -H "Authorization: Bearer $MIXPEEK_ADMIN_KEY"
# -> { "results": [ { "key_id": "...", "name": "...", "status": "active", ... } ], "total": 3 }
```

<Note>
  The organization's primary **`admin-key` is protected** — it cannot be
  updated, rotated, or deleted (the Studio UI depends on it). Create additional
  scoped keys for specific use cases, and if the `admin-key` is ever
  compromised, [contact support](https://mixpeek.com/contact).
</Note>

### Audit trail

Every key lifecycle event is written to your organization's audit log with the
acting user and a timestamp. Keys record `created_by` and `revoked_by`
(with `revoked_at`) directly, and the audit log captures the actions
`api_key_created`, `api_key_rotated`, `api_key_revoked`, and
`api_key_scope_updated` — each with the actor, resource, and what changed.

## Monitor usage per key

Attribute traffic and spend to individual keys. Metrics come from the analytics
pipeline (ClickHouse) and default to the **last 7 days** unless you pass `start`
and `end` (ISO 8601).

```bash theme={null}
curl "https://api.mixpeek.com/v1/organizations/api-keys/key_a1b2c3d4e5f6g7h/usage" \
  -H "Authorization: Bearer $MIXPEEK_ADMIN_KEY"
```

```json theme={null}
{
  "key_id": "key_a1b2c3d4e5f6g7h",
  "start": "2026-06-30T00:00:00Z",
  "end": "2026-07-07T00:00:00Z",
  "total_requests": 14820,
  "total_credits": 512,
  "unique_endpoints": 6,
  "avg_latency_ms": 43.7,
  "p95_latency_ms": 118.0
}
```

For a per-endpoint breakdown, call
`GET /v1/organizations/api-keys/{key_id}/usage/endpoints`.

<Info>
  Usage endpoints are keyed by **`key_id`** (e.g. `key_a1b2…`), not the key's
  `name`. Grab the `key_id` from the list response above. Your billing usage
  records also carry the attributing `key_id`, so credit spend reconciles to the
  exact key that drove it.
</Info>

## Key types

Most keys you create are **standard** keys, but Mixpeek issues a few specialized
types for different jobs:

| Type             | Prefix            | Purpose                                                                                                |
| ---------------- | ----------------- | ------------------------------------------------------------------------------------------------------ |
| Standard         | `mxp_sk_`         | Regular organization key. Everything above.                                                            |
| User-scoped      | `usr_sk_`         | Carries a `principal_id` for [document-level ACL](#end-user-keys-for-multi-tenant-apps).               |
| Retriever-scoped | `ret_sk_`         | Locked to executing one [published retriever](#retriever-scoped-keys).                                 |
| Marketplace      | `sk_marketplace_` | Grants access to a subscribed marketplace retriever.                                                   |
| Session          | —                 | Short-lived key Studio mints on login to back the UI. Hidden from key lists; not something you manage. |

### End-user keys for multi-tenant apps

If you're building an app where your own end-users each see only *their* data,
create a key with a `principal_id`. Mixpeek issues a **`usr_sk_`** user-scoped
key, and every document read made with it is automatically filtered to documents
that principal owns or has been granted access to — enforced server-side, so a
tampered client can't widen its own view.

```bash theme={null}
curl -X POST "https://api.mixpeek.com/v1/organizations/users/you@acme.com/api-keys" \
  -H "Authorization: Bearer $MIXPEEK_ADMIN_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "name": "enduser-jane", "permissions": ["read"], "principal_id": "user_jane_123" }'
```

<Note>
  `principal_id` is an identifier for an **end-user in your application**, not a
  Mixpeek organization user. This is the building block for row-level security —
  see [Document-Level ACL](/operations/document-acl) for the full ownership and
  grant model, and [Permissions](/platform/permissions) for how it composes with
  external OpenFGA authorization.
</Note>

### Retriever-scoped keys

To embed a single retriever in a browser app or hand it to a customer, mint a
**`ret_sk_`** key scoped to just that retriever — it can execute the retriever
and nothing else, and its scope/permissions are fixed at creation. Only the
retriever's owner can create one.

`POST /v1/retrievers/{retriever_id}/api-keys`:

```bash theme={null}
curl -X POST "https://api.mixpeek.com/v1/retrievers/ret_abc123/api-keys" \
  -H "Authorization: Bearer $MIXPEEK_ADMIN_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "public-video-search",
    "expires_at": "2026-12-31T23:59:59Z",
    "allowed_origins": ["https://app.example.com", "https://*.example.com"]
  }'
```

`allowed_origins` works exactly as it does on
[standard keys](#restrict-a-key-to-browser-origins) — a CORS allowlist enforced
for browser requests, and the same defense-in-depth caveat applies. Because a
`ret_sk_` key is designed to be embedded in a browser, always pair it with a
short `expires_at` and treat the key as public.

## Manage keys from Studio

Everything on this page is also available without the API: in
[Studio](https://studio.mixpeek.com), go to **Settings → API Keys** to create,
scope, rotate, revoke, and view per-key usage.

## Related

<CardGroup cols={2}>
  <Card title="Security & Tenancy" icon="shield-halved" href="/operations/security">
    The auth model, namespace isolation, and secrets vault.
  </Card>

  <Card title="Rate Limits & Quotas" icon="gauge-high" href="/operations/rate-limits-quotas">
    Plan tiers, usage pools, and how `rate_limit_override` fits in.
  </Card>

  <Card title="Document-Level ACL" icon="user-lock" href="/operations/document-acl">
    Row-level security driven by user-scoped `principal_id` keys.
  </Card>

  <Card title="Permissions" icon="key" href="/platform/permissions">
    Retrieval-time authorization and OpenFGA integration.
  </Card>

  <Card title="API reference — Create key" icon="code" href="/api-reference/organization-api-keys/create-api-key">
    Full request/response schema for every key endpoint.
  </Card>

  <Card title="API reference — Per-key usage" icon="chart-line" href="/api-reference/organization-usage/get-api-key-usage">
    Usage and endpoint-breakdown response schemas.
  </Card>
</CardGroup>
