> ## 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](https://docs.mixpeek.com/docs/operations/security.md). For rate-limit tiers and usage
pools, see [Rate Limits & Quotas](https://docs.mixpeek.com/docs/operations/rate-limits-quotas.md).

> [!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.

## 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.

## 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).

```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);
```

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

**name** `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.

**permissions** `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).

**scopes** `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).

**rate\_limit\_override** `integer`

Per-key requests-per-minute ceiling. Defaults to your plan limit when absent.
See [Per-key rate limits](#per-key-rate-limits).

**expires\_at** `string (ISO 8601)`

UTC timestamp when the key auto-expires. Omit for a non-expiring key.

**description** `string`

Optional note (≤500 chars) explaining the key's purpose.

**principal\_id** `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).

**allowed\_origins** `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).

## Verify a key

`GET /v1/me` answers "which tenant is this key, and what can it do". It works for every valid key, so it is the check to run the moment you mint one, especially for a key going into CI.

```bash cURL theme={null}
curl https://api.mixpeek.com/v1/me \
  -H "Authorization: Bearer YOUR_MIXPEEK_API_KEY"
```

```json theme={null}
{
  "organization_id": "org_a1b2c3d4e5f6",
  "organization_name": "Acme",
  "account_type": "BUILD",
  "user_email": "you@example.com",
  "key": {
    "name": "ci-ingest",
    "key_type": "SECRET",
    "permissions": ["READ", "WRITE"],
    "expires_at": null
  },
  "rate_limits": {},
  "principal_scoped": false
}
```

| Field | Tells you |
| - | - |
| `organization_id` | Which tenant the key belongs to. Safe to log |
| `organization_name` | That tenant's display name |
| `account_type` | The tier, for example `FREE`, `BUILD`, `SCALE` |
| `user_email` | Which user owns the key |
| `key` | The key's own name, type, permissions, and expiry |
| `rate_limits` | Effective per-minute limits by category |
| `principal_scoped` | `true` when the key is end-user scoped, so document ACL filtering applies |

> [!TIP]
>
> Pointing a job at the wrong tenant fails silently: the calls succeed and the data lands somewhere you did not intend. One `GET /v1/me` at the top of a CI run turns that into an assertion on `organization_id`.

## 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"]
}
```

**resource\_type** `enum` *required*

What kind of resource the scope governs: `namespace`, `collection`, `bucket`,
`retriever`, `cluster`, `taxonomy` (and other [resource
types](#resource-types-and-operations)).

**resource\_id** `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.

**operations** `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.

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.

### Resource types and operations

> [accordion] resource\_type values
>
> `organization`, `user`, `api_key`, `namespace`, `collection`, `bucket`,
> `retriever`, `cluster`, `taxonomy`, `storage_connection`, `alert`,
> `annotation`, `secret`, `webhook`.

> [accordion] 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"]`.

## 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](https://docs.mixpeek.com/docs/operations/rate-limits-quotas.md).

## 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:**

```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": [] }'
```

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.

## Rotate, expire, and revoke

**1. 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"
```

**2. 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.

**3. 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"
```

The call is **refused** when a live published app still embeds the key, or
when the key authenticated in the last 7 days, and the refusal names what it
found. `?force=true` proceeds anyway.

> [!WARNING]
>
> **Before you `force`, find the consumers the API cannot see.** The refusal
> can only name published apps and recent use. A key held in a Kubernetes
> Secret, a CI secret or a deployment config leaves no record in Mixpeek's
> data, so `referencing_apps` comes back empty for it and the refusal looks
> like a false alarm.
>
> That is not hypothetical. A revoke forced past an empty `referencing_apps`
> took a public widget offline for 43 minutes; an earlier one took two
> published apps down for seven weeks before anyone noticed. In both cases
> the holders were found afterwards by scanning the cluster.
>
> Search those places yourself first: every Kubernetes Secret, CI secret
> and deployment config that could hold the key. Compare SHA-256 digests
> so the key itself never prints.

**4. After the revoke, confirm every holder re-keyed**

Search the same places once more when the rotation is done, this time for
the digest of the **old** key. A Secret that still holds it is on the revoked
key and is failing now.

Make this the last step of every rotation. A holder left on the dead key stays
broken for as long as nothing pages on its behalf. After one forced revoke the
consumer with a human-facing alert was repaired in 43 minutes, while a scheduled
job holding the same key ran dark for 26 hours, its failures accumulating only in
`failedJobsHistory` where nobody was watching.

When the holder is a scheduled job, read `lastSuccessfulTime` on the CronJob
rather than the pod list. `failedJobsHistoryLimit` retains only the last few runs,
which dated that 26-hour outage at 67 minutes.

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).

### 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
store 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`.

> [!NOTE]
>
> 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.

## 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](https://docs.mixpeek.com/docs/operations/document-acl.md) for the full ownership and
> grant model, and [Permissions](https://docs.mixpeek.com/docs/platform/permissions.md) for how it composes with
> external OpenFGA authorization.

### 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

> [card] [Security & Tenancy](https://docs.mixpeek.com/docs/operations/security.md)
>
> The auth model, namespace isolation, and secrets vault.

> [card] [Rate Limits & Quotas](https://docs.mixpeek.com/docs/operations/rate-limits-quotas.md)
>
> Plan tiers, usage pools, and how `rate_limit_override` fits in.

> [card] [Document-Level ACL](https://docs.mixpeek.com/docs/operations/document-acl.md)
>
> Row-level security driven by user-scoped `principal_id` keys.

> [card] [Permissions](https://docs.mixpeek.com/docs/platform/permissions.md)
>
> Retrieval-time authorization and OpenFGA integration.

> [card] [API reference — Create key](https://docs.mixpeek.com/docs/api-reference/organization-api-keys/create-api-key.md)
>
> Full request/response schema for every key endpoint.

> [card] [API reference — Per-key usage](https://docs.mixpeek.com/docs/api-reference/organization-usage/get-api-key-usage.md)
>
> Usage and endpoint-breakdown response schemas.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.