> ## 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.
# Analytics query examples
Source: https://www.unkey.com/docs/api-management/analytics/examples
Copy-ready SQL for the questions people ask most about verifications and rate limits.
Copy these queries to answer common questions. Send each as the `query` string of `analytics.getVerifications` or `analytics.getRatelimits`. You don't need a workspace filter.
Some of these reach back seven days or a calendar month, which is longer than some plans keep data. Check your [retention](/docs/api-management/analytics/restrictions-and-quotas) first. If a query fails with `err:user:bad_request:query_range_exceeds_retention`, shorten the window.
## Verifications
### Total verifications in the last 24 hours
The per-hour rollup already sums verifications, so read `sum(count)` rather than `count()`.
```sql theme={"system"}
SELECT sum(count) AS verifications
FROM key_verifications_per_hour_v1
WHERE time >= now() - INTERVAL 24 HOUR
```
### Outcomes per hour for the last day
```sql theme={"system"}
SELECT
time,
outcome,
sum(count) AS verifications
FROM key_verifications_per_hour_v1
WHERE time >= now() - INTERVAL 24 HOUR
GROUP BY time, outcome
ORDER BY time, outcome
```
### Valid versus rejected, per day
`sumIf` lets you split one scan into several totals. Per-day `time` is a `Date`, so compare with `today()`.
```sql theme={"system"}
SELECT
time AS day,
sumIf(count, outcome = 'VALID') AS valid,
sumIf(count, outcome != 'VALID') AS rejected,
round(100 * valid / (valid + rejected), 2) AS valid_percent
FROM key_verifications_per_day_v1
WHERE time >= today() - INTERVAL 7 DAY
GROUP BY day
ORDER BY day
```
### Usage per customer this month
`external_id` is your ID for the identity behind each key, so it's a good way to bill.
```sql theme={"system"}
SELECT
external_id,
sum(count) AS verifications,
sum(spent_credits) AS credits
FROM key_verifications_per_day_v1
WHERE time >= toStartOfMonth(today())
AND external_id != ''
GROUP BY external_id
ORDER BY verifications DESC
LIMIT 100
```
### Keys hitting rate limits or running out of credits
```sql theme={"system"}
SELECT
key_id,
external_id,
sumIf(count, outcome = 'RATE_LIMITED') AS rate_limited,
sumIf(count, outcome = 'USAGE_EXCEEDED') AS usage_exceeded
FROM key_verifications_per_hour_v1
WHERE time >= now() - INTERVAL 1 DAY
GROUP BY key_id, external_id
HAVING rate_limited > 0 OR usage_exceeded > 0
ORDER BY rate_limited + usage_exceeded DESC
LIMIT 50
```
### Latency percentiles from the raw table
Latency stats have to come from the raw table. Raw `time` is in milliseconds, so convert the bound.
```sql theme={"system"}
SELECT
round(avg(latency), 2) AS avg_ms,
round(quantile(0.5)(latency), 2) AS p50_ms,
round(quantile(0.99)(latency), 2) AS p99_ms
FROM key_verifications_v1
WHERE time >= toUnixTimestamp64Milli(now() - INTERVAL 1 HOUR)
```
### Verifications per tag
Tags are an array on each row. `arrayJoin` gives one row per tag, and `has` keeps rows with a specific tag.
```sql theme={"system"}
SELECT
arrayJoin(tags) AS tag,
sum(count) AS verifications
FROM key_verifications_per_day_v1
WHERE time >= today() - INTERVAL 7 DAY
GROUP BY tag
ORDER BY verifications DESC
```
```sql theme={"system"}
SELECT sum(count) AS checkout_calls
FROM key_verifications_per_day_v1
WHERE time >= today() - INTERVAL 7 DAY
AND has(tags, 'endpoint:checkout')
```
### Hourly buckets from the raw table
When you need a dimension the rollups don't carry, such as `region`, bucket the raw rows yourself.
```sql theme={"system"}
SELECT
toStartOfHour(fromUnixTimestamp64Milli(time)) AS hour,
region,
count() AS verifications
FROM key_verifications_v1
WHERE time >= toUnixTimestamp64Milli(now() - INTERVAL 6 HOUR)
GROUP BY hour, region
ORDER BY hour, region
```
### Gateway-verified versus API-verified
Keys verified by a Compute gateway key-auth policy carry `source = 'gateway'` and the `app_id` of the app that fronted them. Direct `keys.verifyKey` calls carry `source = 'api'` and an empty `app_id`.
```sql theme={"system"}
SELECT
source,
app_id,
sum(count) AS verifications
FROM key_verifications_per_day_v1
WHERE time >= today() - INTERVAL 7 DAY
GROUP BY source, app_id
ORDER BY verifications DESC
```
## Rate limits
### Pass rate per namespace over the last day
```sql theme={"system"}
SELECT
namespace_id,
sum(passed) AS passed,
sum(total) AS total,
round(100 * passed / total, 2) AS pass_percent
FROM ratelimits_per_hour_v1
WHERE time >= now() - INTERVAL 24 HOUR
GROUP BY namespace_id
ORDER BY total DESC
```
### Most rejected identifiers
```sql theme={"system"}
SELECT
identifier,
sum(total) - sum(passed) AS rejected
FROM ratelimits_per_hour_v1
WHERE time >= now() - INTERVAL 24 HOUR
GROUP BY identifier
HAVING rejected > 0
ORDER BY rejected DESC
LIMIT 25
```
### Checks that used an override
```sql theme={"system"}
SELECT
override_id,
identifier,
countIf(passed) AS passed,
countIf(NOT passed) AS rejected
FROM ratelimits_v1
WHERE time >= toUnixTimestamp64Milli(now() - INTERVAL 6 HOUR)
AND override_id != ''
GROUP BY override_id, identifier
ORDER BY rejected DESC
```
## Combining tables
Use a CTE to answer a two-step question.
```sql theme={"system"}
WITH top_customers AS (
SELECT external_id
FROM key_verifications_per_day_v1
WHERE time >= today() - INTERVAL 7 DAY
AND external_id != ''
GROUP BY external_id
ORDER BY sum(count) DESC
LIMIT 10
)
SELECT
time,
external_id,
sum(count) AS verifications
FROM key_verifications_per_hour_v1
WHERE time >= now() - INTERVAL 24 HOUR
AND external_id IN (SELECT external_id FROM top_customers)
GROUP BY time, external_id
ORDER BY time, external_id
```
If a query is rejected, the [troubleshooting page](/docs/api-management/analytics/troubleshooting) maps each error to its fix.
# Analytics
Source: https://www.unkey.com/docs/api-management/analytics/overview
Query your key verification and rate limit data with SQL.
`analytics.getGatewayRequests` and `analytics.getRuntimeLogs` query Compute tables and are documented there. The query language, restrictions, and quotas on the pages here apply to them too.
Every key verification and every rate limit decision is recorded, and you can query those records with SQL. Write a `SELECT` and get rows back. You only ever see your own workspace's data. Use it to build usage dashboards, bill on verifications, find keys that are hitting limits, or answer a one-off question.
## Get access
Ask [support@unkey.com](mailto:support@unkey.com) to turn on analytics for your workspace. There's no plan gate and no self-serve switch. Until it's on, the endpoints return HTTP 412 with `err:unkey:data:analytics_not_configured`. Once it's on, the [restrictions and quotas](/docs/api-management/analytics/restrictions-and-quotas) apply.
You need a root key with the permissions listed on this page. Create one in the dashboard under **Settings > Root Keys**. See [Permission reference](/docs/platform/root-keys/permissions-legacy) for every permission.
## The endpoints
There are four, one per data set. Each takes `{"query": ""}` and returns `data` as an array of rows, keyed by the column names you selected.
| Endpoint | Public tables | Root key permission |
| - | - | - |
| `analytics.getVerifications` | `key_verifications_v1` and its per-minute, per-hour, per-day, and per-month rollups | `api.*.read_analytics`, or `api..read_analytics` for specific keyspaces |
| `analytics.getRatelimits` | `ratelimits_v1` and its rollups | `ratelimit.*.read_analytics`, or `ratelimit..read_analytics` for specific namespaces |
| `analytics.getGatewayRequests` | `gateway_requests_v1` | `project.*.read_gateway_requests` |
| `analytics.getRuntimeLogs` | `runtime_logs_v1` | `project.*.read_runtime_logs` |
A root key with `read_analytics` for specific keyspaces (or namespaces) instead of `*` only sees rows for those.
```bash theme={"system"}
curl -X POST https://api.unkey.com/v2/analytics.getVerifications \
-H "Authorization: Bearer " \
-H "Content-Type: application/json" \
-d '{"query": "SELECT outcome, count() AS n FROM key_verifications_v1 WHERE time > toUnixTimestamp64Milli(now() - toIntervalDay(1)) GROUP BY outcome ORDER BY n DESC"}'
```
```json Response theme={"system"}
{
"meta": { "requestId": "req_2c9a0jf23l4k567" },
"data": [
{ "outcome": "VALID", "n": 48213 },
{ "outcome": "RATE_LIMITED", "n": 1207 },
{ "outcome": "USAGE_EXCEEDED", "n": 88 }
]
}
```
## Public tables
`key_verifications_v1` has one row per verification. `key_verifications_per_minute_v1`, `_per_hour_v1`, `_per_day_v1`, and `_per_month_v1` are rollups, much faster for charts and totals. The ratelimits tables follow the same pattern. Any other table name fails with `err:user:bad_request:invalid_analytics_table`. Columns and types are on [key\_verifications tables](/docs/api-management/analytics/tables/key-verifications) and [ratelimits tables](/docs/api-management/analytics/tables/ratelimits).
## Where to go next
What SQL is accepted and which functions you can call.
Size limits, timeouts, retention, and every error code.
Copy-ready queries for the common questions.
Empty results, timeouts, and rejected queries.
You can run the same queries from the CLI with `unkey api analytics get-verifications` and `unkey api analytics get-ratelimits`. The CLI reference starts at [get-verifications](/docs/api-management/cli/analytics/get-verifications).
# Analytics query language
Source: https://www.unkey.com/docs/api-management/analytics/query-language
Write SQL against your verification and rate limit data.
Analytics queries are ClickHouse SQL. You can filter, group, aggregate, and join your own data. Anything not listed here is rejected with a 400 that names the problem.
## What a query must be
A query is one `SELECT` statement with a `FROM` clause. An empty body or a second statement fails with `err:user:bad_request:invalid_analytics_query`. Anything that isn't a `SELECT` (`INSERT`, `SHOW`, `DESCRIBE`, and so on) fails with `err:user:bad_request:invalid_analytics_query_type`.
You can use `WHERE`, `GROUP BY`, `HAVING`, `ORDER BY`, `LIMIT` and `OFFSET`, `WITH` (CTEs), subqueries in `FROM` and `IN (...)`, `JOIN`, `UNION`, and `EXCEPT`. The same rules apply to every nested `SELECT`.
These aren't allowed:
* A `SETTINGS` clause.
* Table functions such as `numbers()`, `url()`, or `remote()`.
* `IN` followed by a table name. Write `IN (SELECT ... FROM ...)` instead.
## Tables
Only the tables listed on the [overview](/docs/api-management/analytics/overview), and CTEs you define, can appear in `FROM` and `JOIN`. Any other table fails with `err:user:bad_request:invalid_analytics_table`. Aliases (`FROM key_verifications_v1 AS v`) work as usual.
## Filters and limits added for you
You don't need a workspace filter. Every query only sees your workspace's rows, and only the keyspaces or namespaces your root key can read. Your own `WHERE` narrows it further. A query for a keyspace you can't see returns no rows, not an error.
Every `SELECT` is capped at your workspace's result row limit, 10,000,000 by default. A smaller `LIMIT` you write is kept.
## Functions
Only the functions in this table are allowed. Names can be any case. Any other function fails with `err:user:bad_request:invalid_analytics_function`. Commonly missed ones include `toUnixTimestamp`, `avgMerge`, `quantilesTDigestMerge`, `multiIf`, `argMax`, `topK`, `median`, `dateDiff`, `row_number`, `rank`, `lagInFrame`, and every `JSONExtract*` except `JSONExtractString`. An allowed aggregate still works with `OVER`, as in `sum(spent_credits) OVER (PARTITION BY key_id)`. Operators such as `+`, `=`, `<`, `AND`, `OR`, `NOT`, `LIKE`, `IN`, and `BETWEEN` always work.
| Family | Allowed functions |
| - | - |
| Aggregates | `count`, `sum`, `avg`, `min`, `max`, `any`, `groupArray`, `groupUniqArray`, `uniq`, `uniqExact`, `quantile` |
| Conditional aggregates and expressions | `countIf`, `sumIf`, `if`, `case`, `coalesce` |
| Date and time | `now`, `now64`, `today`, `toDate`, `toDateTime`, `toDateTime64`, `toStartOfMinute`, `toStartOfHour`, `toStartOfDay`, `toStartOfWeek`, `toStartOfMonth`, `toStartOfQuarter`, `toStartOfYear`, `date_trunc`, `formatDateTime`, `fromUnixTimestamp64Milli`, `toUnixTimestamp64Milli` |
| Intervals | `toIntervalNanosecond`, `toIntervalMicrosecond`, `toIntervalMillisecond`, `toIntervalSecond`, `toIntervalMinute`, `toIntervalHour`, `toIntervalDay`, `toIntervalWeek`, `toIntervalMonth`, `toIntervalQuarter`, `toIntervalYear`. ClickHouse's `INTERVAL N UNIT` literal, as in `now() - INTERVAL 7 DAY`, is syntax rather than a function and is also accepted. |
| Strings | `lower`, `upper`, `substring`, `concat`, `length`, `trim`, `startsWith`, `endsWith` |
| JSON | `JSONExtractString` |
| Math | `round`, `floor`, `ceil`, `abs` |
| Type conversion | `toString`, `toInt32`, `toInt64`, `toFloat64` |
| Arrays | `has`, `hasAny`, `hasAll`, `arrayJoin`, `arrayFilter` |
If you need a function that isn't listed, tell us which one.
## Working with time
In the raw tables, `time` is milliseconds since the Unix epoch (`Int64`), so convert when you compare with "now": `time > toUnixTimestamp64Milli(now() - INTERVAL 1 HOUR)`. To group raw rows by hour, convert the other way: `toStartOfHour(fromUnixTimestamp64Milli(time))`. In the rollups, `time` is a `DateTime` (per-minute, per-hour) or a `Date` (per-day, per-month), so compare directly: `time > now() - INTERVAL 7 DAY`.
Queries only return data inside your plan's retention window. What happens when you ask for older data depends on how you write the time filter:
* **`INTERVAL N UNIT`, a literal date, or `today()`**: a start time older than your retention fails with `err:user:bad_request:query_range_exceeds_retention`.
* **`toIntervalDay(N)` or no start time at all**: the query runs and quietly returns only the rows inside retention.
For example, `time >= now() - INTERVAL 400 DAY` fails, while `time >= now() - toIntervalDay(400)` returns fewer rows with no error. Use `INTERVAL N UNIT` if you'd rather get an error than a short answer. Retention per plan is on [Analytics restrictions and quotas](/docs/api-management/analytics/restrictions-and-quotas).
## Aggregate state columns
In the rollup tables, read `count`, `spent_credits`, `total`, and similar columns with `sum()`, never `count()`, because each row already sums many verifications. You can't read the rollups' `latency_avg`, `latency_p75`, and `latency_p99` columns, because the `-Merge` functions they need aren't allowed. Use the raw table instead: `avg(latency)` and `quantile(0.99)(latency)`. The [table pages](/docs/api-management/analytics/tables/key-verifications) mark which columns are which.
# Analytics restrictions and quotas
Source: https://www.unkey.com/docs/api-management/analytics/restrictions-and-quotas
The limits every analytics query runs under and the error each one returns.
Every analytics query runs under these limits. Most queries never hit them. When you do, find the error code here to see which limit you hit. For what SQL is allowed, see [Analytics query language](/docs/api-management/analytics/query-language).
## Query shape limits
These are checked before the query runs. Going over one returns HTTP 400, except nesting depth.
| Limit | Value | Error code |
| - | - | - |
| Query text | 16 KiB | `err:user:bad_request:invalid_analytics_query` |
| Projected columns across all `SELECT`s | 64 | `err:user:bad_request:invalid_analytics_query` ("Analytics query projects too many columns") |
| Query complexity (parsed expression nodes) | 2,000 | `err:user:bad_request:invalid_analytics_query` ("Analytics query is too complex") |
| Nesting depth | 100 | The query fails when it runs, not before |
| Statements | exactly one `SELECT` with `FROM` | `invalid_analytics_query` or `invalid_analytics_query_type` |
| Tables | public aliases only | `err:user:bad_request:invalid_analytics_table` |
| Functions | the allow-list | `err:user:bad_request:invalid_analytics_function` |
## Execution limits
These apply while the query runs. Hitting one returns HTTP 422.
| Limit | Value | Error code |
| - | - | - |
| Execution time | 10 seconds | `err:user:unprocessable_entity:query_execution_timeout` |
| Memory per query | 1 GB | `err:user:unprocessable_entity:query_memory_limit_exceeded` |
| Result rows | 10,000,000. Also applied as a `LIMIT` on every `SELECT`. | `err:user:unprocessable_entity:query_rows_limit_exceeded` |
| Encoded response size | 4 MiB | `err:user:unprocessable_entity:query_memory_limit_exceeded` ("Query result exceeds the maximum response size.") |
The 4 MiB response size is the limit people hit first. To stay under it, page with `LIMIT` and `OFFSET`, aggregate instead of selecting raw rows, or narrow the time window. `NaN` and infinity come back as `null`.
## Quota per workspace
By default, each workspace can run 1,000 queries and 1,800 seconds of total query time per rolling hour. Going over either returns HTTP 429 with `err:user:too_many_requests:query_quota_exceeded`. Wait for the window to roll over, or contact [support@unkey.com](mailto:support@unkey.com) if you need more.
## Retention
Queries only return rows inside your plan's log retention. If your time filter starts earlier than that, the query fails with HTTP 400 and `err:user:bad_request:query_range_exceeds_retention`, and the detail names the earliest date you can query. A filter written with `toIntervalDay(N)` instead of `INTERVAL N DAY` doesn't fail. It just leaves the older rows out. See [Working with time](/docs/api-management/analytics/query-language#working-with-time).
| Plan | Log retention |
| - | - |
| Free, no Compute plan | 1 day |
| API Pro, any tier | 7 days |
| Compute Starter | 3 days |
| Compute Pro | 7 days |
| Compute Business | 14 days |
Plans agreed with sales have their own retention. Your actual value is the `Log retention` row on **Settings > Limits**. See [Limits](/docs/platform/billing/limits).
Each table also has a maximum age, whatever your plan:
* **Verifications:** raw, per-minute, and per-hour 90 days; per-day 365 days; per-month 3 years.
* **Rate limits:** raw one month; per-minute 7 days; per-hour 30 days; per-day 100 days; per-month 3 years.
## Other errors
| Status | Code | Meaning |
| - | - | - |
| 412 | `err:unkey:data:analytics_not_configured` | Analytics is not enabled for this workspace yet. See [Analytics](/docs/api-management/analytics/overview). |
| 403 | `err:unkey:authorization:insufficient_permissions` | The root key lacks `read_analytics` (or `read_gateway_requests` / `read_runtime_logs`) for the endpoint. |
| 503 | `err:unkey:data:analytics_connection_failed` | Analytics is temporarily unavailable. Retry with backoff. |
All errors use the standard [API error format](/docs/platform/api/errors), and each error's `type` field links to its page.
# key_verifications tables
Source: https://www.unkey.com/docs/api-management/analytics/tables/key-verifications
Every column in the key verification tables and their rollups.
Query key verification data with `analytics.getVerifications` from five tables. `key_verifications_v1` has one row per verification. The four rollups have one row per time bucket with counts already summed. Use a rollup for charts and totals, and the raw table when you need individual requests, latency, or a column the rollups don't have.
## key\_verifications\_v1
One row per call to `keys.verifyKey`.
The `meta.requestId` of the verification request. Join it with your own logs.
When the verification happened, in milliseconds since the Unix epoch. Convert with `fromUnixTimestamp64Milli(time)` to bucket or format it.
When the row was recorded, in milliseconds since the Unix epoch. Filter on `time` instead.
Your workspace. Always the same value.
The keyspace the key belongs to. This is the keyspace's `ks_` ID, not the `api_` ID you pass to endpoints.
The verified key. Empty when the key wasn't found.
The identity attached to the key, or empty.
The identity's `externalId`, your own user or tenant ID. Group by it for per-customer usage.
The verification result: `VALID`, `NOT_FOUND`, `DISABLED`, `EXPIRED`, `FORBIDDEN`, `INSUFFICIENT_PERMISSIONS`, `RATE_LIMITED`, or `USAGE_EXCEEDED`, which are the values `keys.verifyKey` returns in `data.code`, plus `WORKSPACE_DISABLED` and `WORKSPACE_NOT_FOUND`, which the Compute gateway records when the workspace itself can't be used.
The `tags` sent with the verification. Use `has(tags, 'x')` or `arrayJoin(tags)`.
Credits deducted by this verification. Zero for keys without credits.
How long Unkey took to process the verification, in milliseconds.
The region that served the request.
What performed the verification: `api` for calls to `keys.verifyKey`, or `gateway` when a Compute key-auth policy verified the key.
The Compute app whose gateway verified the key, or empty for API calls.
## Rollup tables
`key_verifications_per_minute_v1`, `key_verifications_per_hour_v1`, `key_verifications_per_day_v1`, and `key_verifications_per_month_v1` have the same columns. Each row sums one time bucket, so `sum(count)` equals the raw table's row count for the same filter. The rollups don't have `request_id`, `region`, or `latency`.
Start of the bucket. `DateTime` on per-minute and per-hour, `Date` on per-day and per-month. Compare directly with `now()` or `today()`.
Your workspace.
The keyspace `ks_` ID.
The identity, or empty.
The identity's `externalId`, or empty.
The key, or empty when not found.
Same values as the raw table.
`api` or `gateway`, as on the raw table.
The Compute app, or empty.
The request tags.
Verifications in the bucket. Read with `sum(count)`.
Credits deducted in the bucket. Read with `sum(spent_credits)`.
Can't be read, because `avgMerge` isn't allowed. Compute latency from the raw table instead.
Can't be read. Same as `latency_avg`.
Can't be read. Same as `latency_avg`.
## Which table to use
* **Raw table:** individual requests and latency.
* **Per-minute:** the last few hours in fine detail.
* **Per-hour:** charts over a day to a week.
* **Per-day and per-month:** billing totals.
Retention caps how far back you can query any of them. See [Analytics restrictions and quotas](/docs/api-management/analytics/restrictions-and-quotas).
# ratelimits tables
Source: https://www.unkey.com/docs/api-management/analytics/tables/ratelimits
Every column in the rate limit tables and their rollups.
Query rate limit decisions with `analytics.getRatelimits` from five tables. They record calls to `ratelimit.limit` and `ratelimit.multiLimit`. Limits on keys and identities show up as `RATE_LIMITED` outcomes in the [key\_verifications tables](/docs/api-management/analytics/tables/key-verifications) instead.
## ratelimits\_v1
One row per rate limit check.
The `meta.requestId` of the request that performed the check.
When the check happened, in milliseconds since the Unix epoch.
When the row was recorded, in milliseconds since the Unix epoch. Filter on `time` instead.
Your workspace. Always the same value.
The namespace the check ran in, as an `rlns_` ID.
The identifier you passed: a user ID, an IP, a tenant, whatever you chose to limit on.
`true` if the request was allowed, `false` if it was rejected.
How long Unkey took to process the check, in milliseconds.
The override that applied, or empty when the request used the default limit.
The limit in force for this check.
Requests left in the window after this check.
When the window resets, in milliseconds since the Unix epoch.
The `cost` charged, `1` by default. Zero when the request sent `cost: 0`, or for older rows recorded before this column existed.
## Rollup tables
`ratelimits_per_minute_v1`, `ratelimits_per_hour_v1`, `ratelimits_per_day_v1`, and `ratelimits_per_month_v1` have the same columns: one row per time bucket, namespace, and identifier.
Start of the bucket. `DateTime` on per-minute and per-hour, `Date` on per-day and per-month.
Your workspace.
The namespace `rlns_` ID.
The rate limited identifier.
Checks that were allowed in the bucket. Read with `sum(passed)`.
All checks in the bucket. `sum(total) - sum(passed)` is the number rejected.
Sum of `tokens` over all checks.
Sum of `tokens` over allowed checks.
Can't be read, because `avgMerge` isn't allowed. Use `avg(latency)` on the raw table.
Aggregate state, with the same limitation.
Aggregate state, with the same limitation.
The rate limit tables keep less history than the verification tables: raw 1 month, per-minute 7 days, per-hour 30 days, per-day 100 days, per-month 3 years. For a month-long chart, use the per-day table. Your plan's retention also applies. See [Analytics restrictions and quotas](/docs/api-management/analytics/restrictions-and-quotas).
# Analytics troubleshooting
Source: https://www.unkey.com/docs/api-management/analytics/troubleshooting
Fix empty results, rejected queries, and timeouts.
Start with the HTTP status:
* **200 with no rows:** the query worked but matched nothing.
* **400:** the query was rejected.
* **422 or 429:** the query ran and hit a limit.
* **412:** analytics isn't turned on.
* **403:** the root key is missing a permission.
## No rows came back
**Check the time filter.** Raw tables store `time` in milliseconds, so `WHERE time >= now() - INTERVAL 1 DAY` matches nothing. Write `time >= toUnixTimestamp64Milli(now() - INTERVAL 1 DAY)` instead. Rollup tables take `DateTime` or `Date` values directly.
**Check IDs.** `key_space_id` is the keyspace's `ks_` ID, not the `api_` ID. `namespace_id` is an `rlns_` ID, not the namespace name. Values are case-sensitive, and `outcome` values are upper case (`VALID`, not `valid`).
**Check retention.** Data older than your plan's log retention is left out, often without an error. See [Working with time](/docs/api-management/analytics/query-language#working-with-time).
**Check the root key.** If it has `read_analytics` for specific keyspaces or namespaces only, rows outside them are left out without an error.
## The query was rejected (400)
| Detail | Cause | Fix |
| - | - | - |
| Invalid SQL syntax | The query couldn't be parsed. | Check quoting, commas, and parentheses. Remember that string literals use single quotes. |
| Only SELECT queries are allowed (`invalid_analytics_query_type`) | The statement is not a `SELECT`. | Analytics is read-only; there is no `INSERT`, `SHOW`, or `DESCRIBE`. Columns are listed on the table pages. |
| Analytics queries must contain exactly one statement | Two statements, or a trailing statement after a semicolon. | Send one `SELECT` per request. |
| Query must have a FROM clause | `SELECT 1` or a bare expression. | Every query reads from a public table. |
| Access to table 'x' is not allowed (`invalid_analytics_table`) | A table that isn't one of the public names, a `system` or `information_schema` table, or a typo in a public name. | Use the public names on the [overview](/docs/api-management/analytics/overview), such as `key_verifications_v1`. |
| Function 'x' is not allowed (`invalid_analytics_function`) | A function that isn't allowed, including `row_number`, `rank`, and every `-Merge` function. | Rewrite with an allowed function; see [Analytics query language](/docs/api-management/analytics/query-language). For latency, aggregate the raw table. |
| SETTINGS clauses are not allowed | A `SETTINGS` clause. | Remove it. Execution limits are fixed per workspace. |
| Table-backed IN expressions are not supported | `x IN some_table`. | Write `x IN (SELECT x FROM some_table)`. |
| Analytics query projects too many columns | More than 64 selected columns across all `SELECT`s. | Select fewer columns or split the query. |
| Analytics query is too complex | More than 2,000 parsed expression nodes. | Simplify; long `IN` lists are the usual culprit. |
| Analytics query exceeds the maximum length | The query text is over 16 KiB. | Shorten it, usually by replacing a long `IN` list with a subquery. |
| The query reads a column that is not available | `SELECT *` on `gateway_requests_v1` or `runtime_logs_v1`. Only their documented columns can be read. | Name the columns you want instead of `*`. They are listed on [Gateway request and runtime log analytics](/docs/compute/observe/analytics-api). |
| Cannot query data older than N days (`query_range_exceeds_retention`) | A lower bound on `time` is before your retention window. | Move the bound forward. The detail names the earliest allowed date. Retention per plan is on [Analytics restrictions and quotas](/docs/api-management/analytics/restrictions-and-quotas). |
## The query ran but failed (422 or 429)
| Code | Fix |
| - | - |
| `query_execution_timeout` | The query exceeded 10 seconds. Switch from the raw table to a rollup, add or tighten the `time` bound, and aggregate rather than selecting raw rows. |
| `query_memory_limit_exceeded` with "exceeds the maximum response size" | The encoded result is over 4 MiB. Add `LIMIT`, aggregate, or page with `LIMIT` and `OFFSET`. |
| `query_memory_limit_exceeded` otherwise | The query needed more than 1 GB. `groupArray` over many rows and high-cardinality `GROUP BY` on the raw table are the common causes; aggregate on a rollup. |
| `query_rows_limit_exceeded` | More than 10,000,000 result rows. Aggregate or narrow. |
| `query_quota_exceeded` (429) | More than 1,000 queries or 1,800 seconds of execution in the current hour. Cache results on your side and retry after the window rolls over. |
## Analytics isn't configured (412)
`err:unkey:data:analytics_not_configured` means analytics isn't turned on for your workspace. Email [support@unkey.com](mailto:support@unkey.com) with your workspace ID to turn it on. A 503 with `analytics_connection_failed` is different: analytics was briefly unavailable, so retry with backoff.
## Permission denied (403)
`insufficient_permissions` names the permission the root key needs: `api.*.read_analytics` or `api..read_analytics` for verifications, `ratelimit.*.read_analytics` or `ratelimit..read_analytics` for rate limits. Grant it under **Settings > Root Keys**. See [Root key permissions](/docs/platform/root-keys/permissions).
## Numbers that look wrong
* **A rollup total is far too low.** You probably wrote `count()` instead of `sum(count)`. Each rollup row sums many verifications.
* **A percentage is `null`.** It divided by zero.
* **`external_id` is empty for many rows.** Those keys have no identity. Group by `key_id` instead, or attach identities when you create keys.
# Query key verification data
Source: https://www.unkey.com/docs/api-management/api-reference/analytics/query-key-verification-data
https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v2/analytics.getVerifications
Execute custom SQL queries against your key verification analytics. CTEs, subqueries, UNION, and EXCEPT are supported.
Queries must use one of the five public aliases: `key_verifications_v1`, `key_verifications_per_minute_v1`, `key_verifications_per_hour_v1`, `key_verifications_per_day_v1`, or `key_verifications_per_month_v1`. Physical `default.*` table names are unsupported.
Queries are always restricted to the authenticated workspace. Wildcard analytics permission can read every API in that workspace; API-scoped permissions automatically restrict results to the permitted APIs.
For complete documentation including available tables, columns, data types, query examples, see the schema reference in the API documentation.
# Query rate limit data
Source: https://www.unkey.com/docs/api-management/api-reference/analytics/query-rate-limit-data
https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v2/analytics.getRatelimits
Queries may reference only the five public rate limit analytics aliases: `ratelimits_v1`, `ratelimits_per_minute_v1`, `ratelimits_per_hour_v1`, `ratelimits_per_day_v1`, or `ratelimits_per_month_v1`. CTEs, subqueries, UNION, and EXCEPT are supported.
Queries are always restricted to the authenticated workspace. Wildcard analytics permission can read every namespace in that workspace; namespace-scoped permissions automatically restrict results to the permitted namespace IDs.
Workspace retention and query limits apply.
# Create API namespace
Source: https://www.unkey.com/docs/api-management/api-reference/apis/create-api-namespace
https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v2/apis.createApi
Create an API namespace for organizing keys by environment, service, or product.
Use this to separate production from development keys, isolate different services, or manage multiple products. Each API gets a unique identifier and dedicated infrastructure for secure key operations.
**Important**: API names must be unique within your workspace and cannot be changed after creation.
**Required Permissions**
Your root key must have one of the following permissions:
- `api.*.create_api` (to create APIs in any workspace)
# Delete API namespace
Source: https://www.unkey.com/docs/api-management/api-reference/apis/delete-api-namespace
https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v2/apis.deleteApi
Permanently delete an API namespace and immediately invalidate all associated keys.
Use this for cleaning up development environments, retiring deprecated services, or removing unused resources.
All keys in the namespace are immediately marked as deleted and will fail verification with `code=NOT_FOUND`.
**Important**: This operation is immediate and permanent. Verify you have the correct API ID before deletion.
If delete protection is enabled, disable it first through the dashboard or API configuration.
**Required Permissions**
Your root key must have one of the following permissions:
- `api.*.delete_api` (to delete any API)
- `api..delete_api` (to delete a specific API)
# Get API namespace
Source: https://www.unkey.com/docs/api-management/api-reference/apis/get-api-namespace
https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v2/apis.getApi
Retrieve basic information about an API namespace including its ID and name.
Use this to verify an API exists before performing operations, get the human-readable name when you only have the API ID, or confirm access to a specific namespace. For detailed key information, use the `listKeys` endpoint instead.
**Required Permissions**
Your root key must have one of the following permissions:
- `api.*.read_api` (to read any API)
- `api..read_api` (to read a specific API)
# List API keys
Source: https://www.unkey.com/docs/api-management/api-reference/apis/list-api-keys
https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v2/apis.listKeys
Retrieve a paginated list of API keys for dashboard and administrative interfaces.
Use this to build key management dashboards, filter keys by user with `externalId`, or retrieve key details for administrative purposes. Each key includes status, metadata, permissions, and usage limits.
**Important**: Set `decrypt: true` only in secure contexts to retrieve plaintext key values from recoverable keys.
**Required Permissions**
Your root key must have one of the following permissions for basic key listing:
- `api.*.read_key` (to read keys from any API)
- `api..read_key` (to read keys from a specific API)
Additionally, you need read access to the API itself:
- `api.*.read_api` or `api..read_api`
Additional permission required for decrypt functionality:
- `api.*.decrypt_key` or `api..decrypt_key`
# Create Identity
Source: https://www.unkey.com/docs/api-management/api-reference/identities/create-identity
https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v2/identities.createIdentity
Create an identity to group multiple API keys under a single entity. Identities enable shared rate limits and metadata across all associated keys.
Perfect for users with multiple devices, organizations with multiple API keys, or when you need unified rate limiting across different services.
**Important**
Requires `identity.*.create_identity` permission
# Delete Identity
Source: https://www.unkey.com/docs/api-management/api-reference/identities/delete-identity
https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v2/identities.deleteIdentity
Permanently delete an identity. This operation cannot be undone.
Use this for data cleanup, compliance requirements, or when removing entities from your system.
> **Important**
> Requires `identity.*.delete_identity` permission
> Associated API keys remain functional but lose shared resources
> External ID becomes available for reuse immediately
# Get Identity
Source: https://www.unkey.com/docs/api-management/api-reference/identities/get-identity
https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v2/identities.getIdentity
Retrieve an identity by external ID. Returns metadata, rate limits, and other associated data.
Use this to check if an identity exists, view configurations, or build management dashboards.
> **Important**
> Requires `identity.*.read_identity` permission
# List Identities
Source: https://www.unkey.com/docs/api-management/api-reference/identities/list-identities
https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v2/identities.listIdentities
Get a paginated list of all identities in your workspace. Returns metadata and rate limit configurations.
Perfect for building management dashboards, auditing configurations, or browsing your identities.
> **Important**
> Requires `identity.*.read_identity` permission
# Update Identity
Source: https://www.unkey.com/docs/api-management/api-reference/identities/update-identity
https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v2/identities.updateIdentity
Update an identity's metadata and rate limits. Only specified fields are modified - others remain unchanged.
Perfect for subscription changes, plan upgrades, or updating user information. Changes take effect immediately.
> **Important**
> Requires `identity.*.update_identity` permission
> Rate limit changes propagate within 30 seconds
# Add key permissions
Source: https://www.unkey.com/docs/api-management/api-reference/keys/add-key-permissions
https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v2/keys.addPermissions
Add permissions to a key without affecting existing permissions.
Use this for privilege upgrades, enabling new features, or plan changes that grant additional capabilities. Permissions granted through roles remain unchanged.
**Important**: Changes take effect immediately with up to 30-second edge propagation.
**Required Permissions**
Your root key must have one of the following permissions:
- `api.*.update_key` (to update keys in any API)
- `api..update_key` (to update keys in a specific API)
**Side Effects**
Invalidates the key cache for immediate effect, and makes permissions available for verification within 30 seconds across all regions.
# Add key roles
Source: https://www.unkey.com/docs/api-management/api-reference/keys/add-key-roles
https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v2/keys.addRoles
Add roles to a key without affecting existing roles or permissions.
Use this for privilege upgrades, enabling new feature sets, or subscription changes that grant additional role-based capabilities. Direct permissions remain unchanged.
**Important**: Changes take effect immediately with up to 30-second edge propagation.
**Required Permissions**
Your root key must have one of the following permissions:
- `api.*.update_key` (to update keys in any API)
- `api..update_key` (to update keys in a specific API)
**Side Effects**
Invalidates the key cache for immediate effect, and makes role assignments available for verification within 30 seconds across all regions.
# Create API key
Source: https://www.unkey.com/docs/api-management/api-reference/keys/create-api-key
https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v2/keys.createKey
Create a new API key for user authentication and authorization.
Use this endpoint when users sign up, upgrade subscription tiers, or need additional keys. Keys are cryptographically secure and unique to the specified API namespace.
**Important**: The key is returned only once. Store it immediately and provide it to your user, as it cannot be retrieved later.
**Common use cases:**
- Generate keys for new user registrations
- Create additional keys for different applications
- Issue keys with specific permissions or limits
**Required Permissions**
Your credential needs one of:
- `api.*.create_key` (create keys in any API)
- `api..create_key` (create keys in specific API)
- `unkey:v1::keyspaces/*#create_key` (create keys in any keyspace)
- `unkey:v1::keyspaces/#create_key` (create keys in a specific keyspace)
# Delete API keys
Source: https://www.unkey.com/docs/api-management/api-reference/keys/delete-api-keys
https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v2/keys.deleteKey
Delete API keys from user accounts or for cleanup purposes.
Use this for user-requested key revocation, account deletion workflows, or cleaning up unused keys. Keys are immediately invalidated. Two modes: soft delete (default, preserves audit records) and permanent delete.
**Important**: For temporary access control, use `updateKey` with `enabled: false` instead of deletion.
**Required Permissions**
Your credential must have one of the following permissions:
- `api.*.delete_key` (to delete keys in any API)
- `api..delete_key` (to delete keys in a specific API)
- `unkey:v1::keyspaces/*/keys/*#delete_key` (to delete keys in any keyspace)
- `unkey:v1::keyspaces//keys/*#delete_key` (to delete keys in a specific keyspace)
- `unkey:v1::keyspaces//keys/#delete_key` (to delete a specific key)
# Get API key
Source: https://www.unkey.com/docs/api-management/api-reference/keys/get-api-key
https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v2/keys.getKey
Retrieve detailed key information for dashboard interfaces and administrative purposes.
Use this to build key management dashboards showing users their key details, status, permissions, and usage data. You can identify keys by `keyId` or the actual key string.
**Important**: Set `decrypt: true` only in secure contexts to retrieve plaintext key values from recoverable keys.
**Required Permissions**
Your credential must have one of the following permissions for basic key information:
- `api.*.read_key` (to read keys from any API)
- `api..read_key` (to read keys from a specific API)
- `unkey:v1::keyspaces/*/keys/*#read_key` (to read keys in any keyspace)
- `unkey:v1::keyspaces//keys/*#read_key` (to read keys in a specific keyspace)
- `unkey:v1::keyspaces//keys/#read_key` (to read a specific key)
Additional permission required for decrypt functionality:
- `api.*.decrypt_key` or `api..decrypt_key`
- `unkey:v1::keyspaces/*/keys/*#decrypt_key`
- `unkey:v1::keyspaces//keys/*#decrypt_key`
- `unkey:v1::keyspaces//keys/#decrypt_key`
# Get API key by hash
Source: https://www.unkey.com/docs/api-management/api-reference/keys/get-api-key-by-hash
https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v2/keys.whoami
Find out what key this is.
**Required Permissions**
Your credential must have one of the following permissions for basic key information:
- `api.*.read_key` (to read keys from any API)
- `api..read_key` (to read keys from a specific API)
- `unkey:v1::keyspaces/*/keys/*#read_key` (to read keys in any keyspace)
- `unkey:v1::keyspaces//keys/*#read_key` (to read keys in a specific keyspace)
- `unkey:v1::keyspaces//keys/#read_key` (to read a specific key)
If your credential lacks permissions but the key exists, we may return a 404 status here to prevent leaking the existence of a key to unauthorized clients. If you believe that a key should exist, but receive a 404, please double check your credential has the correct permissions.
# Migrate API key(s)
Source: https://www.unkey.com/docs/api-management/api-reference/keys/migrate-api-keys
https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v2/keys.migrateKeys
Returns HTTP 200 even on partial success; hashes that could not be migrated are listed under `data.failed`.
**Required Permissions**
Your root key must have one of the following permissions for basic key information:
- `api.*.create_key` (to migrate keys to any API)
- `api..create_key` (to migrate keys to a specific API)
# Remove key permissions
Source: https://www.unkey.com/docs/api-management/api-reference/keys/remove-key-permissions
https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v2/keys.removePermissions
Remove permissions from a key without affecting existing roles or other permissions.
Use this for privilege downgrades, removing temporary access, or plan changes that revoke specific capabilities. Permissions granted through roles remain unchanged.
**Important**: Changes take effect immediately with up to 30-second edge propagation.
**Required Permissions**
Your root key must have one of the following permissions:
- `api.*.update_key` (to update keys in any API)
- `api..update_key` (to update keys in a specific API)
**Side Effects**
Invalidates the key cache for immediate effect, and makes permission changes available for verification within 30 seconds across all regions.
# Remove key roles
Source: https://www.unkey.com/docs/api-management/api-reference/keys/remove-key-roles
https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v2/keys.removeRoles
Remove roles from a key without affecting direct permissions or other roles.
Use this for privilege downgrades, removing temporary access, or subscription changes that revoke specific role-based capabilities. Direct permissions remain unchanged.
**Important**: Changes take effect immediately with up to 30-second edge propagation.
**Required Permissions**
Your root key must have one of the following permissions:
- `api.*.update_key` (to update keys in any API)
- `api..update_key` (to update keys in a specific API)
**Side Effects**
Invalidates the key cache for immediate effect, and makes role changes available for verification within 30 seconds across all regions.
# Reroll Key
Source: https://www.unkey.com/docs/api-management/api-reference/keys/reroll-key
https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v2/keys.rerollKey
Generate a new API key while preserving the configuration from an existing key.
This operation creates a fresh key with a new token while maintaining all settings from the original key:
- Permissions and roles
- Custom metadata
- Rate limit configurations
- Identity associations
- Remaining credits
- Recovery settings
**Key Generation:**
- The system attempts to extract the prefix from the original key
- If prefix extraction fails, the default API prefix is used
- Key length follows the API's default byte configuration (or 16 bytes if not specified)
**Original Key Handling:**
- The original key will be revoked after the duration specified in `expiration`
- Set `expiration` to 0 to revoke immediately
- This allows for graceful key rotation with an overlap period
Common use cases include:
- Rotating keys for security compliance
- Issuing replacement keys for compromised credentials
- Creating backup keys with identical permissions
**Important:** Analytics and usage metrics are tracked at both the key level AND identity level. If the original key has an identity, the new key will inherit it, allowing you to track usage across both individual keys and the overall identity.
**Required Permissions**
Your credential must have:
- `api.*.create_key` or `api..create_key`
- `unkey:v1::keyspaces/*#create_key` or `unkey:v1::keyspaces/#create_key`
- `api.*.encrypt_key` or `api..encrypt_key` (only when the original key is recoverable)
- `unkey:v1::keyspaces/*/keys/*#encrypt_key` or `unkey:v1::keyspaces//keys/*#encrypt_key` (only when the original key is recoverable)
# Set key permissions
Source: https://www.unkey.com/docs/api-management/api-reference/keys/set-key-permissions
https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v2/keys.setPermissions
Replace all permissions on a key with the specified set in a single atomic operation.
Use this to synchronize with external systems, reset permissions to a known state, or apply standardized permission templates. Permissions granted through roles remain unchanged.
**Important**: Changes take effect immediately with up to 30-second edge propagation.
**Required Permissions**
Your root key must have one of the following permissions:
- `api.*.update_key` (to update keys in any API)
- `api..update_key` (to update keys in a specific API)
**Side Effects**
Invalidates the key cache for immediate effect, and makes permission changes available for verification within 30 seconds across all regions.
# Set key roles
Source: https://www.unkey.com/docs/api-management/api-reference/keys/set-key-roles
https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v2/keys.setRoles
Replace all roles on a key with the specified set in a single atomic operation.
Use this to synchronize with external systems, reset roles to a known state, or apply standardized role templates. Direct permissions are never affected.
**Important**: Changes take effect immediately with up to 30-second edge propagation.
**Required Permissions**
Your root key must have one of the following permissions:
- `api.*.update_key` (to update keys in any API)
- `api..update_key` (to update keys in a specific API)
**Side Effects**
Invalidates the key cache for immediate effect, and makes role changes available for verification within 30 seconds across all regions.
# Update key credits
Source: https://www.unkey.com/docs/api-management/api-reference/keys/update-key-credits
https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v2/keys.updateCredits
Update credit quotas in response to plan changes, billing cycles, or usage purchases.
Use this for user upgrades/downgrades, monthly quota resets, credit purchases, or promotional bonuses. Supports three operations: set, increment, or decrement credits. Set to null for unlimited usage.
**Important**: Setting unlimited credits automatically clears existing refill configurations.
**Required Permissions**
Your credential must have one of the following permissions:
- `api.*.update_key` (to update keys in any API)
- `api..update_key` (to update keys in a specific API)
- `unkey:v1::keyspaces/*/keys/*#update_key` (to update keys in any keyspace)
- `unkey:v1::keyspaces//keys/*#update_key` (to update keys in a specific keyspace)
- `unkey:v1::keyspaces//keys/#update_key` (to update a specific key)
**Side Effects**
Credit updates remove the key from cache immediately. Setting credits to unlimited automatically clears any existing refill settings. Changes take effect instantly but may take up to 30 seconds to propagate to all edge regions.
# Update key settings
Source: https://www.unkey.com/docs/api-management/api-reference/keys/update-key-settings
https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v2/keys.updateKey
Update key properties in response to plan changes, subscription updates, or account status changes.
Use this for user upgrades/downgrades, role modifications, or administrative changes. Supports partial updates - only specify fields you want to change. Set fields to null to clear them.
**Important**: Permissions and roles are replaced entirely. Use dedicated add/remove endpoints for incremental changes.
**Required Permissions**
Your credential must have one of the following permissions:
- `api.*.update_key` (to update keys in any API)
- `api..update_key` (to update keys in a specific API)
- `unkey:v1::keyspaces/*/keys/*#update_key` (to update keys in any keyspace)
- `unkey:v1::keyspaces//keys/*#update_key` (to update keys in a specific keyspace)
- `unkey:v1::keyspaces//keys/#update_key` (to update a specific key)
**Side Effects**
If you specify an `externalId` that doesn't exist, a new identity will be automatically created and linked to the key. Permission updates will auto-create any permissions that don't exist in your workspace. Changes take effect immediately but may take up to 30 seconds to propagate to all edge regions due to cache invalidation.
# Verify API key
Source: https://www.unkey.com/docs/api-management/api-reference/keys/verify-api-key
https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v2/keys.verifyKey
Verify an API key's validity and permissions for request authentication.
Use this endpoint on every incoming request to your protected resources. It checks key validity, permissions, rate limits, and usage quotas in a single call.
**Important**: Returns HTTP 200 for all verification outcomes — check the `valid` field in response data to determine if the key is authorized. A 429 may be returned if the workspace exceeds its API rate limit.
**Common use cases:**
- Authenticate API requests before processing
- Enforce permission-based access control
- Track usage and apply rate limits
**Required Permissions**
Your credential needs one of:
- `api.*.verify_key` (verify keys in any API)
- `api..verify_key` (verify keys in specific API)
- `unkey:v1::keyspaces/*/keys/*#verify_key` (verify keys in any keyspace)
- `unkey:v1::keyspaces//keys/*#verify_key` (verify keys in a specific keyspace)
- `unkey:v1::keyspaces//keys/#verify_key` (verify a specific key)
**Note**: If your credential has no verify permissions at all, you will receive a `403 Forbidden` error. If your credential has verify permissions for a different API or keyspace than the key you're verifying, you will receive a `200` response with `code: NOT_FOUND` to avoid leaking key existence.
# Create permission
Source: https://www.unkey.com/docs/api-management/api-reference/permissions/create-permission
https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v2/permissions.createPermission
Create a new permission to define specific actions or capabilities in your RBAC system. Permissions can be assigned directly to API keys or included in roles.
Use hierarchical naming patterns like `documents.read`, `admin.users.delete`, or `billing.invoices.create` for clear organization.
**Important:** Permission slugs must be unique within the workspace. Names do not need to be unique. Once created, permissions are immediately available for assignment.
**Required Permissions**
Your root key must have the following permission:
- `rbac.*.create_permission`
# Create role
Source: https://www.unkey.com/docs/api-management/api-reference/permissions/create-role
https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v2/permissions.createRole
Create a new role to group related permissions for easier management. Roles enable consistent permission assignment across multiple API keys. Permission slugs supplied in `permissions` are attached during creation. Missing permissions are created automatically.
**Important:** Role names must be unique within the workspace. Once created, roles are immediately available for assignment.
**Required Permissions**
Your root key must always have:
- `rbac.*.create_role`
When `permissions` is not empty, it must also have:
- `rbac.*.add_permission_to_role`
When any requested permission slug does not exist, it must also have:
- `rbac.*.create_permission`
# Delete permission
Source: https://www.unkey.com/docs/api-management/api-reference/permissions/delete-permission
https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v2/permissions.deletePermission
Remove a permission from your workspace. This also removes the permission from all API keys and roles.
**Important:** This operation cannot be undone and immediately affects all API keys and roles that had this permission assigned.
**Required Permissions**
Your root key must have the following permission:
- `rbac.*.delete_permission`
# Delete role
Source: https://www.unkey.com/docs/api-management/api-reference/permissions/delete-role
https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v2/permissions.deleteRole
Remove a role from your workspace. This also removes the role from all assigned API keys.
**Important:** This operation cannot be undone and immediately affects all API keys that had this role assigned.
**Required Permissions**
Your root key must have the following permission:
- `rbac.*.delete_role`
# Get permission
Source: https://www.unkey.com/docs/api-management/api-reference/permissions/get-permission
https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v2/permissions.getPermission
Retrieve details about a specific permission including its name, description, and metadata.
**Required Permissions**
Your root key must have the following permission:
- `rbac.*.read_permission`
# Get role
Source: https://www.unkey.com/docs/api-management/api-reference/permissions/get-role
https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v2/permissions.getRole
Retrieve details about a specific role including its assigned permissions.
**Required Permissions**
Your root key must have the following permission:
- `rbac.*.read_role`
# List permissions
Source: https://www.unkey.com/docs/api-management/api-reference/permissions/list-permissions
https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v2/permissions.listPermissions
Retrieve all permissions in your workspace.
Results are paginated and sorted by their id.
**Required Permissions**
Your root key must have the following permission:
- `rbac.*.read_permission`
# List roles
Source: https://www.unkey.com/docs/api-management/api-reference/permissions/list-roles
https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v2/permissions.listRoles
Retrieve all roles in your workspace including their assigned permissions.
Results are paginated and sorted by their id.
**Required Permissions**
Your root key must have the following permission:
- `rbac.*.read_role`
# Set role permissions
Source: https://www.unkey.com/docs/api-management/api-reference/permissions/set-role-permissions
https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v2/permissions.setRolePermissions
Atomically replaces all permissions directly assigned to a role. An empty `permissions` array removes every permission from the role. Permissions that do not exist are created when the caller has permission to create them.
**Required Permissions**
Your root key must have:
- `rbac.*.add_permission_to_role`
- `rbac.*.remove_permission_from_role`
When any requested permission slug does not exist, it must also have:
- `rbac.*.create_permission`
# Create portal
Source: https://www.unkey.com/docs/api-management/api-reference/portal/create-portal
https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v2/portal.createPortal
Create a portal for one app or keyspace in your workspace.
Unreleased and subject to change without notice.
Send exactly one of `keyspaceId` or `appId`. That resource must belong to your
workspace, and it can back only one portal, so a second portal for the same
resource is a **409**.
`displayName` is what your end users see. It is yours to set and change
independently of the resource the portal serves.
**Required Permissions**
Your root key must have `portal.*.create_portal`. A grant scoped to a specific
portal id does not authorize creation, because the id does not exist yet.
# Create portal session
Source: https://www.unkey.com/docs/api-management/api-reference/portal/create-portal-session
https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v2/portal.createSession
Create a portal session for an end user and get the URL to redirect them to.
The URL carries a single-use exchange code valid for 15 minutes, which the portal
redeems exactly once for a 24-hour access token via `portal.exchangeCode`.
**Required Permissions**
Authorization runs in two stages, and both must pass.
First, your root key must have one of the following permissions:
- `portal.*.create_portal_session` (to mint sessions for any portal in the workspace)
- `portal..create_portal_session` (to mint sessions for a specific portal)
Second, a session can never carry a capability your root key does not itself
hold. Each requested scope additionally requires the equivalent permission on
every keyspace the portal resolves to:
- `keys:read` requires `api..read_key` **and** `api..read_api`
- `keys:reroll` requires `api..create_key`, plus
`api..encrypt_key` when the keyspace stores encrypted keys
- `analytics:read` requires `api..read_analytics`
The `*` form of each is also accepted. Requesting a scope you do not hold
returns 403 for the whole request rather than minting a reduced session, so a
missing grant is visible instead of surfacing later as a broken portal.
Missing the portal permission itself returns **404**, not 403: a caller who
cannot mint for a portal is not told whether it exists.
Your root key must also be associated with a workspace that has an enabled portal.
# Delete portal
Source: https://www.unkey.com/docs/api-management/api-reference/portal/delete-portal
https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v2/portal.deletePortal
Delete a portal and revoke the sessions it minted.
Unreleased and subject to change without notice.
Its end users lose access rather than keeping it until their tokens expire.
Revocation is not instantaneous: session lookups are cached briefly, so a
request already in flight may still succeed.
The app or keyspace it served is untouched, and its slug becomes free for a
new portal.
**Required Permissions**
Your root key must have one of:
- `portal.*.delete_portal` (to delete any portal in the workspace)
- `portal..delete_portal` (to delete a specific portal)
Without the permission this returns **404**, not 403.
# Get portal
Source: https://www.unkey.com/docs/api-management/api-reference/portal/get-portal
https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v2/portal.getPortal
Read one portal, by its id or slug, or by the resource it serves.
Unreleased and subject to change without notice.
Send exactly one of `portal`, `keyspaceId`, or `appId`. Sending more than one
is a **400**.
**Required Permissions**
Your root key must have one of:
- `portal.*.read_portal` (to read any portal in the workspace)
- `portal..read_portal` (to read a specific portal)
Without the permission this returns **404**, not 403.
# List portal sessions
Source: https://www.unkey.com/docs/api-management/api-reference/portal/list-portal-sessions
https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v2/portal.listSessions
List the end users holding a revocable session on a portal, with each
end user's sessions.
Unreleased and subject to change without notice.
A session is revocable until it expires or is revoked. That includes
sessions whose portal URL was created but not opened yet. Pass an end
user's `externalId` to `portal.revokeSession` to end their sessions.
**Required Permissions**
Your root key must have one of:
- `portal.*.create_portal_session` (for any portal in the workspace)
- `portal..create_portal_session` (for a specific portal)
It also accepts `unkey:v1::projects//portals//sessions/*`
with `#read` or `#write`. Reading the portal itself is not enough.
Without the permission this returns **404**, not 403.
# Revoke portal sessions
Source: https://www.unkey.com/docs/api-management/api-reference/portal/revoke-portal-sessions
https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v2/portal.revokeSession
Revoke every live session an end user holds on a portal.
Unreleased and subject to change without notice.
Sessions that were created but not yet opened are revoked too, so their
portal URLs stop working. Revocation is not instantaneous: session lookups
are cached briefly, so a request already in flight may still succeed.
Revoking ends existing sessions only. To keep the end user out, also stop
calling `portal.createSession` for them.
Calling this again for the same end user is safe and revokes nothing.
**Required Permissions**
Your root key must have one of:
- `portal.*.create_portal_session` (for any portal in the workspace)
- `portal..create_portal_session` (for a specific portal)
It also accepts `unkey:v1::projects//portals//sessions/*#write`,
which dashboard roles carry. Unlike `portal.createSession`, a dashboard
session can call this, not just a root key.
Without the permission this returns **404**, not 403.
# Update portal
Source: https://www.unkey.com/docs/api-management/api-reference/portal/update-portal
https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v2/portal.updatePortal
Change a portal's slug, display name, the resource it serves, its enabled
state, or its branding.
Unreleased and subject to change without notice.
Only the fields you send change. Omitting a field leaves it as it is, and for
branding, sending null clears it. Send at most one of `keyspaceId` or `appId`.
Two changes affect your end users immediately:
- Re-pointing at a different resource revokes the portal's live sessions,
because a session carries the scope it was minted with.
- Disabling stops new sessions and revokes the live ones. Re-enabling does
not restore them.
**Required Permissions**
Your root key must have one of:
- `portal.*.update_portal` (to update any portal in the workspace)
- `portal..update_portal` (to update a specific portal)
Without the permission this returns **404**, not 403.
# Apply multiple rate limit checks
Source: https://www.unkey.com/docs/api-management/api-reference/ratelimit/apply-multiple-rate-limit-checks
https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v2/ratelimit.multiLimit
Check and enforce multiple rate limits in a single request for any identifiers (user IDs, IP addresses, API clients, etc.).
Use this to efficiently check multiple rate limits at once. Each rate limit check is independent and returns its own result with a top-level `passed` indicator showing if all checks succeeded.
**Response Codes**: Rate limit checks return HTTP 200 regardless of whether limits are exceeded — check the `passed` field to see if all limits passed, or the `success` field in each individual result. A 429 may be returned if the workspace exceeds its API rate limit. Other 4xx responses indicate auth, namespace existence/deletion, or validation errors (e.g., 410 Gone for deleted namespaces). 5xx responses indicate server errors.
**Required Permissions**
Your root key must have one of the following permissions:
- `ratelimit.*.limit` (to check limits in any namespace)
- `ratelimit..limit` (to check limits in all specific namespaces being checked)
# Apply rate limiting
Source: https://www.unkey.com/docs/api-management/api-reference/ratelimit/apply-rate-limiting
https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v2/ratelimit.limit
Check and enforce rate limits for any identifier (user ID, IP address, API client, etc.).
Use this for rate limiting beyond API keys - limit users by ID, IPs by address, or any custom identifier. Supports namespace organization, variable costs, and custom overrides.
**Response Codes**: Rate limit checks return HTTP 200 regardless of whether the limit is exceeded — check the `success` field in the response to determine if the request should be allowed. A 429 may be returned if the workspace exceeds its API rate limit. Other 4xx responses indicate auth, namespace existence/deletion, or validation errors (e.g., 410 Gone for deleted namespaces). 5xx responses indicate server errors.
**Required Permissions**
Your root key must have one of the following permissions:
- `ratelimit.*.limit` (to check limits in any namespace)
- `ratelimit..limit` (to check limits in a specific namespace)
# Delete ratelimit override
Source: https://www.unkey.com/docs/api-management/api-reference/ratelimit/delete-ratelimit-override
https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v2/ratelimit.deleteOverride
Permanently remove a rate limit override. Affected identifiers immediately revert to the namespace default.
Use this to remove temporary overrides, reset identifiers to standard limits, or clean up outdated rules.
**Important:** Deletion is immediate and permanent. The override cannot be recovered and must be recreated if needed again.
**Permissions:** Requires `ratelimit.*.delete_override` or `ratelimit..delete_override`
# Get ratelimit override
Source: https://www.unkey.com/docs/api-management/api-reference/ratelimit/get-ratelimit-override
https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v2/ratelimit.getOverride
Retrieve the configuration of a specific rate limit override by its identifier.
Use this to inspect override configurations, audit rate limiting policies, or debug rate limiting behavior.
**Important:** The identifier must match exactly as specified when creating the override, including wildcard patterns.
**Permissions:** Requires `ratelimit.*.read_override` or `ratelimit..read_override`
# List ratelimit overrides
Source: https://www.unkey.com/docs/api-management/api-reference/ratelimit/list-ratelimit-overrides
https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v2/ratelimit.listOverrides
Retrieve a paginated list of all rate limit overrides in a namespace.
Use this to audit rate limiting policies, build admin dashboards, or manage override configurations.
**Important:** Results are paginated. Use the cursor parameter to retrieve additional pages when more results are available.
**Permissions:** Requires `ratelimit.*.read_override` or `ratelimit..read_override`
# Set ratelimit override
Source: https://www.unkey.com/docs/api-management/api-reference/ratelimit/set-ratelimit-override
https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v2/ratelimit.setOverride
Create or update a custom rate limit for specific identifiers, bypassing the namespace default.
Use this to create premium tiers with higher limits, apply stricter limits to specific users, or implement emergency throttling.
**Important:** Overrides take effect immediately and completely replace the default limit for matching identifiers. Use wildcard patterns (e.g., `premium_*`) to match multiple identifiers.
**Permissions:** Requires `ratelimit.*.set_override` or `ratelimit..set_override`
# Managing key roles and permissions
Source: https://www.unkey.com/docs/api-management/authorization/managing-key-permissions
Change the roles and permissions a key holds after it exists.
Change the roles and permissions on a key at any time. Set them when you create the key, replace them with `keys.updateKey`, or add and remove them with six dedicated endpoints. Changes take about 10 seconds to reach verification. [Verifying keys](/docs/api-management/keys/verifying-keys#how-quickly-changes-take-effect) describes that timing.
You need a root key with the permissions listed on this page. Create one in the dashboard under **Settings > Root Keys**. See [Permission reference](/docs/platform/root-keys/permissions-legacy) for every permission.
All of these need `api.*.update_key` or `api..update_key` (creation needs `create_key`). Four of the incremental endpoints also need the matching `rbac.*` action: `add_permission_to_key` for `keys.addPermissions`, `remove_permission_from_key` for `keys.removePermissions`, both for `keys.setPermissions`, and `add_role_to_key` for `keys.addRoles`. `keys.removeRoles` and `keys.setRoles` need nothing beyond `update_key`.
`keys.addPermissions` and `keys.setPermissions` only create a new permission slug if the root key also has `rbac.*.create_permission`. Without it, an unknown slug fails with HTTP 403 `err:unkey:authorization:insufficient_permissions`. (`keys.createKey` and `keys.updateKey` create new slugs without that permission.) Roles are never created for you. An unknown role fails with HTTP 404 `err:unkey:data:role_not_found`.
## At creation
`keys.createKey` accepts `roles` (up to 100 names, each 1 to 128 characters) and `permissions` (up to 1000 slugs, each 1 to 128 characters matching `^[a-zA-Z0-9_:\-\.\*]+$`).
```bash theme={"system"}
curl -X POST https://api.unkey.com/v2/keys.createKey \
-H "Authorization: Bearer $UNKEY_ROOT_KEY" \
-H "Content-Type: application/json" \
-d '{
"apiId": "api_...",
"roles": ["editor"],
"permissions": ["reports.export"]
}'
```
## Replace everything
`keys.updateKey` takes the same two fields. Sending a list replaces it in full. Leaving a field out keeps it as it is.
## Adjust incrementally
Each endpoint takes `keyId` and an array of permission slugs or role names (not IDs). The add and remove endpoints need at least one entry, so `[]` fails with HTTP 400. Send `[]` to `keys.setPermissions` or `keys.setRoles` to remove everything.
| Endpoint | Effect |
| - | - |
| `keys.addPermissions` | Attaches the listed permissions; ones already attached are left alone. |
| `keys.removePermissions` | Detaches the listed permissions; ones not attached are ignored. |
| `keys.setPermissions` | Replaces the direct permissions with the list; `[]` removes all. |
| `keys.addRoles` | Assigns the listed roles. |
| `keys.removeRoles` | Unassigns the listed roles. |
| `keys.setRoles` | Replaces the roles with the list; `[]` removes all. |
```bash theme={"system"}
curl -X POST https://api.unkey.com/v2/keys.addPermissions \
-H "Authorization: Bearer $UNKEY_ROOT_KEY" \
-H "Content-Type: application/json" \
-d '{ "keyId": "key_...", "permissions": ["documents.delete"] }'
```
Every change writes audit events, such as `authorization.connect_permission_and_key` and `authorization.disconnect_role_and_key`, so you can see the history of a key's access.
## Direct permissions versus roles
Removing a permission from a key doesn't help if one of its roles also grants it. To take a permission away completely, remove it from the key and from the key's roles, or remove the role. To change what a role contains, call `permissions.setRolePermissions`. That changes every key with the role. See [Roles and permissions](/docs/api-management/authorization/roles-and-permissions).
## From the dashboard
Choose **Manage roles and permissions** from a key's actions menu, pick roles and permissions, and save. The **Create key** dialog has the same pickers in its permissions step.
# Authorization
Source: https://www.unkey.com/docs/api-management/authorization/overview
Control what each key may do with permissions checked at verification.
Verification tells you a key is real. Authorization tells you what it's allowed to do. Give each key permissions, directly or through roles. Then, when you verify the key, ask whether it has the permissions a request needs. If it doesn't, verification fails with `code: INSUFFICIENT_PERMISSIONS` and no rate limit or credits are used.
## The pieces
A **permission** is a string you define, such as `documents.read` or `billing:write`. It has a readable `name`, a `slug` that keys and queries use, and an optional description. It means whatever your API decides it means.
A **role** is a named group of permissions, such as `editor` with `documents.read` and `documents.write`. A key with the role gets all its permissions, and changing the role changes every key that has it.
A **key** can have permissions, roles, or both. Its permissions are its own plus those from its roles. Verification returns them as `data.permissions`, and the roles as `data.roles`.
A **permission query** is what you send as `permissions` on `keys.verifyKey`: one slug, or slugs combined with `AND`, `OR`, and parentheses. Verification passes only if the key's permissions satisfy it.
```bash theme={"system"}
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": "documents.write OR admin" }'
```
## Key permissions vs root key permissions
The permissions on this page belong to your users' keys, and your API decides what they allow. Root key permissions, such as `api.*.create_key`, control what your root keys can do on `api.unkey.com`. The two never mix. See [Root key permissions](/docs/platform/root-keys/permissions).
## When to check where
Send a `permissions` query when the answer depends only on the key: "can this key write documents?" Unkey answers as part of the verification. Check `data.permissions` in your own code when the answer also depends on your data: "can this key delete *this* document?" needs the document's owner, which Unkey doesn't know.
## Next steps
Create permissions and roles through the API or the dashboard, with slug rules and bounds.
The query grammar, its limits, and why an asterisk isn't a wildcard.
Attach, replace, and remove roles and permissions on a key.
Where the permission check sits among the other verification checks.
# Permission queries
Source: https://www.unkey.com/docs/api-management/authorization/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={"system"}
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={"system"}
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.*`.
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.
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={"system"}
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.
# Roles and permissions
Source: https://www.unkey.com/docs/api-management/authorization/roles-and-permissions
Define the permissions your API checks and group them into roles.
Create a permission for each action your API checks, and group them into roles for the access levels you offer. Permissions and roles belong to the workspace, so one set covers every keyspace. To attach them to keys, see [Managing key roles and permissions](/docs/api-management/authorization/managing-key-permissions).
You need a root key with the permissions listed on this page. Create one in the dashboard under **Settings > Root Keys**. See [Permission reference](/docs/platform/root-keys/permissions-legacy) for every permission.
The endpoints need the matching `rbac.*` permission: `create_permission`, `read_permission`, `delete_permission`, `create_role`, `read_role`, `delete_role`, and `add_permission_to_role` together with `remove_permission_from_role` for `permissions.setRolePermissions`.
## Permissions
1 to 512 characters. A readable label shown in the dashboard.
1 to 128 characters matching `^[a-zA-Z0-9_:\-\.\*]+$`, unique within the workspace. This is the string keys hold and queries check. `documents.read` and `billing:write` are both fine. An asterisk is allowed but is a literal character, not a wildcard. See [Permission queries](/docs/api-management/authorization/permission-queries).
Up to 512 characters of internal documentation.
```bash theme={"system"}
curl -X POST https://api.unkey.com/v2/permissions.createPermission \
-H "Authorization: Bearer $UNKEY_ROOT_KEY" \
-H "Content-Type: application/json" \
-d '{ "name": "Read documents", "slug": "documents.read", "description": "List and fetch documents" }'
```
The response has `permissionId` (`perm_...`). A duplicate slug fails with HTTP 409 [`err:unkey:data:permission_already_exists`](/docs/errors/unkey/data/permission_already_exists).
* `permissions.getPermission` takes the ID or slug.
* `permissions.listPermissions` takes `limit` (1 to 100, default 100), `cursor` (up to 1024 characters), and `search` (up to 256 characters, matched against ID, name, slug, or description, ignoring case).
* `permissions.updatePermission` takes the ID or slug in `permission`, plus any of `name`, `slug`, and `description`. Omitted fields stay unchanged, and `description: null` removes the description. It needs `unkey:v1::projects//rbac/permissions/#write`. A slug that another permission already uses fails with HTTP 409. Keys and roles keep the permission after a slug change. Key verification can return the old slug for a short time while caches refresh.
* `permissions.deletePermission` deletes the permission and removes it from every role and key.
## Roles
1 to 128 characters, unique within the workspace. Keys refer to roles by name. Spaces are allowed.
Up to 512 characters.
Permission slugs to attach. Slugs that don't exist yet are created if the root key also has `rbac.*.create_permission`. Without it, an unknown slug fails with HTTP 403 `err:unkey:authorization:insufficient_permissions`. Leave it out or send `[]` for an empty role.
```bash theme={"system"}
curl -X POST https://api.unkey.com/v2/permissions.createRole \
-H "Authorization: Bearer $UNKEY_ROOT_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "editor",
"description": "Read and write documents",
"permissions": ["documents.read", "documents.write"]
}'
```
The response has `roleId` (`role_...`). A duplicate name fails with HTTP 409 [`err:unkey:data:role_already_exists`](/docs/errors/unkey/data/role_already_exists). `permissions.getRole` and `permissions.listRoles` return roles with their permissions. `permissions.deleteRole` deletes the role, and keys lose its permissions unless they get them another way.
`permissions.updateRole` takes the ID or name in `role`, plus any of `name` and `description`. Omitted fields stay unchanged, and `description: null` removes the description. It needs `unkey:v1::projects//rbac/roles/#write`. A name that another role already uses fails with HTTP 409. Keys keep the role after a rename, but requests that name the role, such as `keys.addRoles`, must use the new name. Key verification can return the old name for a short time while caches refresh.
### Change a role's permissions
`permissions.setRolePermissions` takes `role` (ID or name) and the full list of slugs. Anything not in the list is removed, and `[]` empties the role. (The older `roleId` field still works but is deprecated. Send one or the other.) New slugs are created under the same rule as `createRole`. Every key with the role picks up the change within about 10 seconds, and a few verifications just after that can still see the old permissions. [Verifying keys](/docs/api-management/keys/verifying-keys#how-quickly-changes-take-effect) describes the timing.
```bash theme={"system"}
curl -X POST https://api.unkey.com/v2/permissions.setRolePermissions \
-H "Authorization: Bearer $UNKEY_ROOT_KEY" \
-H "Content-Type: application/json" \
-d '{ "role": "editor", "permissions": ["documents.read", "documents.write", "documents.delete"] }'
```
## Designing the set
* **Name permissions after actions**, not customers: `invoices.read`, `invoices.write`, `invoices.void`.
* **Name roles the way you talk to customers**: `viewer`, `editor`, `admin`, or plan names.
* **Attach roles to keys**, and save direct permissions for one-off grants. Then changing what "editor" means is one `setRolePermissions` call, not an update to every key.
* **Use a role for "everything under documents".** An asterisk isn't a wildcard, so list each `documents.*` permission in the role.
## From the dashboard
Open **Authorization** in the sidebar. On the **Permissions** tab, create and edit permissions. On the **Roles** tab, create and edit roles, pick their permissions (or create new ones), and assign the role to keys.
# unkey api analytics get-ratelimits
Source: https://www.unkey.com/docs/api-management/cli/analytics/get-ratelimits
Run a SQL query against your rate limit analytics.
You need a root key with the permissions listed on this page. Create one in the dashboard under **Settings > Root Keys**. See [Permission reference](/docs/platform/root-keys/permissions-legacy) for every permission.
Query standalone rate limit data with SQL. Use the `ratelimits_v1` table, or the rolled-up `ratelimits_per_minute_v1`, `ratelimits_per_hour_v1`, `ratelimits_per_day_v1`, and `ratelimits_per_month_v1`. The query must be one `SELECT` with a `FROM`, using only allowed functions and documented columns. CTEs, subqueries, `UNION`, and `EXCEPT` work. The rules and their error codes are on [`err:user:bad_request:invalid_analytics_query`](/docs/errors/user/bad_request/invalid_analytics_query). Responses over 4 MiB fail with [`err:user:unprocessable_entity:query_memory_limit_exceeded`](/docs/errors/user/unprocessable_entity/query_memory_limit_exceeded). Query time and result rows are limited too. Calls `POST /v2/analytics.getRatelimits`. See [ratelimits tables](/docs/api-management/analytics/tables/ratelimits), [Analytics restrictions and quotas](/docs/api-management/analytics/restrictions-and-quotas), and [Analytics troubleshooting](/docs/api-management/analytics/troubleshooting).
## Usage
```bash theme={"system"}
unkey api analytics get-ratelimits --query= [flags]
```
## Flags
The SQL `SELECT` to run.
### Shared flags
Every `unkey api` command takes these. See [CLI output and shared flags](/docs/platform/cli/output-and-flags).
Root key used for the request. Falls back to `UNKEY_ROOT_KEY`, then to the key stored by `unkey auth login`.
Base URL of the API. Falls back to `UNKEY_API_BASE_URL`. You don't normally need to set it.
Path of the config file written by `unkey auth login`. Falls back to `UNKEY_CONFIG`.
Output format. Falls back to `UNKEY_OUTPUT`. `json` prints the full response. Any other value prints the request ID and `data`.
Send this JSON as the whole request body instead of using the command's flags. You can't combine it with them.
## Required permissions
`ratelimit.*.read_analytics`, or `ratelimit..read_analytics` for one or more namespaces, which limits results to those namespaces. See [Root key permissions](/docs/platform/root-keys/permissions).
## Examples
```bash Passed versus rejected per namespace theme={"system"}
unkey api analytics get-ratelimits --query="SELECT namespace_id, countIf(passed) AS passed, countIf(NOT passed) AS rejected FROM ratelimits_v1 WHERE time > now() - toIntervalHour(1) GROUP BY namespace_id"
```
Or send the whole request as JSON:
```bash Raw body theme={"system"}
unkey api analytics get-ratelimits --body='{"query":"SELECT count() FROM ratelimits_v1 WHERE time > now() - toIntervalHour(1)"}'
```
# unkey api analytics get-verifications
Source: https://www.unkey.com/docs/api-management/cli/analytics/get-verifications
Run a SQL query against your key verification analytics.
You need a root key with the permissions listed on this page. Create one in the dashboard under **Settings > Root Keys**. See [Permission reference](/docs/platform/root-keys/permissions-legacy) for every permission.
Query key verification data with SQL. Use the `key_verifications_v1` table, or the rolled-up `key_verifications_per_minute_v1`, `key_verifications_per_hour_v1`, `key_verifications_per_day_v1`, and `key_verifications_per_month_v1`. The query must be one `SELECT` with a `FROM`, using only allowed functions and documented columns. The rules and their error codes are on [`err:user:bad_request:invalid_analytics_query`](/docs/errors/user/bad_request/invalid_analytics_query). Responses over 4 MiB fail with [`err:user:unprocessable_entity:query_memory_limit_exceeded`](/docs/errors/user/unprocessable_entity/query_memory_limit_exceeded). Query time and result rows are limited too. Calls `POST /v2/analytics.getVerifications`. See [key\_verifications tables](/docs/api-management/analytics/tables/key-verifications), [Analytics restrictions and quotas](/docs/api-management/analytics/restrictions-and-quotas), and [Analytics troubleshooting](/docs/api-management/analytics/troubleshooting).
## Usage
```bash theme={"system"}
unkey api analytics get-verifications --query= [flags]
```
## Flags
The SQL `SELECT` to run.
### Shared flags
Every `unkey api` command takes these. See [CLI output and shared flags](/docs/platform/cli/output-and-flags).
Root key used for the request. Falls back to `UNKEY_ROOT_KEY`, then to the key stored by `unkey auth login`.
Base URL of the API. Falls back to `UNKEY_API_BASE_URL`. You don't normally need to set it.
Path of the config file written by `unkey auth login`. Falls back to `UNKEY_CONFIG`.
Output format. Falls back to `UNKEY_OUTPUT`. `json` prints the full response. Any other value prints the request ID and `data`.
Send this JSON as the whole request body instead of using the command's flags. You can't combine it with them.
## Required permissions
`api.*.read_analytics`, or `api..read_analytics` for one or more APIs, which limits results to those keyspaces. See [Root key permissions](/docs/platform/root-keys/permissions).
## Examples
```bash Outcomes in the last day theme={"system"}
unkey api analytics get-verifications --query="SELECT outcome, count() AS n FROM key_verifications_v1 WHERE time > now() - toIntervalDay(1) GROUP BY outcome"
```
Or send the whole request as JSON:
```bash Raw body theme={"system"}
unkey api analytics get-verifications --body='{"query":"SELECT outcome, count() AS n FROM key_verifications_v1 WHERE time > now() - toIntervalDay(1) GROUP BY outcome"}'
```
# unkey api apis create-api
Source: https://www.unkey.com/docs/api-management/cli/apis/create-api
Create a keyspace to hold your API keys.
You need a root key with the permissions listed on this page. Create one in the dashboard under **Settings > Root Keys**. See [Permission reference](/docs/platform/root-keys/permissions-legacy) for every permission.
Create a keyspace (still called an API in the CLI). Every key belongs to one keyspace. Make one per service or environment you want to keep separate. Calls `POST /v2/apis.createApi`. See [Keyspaces](/docs/api-management/keyspaces/overview).
## Usage
```bash theme={"system"}
unkey api apis create-api --name= [flags]
```
## Flags
Name of the new API, 3 to 256 characters. The name is just a label. Other commands take the ID from the response.
### Shared flags
Every `unkey api` command takes these. See [CLI output and shared flags](/docs/platform/cli/output-and-flags).
Root key used for the request. Falls back to `UNKEY_ROOT_KEY`, then to the key stored by `unkey auth login`.
Base URL of the API. Falls back to `UNKEY_API_BASE_URL`. You don't normally need to set it.
Path of the config file written by `unkey auth login`. Falls back to `UNKEY_CONFIG`.
Output format. Falls back to `UNKEY_OUTPUT`. `json` prints the full response. Any other value prints the request ID and `data`.
Send this JSON as the whole request body instead of using the command's flags. You can't combine it with them.
## Required permissions
`api.*.create_api`. See [Root key permissions](/docs/platform/root-keys/permissions).
## Examples
```bash Create an API theme={"system"}
unkey api apis create-api --name=payment-service-prod
```
Or send the whole request as JSON:
```bash Raw body theme={"system"}
unkey api apis create-api --body='{"name":"payment-service-prod"}'
```
# unkey api apis delete-api
Source: https://www.unkey.com/docs/api-management/cli/apis/delete-api
Delete a keyspace and invalidate every key in it.
You need a root key with the permissions listed on this page. Create one in the dashboard under **Settings > Root Keys**. See [Permission reference](/docs/platform/root-keys/permissions-legacy) for every permission.
Delete a keyspace. Every key in it stops verifying within about 10 seconds, so treat this as irreversible. If delete protection is on, the call fails with [`err:unkey:application:protected_resource`](/docs/errors/unkey/application/protected_resource). You can only turn delete protection off in the dashboard. Calls `POST /v2/apis.deleteApi`. See [Keyspaces](/docs/api-management/keyspaces/overview).
## Usage
```bash theme={"system"}
unkey api apis delete-api --api-id= [flags]
```
## Flags
Id of the API to delete.
### Shared flags
Every `unkey api` command takes these. See [CLI output and shared flags](/docs/platform/cli/output-and-flags).
Root key used for the request. Falls back to `UNKEY_ROOT_KEY`, then to the key stored by `unkey auth login`.
Base URL of the API. Falls back to `UNKEY_API_BASE_URL`. You don't normally need to set it.
Path of the config file written by `unkey auth login`. Falls back to `UNKEY_CONFIG`.
Output format. Falls back to `UNKEY_OUTPUT`. `json` prints the full response. Any other value prints the request ID and `data`.
Send this JSON as the whole request body instead of using the command's flags. You can't combine it with them.
## Required permissions
`api.*.delete_api` or `api..delete_api`. See [Root key permissions](/docs/platform/root-keys/permissions).
## Examples
```bash Delete an API theme={"system"}
unkey api apis delete-api --api-id=api_1234abcd
```
Or send the whole request as JSON:
```bash Raw body theme={"system"}
unkey api apis delete-api --body='{"apiId":"api_1234abcd"}'
```
# unkey api apis get-api
Source: https://www.unkey.com/docs/api-management/cli/apis/get-api
Look up a keyspace by its ID.
You need a root key with the permissions listed on this page. Create one in the dashboard under **Settings > Root Keys**. See [Permission reference](/docs/platform/root-keys/permissions-legacy) for every permission.
Look up a keyspace by ID. The response has its ID and name. Key counts and settings are in the dashboard. Calls `POST /v2/apis.getApi`. See [Keyspaces](/docs/api-management/keyspaces/overview).
## Usage
```bash theme={"system"}
unkey api apis get-api --api-id= [flags]
```
## Flags
Id of the API to read, starting with `api_`.
### Shared flags
Every `unkey api` command takes these. See [CLI output and shared flags](/docs/platform/cli/output-and-flags).
Root key used for the request. Falls back to `UNKEY_ROOT_KEY`, then to the key stored by `unkey auth login`.
Base URL of the API. Falls back to `UNKEY_API_BASE_URL`. You don't normally need to set it.
Path of the config file written by `unkey auth login`. Falls back to `UNKEY_CONFIG`.
Output format. Falls back to `UNKEY_OUTPUT`. `json` prints the full response. Any other value prints the request ID and `data`.
Send this JSON as the whole request body instead of using the command's flags. You can't combine it with them.
## Required permissions
`api.*.read_api` or `api..read_api`. Without the permission you get a 404, not a 403, so the response doesn't reveal whether the API exists. See [Root key permissions](/docs/platform/root-keys/permissions).
## Examples
```bash Read an API theme={"system"}
unkey api apis get-api --api-id=api_1234abcd
```
Or send the whole request as JSON:
```bash Raw body theme={"system"}
unkey api apis get-api --body='{"apiId":"api_1234abcd"}'
```
# unkey api apis list-keys
Source: https://www.unkey.com/docs/api-management/cli/apis/list-keys
List the keys in a keyspace one page at a time.
You need a root key with the permissions listed on this page. Create one in the dashboard under **Settings > Root Keys**. See [Permission reference](/docs/platform/root-keys/permissions-legacy) for every permission.
List the keys of an API. Results come in pages. Pass the `cursor` from one response to get the next page. Calls `POST /v2/apis.listKeys`. See [Listing keys](/docs/api-management/keyspaces/listing-keys).
## Usage
```bash theme={"system"}
unkey api apis list-keys --api-id= [flags]
```
## Flags
Id of the API whose keys to list.
Cursor from the previous response's `pagination.cursor`.
Include the plaintext key in each result. Only keys created with `--recoverable` have one, and the root key needs `decrypt_key`. The call fails with `412` unless the keyspace has key encryption turned on, which only we can do, through a support request.
Return only keys linked to the identity with this external id. An external id that matches no identity returns an empty page, not an error.
Maximum number of keys per page. 1 to 100. Defaults to 100.
When true, reads keys and their identities from the primary database to avoid replica lag. When false or omitted, reads from a replica, which may not reflect recent changes. Primary reads can have higher latency, depending on your region.
### Shared flags
Every `unkey api` command takes these. See [CLI output and shared flags](/docs/platform/cli/output-and-flags).
Root key used for the request. Falls back to `UNKEY_ROOT_KEY`, then to the key stored by `unkey auth login`.
Base URL of the API. Falls back to `UNKEY_API_BASE_URL`. You don't normally need to set it.
Path of the config file written by `unkey auth login`. Falls back to `UNKEY_CONFIG`.
Output format. Falls back to `UNKEY_OUTPUT`. `json` prints the full response. Any other value prints the request ID and `data`.
Send this JSON as the whole request body instead of using the command's flags. You can't combine it with them.
## Required permissions
`api.*.read_key` or `api..read_key`, together with `api.*.read_api` or `api..read_api`. `--decrypt` also needs `api.*.decrypt_key` or `api..decrypt_key`. Without the permission you get a 404, not a 403, so the response doesn't reveal whether the API exists. See [Root key permissions](/docs/platform/root-keys/permissions).
## Examples
```bash First page theme={"system"}
unkey api apis list-keys --api-id=api_1234abcd --limit=50
```
```bash Keys of one identity theme={"system"}
unkey api apis list-keys --api-id=api_1234abcd --external-id=user_1234abcd
```
Or send the whole request as JSON:
```bash Raw body theme={"system"}
unkey api apis list-keys --body='{"apiId":"api_1234abcd","limit":50,"externalId":"user_1234abcd"}'
```
# unkey api identities create-identity
Source: https://www.unkey.com/docs/api-management/cli/identities/create-identity
Create an identity that groups keys under one user or tenant.
You need a root key with the permissions listed on this page. Create one in the dashboard under **Settings > Root Keys**. See [Permission reference](/docs/platform/root-keys/permissions-legacy) for every permission.
Create an identity. Keys linked to it by `externalId` share its metadata and rate limits, so a user with several keys is rate limited as one. Calls `POST /v2/identities.createIdentity`. See [Identities](/docs/api-management/identities/overview).
## Usage
```bash theme={"system"}
unkey api identities create-identity --external-id= [flags]
```
## Flags
Your identifier for the user, organization, or tenant. Unique per workspace. Reusing one returns 409 [`err:unkey:data:identity_already_exists`](/docs/errors/unkey/data/identity_already_exists).
JSON object stored on the identity and returned when any of its keys is verified.
JSON array of rate limits shared by every key of the identity, each with `name`, `limit`, `duration` in milliseconds, and `autoApply`.
### Shared flags
Every `unkey api` command takes these. See [CLI output and shared flags](/docs/platform/cli/output-and-flags).
Root key used for the request. Falls back to `UNKEY_ROOT_KEY`, then to the key stored by `unkey auth login`.
Base URL of the API. Falls back to `UNKEY_API_BASE_URL`. You don't normally need to set it.
Path of the config file written by `unkey auth login`. Falls back to `UNKEY_CONFIG`.
Output format. Falls back to `UNKEY_OUTPUT`. `json` prints the full response. Any other value prints the request ID and `data`.
Send this JSON as the whole request body instead of using the command's flags. You can't combine it with them.
## Required permissions
`identity.*.create_identity`. See [Root key permissions](/docs/platform/root-keys/permissions).
## Examples
```bash Create theme={"system"}
unkey api identities create-identity --external-id=user_123
```
```bash Create with a shared rate limit theme={"system"}
unkey api identities create-identity --external-id=user_123 --ratelimits='[{"name":"requests","limit":1000,"duration":3600000,"autoApply":true}]'
```
Or send the whole request as JSON:
```bash Raw body theme={"system"}
unkey api identities create-identity --body='{"externalId":"user_123","meta":{"plan":"pro"}}'
```
# unkey api identities delete-identity
Source: https://www.unkey.com/docs/api-management/cli/identities/delete-identity
Delete an identity and the rate limits it shares with its keys.
You need a root key with the permissions listed on this page. Create one in the dashboard under **Settings > Root Keys**. See [Permission reference](/docs/platform/root-keys/permissions-legacy) for every permission.
Delete an identity. Its keys aren't deleted, but they lose the identity's metadata and shared rate limits. You can reuse the external id right away. Calls `POST /v2/identities.deleteIdentity`. See [Identities](/docs/api-management/identities/overview).
## Usage
```bash theme={"system"}
unkey api identities delete-identity --identity= [flags]
```
## Flags
Identity id or external id.
### Shared flags
Every `unkey api` command takes these. See [CLI output and shared flags](/docs/platform/cli/output-and-flags).
Root key used for the request. Falls back to `UNKEY_ROOT_KEY`, then to the key stored by `unkey auth login`.
Base URL of the API. Falls back to `UNKEY_API_BASE_URL`. You don't normally need to set it.
Path of the config file written by `unkey auth login`. Falls back to `UNKEY_CONFIG`.
Output format. Falls back to `UNKEY_OUTPUT`. `json` prints the full response. Any other value prints the request ID and `data`.
Send this JSON as the whole request body instead of using the command's flags. You can't combine it with them.
## Required permissions
`identity.*.delete_identity`. Without the permission you get a 404, not a 403, so the response doesn't reveal whether the identity exists. See [Root key permissions](/docs/platform/root-keys/permissions).
## Examples
```bash Delete theme={"system"}
unkey api identities delete-identity --identity=user_123
```
Or send the whole request as JSON:
```bash Raw body theme={"system"}
unkey api identities delete-identity --body='{"identity":"user_123"}'
```
# unkey api identities get-identity
Source: https://www.unkey.com/docs/api-management/cli/identities/get-identity
Look up an identity with its metadata and rate limits.
You need a root key with the permissions listed on this page. Create one in the dashboard under **Settings > Root Keys**. See [Permission reference](/docs/platform/root-keys/permissions-legacy) for every permission.
Read an identity by id or external id. Calls `POST /v2/identities.getIdentity`. See [Identities](/docs/api-management/identities/overview).
## Usage
```bash theme={"system"}
unkey api identities get-identity --identity= [flags]
```
## Flags
Identity id (`id_...`) or external id.
### Shared flags
Every `unkey api` command takes these. See [CLI output and shared flags](/docs/platform/cli/output-and-flags).
Root key used for the request. Falls back to `UNKEY_ROOT_KEY`, then to the key stored by `unkey auth login`.
Base URL of the API. Falls back to `UNKEY_API_BASE_URL`. You don't normally need to set it.
Path of the config file written by `unkey auth login`. Falls back to `UNKEY_CONFIG`.
Output format. Falls back to `UNKEY_OUTPUT`. `json` prints the full response. Any other value prints the request ID and `data`.
Send this JSON as the whole request body instead of using the command's flags. You can't combine it with them.
## Required permissions
`identity.*.read_identity` or `identity..read_identity`. The second form takes the `id_...` ID, not the external id. See [Root key permissions](/docs/platform/root-keys/permissions).
## Examples
```bash Read theme={"system"}
unkey api identities get-identity --identity=user_123
```
Or send the whole request as JSON:
```bash Raw body theme={"system"}
unkey api identities get-identity --body='{"identity":"user_123"}'
```
# unkey api identities list-identities
Source: https://www.unkey.com/docs/api-management/cli/identities/list-identities
List the identities in your workspace one page at a time.
You need a root key with the permissions listed on this page. Create one in the dashboard under **Settings > Root Keys**. See [Permission reference](/docs/platform/root-keys/permissions-legacy) for every permission.
List identities. Results come in pages. Pass the `cursor` from one response to get the next page. Calls `POST /v2/identities.listIdentities`. See [Identities](/docs/api-management/identities/overview).
## Usage
```bash theme={"system"}
unkey api identities list-identities [flags]
```
## Flags
Cursor from the previous response.
Maximum number of identities per page. 1 to 100. Defaults to 100.
Return only identities whose id or external id contains this text. Not case-sensitive. Up to 256 characters.
### Shared flags
Every `unkey api` command takes these. See [CLI output and shared flags](/docs/platform/cli/output-and-flags).
Root key used for the request. Falls back to `UNKEY_ROOT_KEY`, then to the key stored by `unkey auth login`.
Base URL of the API. Falls back to `UNKEY_API_BASE_URL`. You don't normally need to set it.
Path of the config file written by `unkey auth login`. Falls back to `UNKEY_CONFIG`.
Output format. Falls back to `UNKEY_OUTPUT`. `json` prints the full response. Any other value prints the request ID and `data`.
Send this JSON as the whole request body instead of using the command's flags. You can't combine it with them.
## Required permissions
`identity.*.read_identity`. A root key with only `identity..read_identity` fails as soon as the page includes any other identity. See [Root key permissions](/docs/platform/root-keys/permissions).
## Examples
```bash First page theme={"system"}
unkey api identities list-identities --limit=50
```
```bash Search theme={"system"}
unkey api identities list-identities --search=user_123
```
Or send the whole request as JSON:
```bash Raw body theme={"system"}
unkey api identities list-identities --body='{"limit":50,"search":"user_"}'
```
# unkey api identities update-identity
Source: https://www.unkey.com/docs/api-management/cli/identities/update-identity
Replace the metadata or rate limits of an identity.
You need a root key with the permissions listed on this page. Create one in the dashboard under **Settings > Root Keys**. See [Permission reference](/docs/platform/root-keys/permissions-legacy) for every permission.
Update an identity. `--meta` and `--ratelimits` each replace the whole existing value, so pass everything you want to keep. Changes can take up to about 10 seconds to reach verifications. Calls `POST /v2/identities.updateIdentity`. See [Identities](/docs/api-management/identities/overview).
## Usage
```bash theme={"system"}
unkey api identities update-identity --identity= [flags]
```
## Flags
Identity id or external id.
JSON object that replaces the identity's metadata.
JSON array that replaces the identity's rate limits.
### Shared flags
Every `unkey api` command takes these. See [CLI output and shared flags](/docs/platform/cli/output-and-flags).
Root key used for the request. Falls back to `UNKEY_ROOT_KEY`, then to the key stored by `unkey auth login`.
Base URL of the API. Falls back to `UNKEY_API_BASE_URL`. You don't normally need to set it.
Path of the config file written by `unkey auth login`. Falls back to `UNKEY_CONFIG`.
Output format. Falls back to `UNKEY_OUTPUT`. `json` prints the full response. Any other value prints the request ID and `data`.
Send this JSON as the whole request body instead of using the command's flags. You can't combine it with them.
## Required permissions
`identity.*.update_identity`. Without the permission you get a 404, not a 403, so the response doesn't reveal whether the identity exists. See [Root key permissions](/docs/platform/root-keys/permissions).
## Examples
```bash Replace metadata theme={"system"}
unkey api identities update-identity --identity=user_123 --meta='{"plan":"enterprise"}'
```
Or send the whole request as JSON:
```bash Raw body theme={"system"}
unkey api identities update-identity --body='{"identity":"user_123","ratelimits":[{"name":"requests","limit":5000,"duration":3600000,"autoApply":true}]}'
```
# unkey api keys add-permissions
Source: https://www.unkey.com/docs/api-management/cli/keys/add-permissions
Grant extra permissions to a key without removing the ones it has.
You need a root key with the permissions listed on this page. Create one in the dashboard under **Settings > Root Keys**. See [Permission reference](/docs/platform/root-keys/permissions-legacy) for every permission.
Add direct permissions to a key. Existing permissions and roles are kept. A slug that doesn't exist yet is created, which needs an extra root key permission. Changes can take up to about 10 seconds to apply. Calls `POST /v2/keys.addPermissions`. See [Managing key permissions](/docs/api-management/authorization/managing-key-permissions).
## Usage
```bash theme={"system"}
unkey api keys add-permissions --key-id= --permissions= [flags]
```
## Flags
Id of the key.
Comma-separated permission slugs to add.
### Shared flags
Every `unkey api` command takes these. See [CLI output and shared flags](/docs/platform/cli/output-and-flags).
Root key used for the request. Falls back to `UNKEY_ROOT_KEY`, then to the key stored by `unkey auth login`.
Base URL of the API. Falls back to `UNKEY_API_BASE_URL`. You don't normally need to set it.
Path of the config file written by `unkey auth login`. Falls back to `UNKEY_CONFIG`.
Output format. Falls back to `UNKEY_OUTPUT`. `json` prints the full response. Any other value prints the request ID and `data`.
Send this JSON as the whole request body instead of using the command's flags. You can't combine it with them.
## Required permissions
`api.*.update_key` or `api..update_key` together with `rbac.*.add_permission_to_key`. Creating a new slug also needs `rbac.*.create_permission`. See [Root key permissions](/docs/platform/root-keys/permissions).
## Examples
```bash Add two permissions theme={"system"}
unkey api keys add-permissions --key-id=key_1234abcd --permissions=documents.read,documents.write
```
Or send the whole request as JSON:
```bash Raw body theme={"system"}
unkey api keys add-permissions --body='{"keyId":"key_1234abcd","permissions":["documents.read","documents.write"]}'
```
# unkey api keys add-roles
Source: https://www.unkey.com/docs/api-management/cli/keys/add-roles
Assign extra roles to a key without removing the ones it has.
You need a root key with the permissions listed on this page. Create one in the dashboard under **Settings > Root Keys**. See [Permission reference](/docs/platform/root-keys/permissions-legacy) for every permission.
Add roles to a key. Existing roles and direct permissions are kept. Each role must already exist. Changes can take up to about 10 seconds to apply. Calls `POST /v2/keys.addRoles`. See [Managing key permissions](/docs/api-management/authorization/managing-key-permissions).
## Usage
```bash theme={"system"}
unkey api keys add-roles --key-id= --roles= [flags]
```
## Flags
Id of the key.
Comma-separated role names to add.
### Shared flags
Every `unkey api` command takes these. See [CLI output and shared flags](/docs/platform/cli/output-and-flags).
Root key used for the request. Falls back to `UNKEY_ROOT_KEY`, then to the key stored by `unkey auth login`.
Base URL of the API. Falls back to `UNKEY_API_BASE_URL`. You don't normally need to set it.
Path of the config file written by `unkey auth login`. Falls back to `UNKEY_CONFIG`.
Output format. Falls back to `UNKEY_OUTPUT`. `json` prints the full response. Any other value prints the request ID and `data`.
Send this JSON as the whole request body instead of using the command's flags. You can't combine it with them.
## Required permissions
`api.*.update_key` or `api..update_key` together with `rbac.*.add_role_to_key`. See [Root key permissions](/docs/platform/root-keys/permissions).
## Examples
```bash Add roles theme={"system"}
unkey api keys add-roles --key-id=key_1234abcd --roles=api_admin,billing_reader
```
Or send the whole request as JSON:
```bash Raw body theme={"system"}
unkey api keys add-roles --body='{"keyId":"key_1234abcd","roles":["api_admin","billing_reader"]}'
```
# unkey api keys create-key
Source: https://www.unkey.com/docs/api-management/cli/keys/create-key
Create an API key in a keyspace from the command line.
You need a root key with the permissions listed on this page. Create one in the dashboard under **Settings > Root Keys**. See [Permission reference](/docs/platform/root-keys/permissions-legacy) for every permission.
Create a key. The plaintext comes back once in this response. You can't fetch it again unless you pass `--recoverable`. Calls `POST /v2/keys.createKey`. See [Creating keys](/docs/api-management/keys/creating-keys).
## Usage
```bash theme={"system"}
unkey api keys create-key --api-id= [flags]
```
## Flags
Id of the API (keyspace) the key belongs to.
Length of the random part of the key in bytes. The API accepts 16 to 255. If you leave the flag off, it uses the keyspace's default byte length, or 16 if the keyspace doesn't set one.
JSON object with `remaining` and an optional `refill` of `interval` (`daily` or `monthly`), `amount`, and `refillDay`. `refillDay` is a day of the month from 1 to 31. It's required for `monthly`, and a monthly refill without it fails with `400`. It's ignored for `daily`.
Whether the key can be used. Create it disabled to turn it on later.
Unix timestamp in milliseconds after which the key stops verifying.
Your identifier for the user or tenant, up to 255 characters. Links the key to the identity with this external id, and creates the identity if needed.
JSON object stored on the key and returned by every verification. At most 100 top-level properties.
Name shown in the dashboard, up to 255 characters.
Comma-separated permission slugs to grant directly, at most 1000 of them. A slug that doesn't exist yet is created.
Prefix added to the start of the key so users can tell keys apart, for example `prod`. Up to 16 characters of letters, digits, and underscores. If you leave it off, the keyspace's default prefix is used, if it has one.
JSON array of rate limits, each with `name`, `limit`, `duration` in milliseconds, and `autoApply`.
Store the plaintext encrypted so `get-key --decrypt` can return it later. Needs `encrypt_key`. Fails with `412` unless the keyspace has key encryption turned on, which only we can do, through a support request.
Comma-separated role names to assign, at most 100 of them. Unlike permissions, each role must already exist. An unknown name fails the whole call.
### Shared flags
Every `unkey api` command takes these. See [CLI output and shared flags](/docs/platform/cli/output-and-flags).
Root key used for the request. Falls back to `UNKEY_ROOT_KEY`, then to the key stored by `unkey auth login`.
Base URL of the API. Falls back to `UNKEY_API_BASE_URL`. You don't normally need to set it.
Path of the config file written by `unkey auth login`. Falls back to `UNKEY_CONFIG`.
Output format. Falls back to `UNKEY_OUTPUT`. `json` prints the full response. Any other value prints the request ID and `data`.
Send this JSON as the whole request body instead of using the command's flags. You can't combine it with them.
## Required permissions
`api.*.create_key` or `api..create_key`. `--recoverable` also needs `api.*.encrypt_key` or `api..encrypt_key`. Without the permission you get a 404, not a 403, so the response doesn't reveal whether the API exists. See [Root key permissions](/docs/platform/root-keys/permissions).
## Examples
```bash Plain key theme={"system"}
unkey api keys create-key --api-id=api_1234abcd --prefix=prod --name='Payment Service Key'
```
```bash Key for a user with roles theme={"system"}
unkey api keys create-key --api-id=api_1234abcd --external-id=user_1234abcd --roles=api_admin,billing_reader
```
```bash Key with credits and a rate limit theme={"system"}
unkey api keys create-key --api-id=api_1234abcd \
--credits='{"remaining":1000,"refill":{"interval":"monthly","amount":1000,"refillDay":1}}' \
--ratelimits='[{"name":"requests","limit":100,"duration":60000,"autoApply":true}]'
```
Or send the whole request as JSON:
```bash Raw body theme={"system"}
unkey api keys create-key --body='{"apiId":"api_1234abcd","prefix":"prod","name":"Payment Service Key","externalId":"user_1234abcd","meta":{"plan":"pro"}}'
```
# unkey api keys delete-key
Source: https://www.unkey.com/docs/api-management/cli/keys/delete-key
Delete a key, softly by default or permanently.
You need a root key with the permissions listed on this page. Create one in the dashboard under **Settings > Root Keys**. See [Permission reference](/docs/platform/root-keys/permissions-legacy) for every permission.
Delete a key. By default we keep a record of the deleted key for audit logs and analytics, but you can't restore it. `--permanent` erases the key completely, including its metadata. The key can keep verifying for up to about 10 seconds. Calls `POST /v2/keys.deleteKey`. See [Enabling, disabling, and deleting keys](/docs/api-management/keys/enable-disable-delete).
## Usage
```bash theme={"system"}
unkey api keys delete-key --key-id= [flags]
```
## Flags
Id of the key to delete.
Erase the key completely instead of keeping a record. Use it for compliance deletions or to reuse a key string. Analytics rows for the key lose their link to it.
### Shared flags
Every `unkey api` command takes these. See [CLI output and shared flags](/docs/platform/cli/output-and-flags).
Root key used for the request. Falls back to `UNKEY_ROOT_KEY`, then to the key stored by `unkey auth login`.
Base URL of the API. Falls back to `UNKEY_API_BASE_URL`. You don't normally need to set it.
Path of the config file written by `unkey auth login`. Falls back to `UNKEY_CONFIG`.
Output format. Falls back to `UNKEY_OUTPUT`. `json` prints the full response. Any other value prints the request ID and `data`.
Send this JSON as the whole request body instead of using the command's flags. You can't combine it with them.
## Required permissions
`api.*.delete_key` or `api..delete_key`. See [Root key permissions](/docs/platform/root-keys/permissions).
## Examples
```bash Soft delete theme={"system"}
unkey api keys delete-key --key-id=key_1234abcd
```
```bash Permanent delete theme={"system"}
unkey api keys delete-key --key-id=key_1234abcd --permanent
```
Or send the whole request as JSON:
```bash Raw body theme={"system"}
unkey api keys delete-key --body='{"keyId":"key_1234abcd","permanent":true}'
```
# unkey api keys get-key
Source: https://www.unkey.com/docs/api-management/cli/keys/get-key
Look up a key's configuration by its ID.
You need a root key with the permissions listed on this page. Create one in the dashboard under **Settings > Root Keys**. See [Permission reference](/docs/platform/root-keys/permissions-legacy) for every permission.
Read a key by id: its name, identity, permissions, roles, credits, rate limits, and expiry. Calls `POST /v2/keys.getKey`. See [Looking up keys](/docs/api-management/keys/looking-up-keys).
## Usage
```bash theme={"system"}
unkey api keys get-key --key-id= [flags]
```
## Flags
Id of the key, starting with `key_`.
Include the plaintext key. Works only for keys created with `--recoverable` and needs `decrypt_key`. Fails with `412` unless the keyspace has key encryption turned on, which only we can do, through a support request.
### Shared flags
Every `unkey api` command takes these. See [CLI output and shared flags](/docs/platform/cli/output-and-flags).
Root key used for the request. Falls back to `UNKEY_ROOT_KEY`, then to the key stored by `unkey auth login`.
Base URL of the API. Falls back to `UNKEY_API_BASE_URL`. You don't normally need to set it.
Path of the config file written by `unkey auth login`. Falls back to `UNKEY_CONFIG`.
Output format. Falls back to `UNKEY_OUTPUT`. `json` prints the full response. Any other value prints the request ID and `data`.
Send this JSON as the whole request body instead of using the command's flags. You can't combine it with them.
## Required permissions
`api.*.read_key` or `api..read_key`. `--decrypt` also needs `api.*.decrypt_key` or `api..decrypt_key`. See [Root key permissions](/docs/platform/root-keys/permissions).
## Examples
```bash Read a key theme={"system"}
unkey api keys get-key --key-id=key_1234abcd
```
```bash Read a recoverable key's plaintext theme={"system"}
unkey api keys get-key --key-id=key_1234abcd --decrypt
```
Or send the whole request as JSON:
```bash Raw body theme={"system"}
unkey api keys get-key --body='{"keyId":"key_1234abcd","decrypt":false}'
```
# unkey api keys migrate-keys
Source: https://www.unkey.com/docs/api-management/cli/keys/migrate-keys
Import existing key hashes into a keyspace under a migration ID.
Move keys from a homegrown system into a keyspace with `unkey api keys migrate-keys`. You send hashes, not plaintext keys. Users keep the keys they already have, and your backend then calls `keys.verifyKey` instead of your own lookup. The steps, the SHA-256 base64 format, and what you stop maintaining are on [Migrating keys](/docs/api-management/keys/migrating-keys).
You need a root key with the permissions listed on this page. Create one in the dashboard under **Settings > Root Keys**. See [Permission reference](/docs/platform/root-keys/permissions-legacy) for every permission.
The command imports the hashes under a migration ID from support, so the keys work with Unkey without being reissued. It returns `200` even when some keys fail. `data.migrated` lists each imported hash with its new `keyId`. `data.failed` lists the hashes that didn't import, usually because the hash already exists in Unkey. Hashes must be unique across all of Unkey, not just your workspace. Calls `POST /v2/keys.migrateKeys`.
## Usage
```bash theme={"system"}
unkey api keys migrate-keys --api-id= --keys= --migration-id= [flags]
```
## Flags
Id of the API to import the keys into.
JSON array of key objects. Only `hash` is required. Each object also takes `name`, `externalId`, `meta`, `roles`, `permissions`, `expires`, `enabled`, `credits`, and `ratelimits`, with the same meaning as on `create-key`.
Id of the migration provider configured for your workspace. Unkey support issues it.
### Shared flags
Every `unkey api` command takes these. See [CLI output and shared flags](/docs/platform/cli/output-and-flags).
Root key used for the request. Falls back to `UNKEY_ROOT_KEY`, then to the key stored by `unkey auth login`.
Base URL of the API. Falls back to `UNKEY_API_BASE_URL`. You don't normally need to set it.
Path of the config file written by `unkey auth login`. Falls back to `UNKEY_CONFIG`.
Output format. Falls back to `UNKEY_OUTPUT`. `json` prints the full response. Any other value prints the request ID and `data`.
Send this JSON as the whole request body instead of using the command's flags. You can't combine it with them.
## Required permissions
`api.*.create_key` or `api..create_key`. See [Root key permissions](/docs/platform/root-keys/permissions).
## Examples
```bash Import two hashes theme={"system"}
unkey api keys migrate-keys --migration-id=mig_1234abcd --api-id=api_1234abcd \
--keys='[{"hash":"9f86d08...","externalId":"user_1"},{"hash":"e3b0c44..."}]'
```
Or send the whole request as JSON:
```bash Raw body theme={"system"}
unkey api keys migrate-keys --body='{"migrationId":"mig_1234abcd","apiId":"api_1234abcd","keys":[{"hash":"9f86d08...","externalId":"user_1"}]}'
```
# unkey api keys remove-permissions
Source: https://www.unkey.com/docs/api-management/cli/keys/remove-permissions
Revoke direct permissions from a key without touching its roles.
You need a root key with the permissions listed on this page. Create one in the dashboard under **Settings > Root Keys**. See [Permission reference](/docs/platform/root-keys/permissions-legacy) for every permission.
Remove direct permissions from a key. This doesn't touch permissions the key gets from a role. Remove the role for those. Changes can take up to about 10 seconds to apply. Calls `POST /v2/keys.removePermissions`. See [Managing key permissions](/docs/api-management/authorization/managing-key-permissions).
## Usage
```bash theme={"system"}
unkey api keys remove-permissions --key-id= --permissions= [flags]
```
## Flags
Id of the key.
Comma-separated permission slugs to remove. A slug that doesn't exist fails the call instead of being ignored.
### Shared flags
Every `unkey api` command takes these. See [CLI output and shared flags](/docs/platform/cli/output-and-flags).
Root key used for the request. Falls back to `UNKEY_ROOT_KEY`, then to the key stored by `unkey auth login`.
Base URL of the API. Falls back to `UNKEY_API_BASE_URL`. You don't normally need to set it.
Path of the config file written by `unkey auth login`. Falls back to `UNKEY_CONFIG`.
Output format. Falls back to `UNKEY_OUTPUT`. `json` prints the full response. Any other value prints the request ID and `data`.
Send this JSON as the whole request body instead of using the command's flags. You can't combine it with them.
## Required permissions
`api.*.update_key` or `api..update_key` together with `rbac.*.remove_permission_from_key`. See [Root key permissions](/docs/platform/root-keys/permissions).
## Examples
```bash Remove a permission theme={"system"}
unkey api keys remove-permissions --key-id=key_1234abcd --permissions=documents.write
```
Or send the whole request as JSON:
```bash Raw body theme={"system"}
unkey api keys remove-permissions --body='{"keyId":"key_1234abcd","permissions":["documents.write"]}'
```
# unkey api keys remove-roles
Source: https://www.unkey.com/docs/api-management/cli/keys/remove-roles
Remove roles from a key without touching its direct permissions.
You need a root key with the permissions listed on this page. Create one in the dashboard under **Settings > Root Keys**. See [Permission reference](/docs/platform/root-keys/permissions-legacy) for every permission.
Remove roles from a key. Direct permissions and other roles stay. Changes can take up to about 10 seconds to apply. Calls `POST /v2/keys.removeRoles`. See [Managing key permissions](/docs/api-management/authorization/managing-key-permissions).
## Usage
```bash theme={"system"}
unkey api keys remove-roles --key-id= --roles= [flags]
```
## Flags
Id of the key.
Comma-separated role names to remove. An unknown name fails the call instead of being ignored.
### Shared flags
Every `unkey api` command takes these. See [CLI output and shared flags](/docs/platform/cli/output-and-flags).
Root key used for the request. Falls back to `UNKEY_ROOT_KEY`, then to the key stored by `unkey auth login`.
Base URL of the API. Falls back to `UNKEY_API_BASE_URL`. You don't normally need to set it.
Path of the config file written by `unkey auth login`. Falls back to `UNKEY_CONFIG`.
Output format. Falls back to `UNKEY_OUTPUT`. `json` prints the full response. Any other value prints the request ID and `data`.
Send this JSON as the whole request body instead of using the command's flags. You can't combine it with them.
## Required permissions
`api.*.update_key` or `api..update_key` for the API the key belongs to. See [Root key permissions](/docs/platform/root-keys/permissions).
## Examples
```bash Remove a role theme={"system"}
unkey api keys remove-roles --key-id=key_1234abcd --roles=billing_reader
```
Or send the whole request as JSON:
```bash Raw body theme={"system"}
unkey api keys remove-roles --body='{"keyId":"key_1234abcd","roles":["billing_reader"]}'
```
# unkey api keys reroll-key
Source: https://www.unkey.com/docs/api-management/cli/keys/reroll-key
Issue a replacement key and let the old one expire after a grace period.
You need a root key with the permissions listed on this page. Create one in the dashboard under **Settings > Root Keys**. See [Permission reference](/docs/platform/root-keys/permissions-legacy) for every permission.
Replace a key with a new one. The new key copies the old one's identity, permissions, roles, credits, rate limits, metadata, name, enabled state, and expiry. The expiry is the same date, so a key close to expiry gets a replacement close to expiry too. Use [`update-key`](/docs/api-management/cli/keys/update-key) to set a new one.
The response has the new key's plaintext, and you only see it once, so save it. The old key expires after `--expiration` milliseconds so callers have time to switch. It can keep verifying for about 10 seconds past that. Calls `POST /v2/keys.rerollKey`. See [Rerolling keys](/docs/api-management/keys/rerolling-keys).
## Usage
```bash theme={"system"}
unkey api keys reroll-key --expiration= --key-id= [flags]
```
## Flags
Milliseconds until the old key expires. The expiry is rounded up to the next whole minute, so the old key can work up to a minute longer than you asked. `0` expires it right away. The old key isn't deleted. After it expires, it verifies with `valid: false` and code `EXPIRED`.
Id of the key to reroll.
### Shared flags
Every `unkey api` command takes these. See [CLI output and shared flags](/docs/platform/cli/output-and-flags).
Root key used for the request. Falls back to `UNKEY_ROOT_KEY`, then to the key stored by `unkey auth login`.
Base URL of the API. Falls back to `UNKEY_API_BASE_URL`. You don't normally need to set it.
Path of the config file written by `unkey auth login`. Falls back to `UNKEY_CONFIG`.
Output format. Falls back to `UNKEY_OUTPUT`. `json` prints the full response. Any other value prints the request ID and `data`.
Send this JSON as the whole request body instead of using the command's flags. You can't combine it with them.
## Required permissions
`api.*.create_key` or `api..create_key`, plus `api.*.encrypt_key` or `api..encrypt_key` when the original key is recoverable. See [Root key permissions](/docs/platform/root-keys/permissions).
## Examples
```bash Reroll with a one day overlap theme={"system"}
unkey api keys reroll-key --key-id=key_1234abcd --expiration=86400000
```
```bash Reroll and revoke immediately theme={"system"}
unkey api keys reroll-key --key-id=key_1234abcd --expiration=0
```
Or send the whole request as JSON:
```bash Raw body theme={"system"}
unkey api keys reroll-key --body='{"keyId":"key_1234abcd","expiration":86400000}'
```
# unkey api keys set-permissions
Source: https://www.unkey.com/docs/api-management/cli/keys/set-permissions
Replace the direct permissions of a key with a new set.
You need a root key with the permissions listed on this page. Create one in the dashboard under **Settings > Root Keys**. See [Permission reference](/docs/platform/root-keys/permissions-legacy) for every permission.
Replace all direct permissions on a key in one step. Permissions not in the list are removed, and roles are untouched. Changes can take up to about 10 seconds to apply. Calls `POST /v2/keys.setPermissions`. See [Managing key permissions](/docs/api-management/authorization/managing-key-permissions).
## Usage
```bash theme={"system"}
unkey api keys set-permissions --key-id= --permissions= [flags]
```
## Flags
Id of the key.
Comma-separated permission slugs that become the key's full set of direct permissions. A slug that doesn't exist yet is created.
### Shared flags
Every `unkey api` command takes these. See [CLI output and shared flags](/docs/platform/cli/output-and-flags).
Root key used for the request. Falls back to `UNKEY_ROOT_KEY`, then to the key stored by `unkey auth login`.
Base URL of the API. Falls back to `UNKEY_API_BASE_URL`. You don't normally need to set it.
Path of the config file written by `unkey auth login`. Falls back to `UNKEY_CONFIG`.
Output format. Falls back to `UNKEY_OUTPUT`. `json` prints the full response. Any other value prints the request ID and `data`.
Send this JSON as the whole request body instead of using the command's flags. You can't combine it with them.
## Required permissions
`api.*.update_key` or `api..update_key` together with `rbac.*.add_permission_to_key` and `rbac.*.remove_permission_from_key`. Creating a new slug also needs `rbac.*.create_permission`. See [Root key permissions](/docs/platform/root-keys/permissions).
## Examples
```bash Set the exact permissions theme={"system"}
unkey api keys set-permissions --key-id=key_1234abcd --permissions=documents.read,documents.write,settings.view
```
Or send the whole request as JSON:
```bash Raw body theme={"system"}
unkey api keys set-permissions --body='{"keyId":"key_1234abcd","permissions":["documents.read","settings.view"]}'
```
# unkey api keys set-roles
Source: https://www.unkey.com/docs/api-management/cli/keys/set-roles
Replace the roles of a key with a new set.
You need a root key with the permissions listed on this page. Create one in the dashboard under **Settings > Root Keys**. See [Permission reference](/docs/platform/root-keys/permissions-legacy) for every permission.
Replace all roles on a key in one step. Roles not in the list are removed, and direct permissions are untouched. Changes can take up to about 10 seconds to apply. Calls `POST /v2/keys.setRoles`. See [Managing key permissions](/docs/api-management/authorization/managing-key-permissions).
## Usage
```bash theme={"system"}
unkey api keys set-roles --key-id= --roles= [flags]
```
## Flags
Id of the key.
Comma-separated role names that become the key's full set of roles. Each role must already exist. An unknown name fails the call and nothing changes.
### Shared flags
Every `unkey api` command takes these. See [CLI output and shared flags](/docs/platform/cli/output-and-flags).
Root key used for the request. Falls back to `UNKEY_ROOT_KEY`, then to the key stored by `unkey auth login`.
Base URL of the API. Falls back to `UNKEY_API_BASE_URL`. You don't normally need to set it.
Path of the config file written by `unkey auth login`. Falls back to `UNKEY_CONFIG`.
Output format. Falls back to `UNKEY_OUTPUT`. `json` prints the full response. Any other value prints the request ID and `data`.
Send this JSON as the whole request body instead of using the command's flags. You can't combine it with them.
## Required permissions
`api.*.update_key` or `api..update_key` for the API the key belongs to. See [Root key permissions](/docs/platform/root-keys/permissions).
## Examples
```bash Set the exact roles theme={"system"}
unkey api keys set-roles --key-id=key_1234abcd --roles=api_admin,billing_reader
```
Or send the whole request as JSON:
```bash Raw body theme={"system"}
unkey api keys set-roles --body='{"keyId":"key_1234abcd","roles":["api_admin"]}'
```
# unkey api keys update-credits
Source: https://www.unkey.com/docs/api-management/cli/keys/update-credits
Set or adjust the remaining credits on a key.
You need a root key with the permissions listed on this page. Create one in the dashboard under **Settings > Root Keys**. See [Permission reference](/docs/platform/root-keys/permissions-legacy) for every permission.
Change a key's credits and nothing else. The new balance applies to the next verification. Switching between limited and unlimited credits can take up to about 10 seconds. Calls `POST /v2/keys.updateCredits`. See [Credits and refill](/docs/api-management/keys/credits-and-refill).
## Usage
```bash theme={"system"}
unkey api keys update-credits --key-id= --operation= [flags]
```
## Flags
Id of the key.
One of `set`, `increment`, or `decrement`. `increment` and `decrement` fail with `400` on a key with unlimited credits. Use `set` to give it a balance first.
Credits to set, add, or subtract. Required for `increment` and `decrement`. Leave it off with `set` to make the key unlimited and keep any refill schedule. Sending `"value": null` through `--body` makes it unlimited and also clears the refill schedule. `--value=0` is treated as leaving the flag off, so use `--body` to set a key to zero credits.
### Shared flags
Every `unkey api` command takes these. See [CLI output and shared flags](/docs/platform/cli/output-and-flags).
Root key used for the request. Falls back to `UNKEY_ROOT_KEY`, then to the key stored by `unkey auth login`.
Base URL of the API. Falls back to `UNKEY_API_BASE_URL`. You don't normally need to set it.
Path of the config file written by `unkey auth login`. Falls back to `UNKEY_CONFIG`.
Output format. Falls back to `UNKEY_OUTPUT`. `json` prints the full response. Any other value prints the request ID and `data`.
Send this JSON as the whole request body instead of using the command's flags. You can't combine it with them.
## Required permissions
`api.*.update_key` or `api..update_key` for the API the key belongs to. See [Root key permissions](/docs/platform/root-keys/permissions).
## Examples
```bash Set to 1000 theme={"system"}
unkey api keys update-credits --key-id=key_1234abcd --operation=set --value=1000
```
```bash Top up by 500 theme={"system"}
unkey api keys update-credits --key-id=key_1234abcd --operation=increment --value=500
```
Or send the whole request as JSON:
```bash Raw body theme={"system"}
unkey api keys update-credits --body='{"keyId":"key_1234abcd","operation":"set","value":null}'
```
# unkey api keys update-key
Source: https://www.unkey.com/docs/api-management/cli/keys/update-key
Change the settings of an existing key without reissuing it.
You need a root key with the permissions listed on this page. Create one in the dashboard under **Settings > Root Keys**. See [Permission reference](/docs/platform/root-keys/permissions-legacy) for every permission.
Update a key in place. Only the fields you pass change. Changes can take up to about 10 seconds to apply. To clear a field, use `--body` and set the field to `null`. The flags can't do this. Calls `POST /v2/keys.updateKey`. See [Enabling, disabling, and deleting keys](/docs/api-management/keys/enable-disable-delete).
## Usage
```bash theme={"system"}
unkey api keys update-key --key-id= [flags]
```
## Flags
Id of the key to update.
JSON object with an optional `remaining` and an optional `refill` of `interval` (`daily` or `monthly`), `amount`, and `refillDay`. `refillDay` is a day of the month from 1 to 31. It's required for `monthly` and not allowed for `daily`. Either mistake fails with `400`. Setting `remaining` to `null` makes the key unlimited and drops its refill schedule.
Enable (`--enabled`) or disable (`--enabled=false`) the key. Leave it off to keep the current state.
New expiry as a Unix timestamp in milliseconds.
Link the key to the identity with this external id. The identity is created if it doesn't exist.
JSON object that replaces the key's metadata.
New name.
Comma-separated permission slugs that replace the key's direct permissions. A slug that doesn't exist yet is created.
JSON array of rate limits, each with `name`, `limit`, `duration` in milliseconds, and `autoApply`. Replaces the key's rate limits.
Comma-separated role names that replace the key's roles. Each role must already exist. An unknown name fails the call.
### Shared flags
Every `unkey api` command takes these. See [CLI output and shared flags](/docs/platform/cli/output-and-flags).
Root key used for the request. Falls back to `UNKEY_ROOT_KEY`, then to the key stored by `unkey auth login`.
Base URL of the API. Falls back to `UNKEY_API_BASE_URL`. You don't normally need to set it.
Path of the config file written by `unkey auth login`. Falls back to `UNKEY_CONFIG`.
Output format. Falls back to `UNKEY_OUTPUT`. `json` prints the full response. Any other value prints the request ID and `data`.
Send this JSON as the whole request body instead of using the command's flags. You can't combine it with them.
## Required permissions
`api.*.update_key` or `api..update_key` for the API the key belongs to. See [Root key permissions](/docs/platform/root-keys/permissions).
## Examples
```bash Rename theme={"system"}
unkey api keys update-key --key-id=key_1234abcd --name='Updated Key Name'
```
```bash Disable theme={"system"}
unkey api keys update-key --key-id=key_1234abcd --enabled=false
```
Or send the whole request as JSON:
```bash Raw body theme={"system"}
unkey api keys update-key --body='{"keyId":"key_1234abcd","expires":null,"meta":{"plan":"enterprise"}}'
```
# unkey api keys verify-key
Source: https://www.unkey.com/docs/api-management/cli/keys/verify-key
Verify a key the way your API would on a request.
You need a root key with the permissions listed on this page. Create one in the dashboard under **Settings > Root Keys**. See [Permission reference](/docs/platform/root-keys/permissions-legacy) for every permission.
Verify a key the way your API would. An invalid key still returns `200`, with `valid: false`, and the command exits successfully. Check `data.valid` and `data.code`, not the exit status. Calls `POST /v2/keys.verifyKey`. See [Verifying keys](/docs/api-management/keys/verifying-keys).
## Usage
```bash theme={"system"}
unkey api keys verify-key --key= [flags]
```
## Flags
The key to verify, including its prefix.
JSON object with `cost`, the number of credits this verification uses. Defaults to 1.
Migration id, to verify a key that hasn't been imported yet. The key is imported on first use.
Permission query the key must satisfy, for example `documents.read AND (billing.read OR billing.write)`.
JSON array of rate limits to check, each with `name` and optional `cost`, `limit`, and `duration` overrides.
Comma-separated `key=value` tags recorded on the verification for analytics. At most 20 tags, each up to 512 characters.
### Shared flags
Every `unkey api` command takes these. See [CLI output and shared flags](/docs/platform/cli/output-and-flags).
Root key used for the request. Falls back to `UNKEY_ROOT_KEY`, then to the key stored by `unkey auth login`.
Base URL of the API. Falls back to `UNKEY_API_BASE_URL`. You don't normally need to set it.
Path of the config file written by `unkey auth login`. Falls back to `UNKEY_CONFIG`.
Output format. Falls back to `UNKEY_OUTPUT`. `json` prints the full response. Any other value prints the request ID and `data`.
Send this JSON as the whole request body instead of using the command's flags. You can't combine it with them. The endpoint also accepts `keyspaces`, a list of up to five keyspace ids that the key has to be in. It has no flag of its own, and a key outside them comes back as `NOT_FOUND` without spending credits or rate limits.
## Required permissions
`api.*.verify_key` or `api..verify_key` for the API the key belongs to. A missing permission never returns `403`. Whether the root key has no verify permission or has one for a different API, you get `200` with `valid: false` and code `NOT_FOUND`. See [Root key permissions](/docs/platform/root-keys/permissions).
## Examples
```bash Verify theme={"system"}
unkey api keys verify-key --key=sk_1234abcdef
```
```bash Verify with a permission query and tags theme={"system"}
unkey api keys verify-key --key=sk_1234abcdef --permissions='documents.read AND users.view' --tags=endpoint=/users/profile,method=GET
```
Or send the whole request as JSON:
```bash Raw body theme={"system"}
unkey api keys verify-key --body='{"key":"sk_1234abcdef","permissions":"documents.read AND users.view","credits":{"cost":1}}'
```
# unkey api keys whoami
Source: https://www.unkey.com/docs/api-management/cli/keys/whoami
Look up a key by its plaintext value.
You need a root key with the permissions listed on this page. Create one in the dashboard under **Settings > Root Keys**. See [Permission reference](/docs/platform/root-keys/permissions-legacy) for every permission.
Find out which key a key string belongs to. Use it when a user pastes a key and you need its ID, identity, or permissions without verifying it. Calls `POST /v2/keys.whoami`. See [Looking up keys](/docs/api-management/keys/looking-up-keys).
## Usage
```bash theme={"system"}
unkey api keys whoami --key= [flags]
```
## Flags
The full key string including its prefix.
### Shared flags
Every `unkey api` command takes these. See [CLI output and shared flags](/docs/platform/cli/output-and-flags).
Root key used for the request. Falls back to `UNKEY_ROOT_KEY`, then to the key stored by `unkey auth login`.
Base URL of the API. Falls back to `UNKEY_API_BASE_URL`. You don't normally need to set it.
Path of the config file written by `unkey auth login`. Falls back to `UNKEY_CONFIG`.
Output format. Falls back to `UNKEY_OUTPUT`. `json` prints the full response. Any other value prints the request ID and `data`.
Send this JSON as the whole request body instead of using the command's flags. You can't combine it with them.
## Required permissions
`api.*.read_key` or `api..read_key`. Without the permission you get a 404, not a 403, so the response doesn't reveal whether the key exists. See [Root key permissions](/docs/platform/root-keys/permissions).
## Examples
```bash Identify a key theme={"system"}
unkey api keys whoami --key=sk_1234abcdef5678
```
Or send the whole request as JSON:
```bash Raw body theme={"system"}
unkey api keys whoami --body='{"key":"sk_1234abcdef5678"}'
```
# unkey api permissions create-permission
Source: https://www.unkey.com/docs/api-management/cli/permissions/create-permission
Create a permission that keys and roles can be granted.
You need a root key with the permissions listed on this page. Create one in the dashboard under **Settings > Root Keys**. See [Permission reference](/docs/platform/root-keys/permissions-legacy) for every permission.
Create a permission. You grant the slug to keys and roles, and check it with `keys verify-key --permissions`. Calls `POST /v2/permissions.createPermission`. See [Roles and permissions](/docs/api-management/authorization/roles-and-permissions).
## Usage
```bash theme={"system"}
unkey api permissions create-permission --name= --slug= [flags]
```
## Flags
Display name, 1 to 512 characters.
Name used when granting and checking the permission. Unique per workspace. Reusing one returns 409 [`err:unkey:data:permission_already_exists`](/docs/errors/unkey/data/permission_already_exists). 1 to 128 characters matching `^[a-zA-Z0-9_:\-\.\*]+$`: letters, digits, underscores, colons, hyphens, periods, and asterisks.
What the permission allows, for the dashboard. Up to 512 characters.
### Shared flags
Every `unkey api` command takes these. See [CLI output and shared flags](/docs/platform/cli/output-and-flags).
Root key used for the request. Falls back to `UNKEY_ROOT_KEY`, then to the key stored by `unkey auth login`.
Base URL of the API. Falls back to `UNKEY_API_BASE_URL`. You don't normally need to set it.
Path of the config file written by `unkey auth login`. Falls back to `UNKEY_CONFIG`.
Output format. Falls back to `UNKEY_OUTPUT`. `json` prints the full response. Any other value prints the request ID and `data`.
Send this JSON as the whole request body instead of using the command's flags. You can't combine it with them.
## Required permissions
`rbac.*.create_permission`. See [Root key permissions](/docs/platform/root-keys/permissions).
## Examples
```bash Create theme={"system"}
unkey api permissions create-permission --name='Read documents' --slug=documents.read
```
Or send the whole request as JSON:
```bash Raw body theme={"system"}
unkey api permissions create-permission --body='{"name":"Read documents","slug":"documents.read","description":"List and read documents"}'
```
# unkey api permissions create-role
Source: https://www.unkey.com/docs/api-management/cli/permissions/create-role
Create a role, optionally with an initial set of permissions.
You need a root key with the permissions listed on this page. Create one in the dashboard under **Settings > Root Keys**. See [Permission reference](/docs/platform/root-keys/permissions-legacy) for every permission.
Create a role. A role groups permissions so you can give a key many at once. Calls `POST /v2/permissions.createRole`. See [Roles and permissions](/docs/api-management/authorization/roles-and-permissions).
## Usage
```bash theme={"system"}
unkey api permissions create-role --name= [flags]
```
## Flags
Role name, 1 to 128 characters. Unique per workspace. Reusing one returns 409 [`err:unkey:data:role_already_exists`](/docs/errors/unkey/data/role_already_exists). Keys refer to roles by name.
What the role is for.
Comma-separated permission slugs to attach. Slugs that don't exist yet are created. Each slug is 1 to 128 characters matching `^[a-zA-Z0-9_:\-\.\*]+$`, the same as `create-permission`.
### Shared flags
Every `unkey api` command takes these. See [CLI output and shared flags](/docs/platform/cli/output-and-flags).
Root key used for the request. Falls back to `UNKEY_ROOT_KEY`, then to the key stored by `unkey auth login`.
Base URL of the API. Falls back to `UNKEY_API_BASE_URL`. You don't normally need to set it.
Path of the config file written by `unkey auth login`. Falls back to `UNKEY_CONFIG`.
Output format. Falls back to `UNKEY_OUTPUT`. `json` prints the full response. Any other value prints the request ID and `data`.
Send this JSON as the whole request body instead of using the command's flags. You can't combine it with them.
## Required permissions
`rbac.*.create_role`, plus `rbac.*.add_permission_to_role` when `--permissions` is given. Creating new slugs also needs `rbac.*.create_permission`. See [Root key permissions](/docs/platform/root-keys/permissions).
## Examples
```bash Create with permissions theme={"system"}
unkey api permissions create-role --name=api_reader --permissions=documents.read,settings.view
```
Or send the whole request as JSON:
```bash Raw body theme={"system"}
unkey api permissions create-role --body='{"name":"api_reader","description":"Read-only access","permissions":["documents.read"]}'
```
# unkey api permissions delete-permission
Source: https://www.unkey.com/docs/api-management/cli/permissions/delete-permission
Delete a permission and remove it from every key and role.
You need a root key with the permissions listed on this page. Create one in the dashboard under **Settings > Root Keys**. See [Permission reference](/docs/platform/root-keys/permissions-legacy) for every permission.
Delete a permission. Roles lose it right away. Keys that had it stop passing checks for it within about 10 seconds. Calls `POST /v2/permissions.deletePermission`. See [Roles and permissions](/docs/api-management/authorization/roles-and-permissions).
## Usage
```bash theme={"system"}
unkey api permissions delete-permission --permission= [flags]
```
## Flags
Permission id or slug.
### Shared flags
Every `unkey api` command takes these. See [CLI output and shared flags](/docs/platform/cli/output-and-flags).
Root key used for the request. Falls back to `UNKEY_ROOT_KEY`, then to the key stored by `unkey auth login`.
Base URL of the API. Falls back to `UNKEY_API_BASE_URL`. You don't normally need to set it.
Path of the config file written by `unkey auth login`. Falls back to `UNKEY_CONFIG`.
Output format. Falls back to `UNKEY_OUTPUT`. `json` prints the full response. Any other value prints the request ID and `data`.
Send this JSON as the whole request body instead of using the command's flags. You can't combine it with them.
## Required permissions
`rbac.*.delete_permission`. Without the permission you get a 404, not a 403, so the response doesn't reveal whether the resource exists. See [Root key permissions](/docs/platform/root-keys/permissions).
## Examples
```bash Delete theme={"system"}
unkey api permissions delete-permission --permission=documents.read
```
Or send the whole request as JSON:
```bash Raw body theme={"system"}
unkey api permissions delete-permission --body='{"permission":"documents.read"}'
```
# unkey api permissions delete-role
Source: https://www.unkey.com/docs/api-management/cli/permissions/delete-role
Delete a role and remove it from every key that has it.
You need a root key with the permissions listed on this page. Create one in the dashboard under **Settings > Root Keys**. See [Permission reference](/docs/platform/root-keys/permissions-legacy) for every permission.
Delete a role. Keys that had it lose its permissions within about 10 seconds. The permissions themselves aren't deleted. Calls `POST /v2/permissions.deleteRole`. See [Roles and permissions](/docs/api-management/authorization/roles-and-permissions).
## Usage
```bash theme={"system"}
unkey api permissions delete-role --role= [flags]
```
## Flags
Role id or name.
### Shared flags
Every `unkey api` command takes these. See [CLI output and shared flags](/docs/platform/cli/output-and-flags).
Root key used for the request. Falls back to `UNKEY_ROOT_KEY`, then to the key stored by `unkey auth login`.
Base URL of the API. Falls back to `UNKEY_API_BASE_URL`. You don't normally need to set it.
Path of the config file written by `unkey auth login`. Falls back to `UNKEY_CONFIG`.
Output format. Falls back to `UNKEY_OUTPUT`. `json` prints the full response. Any other value prints the request ID and `data`.
Send this JSON as the whole request body instead of using the command's flags. You can't combine it with them.
## Required permissions
`rbac.*.delete_role`. Without the permission you get a 404, not a 403, so the response doesn't reveal whether the resource exists. See [Root key permissions](/docs/platform/root-keys/permissions).
## Examples
```bash Delete theme={"system"}
unkey api permissions delete-role --role=api_reader
```
Or send the whole request as JSON:
```bash Raw body theme={"system"}
unkey api permissions delete-role --body='{"role":"api_reader"}'
```
# unkey api permissions get-permission
Source: https://www.unkey.com/docs/api-management/cli/permissions/get-permission
Look up a permission by its ID or slug.
You need a root key with the permissions listed on this page. Create one in the dashboard under **Settings > Root Keys**. See [Permission reference](/docs/platform/root-keys/permissions-legacy) for every permission.
Read a permission. Calls `POST /v2/permissions.getPermission`. See [Roles and permissions](/docs/api-management/authorization/roles-and-permissions).
## Usage
```bash theme={"system"}
unkey api permissions get-permission --permission= [flags]
```
## Flags
Permission id (`perm_...`) or slug.
### Shared flags
Every `unkey api` command takes these. See [CLI output and shared flags](/docs/platform/cli/output-and-flags).
Root key used for the request. Falls back to `UNKEY_ROOT_KEY`, then to the key stored by `unkey auth login`.
Base URL of the API. Falls back to `UNKEY_API_BASE_URL`. You don't normally need to set it.
Path of the config file written by `unkey auth login`. Falls back to `UNKEY_CONFIG`.
Output format. Falls back to `UNKEY_OUTPUT`. `json` prints the full response. Any other value prints the request ID and `data`.
Send this JSON as the whole request body instead of using the command's flags. You can't combine it with them.
## Required permissions
`rbac.*.read_permission`. Without the permission you get a 404, not a 403, so the response doesn't reveal whether the resource exists. See [Root key permissions](/docs/platform/root-keys/permissions).
## Examples
```bash Read by slug theme={"system"}
unkey api permissions get-permission --permission=documents.read
```
Or send the whole request as JSON:
```bash Raw body theme={"system"}
unkey api permissions get-permission --body='{"permission":"documents.read"}'
```
# unkey api permissions get-role
Source: https://www.unkey.com/docs/api-management/cli/permissions/get-role
Look up a role and the permissions attached to it.
You need a root key with the permissions listed on this page. Create one in the dashboard under **Settings > Root Keys**. See [Permission reference](/docs/platform/root-keys/permissions-legacy) for every permission.
Read a role, including the permissions attached to it. Calls `POST /v2/permissions.getRole`. See [Roles and permissions](/docs/api-management/authorization/roles-and-permissions).
## Usage
```bash theme={"system"}
unkey api permissions get-role --role= [flags]
```
## Flags
Role id (`role_...`) or name.
### Shared flags
Every `unkey api` command takes these. See [CLI output and shared flags](/docs/platform/cli/output-and-flags).
Root key used for the request. Falls back to `UNKEY_ROOT_KEY`, then to the key stored by `unkey auth login`.
Base URL of the API. Falls back to `UNKEY_API_BASE_URL`. You don't normally need to set it.
Path of the config file written by `unkey auth login`. Falls back to `UNKEY_CONFIG`.
Output format. Falls back to `UNKEY_OUTPUT`. `json` prints the full response. Any other value prints the request ID and `data`.
Send this JSON as the whole request body instead of using the command's flags. You can't combine it with them.
## Required permissions
`rbac.*.read_role`. Without the permission you get a 404, not a 403, so the response doesn't reveal whether the resource exists. See [Root key permissions](/docs/platform/root-keys/permissions).
## Examples
```bash Read by name theme={"system"}
unkey api permissions get-role --role=api_reader
```
Or send the whole request as JSON:
```bash Raw body theme={"system"}
unkey api permissions get-role --body='{"role":"api_reader"}'
```
# unkey api permissions list-permissions
Source: https://www.unkey.com/docs/api-management/cli/permissions/list-permissions
List the permissions in your workspace one page at a time.
You need a root key with the permissions listed on this page. Create one in the dashboard under **Settings > Root Keys**. See [Permission reference](/docs/platform/root-keys/permissions-legacy) for every permission.
List permissions. Results come in pages. Calls `POST /v2/permissions.listPermissions`. See [Roles and permissions](/docs/api-management/authorization/roles-and-permissions).
## Usage
```bash theme={"system"}
unkey api permissions list-permissions [flags]
```
## Flags
Cursor from the previous response.
Maximum number of permissions per page, from 1 to 100. Defaults to 100.
Return only permissions whose id, name, slug, or description matches.
### Shared flags
Every `unkey api` command takes these. See [CLI output and shared flags](/docs/platform/cli/output-and-flags).
Root key used for the request. Falls back to `UNKEY_ROOT_KEY`, then to the key stored by `unkey auth login`.
Base URL of the API. Falls back to `UNKEY_API_BASE_URL`. You don't normally need to set it.
Path of the config file written by `unkey auth login`. Falls back to `UNKEY_CONFIG`.
Output format. Falls back to `UNKEY_OUTPUT`. `json` prints the full response. Any other value prints the request ID and `data`.
Send this JSON as the whole request body instead of using the command's flags. You can't combine it with them.
## Required permissions
`rbac.*.read_permission`. See [Root key permissions](/docs/platform/root-keys/permissions).
## Examples
```bash Search theme={"system"}
unkey api permissions list-permissions --search=documents --limit=50
```
Or send the whole request as JSON:
```bash Raw body theme={"system"}
unkey api permissions list-permissions --body='{"limit":50,"search":"documents"}'
```
# unkey api permissions list-roles
Source: https://www.unkey.com/docs/api-management/cli/permissions/list-roles
List the roles in your workspace one page at a time.
You need a root key with the permissions listed on this page. Create one in the dashboard under **Settings > Root Keys**. See [Permission reference](/docs/platform/root-keys/permissions-legacy) for every permission.
List roles with their permissions. Results come in pages. Calls `POST /v2/permissions.listRoles`. See [Roles and permissions](/docs/api-management/authorization/roles-and-permissions).
## Usage
```bash theme={"system"}
unkey api permissions list-roles [flags]
```
## Flags
Cursor from the previous response.
Maximum number of roles per page, from 1 to 100. Defaults to 100.
Return only roles whose id, name, or description matches.
### Shared flags
Every `unkey api` command takes these. See [CLI output and shared flags](/docs/platform/cli/output-and-flags).
Root key used for the request. Falls back to `UNKEY_ROOT_KEY`, then to the key stored by `unkey auth login`.
Base URL of the API. Falls back to `UNKEY_API_BASE_URL`. You don't normally need to set it.
Path of the config file written by `unkey auth login`. Falls back to `UNKEY_CONFIG`.
Output format. Falls back to `UNKEY_OUTPUT`. `json` prints the full response. Any other value prints the request ID and `data`.
Send this JSON as the whole request body instead of using the command's flags. You can't combine it with them.
## Required permissions
`rbac.*.read_role`. See [Root key permissions](/docs/platform/root-keys/permissions).
## Examples
```bash Search theme={"system"}
unkey api permissions list-roles --search=admin
```
Or send the whole request as JSON:
```bash Raw body theme={"system"}
unkey api permissions list-roles --body='{"limit":50}'
```
# unkey api permissions set-role-permissions
Source: https://www.unkey.com/docs/api-management/cli/permissions/set-role-permissions
Replace the permissions attached to a role.
You need a root key with the permissions listed on this page. Create one in the dashboard under **Settings > Root Keys**. See [Permission reference](/docs/platform/root-keys/permissions-legacy) for every permission.
Replace a role's permissions. An empty set removes them all. Keys with the role pick up the change within about 10 seconds. Calls `POST /v2/permissions.setRolePermissions`. See [Roles and permissions](/docs/api-management/authorization/roles-and-permissions).
## Usage
```bash theme={"system"}
unkey api permissions set-role-permissions (--role= | --role-id=) --permissions= [flags]
```
## Flags
Role id or name. Pass this or `--role-id`.
Old name for `--role`, kept so existing scripts work. Use `--role`.
Comma-separated permission slugs that become the role's full set. Slugs that don't exist yet are created. Pass an empty value to remove all. Required unless you use `--body`.
### Shared flags
Every `unkey api` command takes these. See [CLI output and shared flags](/docs/platform/cli/output-and-flags).
Root key used for the request. Falls back to `UNKEY_ROOT_KEY`, then to the key stored by `unkey auth login`.
Base URL of the API. Falls back to `UNKEY_API_BASE_URL`. You don't normally need to set it.
Path of the config file written by `unkey auth login`. Falls back to `UNKEY_CONFIG`.
Output format. Falls back to `UNKEY_OUTPUT`. `json` prints the full response. Any other value prints the request ID and `data`.
Send this JSON as the whole request body instead of using the command's flags. You can't combine it with them.
## Required permissions
`rbac.*.add_permission_to_role` and `rbac.*.remove_permission_from_role`. Creating new slugs also needs `rbac.*.create_permission`. See [Root key permissions](/docs/platform/root-keys/permissions).
## Examples
```bash Set theme={"system"}
unkey api permissions set-role-permissions --role=role_1234abcd --permissions=documents.read,documents.write
```
```bash Clear theme={"system"}
unkey api permissions set-role-permissions --role=role_1234abcd --permissions=
```
Or send the whole request as JSON:
```bash Raw body theme={"system"}
unkey api permissions set-role-permissions --body='{"role":"role_1234abcd","permissions":["documents.read"]}'
```
# unkey api portal create-portal
Source: https://www.unkey.com/docs/api-management/cli/portal/create-portal
Create a portal for one app or keyspace.
You need a root key with the permissions listed on this page. Create one in the dashboard under **Settings > Root Keys**. See [Permission reference](/docs/platform/root-keys/permissions-legacy) for every permission.
Create a [portal](/docs/api-management/portal/overview) for one keyspace or one app in your workspace. Each keyspace or app can have only one portal. A second portal for the same one, or a slug that's already taken, returns 409 [`err:unkey:data:portal_already_exists`](/docs/errors/unkey/data/portal_already_exists). Calls `POST /v2/portal.createPortal`.
## Usage
```bash theme={"system"}
unkey api portal create-portal --slug= --display-name= (--keyspace-id= | --app-id=) [flags]
```
## Flags
Portal handle, unique in your workspace. 3 to 64 lowercase letters, digits, and hyphens, not starting or ending with a hyphen. You pass it as `--portal` to the other portal commands. End users never see it.
Name shown to end users in the portal header, up to 64 characters. You can change it later.
Keyspace this portal serves. Pass exactly one of `--keyspace-id` or `--app-id`.
App this portal serves. Pass exactly one of `--keyspace-id` or `--app-id`.
Whether you can create sessions for the portal. Pass `--enabled=false` to create it turned off.
Absolute HTTPS URL of the logo shown in the portal header, up to 500 characters.
Six-digit hex color used for primary actions, for example `#6366f1`.
### Shared flags
Every `unkey api` command takes these. See [CLI output and shared flags](/docs/platform/cli/output-and-flags).
Root key used for the request. Falls back to `UNKEY_ROOT_KEY`, then to the key stored by `unkey auth login`.
Base URL of the API. Falls back to `UNKEY_API_BASE_URL`. You don't normally need to set it.
Path of the config file written by `unkey auth login`. Falls back to `UNKEY_CONFIG`.
Output format. Falls back to `UNKEY_OUTPUT`. `json` prints the full response. Any other value prints the request ID and `data`.
Send this JSON as the whole request body instead of using the command's flags. You can't combine it with them.
## Required permissions
`portal.*.create_portal`, plus read on the resource the portal will serve: `api.*.read_api` or `api..read_api` for a keyspace, `app.*.read_app` or `app..read_app` for an app. Without the read permission, the call fails with 403 and "You do not have permission to point a portal at that resource." Unlike the other portal commands, a missing `portal.*.create_portal` returns 403, not 404. See [Root key permissions](/docs/platform/root-keys/permissions).
## Examples
```bash Keyspace portal theme={"system"}
unkey api portal create-portal --slug=acme-portal --display-name=Acme --keyspace-id=ks_1234abcd
```
```bash Branded app portal theme={"system"}
unkey api portal create-portal --slug=developer-portal --display-name='Developer Portal' --app-id=app_1234abcd --logo-url=https://cdn.example.com/logo.svg --primary-color=#6366f1
```
Or send the whole request as JSON:
```bash Raw body theme={"system"}
unkey api portal create-portal --body='{"slug":"acme-portal","displayName":"Acme","keyspaceId":"ks_1234abcd"}'
```
## Related
What a portal is and how your users reach it.
Mint the session that lets one end user in.
# unkey api portal create-session
Source: https://www.unkey.com/docs/api-management/cli/portal/create-session
Create a short-lived customer portal session for an end user.
You need a root key with the permissions listed on this page. Create one in the dashboard under **Settings > Root Keys**. See [Permission reference](/docs/platform/root-keys/permissions-legacy) for every permission.
Create a portal session so one of your end users can sign in to the portal. Redirect them to `data.url`. It works once and expires after 15 minutes. After they sign in, their session lasts 24 hours. The scopes you pass decide what they can do. `data.id` identifies the session. It isn't a credential, so it's safe to log. A disabled portal returns 403 "Portal is disabled". A portal that doesn't exist, or that your root key can't reach, returns 404. Calls `POST /v2/portal.createSession`. See [Portal sessions](/docs/api-management/portal/sessions).
## Usage
```bash theme={"system"}
unkey api portal create-session --external-id= --portal= --scopes= [flags]
```
## Flags
The end user's ID in your system. The session only sees that identity's keys.
Portal id or slug.
Comma-separated capabilities from `keys:read`, `keys:reroll`, and `analytics:read`. `keys:reroll` and `analytics:read` each need `keys:read` too, because both are reached from the keys page. The command rejects them without it.
Create a preview session for testing the portal without a real end user.
Full `https://` URL the portal sends the user back to when they're done, up to 500 characters. An `http://` URL, a `//host/path`, or a bare path fails with 400.
### Shared flags
Every `unkey api` command takes these. See [CLI output and shared flags](/docs/platform/cli/output-and-flags).
Root key used for the request. Falls back to `UNKEY_ROOT_KEY`, then to the key stored by `unkey auth login`.
Base URL of the API. Falls back to `UNKEY_API_BASE_URL`. You don't normally need to set it.
Path of the config file written by `unkey auth login`. Falls back to `UNKEY_CONFIG`.
Output format. Falls back to `UNKEY_OUTPUT`. `json` prints the full response. Any other value prints the request ID and `data`.
Send this JSON as the whole request body instead of using the command's flags. You can't combine it with them.
## Required permissions
`portal.*.create_portal_session` or `portal..create_portal_session`. Permissions to manage a portal don't include this one. You also need the matching permission on the APIs behind the portal for each scope:
* `keys:read` needs `read_key` and `read_api`.
* `keys:reroll` needs `create_key`, plus `encrypt_key` when the keyspace stores recoverable keys.
* `analytics:read` needs `read_analytics`.
Without the session permission, you get a 404 as if the portal didn't exist. With it but missing a scope's permission, the whole request fails with 403. We never create a session with fewer scopes than you asked for. See [Root key permissions](/docs/platform/root-keys/permissions).
## Examples
```bash Read and reroll keys theme={"system"}
unkey api portal create-session --portal=my-portal --external-id=user_123 --scopes=keys:read,keys:reroll
```
```bash Analytics with a return URL theme={"system"}
unkey api portal create-session --portal=my-portal --external-id=user_123 --scopes=keys:read,analytics:read --return-url=https://app.example.com/settings/api-keys
```
Or send the whole request as JSON:
```bash Raw body theme={"system"}
unkey api portal create-session --body='{"portal":"my-portal","externalId":"user_123","scopes":["keys:read","keys:reroll"],"returnUrl":"https://app.example.com/settings/api-keys"}'
```
# unkey api portal delete-portal
Source: https://www.unkey.com/docs/api-management/cli/portal/delete-portal
Delete a portal and cut off the sessions it minted.
You need a root key with the permissions listed on this page. Create one in the dashboard under **Settings > Root Keys**. See [Permission reference](/docs/platform/root-keys/permissions-legacy) for every permission.
Delete a [portal](/docs/api-management/portal/overview) and sign out all of its users. Most sessions stop working within about 10 seconds, but some can keep working for up to 5 minutes. The keyspace or app behind the portal isn't touched, its keys keep working, and the slug is free to reuse. Calls `POST /v2/portal.deletePortal`.
To stop new sessions without cutting off the users who already have one, disable the portal with [update-portal](/docs/api-management/cli/portal/update-portal) instead.
## Usage
```bash theme={"system"}
unkey api portal delete-portal --portal= [flags]
```
## Flags
Portal id or slug.
### Shared flags
Every `unkey api` command takes these. See [CLI output and shared flags](/docs/platform/cli/output-and-flags).
Root key used for the request. Falls back to `UNKEY_ROOT_KEY`, then to the key stored by `unkey auth login`.
Base URL of the API. Falls back to `UNKEY_API_BASE_URL`. You don't normally need to set it.
Path of the config file written by `unkey auth login`. Falls back to `UNKEY_CONFIG`.
Output format. Falls back to `UNKEY_OUTPUT`. `json` prints the full response. Any other value prints the request ID and `data`.
Send this JSON as the whole request body instead of using the command's flags. You can't combine it with them.
## Required permissions
`portal.*.delete_portal` or `portal..delete_portal`. Without it you get a 404, as if the portal didn't exist. See [Root key permissions](/docs/platform/root-keys/permissions).
## Examples
```bash Delete theme={"system"}
unkey api portal delete-portal --portal=acme-portal
```
Or send the whole request as JSON:
```bash Raw body theme={"system"}
unkey api portal delete-portal --body='{"portal":"acme-portal"}'
```
# unkey api portal get-portal
Source: https://www.unkey.com/docs/api-management/cli/portal/get-portal
Read one portal by its handle or by the resource it serves.
You need a root key with the permissions listed on this page. Create one in the dashboard under **Settings > Root Keys**. See [Permission reference](/docs/platform/root-keys/permissions-legacy) for every permission.
Read a [portal](/docs/api-management/portal/overview): its branding, whether it's enabled, and the keyspace or app behind it. Look it up by ID or slug, or by the keyspace or app it serves to check whether one has a portal. Pass exactly one of the three. More than one fails with 400. Calls `POST /v2/portal.getPortal`.
## Usage
```bash theme={"system"}
unkey api portal get-portal (--portal= | --keyspace-id= | --app-id=) [flags]
```
## Flags
Portal id or slug.
Find the portal for this keyspace.
Find the portal for this app.
### Shared flags
Every `unkey api` command takes these. See [CLI output and shared flags](/docs/platform/cli/output-and-flags).
Root key used for the request. Falls back to `UNKEY_ROOT_KEY`, then to the key stored by `unkey auth login`.
Base URL of the API. Falls back to `UNKEY_API_BASE_URL`. You don't normally need to set it.
Path of the config file written by `unkey auth login`. Falls back to `UNKEY_CONFIG`.
Output format. Falls back to `UNKEY_OUTPUT`. `json` prints the full response. Any other value prints the request ID and `data`.
Send this JSON as the whole request body instead of using the command's flags. You can't combine it with them.
## Required permissions
`portal.*.read_portal` or `portal..read_portal`. Without it you get a 404, so a 404 means either there's no such portal or you don't have permission. See [Root key permissions](/docs/platform/root-keys/permissions).
## Examples
```bash By handle theme={"system"}
unkey api portal get-portal --portal=acme-portal
```
```bash By the keyspace it serves theme={"system"}
unkey api portal get-portal --keyspace-id=ks_1234abcd
```
Or send the whole request as JSON:
```bash Raw body theme={"system"}
unkey api portal get-portal --body='{"portal":"acme-portal"}'
```
# unkey api portal update-portal
Source: https://www.unkey.com/docs/api-management/cli/portal/update-portal
Change a portal's handle, name, branding, enabled state, or the resource it serves.
You need a root key with the permissions listed on this page. Create one in the dashboard under **Settings > Root Keys**. See [Permission reference](/docs/platform/root-keys/permissions-legacy) for every permission.
Change one or more fields on a [portal](/docs/api-management/portal/overview). Only the flags you pass change. For the two branding fields, `null` clears the value. Calls `POST /v2/portal.updatePortal`.
Two changes affect your end users right away:
* **Pointing the portal at a different keyspace or app** signs out every current [session](/docs/api-management/portal/sessions). Sending the same keyspace or app it already has changes nothing. The new one must be in the same project as the old one, or the call fails with 412. Keyspaces are in the workspace's default project and apps are in the project you created them in, so moving between a keyspace and an app often hits this. Create a new portal instead.
* **Disabling the portal** stops new sessions and ends the current ones. Re-enabling it does not bring them back. To also free the slug, use [delete-portal](/docs/api-management/cli/portal/delete-portal).
## Usage
```bash theme={"system"}
unkey api portal update-portal --portal= [--slug=] [--display-name=] [--keyspace-id=|--app-id=] [--enabled=] [--logo-url=|null] [--primary-color=|null]
```
## Flags
Portal id or slug to update.
New portal handle, unique in the workspace. A slug another portal has returns 409 [`err:unkey:data:portal_already_exists`](/docs/errors/unkey/data/portal_already_exists). Update any calls that use the old slug.
New name shown to end users.
Keyspace the portal should serve instead. Mutually exclusive with `--app-id`.
App the portal should serve instead. Mutually exclusive with `--keyspace-id`.
Whether you can create new sessions. Leave it off to keep the current state.
New absolute HTTPS logo URL, or `null` to remove the logo.
New six-digit hex color, or `null` to fall back to the default styling.
### Shared flags
Every `unkey api` command takes these. See [CLI output and shared flags](/docs/platform/cli/output-and-flags).
Root key used for the request. Falls back to `UNKEY_ROOT_KEY`, then to the key stored by `unkey auth login`.
Base URL of the API. Falls back to `UNKEY_API_BASE_URL`. You don't normally need to set it.
Path of the config file written by `unkey auth login`. Falls back to `UNKEY_CONFIG`.
Output format. Falls back to `UNKEY_OUTPUT`. `json` prints the full response. Any other value prints the request ID and `data`.
Send this JSON as the whole request body instead of using the command's flags. You can't combine it with them.
## Required permissions
`portal.*.update_portal` or `portal..update_portal`. Without it you get a 404, as if the portal didn't exist. Pointing the portal at a new resource also needs read on it: `api.*.read_api` or `api..read_api` for a keyspace, `app.*.read_app` or `app..read_app` for an app. Without that, the call fails with 403 and "You do not have permission to point a portal at that resource." See [Root key permissions](/docs/platform/root-keys/permissions).
## Examples
```bash Rename theme={"system"}
unkey api portal update-portal --portal=acme-portal --display-name='Acme Developer Portal'
```
```bash Re-point and disable theme={"system"}
unkey api portal update-portal --portal=acme-portal --app-id=app_1234abcd --enabled=false
```
```bash Clear branding theme={"system"}
unkey api portal update-portal --portal=acme-portal --logo-url=null --primary-color=null
```
Or send the whole request as JSON:
```bash Raw body theme={"system"}
unkey api portal update-portal --body='{"portal":"acme-portal","displayName":"Acme Developer Portal"}'
```
# unkey api ratelimit delete-override
Source: https://www.unkey.com/docs/api-management/cli/ratelimit/delete-override
Remove an override so an identifier falls back to the default limit.
You need a root key with the permissions listed on this page. Create one in the dashboard under **Settings > Root Keys**. See [Permission reference](/docs/platform/root-keys/permissions-legacy) for every permission.
Delete an override. After that, the identifier gets the limit that `ratelimit limit` sends. Calls `POST /v2/ratelimit.deleteOverride`. See [Rate limit overrides](/docs/api-management/ratelimiting/overrides).
## Usage
```bash theme={"system"}
unkey api ratelimit delete-override --identifier= --namespace= [flags]
```
## Flags
The override's identifier, exactly as stored. For a wildcard override, pass the pattern, such as `premium_*`, not an identifier it matches.
Namespace id or name.
### Shared flags
Every `unkey api` command takes these. See [CLI output and shared flags](/docs/platform/cli/output-and-flags).
Root key used for the request. Falls back to `UNKEY_ROOT_KEY`, then to the key stored by `unkey auth login`.
Base URL of the API. Falls back to `UNKEY_API_BASE_URL`. You don't normally need to set it.
Path of the config file written by `unkey auth login`. Falls back to `UNKEY_CONFIG`.
Output format. Falls back to `UNKEY_OUTPUT`. `json` prints the full response. Any other value prints the request ID and `data`.
Send this JSON as the whole request body instead of using the command's flags. You can't combine it with them.
## Required permissions
`ratelimit.*.delete_override` or `ratelimit..delete_override`. Without the permission you get a 404, not a 403, so the response doesn't reveal whether the resource exists. See [Root key permissions](/docs/platform/root-keys/permissions).
## Examples
```bash Delete theme={"system"}
unkey api ratelimit delete-override --namespace=api.requests --identifier=premium_user_123
```
Or send the whole request as JSON:
```bash Raw body theme={"system"}
unkey api ratelimit delete-override --body='{"namespace":"api.requests","identifier":"premium_user_123"}'
```
# unkey api ratelimit get-override
Source: https://www.unkey.com/docs/api-management/cli/ratelimit/get-override
Look up the rate limit override for an identifier.
You need a root key with the permissions listed on this page. Create one in the dashboard under **Settings > Root Keys**. See [Permission reference](/docs/platform/root-keys/permissions-legacy) for every permission.
See which override applies to an identifier. An exact match comes first. Otherwise it checks wildcard patterns, so `premium_user_123` returns the `premium_*` override. If an identifier matches more than one pattern, there's no guarantee which one you see. (`ratelimit delete-override` doesn't match patterns. It needs the exact stored identifier.)
A namespace that doesn't exist or was deleted returns 404 [`err:unkey:data:ratelimit_namespace_not_found`](/docs/errors/unkey/data/ratelimit_namespace_not_found). An identifier with no override returns a different 404. Calls `POST /v2/ratelimit.getOverride`. See [Rate limit overrides](/docs/api-management/ratelimiting/overrides).
## Usage
```bash theme={"system"}
unkey api ratelimit get-override --identifier= --namespace= [flags]
```
## Flags
The identifier to resolve. Pass a concrete identifier to see which override applies to it, or the pattern itself, such as `premium_*`, to read that override directly.
Namespace id or name.
### Shared flags
Every `unkey api` command takes these. See [CLI output and shared flags](/docs/platform/cli/output-and-flags).
Root key used for the request. Falls back to `UNKEY_ROOT_KEY`, then to the key stored by `unkey auth login`.
Base URL of the API. Falls back to `UNKEY_API_BASE_URL`. You don't normally need to set it.
Path of the config file written by `unkey auth login`. Falls back to `UNKEY_CONFIG`.
Output format. Falls back to `UNKEY_OUTPUT`. `json` prints the full response. Any other value prints the request ID and `data`.
Send this JSON as the whole request body instead of using the command's flags. You can't combine it with them.
## Required permissions
`ratelimit.*.read_override` or `ratelimit..read_override`. Without the permission you get a 404, not a 403, so the response doesn't reveal whether the resource exists. See [Root key permissions](/docs/platform/root-keys/permissions).
## Examples
```bash Read theme={"system"}
unkey api ratelimit get-override --namespace=api.requests --identifier=premium_user_123
```
Or send the whole request as JSON:
```bash Raw body theme={"system"}
unkey api ratelimit get-override --body='{"namespace":"api.requests","identifier":"premium_user_123"}'
```
# unkey api ratelimit limit
Source: https://www.unkey.com/docs/api-management/cli/ratelimit/limit
Check and consume a rate limit for an identifier in a namespace.
You need a root key with the permissions listed on this page. Create one in the dashboard under **Settings > Root Keys**. See [Permission reference](/docs/platform/root-keys/permissions-legacy) for every permission.
Check a rate limit for any identifier, such as a user ID or IP address. The response says whether the request passed and how much is left in the window. The namespace is created on first use. If an [override](/docs/api-management/ratelimiting/overrides) matches the identifier, its limit and window are used instead of yours. A deleted namespace isn't recreated. The call fails with 410 Gone and [`err:unkey:data:ratelimit_namespace_gone`](/docs/errors/unkey/data/ratelimit_namespace_gone). Calls `POST /v2/ratelimit.limit`. See [Limit and multi-limit](/docs/api-management/ratelimiting/limit-and-multi-limit).
## Usage
```bash theme={"system"}
unkey api ratelimit limit --duration= --identifier= --limit= --namespace= [flags]
```
## Flags
Window length in milliseconds, from 1000 (one second) to 2592000000 (30 days).
The thing being limited, for example a user id or IP address. Up to 512 characters.
Maximum operations allowed in the window. At least 1.
Namespace id or name, up to 512 characters. A new name creates the namespace.
How much of the limit this call uses. Defaults to 1. A cost of 0 checks the limit without using any, but `--cost=0` is treated as leaving the flag off, so it charges 1. Use `--body` to send a cost of 0.
### Shared flags
Every `unkey api` command takes these. See [CLI output and shared flags](/docs/platform/cli/output-and-flags).
Root key used for the request. Falls back to `UNKEY_ROOT_KEY`, then to the key stored by `unkey auth login`.
Base URL of the API. Falls back to `UNKEY_API_BASE_URL`. You don't normally need to set it.
Path of the config file written by `unkey auth login`. Falls back to `UNKEY_CONFIG`.
Output format. Falls back to `UNKEY_OUTPUT`. `json` prints the full response. Any other value prints the request ID and `data`.
Send this JSON as the whole request body instead of using the command's flags. You can't combine it with them.
## Required permissions
`ratelimit.*.limit` or `ratelimit..limit`. Creating a namespace on first use also needs `ratelimit.*.create_namespace`. See [Root key permissions](/docs/platform/root-keys/permissions).
## Examples
```bash 100 requests per minute theme={"system"}
unkey api ratelimit limit --namespace=api.requests --identifier=user_abc123 --limit=100 --duration=60000
```
```bash Expensive operation costing 5 theme={"system"}
unkey api ratelimit limit --namespace=api.heavy --identifier=user_def456 --limit=50 --duration=3600000 --cost=5
```
Or send the whole request as JSON:
```bash Raw body theme={"system"}
unkey api ratelimit limit --body='{"namespace":"api.requests","identifier":"user_abc123","limit":100,"duration":60000}'
```
# unkey api ratelimit list-overrides
Source: https://www.unkey.com/docs/api-management/cli/ratelimit/list-overrides
List the rate limit overrides in a namespace.
You need a root key with the permissions listed on this page. Create one in the dashboard under **Settings > Root Keys**. See [Permission reference](/docs/platform/root-keys/permissions-legacy) for every permission.
List the overrides of a namespace. Results come in pages. Calls `POST /v2/ratelimit.listOverrides`. See [Rate limit overrides](/docs/api-management/ratelimiting/overrides).
## Usage
```bash theme={"system"}
unkey api ratelimit list-overrides --namespace= [flags]
```
## Flags
Namespace id or name.
Cursor from the previous response.
Maximum number of overrides per page, from 1 to 100. Defaults to 50.
### Shared flags
Every `unkey api` command takes these. See [CLI output and shared flags](/docs/platform/cli/output-and-flags).
Root key used for the request. Falls back to `UNKEY_ROOT_KEY`, then to the key stored by `unkey auth login`.
Base URL of the API. Falls back to `UNKEY_API_BASE_URL`. You don't normally need to set it.
Path of the config file written by `unkey auth login`. Falls back to `UNKEY_CONFIG`.
Output format. Falls back to `UNKEY_OUTPUT`. `json` prints the full response. Any other value prints the request ID and `data`.
Send this JSON as the whole request body instead of using the command's flags. You can't combine it with them.
## Required permissions
`ratelimit.*.read_override` or `ratelimit..read_override`. See [Root key permissions](/docs/platform/root-keys/permissions).
## Examples
```bash List theme={"system"}
unkey api ratelimit list-overrides --namespace=api.requests --limit=50
```
Or send the whole request as JSON:
```bash Raw body theme={"system"}
unkey api ratelimit list-overrides --body='{"namespace":"api.requests","limit":50}'
```
# unkey api ratelimit multi-limit
Source: https://www.unkey.com/docs/api-management/cli/ratelimit/multi-limit
Check several rate limits in one request that passes only if all of them pass.
You need a root key with the permissions listed on this page. Create one in the dashboard under **Settings > Root Keys**. See [Permission reference](/docs/platform/root-keys/permissions-legacy) for every permission.
Check several rate limits at once. `data.passed` is true only when every check passes. Each check's result is in `data.limits`. If one check fails, none of the limits are charged. A deleted namespace anywhere in the list fails the whole request with 410 Gone and [`err:unkey:data:ratelimit_namespace_gone`](/docs/errors/unkey/data/ratelimit_namespace_gone). Calls `POST /v2/ratelimit.multiLimit`. See [Limit and multi-limit](/docs/api-management/ratelimiting/limit-and-multi-limit).
## Usage
```bash theme={"system"}
unkey api ratelimit multi-limit --limits= [flags]
```
`--body` takes a JSON array, not an object.
## Flags
JSON array of 1 to 100 checks, each with `namespace`, `identifier`, `limit`, `duration`, and optional `cost`, the same fields `ratelimit limit` takes as flags.
### Shared flags
Every `unkey api` command takes these. See [CLI output and shared flags](/docs/platform/cli/output-and-flags).
Root key used for the request. Falls back to `UNKEY_ROOT_KEY`, then to the key stored by `unkey auth login`.
Base URL of the API. Falls back to `UNKEY_API_BASE_URL`. You don't normally need to set it.
Path of the config file written by `unkey auth login`. Falls back to `UNKEY_CONFIG`.
Output format. Falls back to `UNKEY_OUTPUT`. `json` prints the full response. Any other value prints the request ID and `data`.
Send this JSON as the whole request body instead of using the command's flags. You can't combine it with them.
## Required permissions
`ratelimit.*.limit`, or `ratelimit..limit` for every namespace in the request. Namespaces created on first use also need `ratelimit.*.create_namespace`. See [Root key permissions](/docs/platform/root-keys/permissions).
## Examples
```bash Two limits theme={"system"}
unkey api ratelimit multi-limit --limits='[{"namespace":"api.requests","identifier":"user_abc","limit":100,"duration":60000},{"namespace":"api.daily","identifier":"user_abc","limit":5000,"duration":86400000}]'
```
Or send the whole request as JSON:
```bash Raw body theme={"system"}
unkey api ratelimit multi-limit --body='[{"namespace":"api.requests","identifier":"user_abc","limit":100,"duration":60000}]'
```
# unkey api ratelimit set-override
Source: https://www.unkey.com/docs/api-management/cli/ratelimit/set-override
Give an identifier or pattern its own rate limit in a namespace.
You need a root key with the permissions listed on this page. Create one in the dashboard under **Settings > Root Keys**. See [Permission reference](/docs/platform/root-keys/permissions-legacy) for every permission.
Give one identifier, or a wildcard pattern, its own limit and window. It's used instead of the values sent to `ratelimit limit`. Setting it again replaces the old one. An exact identifier beats a pattern. If an identifier matches two patterns, such as `premium_*` and `*_eu`, there's no guarantee which applies, so keep patterns from overlapping. Calls `POST /v2/ratelimit.setOverride`. See [Rate limit overrides](/docs/api-management/ratelimiting/overrides).
## Usage
```bash theme={"system"}
unkey api ratelimit set-override --duration= --identifier= --limit= --namespace= [flags]
```
## Flags
Window length in milliseconds, at least 1000. It doesn't have to match the window the caller sends.
Identifier to override, or a pattern with `*` such as `premium_*`.
Maximum operations allowed in the window. The API accepts 0, but checks against it then fail with a server error instead of being denied. To nearly block an identifier, use 1 with a long `--duration`.
Namespace id or name. The namespace must already exist.
### Shared flags
Every `unkey api` command takes these. See [CLI output and shared flags](/docs/platform/cli/output-and-flags).
Root key used for the request. Falls back to `UNKEY_ROOT_KEY`, then to the key stored by `unkey auth login`.
Base URL of the API. Falls back to `UNKEY_API_BASE_URL`. You don't normally need to set it.
Path of the config file written by `unkey auth login`. Falls back to `UNKEY_CONFIG`.
Output format. Falls back to `UNKEY_OUTPUT`. `json` prints the full response. Any other value prints the request ID and `data`.
Send this JSON as the whole request body instead of using the command's flags. You can't combine it with them.
## Required permissions
`ratelimit.*.set_override` or `ratelimit..set_override`. See [Root key permissions](/docs/platform/root-keys/permissions).
## Examples
```bash Raise one user's limit theme={"system"}
unkey api ratelimit set-override --namespace=api.requests --identifier=premium_user_123 --limit=1000 --duration=60000
```
```bash Pattern override theme={"system"}
unkey api ratelimit set-override --namespace=api.requests --identifier='premium_*' --limit=500 --duration=60000
```
Or send the whole request as JSON:
```bash Raw body theme={"system"}
unkey api ratelimit set-override --body='{"namespace":"api.requests","identifier":"premium_*","limit":500,"duration":60000}'
```
# Per-endpoint rate limits
Source: https://www.unkey.com/docs/api-management/cookbook/endpoint-rate-limit
Give each endpoint its own rate limit budget.
**Outcome:** a burst of reads can't starve writes, and one expensive endpoint can't exhaust the budget of cheap ones, because each route checks its own rate limit.
You need a root key with the permissions listed on this page. Create one in the dashboard under **Settings > Root Keys**. See [Permission reference](/docs/platform/root-keys/permissions-legacy) for every permission.
## Named limits on the key
Give the key one limit per kind of request, with `autoApply: false`. Those limits are only checked when a verification names them, so each route names its own limit and the others aren't touched.
```bash create a key with two limits theme={"system"}
curl -X POST https://api.unkey.com/v2/keys.createKey \
-H "Authorization: Bearer $UNKEY_ROOT_KEY" \
-H "Content-Type: application/json" \
-d '{
"apiId": "api_...",
"ratelimits": [
{ "name": "reads", "limit": 1000, "duration": 60000, "autoApply": false },
{ "name": "writes", "limit": 100, "duration": 60000, "autoApply": false }
]
}'
```
```ts name the limit per route theme={"system"}
import { Unkey } from "@unkey/api";
const unkey = new Unkey({ rootKey: process.env.UNKEY_ROOT_KEY ?? "" });
export async function verifyFor(key: string, method: string) {
const name = method === "GET" ? "reads" : "writes";
const result = await unkey.keys.verifyKey({ key, ratelimits: [{ name }] });
return result.data;
}
```
Naming a limit that isn't on the key or its identity fails the request with HTTP 412, so keep the names in one place in your code. To add a limit later, use `keys.updateKey`. Its `ratelimits` field replaces the whole list, so send every limit, not just the new one.
## Weight expensive operations
`cost` is how much of the limit one call uses, 1 by default. If search costs 10, a client can make a tenth as many searches as list calls from the same limit.
To give a route its own limit without saving one on the key, pass both `limit` and `duration` in the entry. The name doesn't have to exist on the key, and the count is kept against the key, not the identity. If you pass only one of the two, it's ignored and the saved limit is used. See [Inline limits](/docs/api-management/ratelimiting/key-and-identity-limits#inline-limits) for the minimum values.
```bash charge 10 against the reads limit theme={"system"}
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_...", "ratelimits": [{ "name": "reads", "cost": 10 }] }'
```
## Alternative: a namespace per endpoint
When the caller has no API key, or you want the limit in code next to the route, call `ratelimit.limit` with a namespace named after the route. Each route sends its own numbers, and `cost` works the same way.
```ts one namespace per route theme={"system"}
const rl = await unkey.ratelimit.limit({
namespace: "POST /v1/items",
identifier: userId,
limit: 100,
duration: 60_000,
cost: 1,
});
if (!rl.data.success) {
return Response.json({ error: "rate limited" }, { status: 429 });
}
```
Namespaces are created on first use. To check a per-user and a per-endpoint limit on the same request, use `ratelimit.multiLimit`. `data.passed` is true only when every check passed.
## Related
* [Creating keys](/docs/api-management/keys/creating-keys) for the `ratelimits` field and its bounds.
* [Verifying keys](/docs/api-management/keys/verifying-keys) for the `ratelimits` request entries and the 412 on unknown names.
* Standalone rate limiting and `multiLimit` are documented under [limit and multiLimit](/docs/api-management/ratelimiting/limit-and-multi-limit).
# FastAPI authentication
Source: https://www.unkey.com/docs/api-management/cookbook/fastapi-auth
Verify API keys in FastAPI with a typed dependency.
**Outcome:** a route adds `Depends(require_key())` or `Depends(require_key("reports.read"))`, and the dependency verifies the key, rejects bad requests with the right status, gives the handler the verification data, and adds `X-RateLimit-*` headers.
You need a root key with the permissions listed on this page. Create one in the dashboard under **Settings > Root Keys**. See [Permission reference](/docs/platform/root-keys/permissions-legacy) for every permission.
The root key needs `api.*.verify_key`. Install with `pip install unkey.py fastapi uvicorn`.
## The dependency
`require_key` takes an optional permission query, so each route can ask for its own. FastAPI's `HTTPBearer` reads the header and returns 401 if it's missing.
Catch `httpx.HTTPError` as well as `errors.UnkeyError`. A timeout or connection failure raises an `httpx` error, and without catching it an outage becomes a 500 instead of a 503.
```python app/auth.py theme={"system"}
import os
from typing import Annotated
import httpx
from fastapi import Depends, HTTPException, Response
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
from unkey.py import Unkey, errors, models
unkey = Unkey(root_key=os.environ["UNKEY_ROOT_KEY"])
bearer = HTTPBearer(auto_error=True)
STATUS = {
"RATE_LIMITED": 429,
"USAGE_EXCEEDED": 402,
"FORBIDDEN": 403,
"INSUFFICIENT_PERMISSIONS": 403,
}
def require_key(permissions: str | None = None):
def dependency(
response: Response,
credentials: Annotated[HTTPAuthorizationCredentials, Depends(bearer)],
) -> models.V2KeysVerifyKeyResponseData:
try:
res = unkey.keys.verify_key(key=credentials.credentials, permissions=permissions)
except (errors.UnkeyError, httpx.HTTPError) as e:
raise HTTPException(status_code=503, detail="authentication unavailable") from e
data = res.data
if data.ratelimits:
# Report the tightest limit that was checked.
rl = min(data.ratelimits, key=lambda r: r.remaining)
response.headers["X-RateLimit-Limit"] = str(rl.limit)
response.headers["X-RateLimit-Remaining"] = str(rl.remaining)
# rl.reset is a Unix timestamp in milliseconds; the header is conventionally seconds.
response.headers["X-RateLimit-Reset"] = str(rl.reset // 1000)
if not data.valid:
code = data.code
raise HTTPException(status_code=STATUS.get(code, 401), detail=code)
return data
return dependency
```
## Routes
```python app/main.py theme={"system"}
from typing import Annotated
from fastapi import Depends, FastAPI
from unkey.py import models
from .auth import require_key
app = FastAPI()
@app.get("/items")
def list_items(verification: Annotated[models.V2KeysVerifyKeyResponseData, Depends(require_key())]):
owner = verification.identity.external_id if verification.identity else None
return {"items": [], "owner": owner, "credits_left": verification.credits}
@app.get("/reports")
def reports(verification: Annotated[models.V2KeysVerifyKeyResponseData, Depends(require_key("reports.read"))]):
return {"reports": [], "key": verification.key_id}
```
Headers set on the injected `Response` reach the client when the request succeeds. When the dependency raises an `HTTPException`, such as a 429, those headers are dropped, so pass them in the exception's `headers` argument if you need them on rejections too.
## Async
For an async application, use `async with Unkey(...)` in the lifespan and `await unkey.keys.verify_key_async(...)` in an `async def` dependency. The rest of the code is unchanged.
## Related
* [Python guide](/docs/api-management/guides/python) for creating keys and the basic flow.
* [Verifying keys](/docs/api-management/keys/verifying-keys) for every response field.
# Go middleware for net/http, Gin, and Echo
Source: https://www.unkey.com/docs/api-management/cookbook/go-middleware
Drop-in key verification middleware for net/http, Gin, and Echo.
**Outcome:** one `Verify` function with small adapters for net/http, Gin, and Echo. Each reads the bearer key, checks an optional permission query, returns the right status, and puts the verification data on the request.
You need a root key with the permissions listed on this page. Create one in the dashboard under **Settings > Root Keys**. See [Permission reference](/docs/platform/root-keys/permissions-legacy) for every permission.
The root key needs `api.*.verify_key`. Install the SDK with `go get github.com/unkeyed/sdks/api/go/v3`, then the frameworks you use.
## The shared core
```go auth/auth.go theme={"system"}
package auth
import (
"context"
"errors"
"net/http"
"strings"
unkey "github.com/unkeyed/sdks/api/go/v3"
"github.com/unkeyed/sdks/api/go/v3/models/components"
)
type Verifier struct {
client *unkey.Unkey
}
func New(rootKey string) *Verifier {
return &Verifier{client: unkey.New(unkey.WithSecurity(rootKey))}
}
// Result is what the adapters hand to handlers on success, or use to reject.
type Result struct {
Data components.V2KeysVerifyKeyResponseData
Status int // HTTP status to return when !Data.Valid
Reason string // what to tell the client when !Data.Valid
Err error
}
func (v *Verifier) Verify(ctx context.Context, header string, permissions string) Result {
key := strings.TrimPrefix(header, "Bearer ")
if key == "" || key == header {
// No key was sent. Saying NOT_FOUND here would claim their key does not exist.
return Result{Status: http.StatusUnauthorized, Reason: "missing bearer token"}
}
req := components.V2KeysVerifyKeyRequestBody{Key: key}
if permissions != "" {
req.Permissions = &permissions
}
res, err := v.client.Keys.VerifyKey(ctx, req)
if err != nil {
// The call failed (bad root key, throttle, outage); do not treat as an invalid key.
return Result{Status: http.StatusServiceUnavailable, Err: err}
}
// V2KeysVerifyKeyResponseBody is a pointer, so check it even when err is nil.
if res.V2KeysVerifyKeyResponseBody == nil {
return Result{Status: http.StatusServiceUnavailable, Err: errors.New("empty response body")}
}
data := res.V2KeysVerifyKeyResponseBody.Data
return Result{Data: data, Status: statusFor(data.Code), Reason: string(data.Code)}
}
func statusFor(code components.Code) int {
switch code {
case components.CodeValid:
return http.StatusOK
case components.CodeRateLimited:
return http.StatusTooManyRequests
case components.CodeUsageExceeded:
return http.StatusPaymentRequired
case components.CodeForbidden, components.CodeInsufficientPermissions:
return http.StatusForbidden
default:
return http.StatusUnauthorized
}
}
```
`Reason` tells a caller who sent no key that the token is missing, instead of `NOT_FOUND`. `Err` is set only when the call to Unkey failed, so the adapters can return 503.
## Adapters
```go net/http theme={"system"}
package auth
import (
"context"
"net/http"
"github.com/unkeyed/sdks/api/go/v3/models/components"
)
type ctxKey struct{}
// Middleware verifies the bearer key and, when permissions is non-empty,
// requires the key to satisfy that permission query.
func (v *Verifier) Middleware(permissions string) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
res := v.Verify(r.Context(), r.Header.Get("Authorization"), permissions)
if res.Err != nil {
http.Error(w, "authentication unavailable", res.Status)
return
}
if !res.Data.Valid {
http.Error(w, res.Reason, res.Status)
return
}
next.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), ctxKey{}, res.Data)))
})
}
}
// FromContext returns the verification data stored by Middleware.
func FromContext(ctx context.Context) (components.V2KeysVerifyKeyResponseData, bool) {
d, ok := ctx.Value(ctxKey{}).(components.V2KeysVerifyKeyResponseData)
return d, ok
}
```
```go Gin theme={"system"}
package auth
import (
"github.com/gin-gonic/gin"
)
func (v *Verifier) Gin(permissions string) gin.HandlerFunc {
return func(c *gin.Context) {
res := v.Verify(c.Request.Context(), c.GetHeader("Authorization"), permissions)
if res.Err != nil {
c.AbortWithStatusJSON(res.Status, gin.H{"error": "authentication unavailable"})
return
}
if !res.Data.Valid {
c.AbortWithStatusJSON(res.Status, gin.H{"error": res.Reason})
return
}
c.Set("unkey", res.Data)
c.Next()
}
}
```
```go Echo theme={"system"}
package auth
import (
"github.com/labstack/echo/v4"
)
func (v *Verifier) Echo(permissions string) echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
res := v.Verify(c.Request().Context(), c.Request().Header.Get("Authorization"), permissions)
if res.Err != nil {
return c.JSON(res.Status, map[string]string{"error": "authentication unavailable"})
}
if !res.Data.Valid {
return c.JSON(res.Status, map[string]string{"error": res.Reason})
}
c.Set("unkey", res.Data)
return next(c)
}
}
}
```
## Using it
```go wiring theme={"system"}
verifier := auth.New(os.Getenv("UNKEY_ROOT_KEY"))
// net/http
mux := http.NewServeMux()
mux.Handle("/items", verifier.Middleware("")(http.HandlerFunc(listItems)))
mux.Handle("/admin", verifier.Middleware("admin.read")(http.HandlerFunc(admin)))
// Gin
r := gin.Default()
r.GET("/items", verifier.Gin(""), func(c *gin.Context) {
data := c.MustGet("unkey").(components.V2KeysVerifyKeyResponseData)
c.JSON(200, gin.H{"keyId": data.KeyID})
})
// Echo
e := echo.New()
e.GET("/items", listItemsEcho, verifier.Echo(""))
```
To forward rate limit state to clients, read `res.Data.Ratelimits`. Each entry has `Limit`, `Remaining`, and `Reset` (Unix milliseconds) for one checked limit.
## Related
* [Go guide](/docs/api-management/guides/go) for creating keys and the full walk-through.
* [Verifying keys](/docs/api-management/keys/verifying-keys) for the fields on `V2KeysVerifyKeyResponseData`.
# Cookbook
Source: https://www.unkey.com/docs/api-management/cookbook/index
Recipes for rate limiting, billing, and framework middleware built on Unkey.
Each recipe solves one common problem from start to finish. They assume you already have a keyspace, a root key, and a request handler that calls `keys.verifyKey`. If you don't yet, start with a [framework guide](/docs/api-management/guides/nextjs).
Share one limit across all of a user's keys with identities, and give tiers different limits with overrides.
Separate budgets for reads and writes, with named key limits or a namespace per route.
Meter requests with credits, refill monthly, charge more for heavy operations, and sell top-ups.
Encode free, pro, and enterprise plans in a key's limits, credits, permissions, and metadata, and change plans in place.
Drop-in authentication middleware for the three common Go HTTP stacks.
A typed FastAPI dependency with permission checks and rate limit headers.
# Per-user rate limits
Source: https://www.unkey.com/docs/api-management/cookbook/per-user-rate-limit
Rate limit a user across all of their keys.
**Outcome:** a user who holds several keys gets one shared rate limit, enforced by `keys.verifyKey` with no extra call, and users on a higher plan get a higher limit without a code change.
You need a root key with the permissions listed on this page. Create one in the dashboard under **Settings > Root Keys**. See [Permission reference](/docs/platform/root-keys/permissions-legacy) for every permission.
## Put the limit on the identity
A limit on a key applies to that key alone. A limit on an identity is shared by all the user's keys. Create the identity with the limit, then create keys with the same `externalId`, and they're linked automatically. `autoApply: true` checks the limit on every verification.
The root key needs `identity.*.create_identity` for the first call, and `api.*.create_key` or `api..create_key` for the second.
```bash create the identity theme={"system"}
curl -X POST https://api.unkey.com/v2/identities.createIdentity \
-H "Authorization: Bearer $UNKEY_ROOT_KEY" \
-H "Content-Type: application/json" \
-d '{
"externalId": "user_123",
"meta": { "plan": "free" },
"ratelimits": [
{ "name": "requests", "limit": 100, "duration": 60000, "autoApply": true }
]
}'
```
```bash create a key for that user theme={"system"}
curl -X POST https://api.unkey.com/v2/keys.createKey \
-H "Authorization: Bearer $UNKEY_ROOT_KEY" \
-H "Content-Type: application/json" \
-d '{ "apiId": "api_...", "externalId": "user_123" }'
```
Your handler doesn't change. `keys.verifyKey` returns `code: RATE_LIMITED` when the shared limit is used up, and `data.ratelimits` has `remaining` and `reset` so you can send `X-RateLimit-*` headers.
```ts verify and forward limit headers theme={"system"}
const result = await unkey.keys.verifyKey({ key });
const rl = result.data.ratelimits?.find((r) => r.name === "requests");
const headers = rl
? {
"X-RateLimit-Limit": String(rl.limit),
"X-RateLimit-Remaining": String(rl.remaining),
"X-RateLimit-Reset": String(rl.reset),
}
: {};
if (!result.data.valid) {
return Response.json({ error: result.data.code }, { status: result.data.code === "RATE_LIMITED" ? 429 : 401, headers });
}
```
When a user upgrades, call `identities.updateIdentity` with a new `ratelimits` array (needs `identity.*.update_identity`). It replaces the whole list, so send every limit the user should keep. All their keys pick up the change within about 10 seconds.
## Alternative: the standalone ratelimit API
If you're limiting something other than a key holder (a logged-in session, an IP address, a tenant), use `ratelimit.limit`. The `namespace` names what you're limiting, the `identifier` is the user, and you send the limit with each request. The response is always HTTP 200, so read `data.success`.
```bash curl theme={"system"}
curl -X POST https://api.unkey.com/v2/ratelimit.limit \
-H "Authorization: Bearer $UNKEY_ROOT_KEY" \
-H "Content-Type: application/json" \
-d '{ "namespace": "api.requests", "identifier": "user_123", "limit": 100, "duration": 60000 }'
```
```ts TypeScript theme={"system"}
const rl = await unkey.ratelimit.limit({
namespace: "api.requests",
identifier: userId,
limit: 100,
duration: 60_000,
});
if (!rl.data.success) {
return Response.json({ error: "rate limited" }, { status: 429, headers: { "Retry-After": String(Math.ceil((rl.data.reset - Date.now()) / 1000)) } });
}
```
For plans, use overrides. An override gives one identifier a different limit from the one in the request. Set one when a user upgrades and delete it when they downgrade. `data.overrideId` in the response shows when an override applied. The root key needs `ratelimit.*.limit` to check limits and `ratelimit.*.set_override` to create overrides.
```bash give one user a higher limit theme={"system"}
curl -X POST https://api.unkey.com/v2/ratelimit.setOverride \
-H "Authorization: Bearer $UNKEY_ROOT_KEY" \
-H "Content-Type: application/json" \
-d '{ "namespace": "api.requests", "identifier": "user_123", "limit": 1000, "duration": 60000 }'
```
## Related
* [Creating keys](/docs/api-management/keys/creating-keys) for the `externalId` and `ratelimits` fields.
* [Verifying keys](/docs/api-management/keys/verifying-keys) for the `ratelimits` response entries.
* [Identities](/docs/api-management/identities/overview), [standalone rate limiting](/docs/api-management/ratelimiting/overview), and [overrides](/docs/api-management/ratelimiting/overrides) are documented under API Management.
# Tiered subscriptions
Source: https://www.unkey.com/docs/api-management/cookbook/tiered-subscriptions
Model free, pro, and enterprise plans on a key.
**Outcome:** each plan is a small object in your code, a key carries its plan's limits, credits, permissions, and a `meta.plan` label, and upgrading a customer is one `keys.updateKey` call that takes effect within about ten seconds.
You need a root key with the permissions listed on this page. Create one in the dashboard under **Settings > Root Keys**. See [Permission reference](/docs/platform/root-keys/permissions-legacy) for every permission.
## Define the tiers
Define each plan once and build every key from it. Rate limits cap how often, credits cap how much, permissions unlock features, and `meta` returns the plan name on every verification so your handlers don't need a database lookup.
```ts src/plans.ts theme={"system"}
export const PLANS = {
free: {
ratelimits: [{ name: "requests", limit: 60, duration: 60_000, autoApply: true }],
credits: { remaining: 1_000, refill: { interval: "monthly" as const, amount: 1_000, refillDay: 1 } },
permissions: ["items.read"],
},
pro: {
ratelimits: [{ name: "requests", limit: 600, duration: 60_000, autoApply: true }],
credits: { remaining: 100_000, refill: { interval: "monthly" as const, amount: 100_000, refillDay: 1 } },
permissions: ["items.read", "items.write", "exports.create"],
},
enterprise: {
ratelimits: [{ name: "requests", limit: 6_000, duration: 60_000, autoApply: true }],
credits: null,
permissions: ["items.read", "items.write", "exports.create", "webhooks.manage"],
},
};
export type Plan = keyof typeof PLANS;
```
You don't have to create these permissions first. `keys.createKey` creates any that don't exist. `keys.setPermissions` does too, but only if the root key has `rbac.*.create_permission`; otherwise it returns 403. Don't use `*` in slugs, because it isn't a wildcard.
## Create a key for a plan
```ts src/keys.ts theme={"system"}
import { Unkey } from "@unkey/api";
import { PLANS, type Plan } from "./plans";
const unkey = new Unkey({ rootKey: process.env.UNKEY_ROOT_KEY ?? "" });
export async function createKeyFor(customerId: string, plan: Plan) {
const tier = PLANS[plan];
const created = await unkey.keys.createKey({
apiId: process.env.UNKEY_API_ID ?? "",
externalId: customerId,
meta: { plan },
ratelimits: tier.ratelimits,
permissions: tier.permissions,
...(tier.credits ? { credits: tier.credits } : {}),
});
return created.data; // key (plaintext, show once) and keyId
}
```
## Read the plan during verification
Gate features with a `permissions` query so Unkey makes the decision. Use `meta.plan` only for softer choices, such as page size.
```ts src/handler.ts theme={"system"}
const result = await unkey.keys.verifyKey({ key, permissions: "exports.create" });
if (!result.data.valid) {
// INSUFFICIENT_PERMISSIONS here means "not on a plan that includes exports"
return Response.json({ error: result.data.code }, { status: result.data.code === "INSUFFICIENT_PERMISSIONS" ? 403 : 401 });
}
const plan = (result.data.meta as { plan?: string } | undefined)?.plan ?? "free";
const pageSize = plan === "enterprise" ? 1000 : 100;
```
## Change a customer's plan
`keys.updateKey` changes a key in place. Fields you leave out keep their value, `ratelimits` replaces the whole list, and `credits: null` removes the cap and any refill. Use `keys.setPermissions` for permissions. It replaces the key's permissions, so a downgrade removes what the higher plan granted.
```ts TypeScript theme={"system"}
export async function changePlan(keyId: string, plan: Plan) {
const tier = PLANS[plan];
await unkey.keys.updateKey({
keyId,
meta: { plan },
ratelimits: tier.ratelimits,
credits: tier.credits,
});
await unkey.keys.setPermissions({ keyId, permissions: tier.permissions });
}
```
```bash curl (downgrade to free) theme={"system"}
curl -X POST https://api.unkey.com/v2/keys.updateKey \
-H "Authorization: Bearer $UNKEY_ROOT_KEY" \
-H "Content-Type: application/json" \
-d '{
"keyId": "key_...",
"meta": { "plan": "free" },
"ratelimits": [{ "name": "requests", "limit": 60, "duration": 60000, "autoApply": true }],
"credits": { "remaining": 1000, "refill": { "interval": "monthly", "amount": 1000, "refillDay": 1 } }
}'
curl -X POST https://api.unkey.com/v2/keys.setPermissions \
-H "Authorization: Bearer $UNKEY_ROOT_KEY" \
-H "Content-Type: application/json" \
-d '{ "keyId": "key_...", "permissions": ["items.read"] }'
```
Changes take about 10 seconds to reach verification. Credit changes apply right away. If a customer has several keys, put the plan's rate limits on their identity so all keys share one limit, as shown in [Per-user rate limits](/docs/api-management/cookbook/per-user-rate-limit).
## Related
* [Creating keys](/docs/api-management/keys/creating-keys) for every field and its bounds.
* [Usage-based billing with credits](/docs/api-management/cookbook/usage-billing) for metering inside a plan.
* Roles and permissions are documented under [Roles and permissions](/docs/api-management/authorization/roles-and-permissions).
# Usage-based billing with credits
Source: https://www.unkey.com/docs/api-management/cookbook/usage-billing
Meter and bill API usage with credits.
**Outcome:** each customer gets a monthly allowance, heavy operations cost more than light ones, a customer who runs out gets a 402, and a purchased top-up works on the next request. Unkey does the counting.
You need a root key with the permissions listed on this page. Create one in the dashboard under **Settings > Root Keys**. See [Permission reference](/docs/platform/root-keys/permissions-legacy) for every permission.
## Give the key a monthly allowance
`credits.remaining` is the balance and `credits.refill` resets it on a schedule. A monthly refill needs a `refillDay` (in shorter months it runs on the last day). A refill resets the balance to `refill.amount` rather than adding to it, so unused credits don't roll over.
```bash 10,000 requests a month, reset on the 1st theme={"system"}
curl -X POST https://api.unkey.com/v2/keys.createKey \
-H "Authorization: Bearer $UNKEY_ROOT_KEY" \
-H "Content-Type: application/json" \
-d '{
"apiId": "api_...",
"externalId": "customer_123",
"credits": {
"remaining": 10000,
"refill": { "interval": "monthly", "amount": 10000, "refillDay": 1 }
}
}'
```
## Charge per operation
Each verification spends `credits.cost`, 1 by default. Set the cost per operation: a bulk export might cost 50 and a status check 0. A request that fails any other check, such as a rate limit or permission, spends nothing.
```ts src/billing.ts theme={"system"}
import { Unkey } from "@unkey/api";
const unkey = new Unkey({ rootKey: process.env.UNKEY_ROOT_KEY ?? "" });
const COST: Record = {
"GET /v1/status": 0,
"GET /v1/items": 1,
"POST /v1/export": 50,
};
export async function meter(key: string, route: string) {
const result = await unkey.keys.verifyKey({
key,
credits: { cost: COST[route] ?? 1 },
tags: [`route=${route}`],
});
if (!result.data.valid) {
if (result.data.code === "USAGE_EXCEEDED") {
// data.credits is the unchanged balance; nothing was deducted.
return { ok: false as const, status: 402, remaining: result.data.credits ?? 0 };
}
return { ok: false as const, status: 401, remaining: result.data.credits };
}
// data.credits is the balance after this call. Surface it so clients can plan.
return { ok: true as const, remaining: result.data.credits, customer: result.data.identity?.externalId };
}
```
Return the balance in a header such as `X-Credits-Remaining` so customers can see it. Analytics records the credits spent per key and identity, so you can build invoices from it.
## Sell top-ups and change plans
`keys.updateCredits` changes the balance and leaves the refill schedule alone. Use `increment` when a customer buys credits, `decrement` to take some back, and `set` for a new balance. `set` with `value: null` makes the key unlimited, for example for an enterprise plan with no cap.
```bash add 5,000 purchased credits theme={"system"}
curl -X POST https://api.unkey.com/v2/keys.updateCredits \
-H "Authorization: Bearer $UNKEY_ROOT_KEY" \
-H "Content-Type: application/json" \
-d '{ "keyId": "key_...", "operation": "increment", "value": 5000 }'
```
```ts TypeScript theme={"system"}
await unkey.keys.updateCredits({ keyId, operation: "increment", value: 5000 });
```
To change the monthly allowance, use `keys.updateKey` with a new `credits.refill.amount`. It applies at the next refill.
A refill never lowers a balance, but it doesn't add to one either. If a top-up leaves the customer below their allowance, the next refill wipes it out. If it leaves them above, the refill skips them. To make top-ups always add on top, track them in your own records and re-apply them after the refill day, or sell them as a separate key with no refill.
## Related
* [Credits and refill](/docs/api-management/keys/credits-and-refill) for every field and the refill schedule.
* [Verifying keys](/docs/api-management/keys/verifying-keys) for why credits are checked last.
* [Tiered subscriptions](/docs/api-management/cookbook/tiered-subscriptions) for combining credits with limits and permissions per plan.
# Keyspaces, keys, identities, and root keys
Source: https://www.unkey.com/docs/api-management/get-started/concepts
The four objects you work with and how they fit together.
To issue and verify API keys for a public API, use API Management. A keyspace holds the keys, `keys.createKey` issues one, and your server calls `keys.verifyKey` on each request. You don't build the hash lookup yourself.
API Management has four objects. A keyspace groups keys. A key is what your user sends you. An identity ties several keys to one user or organization. A root key is what you send to Unkey. Credits, rate limits, permissions, and analytics are all settings on, or views of, these four.
## Keyspace
A keyspace is the container for related keys. You might have one per product, one per environment such as production and staging, or one per pricing tier. Every key belongs to exactly one keyspace, and you name the keyspace by its API ID (`api_...`) whenever you create a key.
A keyspace sets defaults for new keys (a prefix and a key length). It can also have delete protection and an IP allow list that applies to every key in it. It has a second identifier, the Keyspace ID (`ks_...`), which analytics and the customer portal use. You'll find both IDs on the keyspace's settings page. [Keyspaces](/docs/api-management/keyspaces/overview) explains when to create more than one.
## Key
A key is a random string, with an optional prefix, that you give to a user of your application. Unkey stores only a hash of the key, never the key itself, unless you turn on [recoverable keys](/docs/api-management/keys/recoverable-keys). On every request, your backend sends the key to `keys.verifyKey` and Unkey runs the checks set on it.
Every check is optional: whether the key is enabled, when it expires, how many credits it has left, which rate limits apply, and which permissions and roles it has. A key can also carry JSON metadata that comes back on every verification, so your backend can read a plan tier or feature flag without a database lookup. [Creating keys](/docs/api-management/keys/creating-keys) lists each field with its bounds.
## Identity
An identity is one user, organization, or service account in your system, named by an `externalId` you choose. Keys linked to an identity share its rate limits and metadata. Pass `externalId` when you create a key, and Unkey creates the identity if needed and links the key, so most apps never call the identity endpoints directly. [Identities](/docs/api-management/identities/overview) covers shared rate limits and metadata in detail.
## Root key
A root key authenticates you, not your users. Every call to `api.unkey.com` sends a root key in the `Authorization: Bearer` header, and its permissions decide which calls succeed. Create root keys in the dashboard under **Settings > Root Keys**. Permissions such as `api.*.create_key` or `api..verify_key` apply to every keyspace or to one. See [Root keys](/docs/platform/root-keys/overview) for how permissions are granted and rotated.
## How the objects relate
```text theme={"system"}
Workspace
└── Keyspace (api_..., ks_...)
├── defaults: prefix, bytes, encrypted storage, IP allow list, delete protection
└── Key (key_...)
├── enabled, expires, credits, rate limits, permissions, roles, meta
└── Identity (externalId) shared rate limits and meta across keys
Root key (unkey_...) authenticates your calls; permissions name keyspaces by api_ id
```
So one request from your user involves two keys: their key, which they send to you, and your root key, which you send to Unkey to verify theirs. Your user's key never grants access to the Unkey API, and your root key should never leave your servers.
## Next steps
Put the four objects to work in a few minutes.
Decide how many keyspaces you need and what they store.
# Issue and verify your first key
Source: https://www.unkey.com/docs/api-management/get-started/quickstart
Create a keyspace and verify your first key in a few minutes.
Issue API keys for your public API with `keys.createKey`, then verify each request with `keys.verifyKey`. Both are HTTPS calls to `api.unkey.com`, from whatever language and host you already use. If you already have keys in your own database, [Migrating keys](/docs/api-management/keys/migrating-keys) shows how to import the hashes, so users keep the same keys and you stop storing the plaintext.
Create a keyspace, issue a key, and verify it from your backend.
You need a root key with the permissions listed on this page. Create one in the dashboard under **Settings > Root Keys**. See [Permission reference](/docs/platform/root-keys/permissions-legacy) for every permission.
The root key you create needs `api.*.create_key` and `api.*.verify_key`. Pick your language with the switcher. Only the steps that call the API change.
A keyspace holds the keys for one product, environment, or tier. In the dashboard, open **Keyspaces (APIs)** in the sidebar, click **Create keyspace**, and give it a name. Copy the **API ID** (`api_...`) from its settings page. You pass it when you create keys.
Call `keys.createKey` with the API ID. The `name` is only for you. Your users never see it.
```bash theme={"system"}
curl -X POST https://api.unkey.com/v2/keys.createKey \
-H "Authorization: Bearer $UNKEY_ROOT_KEY" \
-H "Content-Type: application/json" \
-d '{
"apiId": "api_...",
"name": "My first key"
}'
```
This is the only time you see the key. Copy `data.key` now and give it to your user.
```json theme={"system"}
{
"meta": { "requestId": "req_..." },
"data": {
"keyId": "key_...",
"key": "..."
}
}
```
This is the call your backend makes on every incoming request. There's no `apiId` field because the key alone identifies its keyspace.
```bash theme={"system"}
curl -X POST https://api.unkey.com/v2/keys.verifyKey \
-H "Authorization: Bearer $UNKEY_ROOT_KEY" \
-H "Content-Type: application/json" \
-d '{ "key": "" }'
```
```bash theme={"system"}
npm install @unkey/api
```
```typescript create-key.ts theme={"system"}
import { Unkey } from "@unkey/api";
const unkey = new Unkey({ rootKey: process.env.UNKEY_ROOT_KEY });
const { data } = await unkey.keys.createKey({
apiId: "api_...",
name: "My first key",
});
// Shown once. Store it or send it to your user now.
console.log(data.key);
```
There's no `apiId` field because the key alone identifies its keyspace.
```typescript verify-key.ts theme={"system"}
const { data } = await unkey.keys.verifyKey({ key: incomingKey });
if (!data.valid) {
// data.code says why: NOT_FOUND, EXPIRED, RATE_LIMITED, ...
throw new Error(`denied: ${data.code}`);
}
console.log(data.keyId);
```
```bash theme={"system"}
go get github.com/unkeyed/sdks/api/go/v3@latest
```
```go main.go theme={"system"}
package main
import (
"context"
"fmt"
"os"
unkey "github.com/unkeyed/sdks/api/go/v3"
"github.com/unkeyed/sdks/api/go/v3/models/components"
)
func main() {
client := unkey.New(unkey.WithSecurity(os.Getenv("UNKEY_ROOT_KEY")))
res, err := client.Keys.CreateKey(context.Background(), components.V2KeysCreateKeyRequestBody{
APIID: "api_...",
Name: unkey.String("My first key"),
})
if err != nil {
panic(err)
}
// Shown once. Store it or send it to your user now.
fmt.Println(res.V2KeysCreateKeyResponseBody.Data.Key)
}
```
There's no `apiId` field because the key alone identifies its keyspace.
```go verify.go theme={"system"}
res, err := client.Keys.VerifyKey(ctx, components.V2KeysVerifyKeyRequestBody{
Key: incomingKey,
})
if err != nil {
return err
}
data := res.V2KeysVerifyKeyResponseBody.Data
if !data.Valid {
// data.Code says why: NOT_FOUND, EXPIRED, RATE_LIMITED, ...
return fmt.Errorf("denied: %s", data.Code)
}
```
```bash theme={"system"}
pip install unkey.py
```
```python create_key.py theme={"system"}
import os
from unkey.py import Unkey
with Unkey(root_key=os.environ["UNKEY_ROOT_KEY"]) as unkey:
res = unkey.keys.create_key(api_id="api_...", name="My first key")
# Shown once. Store it or send it to your user now.
print(res.data.key)
```
There's no `api_id` argument because the key alone identifies its keyspace.
```python verify_key.py theme={"system"}
res = unkey.keys.verify_key(key=incoming_key)
if not res.data.valid:
# res.data.code says why: NOT_FOUND, EXPIRED, RATE_LIMITED, ...
raise PermissionError(f"denied: {res.data.code}")
```
## How you read the verification response
Verification returns HTTP 200 even when the key is rejected. `data.valid` tells you whether the key passed. When it didn't, `data.code` says why.
```json theme={"system"}
{
"meta": { "requestId": "req_..." },
"data": {
"valid": true,
"code": "VALID",
"keyId": "key_...",
"name": "My first key",
"enabled": true
}
}
```
The response gets more fields as you set more on the key, such as `credits`, `ratelimits`, `meta`, and `identity`. [Verifying keys](/docs/api-management/keys/verifying-keys) lists every field and every `code` value.
## Next steps
Prefixes, expiry, credits, rate limits, permissions, and metadata on one key.
The order of checks, the `code` enum, and caching behavior.
Sell a fixed number of requests and refill them on a schedule.
How the objects you just used fit together.
If you host your app on Unkey, the gateway's key-auth policy can run this verification before a request reaches your code.
# Bun guide
Source: https://www.unkey.com/docs/api-management/guides/bun
Verify API keys in a Bun.serve handler.
Protect a Bun HTTP server with API keys. `Bun.serve` has one `fetch` handler, so you call a verify helper at the top of it, and `keys.verifyKey` decides whether the request goes on.
You need a root key with the permissions listed on this page. Create one in the dashboard under **Settings > Root Keys**. See [Permission reference](/docs/platform/root-keys/permissions-legacy) for every permission.
In the dashboard, go to **Settings > Root Keys** and create a root key with `api.*.create_api`, `api.*.create_key`, and `api.*.verify_key`. Put it in your server's `UNKEY_ROOT_KEY` environment variable. Never send it to a browser or mobile app.
Then create a keyspace, which holds your keys. Use **Keyspaces (APIs)** in the dashboard, or `apis.createApi`:
```bash create a keyspace theme={"system"}
curl -X POST https://api.unkey.com/v2/apis.createApi \
-H "Authorization: Bearer $UNKEY_ROOT_KEY" \
-H "Content-Type: application/json" \
-d '{ "name": "my-api" }'
```
Keep the `data.apiId` from the response. You need it to create keys. See [Root keys](/docs/platform/root-keys/overview) for more on root keys.
`@unkey/api` (version ) runs unchanged on Bun.
```bash theme={"system"}
bun add @unkey/api
```
Create a key from your backend when a user signs up or asks for one. `externalId` links the key to the user, so verification tells you who's calling. `meta` comes back on every verification. You get `data.key` only once: show it to the user and store only `data.keyId`.
```bash curl theme={"system"}
curl -X POST https://api.unkey.com/v2/keys.createKey \
-H "Authorization: Bearer $UNKEY_ROOT_KEY" \
-H "Content-Type: application/json" \
-d '{ "apiId": "api_...", "prefix": "sk_live", "externalId": "user_123", "meta": { "plan": "free" } }'
```
```ts TypeScript theme={"system"}
import { Unkey } from "@unkey/api";
const unkey = new Unkey({ rootKey: process.env.UNKEY_ROOT_KEY ?? "" });
const created = await unkey.keys.createKey({
apiId: process.env.UNKEY_API_ID ?? "",
prefix: "sk_live",
externalId: "user_123",
meta: { plan: "free" },
});
// created.data.key is the plaintext, created.data.keyId the handle you keep
```
Every field is described in [Creating keys](/docs/api-management/keys/creating-keys).
The helper returns either the verification data or a `Response` to send back. Its optional second argument is a permission query, for routes that need more than a valid key. You can read the root key from `Bun.env` or `process.env`.
```ts server.ts theme={"system"}
import { Unkey } from "@unkey/api";
import * as errors from "@unkey/api/models/errors";
const unkey = new Unkey({ rootKey: Bun.env.UNKEY_ROOT_KEY ?? "" });
async function authenticate(req: Request, permissions?: string) {
const auth = req.headers.get("authorization") ?? "";
const key = auth.startsWith("Bearer ") ? auth.slice("Bearer ".length) : "";
if (!key) {
return Response.json({ error: "missing API key" }, { status: 401 });
}
try {
const result = await unkey.keys.verifyKey({ key, permissions });
if (!result.data.valid) {
return Response.json({ error: result.data.code }, { status: statusFor(result.data.code) });
}
return result.data;
} catch (err) {
if (err instanceof errors.UnkeyError) {
console.error("unkey", err.statusCode, err.message);
}
return Response.json({ error: "authentication unavailable" }, { status: 503 });
}
}
function statusFor(code: string): number {
switch (code) {
case "RATE_LIMITED":
return 429;
case "USAGE_EXCEEDED":
return 402;
case "FORBIDDEN":
case "INSUFFICIENT_PERMISSIONS":
return 403;
default:
return 401;
}
}
Bun.serve({
port: 3000,
async fetch(req) {
const url = new URL(req.url);
if (url.pathname === "/items") {
const auth = await authenticate(req);
if (auth instanceof Response) return auth;
return Response.json({ items: [], owner: auth.identity?.externalId });
}
if (url.pathname === "/exports") {
const auth = await authenticate(req, "exports.create");
if (auth instanceof Response) return auth;
return Response.json({ started: true });
}
return new Response("not found", { status: 404 });
},
});
```
```bash theme={"system"}
bun run server.ts
curl http://localhost:3000/items -H "Authorization: Bearer sk_live_..."
```
`keys.verifyKey` returns HTTP 200 for every outcome. Check `data.valid`, then `data.code` for the reason, and return the matching status from your API:
| `data.code` | Meaning | Return |
| - | - | - |
| `VALID` | Every check passed. | continue |
| `NOT_FOUND` | No such key, or your root key may not verify this keyspace. | 401 |
| `DISABLED` | The key was disabled. | 401 |
| `EXPIRED` | The key's expiry passed. | 401 |
| `FORBIDDEN` | Client IP not on the keyspace allow list, or the workspace is disabled. | 403 |
| `INSUFFICIENT_PERMISSIONS` | The `permissions` query was not satisfied. | 403 |
| `RATE_LIMITED` | A rate limit on the key or its identity was exceeded. | 429 |
| `USAGE_EXCEEDED` | The key has no credits left. | 402 or 429 |
Remaining credits are in `data.credits`, and each checked rate limit is in `data.ratelimits` with `remaining` and `reset`. The call only fails with an HTTP error (which the SDKs throw) when the call itself is wrong, such as a bad root key or a malformed body. See [Verifying keys](/docs/api-management/keys/verifying-keys) for every field.
Every request field, the order of checks, and every response field.
Meter usage per key and refill balances on a schedule.
Copy-ready recipes for rate limits, billing, and subscription tiers.
# Cloudflare Workers guide
Source: https://www.unkey.com/docs/api-management/guides/cloudflare-workers
Verify API keys at the edge in a Cloudflare Worker.
Protect a Cloudflare Worker with API keys. The Worker's `fetch` handler calls `keys.verifyKey` on each request. Store the root key as a Worker secret, never in `wrangler.toml`.
You need a root key with the permissions listed on this page. Create one in the dashboard under **Settings > Root Keys**. See [Permission reference](/docs/platform/root-keys/permissions-legacy) for every permission.
In the dashboard, go to **Settings > Root Keys** and create a root key with `api.*.create_api`, `api.*.create_key`, and `api.*.verify_key`. Export it as `UNKEY_ROOT_KEY` in your shell for the `curl` commands. You'll store it as a Worker secret in step 2. Never send it to a browser or mobile app.
Then create a keyspace, which holds your keys. Use **Keyspaces (APIs)** in the dashboard, or `apis.createApi`:
```bash create a keyspace theme={"system"}
curl -X POST https://api.unkey.com/v2/apis.createApi \
-H "Authorization: Bearer $UNKEY_ROOT_KEY" \
-H "Content-Type: application/json" \
-d '{ "name": "my-api" }'
```
Keep the `data.apiId` from the response. You need it to create keys. See [Root keys](/docs/platform/root-keys/overview) for more on root keys.
`@unkey/api` (version ) runs on Workers with no polyfills.
```bash theme={"system"}
npm install @unkey/api
npx wrangler secret put UNKEY_ROOT_KEY
```
Your code reads it as `env.UNKEY_ROOT_KEY`. If your Worker also creates keys, add the keyspace ID from step 1 as an `UNKEY_API_ID` variable in `wrangler.toml`. It isn't secret.
Create a key when a user signs up or asks for one. `externalId` links the key to the user, so verification tells you who's calling. `meta` comes back on every verification. You get `data.key` only once: show it to the user and store only `data.keyId`.
Workers have no `process.env`, so read the secret and keyspace ID from the handler's `env` argument.
```bash curl theme={"system"}
curl -X POST https://api.unkey.com/v2/keys.createKey \
-H "Authorization: Bearer $UNKEY_ROOT_KEY" \
-H "Content-Type: application/json" \
-d '{ "apiId": "api_...", "prefix": "sk_live", "externalId": "user_123", "meta": { "plan": "free" } }'
```
```ts src/issue.ts theme={"system"}
import { Unkey } from "@unkey/api";
interface Env {
UNKEY_ROOT_KEY: string;
UNKEY_API_ID: string;
}
// Call this from a route your own session check already protects.
export async function issueKey(env: Env, userId: string): Promise {
const unkey = new Unkey({ rootKey: env.UNKEY_ROOT_KEY });
const created = await unkey.keys.createKey({
apiId: env.UNKEY_API_ID,
prefix: "sk_live",
externalId: userId,
meta: { plan: "free" },
});
// created.data.key is the plaintext, created.data.keyId the handle you keep
return created.data.key;
}
```
Every field is described in [Creating keys](/docs/api-management/keys/creating-keys).
`env` is only available inside the handler, so create the client there. The code below creates it once and reuses it.
```ts src/index.ts theme={"system"}
import { Unkey } from "@unkey/api";
import * as errors from "@unkey/api/models/errors";
interface Env {
UNKEY_ROOT_KEY: string;
}
let unkey: Unkey | undefined;
function client(env: Env): Unkey {
unkey ??= new Unkey({ rootKey: env.UNKEY_ROOT_KEY });
return unkey;
}
function statusFor(code: string): number {
switch (code) {
case "RATE_LIMITED":
return 429;
case "USAGE_EXCEEDED":
return 402;
case "FORBIDDEN":
case "INSUFFICIENT_PERMISSIONS":
return 403;
default:
return 401;
}
}
export default {
async fetch(req: Request, env: Env): Promise {
const auth = req.headers.get("authorization") ?? "";
const key = auth.startsWith("Bearer ") ? auth.slice("Bearer ".length) : "";
if (!key) {
return Response.json({ error: "missing API key" }, { status: 401 });
}
try {
const result = await client(env).keys.verifyKey({
key,
tags: [`colo=${req.cf?.colo ?? "unknown"}`],
});
if (!result.data.valid) {
return Response.json({ error: result.data.code }, { status: statusFor(result.data.code) });
}
return Response.json({ items: [], owner: result.data.identity?.externalId });
} catch (err) {
if (err instanceof errors.UnkeyError) {
console.error("unkey", err.statusCode, err.message);
}
return Response.json({ error: "authentication unavailable" }, { status: 503 });
}
},
} satisfies ExportedHandler;
```
```bash theme={"system"}
npx wrangler dev
curl http://localhost:8787/ -H "Authorization: Bearer sk_live_..."
```
Prefer a router? The [Hono guide](/docs/api-management/guides/hono) runs unchanged on Workers. Read the root key from `c.env.UNKEY_ROOT_KEY` instead of `process.env`.
`keys.verifyKey` returns HTTP 200 for every outcome. Check `data.valid`, then `data.code` for the reason, and return the matching status from your API:
| `data.code` | Meaning | Return |
| - | - | - |
| `VALID` | Every check passed. | continue |
| `NOT_FOUND` | No such key, or your root key may not verify this keyspace. | 401 |
| `DISABLED` | The key was disabled. | 401 |
| `EXPIRED` | The key's expiry passed. | 401 |
| `FORBIDDEN` | Client IP not on the keyspace allow list, or the workspace is disabled. | 403 |
| `INSUFFICIENT_PERMISSIONS` | The `permissions` query was not satisfied. | 403 |
| `RATE_LIMITED` | A rate limit on the key or its identity was exceeded. | 429 |
| `USAGE_EXCEEDED` | The key has no credits left. | 402 or 429 |
Remaining credits are in `data.credits`, and each checked rate limit is in `data.ratelimits` with `remaining` and `reset`. The call only fails with an HTTP error (which the SDKs throw) when the call itself is wrong, such as a bad root key or a malformed body. See [Verifying keys](/docs/api-management/keys/verifying-keys) for every field.
Every request field, the order of checks, and every response field.
Meter usage per key and refill balances on a schedule.
Copy-ready recipes for rate limits, billing, and subscription tiers.
# Express guide
Source: https://www.unkey.com/docs/api-management/guides/express
Protect Express routes with API key middleware.
Protect Express routes with API keys. A middleware reads the bearer token, calls `keys.verifyKey`, rejects bad keys with the right status, and puts the result on `res.locals.unkey` for your handlers.
You need a root key with the permissions listed on this page. Create one in the dashboard under **Settings > Root Keys**. See [Permission reference](/docs/platform/root-keys/permissions-legacy) for every permission.
In the dashboard, go to **Settings > Root Keys** and create a root key with `api.*.create_api`, `api.*.create_key`, and `api.*.verify_key`. Put it in your server's `UNKEY_ROOT_KEY` environment variable. Never send it to a browser or mobile app.
Then create a keyspace, which holds your keys. Use **Keyspaces (APIs)** in the dashboard, or `apis.createApi`:
```bash create a keyspace theme={"system"}
curl -X POST https://api.unkey.com/v2/apis.createApi \
-H "Authorization: Bearer $UNKEY_ROOT_KEY" \
-H "Content-Type: application/json" \
-d '{ "name": "my-api" }'
```
Keep the `data.apiId` from the response. You need it to create keys. See [Root keys](/docs/platform/root-keys/overview) for more on root keys.
`@unkey/api` (version ) is the client for the whole API.
```bash npm theme={"system"}
npm install @unkey/api express
```
```bash pnpm theme={"system"}
pnpm add @unkey/api express
```
Create a key from your backend when a user signs up or asks for one. `externalId` links the key to the user, so verification tells you who's calling. `meta` comes back on every verification. You get `data.key` only once: show it to the user and store only `data.keyId`.
```bash curl theme={"system"}
curl -X POST https://api.unkey.com/v2/keys.createKey \
-H "Authorization: Bearer $UNKEY_ROOT_KEY" \
-H "Content-Type: application/json" \
-d '{ "apiId": "api_...", "prefix": "sk_live", "externalId": "user_123", "meta": { "plan": "free" } }'
```
```ts TypeScript theme={"system"}
import { Unkey } from "@unkey/api";
const unkey = new Unkey({ rootKey: process.env.UNKEY_ROOT_KEY ?? "" });
const created = await unkey.keys.createKey({
apiId: process.env.UNKEY_API_ID ?? "",
prefix: "sk_live",
externalId: "user_123",
meta: { plan: "free" },
});
// created.data.key is the plaintext, created.data.keyId the handle you keep
```
Every field is described in [Creating keys](/docs/api-management/keys/creating-keys).
The middleware verifies the key once per request and puts the result on `res.locals.unkey`. Routes that need extra permissions pass a `permissions` query.
Keep `next()` outside the `try`. Otherwise an error in a route handler is caught here and answered as a 503 authentication failure.
```ts src/unkey.ts theme={"system"}
import { Unkey } from "@unkey/api";
import * as errors from "@unkey/api/models/errors";
import type { V2KeysVerifyKeyResponseBody } from "@unkey/api/models/components";
import type { NextFunction, Request, Response } from "express";
const unkey = new Unkey({ rootKey: process.env.UNKEY_ROOT_KEY ?? "" });
export function requireKey(permissions?: string) {
return async (req: Request, res: Response, next: NextFunction) => {
const auth = req.header("authorization") ?? "";
const key = auth.startsWith("Bearer ") ? auth.slice("Bearer ".length) : "";
if (!key) {
return res.status(401).json({ error: "missing API key" });
}
let result: V2KeysVerifyKeyResponseBody;
try {
result = await unkey.keys.verifyKey({
key,
permissions,
tags: [`endpoint=${req.path}`, `method=${req.method}`],
});
} catch (err) {
if (err instanceof errors.UnkeyError) {
console.error("unkey", err.statusCode, err.message);
}
return res.status(503).json({ error: "authentication unavailable" });
}
if (!result.data.valid) {
return res.status(statusFor(result.data.code)).json({ error: result.data.code });
}
res.locals.unkey = result.data;
next();
};
}
function statusFor(code: string): number {
switch (code) {
case "RATE_LIMITED":
return 429;
case "USAGE_EXCEEDED":
return 402;
case "FORBIDDEN":
case "INSUFFICIENT_PERMISSIONS":
return 403;
default:
return 401;
}
}
```
```ts src/server.ts theme={"system"}
import express from "express";
import { requireKey } from "./unkey";
const app = express();
app.get("/items", requireKey(), (_req, res) => {
res.json({ items: [], owner: res.locals.unkey.identity?.externalId });
});
app.post("/items", requireKey("items.write"), (_req, res) => {
res.status(201).json({ ok: true });
});
app.listen(3000);
```
```bash theme={"system"}
curl http://localhost:3000/items -H "Authorization: Bearer sk_live_..."
```
`keys.verifyKey` returns HTTP 200 for every outcome. Check `data.valid`, then `data.code` for the reason, and return the matching status from your API:
| `data.code` | Meaning | Return |
| - | - | - |
| `VALID` | Every check passed. | continue |
| `NOT_FOUND` | No such key, or your root key may not verify this keyspace. | 401 |
| `DISABLED` | The key was disabled. | 401 |
| `EXPIRED` | The key's expiry passed. | 401 |
| `FORBIDDEN` | Client IP not on the keyspace allow list, or the workspace is disabled. | 403 |
| `INSUFFICIENT_PERMISSIONS` | The `permissions` query was not satisfied. | 403 |
| `RATE_LIMITED` | A rate limit on the key or its identity was exceeded. | 429 |
| `USAGE_EXCEEDED` | The key has no credits left. | 402 or 429 |
Remaining credits are in `data.credits`, and each checked rate limit is in `data.ratelimits` with `remaining` and `reset`. The call only fails with an HTTP error (which the SDKs throw) when the call itself is wrong, such as a bad root key or a malformed body. See [Verifying keys](/docs/api-management/keys/verifying-keys) for every field.
Every request field, the order of checks, and every response field.
Meter usage per key and refill balances on a schedule.
Copy-ready recipes for rate limits, billing, and subscription tiers.
# Go guide
Source: https://www.unkey.com/docs/api-management/guides/go
Verify API keys in a Go net/http handler.
Protect a Go HTTP server with API keys, using the official Go SDK, `github.com/unkeyed/sdks/api/go/v3` (version ). For ready-made middleware for `net/http`, Gin, and Echo, see [Go middleware](/docs/api-management/cookbook/go-middleware).
You need a root key with the permissions listed on this page. Create one in the dashboard under **Settings > Root Keys**. See [Permission reference](/docs/platform/root-keys/permissions-legacy) for every permission.
In the dashboard, go to **Settings > Root Keys** and create a root key with `api.*.create_api`, `api.*.create_key`, and `api.*.verify_key`. Put it in your server's `UNKEY_ROOT_KEY` environment variable. Never send it to a browser or mobile app.
Then create a keyspace, which holds your keys. Use **Keyspaces (APIs)** in the dashboard, or `apis.createApi`:
```bash create a keyspace theme={"system"}
curl -X POST https://api.unkey.com/v2/apis.createApi \
-H "Authorization: Bearer $UNKEY_ROOT_KEY" \
-H "Content-Type: application/json" \
-d '{ "name": "my-api" }'
```
Keep the `data.apiId` from the response. You need it to create keys. See [Root keys](/docs/platform/root-keys/overview) for more on root keys.
```bash theme={"system"}
go get github.com/unkeyed/sdks/api/go/v3
```
Create the client once with `unkey.New(unkey.WithSecurity(rootKey))`. It's safe to share across goroutines.
Create a key from your backend when a user signs up or asks for one. `externalId` links the key to the user, so verification tells you who's calling. `meta` comes back on every verification. You get `data.key` only once: show it to the user and store only `data.keyId`.
```bash curl theme={"system"}
curl -X POST https://api.unkey.com/v2/keys.createKey \
-H "Authorization: Bearer $UNKEY_ROOT_KEY" \
-H "Content-Type: application/json" \
-d '{ "apiId": "api_...", "prefix": "sk_live", "externalId": "user_123", "meta": { "plan": "free" } }'
```
```go Go theme={"system"}
package main
import (
"context"
"fmt"
"log"
"os"
unkey "github.com/unkeyed/sdks/api/go/v3"
"github.com/unkeyed/sdks/api/go/v3/models/components"
)
func main() {
client := unkey.New(unkey.WithSecurity(os.Getenv("UNKEY_ROOT_KEY")))
prefix := "sk_live"
externalID := "user_123"
res, err := client.Keys.CreateKey(context.Background(), components.V2KeysCreateKeyRequestBody{
APIID: os.Getenv("UNKEY_API_ID"),
Prefix: &prefix,
ExternalID: &externalID,
Meta: map[string]any{"plan": "free"},
})
if err != nil {
log.Fatal(err)
}
if res.V2KeysCreateKeyResponseBody == nil {
log.Fatal("unkey: empty response body")
}
// res.V2KeysCreateKeyResponseBody.Data.Key is the plaintext, Data.KeyID the handle you keep
fmt.Println(res.V2KeysCreateKeyResponseBody.Data.KeyID)
}
```
Every field is described in [Creating keys](/docs/api-management/keys/creating-keys).
The answer is in `res.V2KeysVerifyKeyResponseBody.Data.Valid` and `.Data.Code`. `V2KeysVerifyKeyResponseBody` is a pointer, so check it for `nil` even when `err` is `nil`. Use the `components.Code*` constants to switch on the outcome. An `error` means the call itself failed, not that the key is invalid.
```go main.go theme={"system"}
package main
import (
"context"
"encoding/json"
"log"
"net/http"
"os"
"strings"
unkey "github.com/unkeyed/sdks/api/go/v3"
"github.com/unkeyed/sdks/api/go/v3/models/components"
)
var client = unkey.New(unkey.WithSecurity(os.Getenv("UNKEY_ROOT_KEY")))
func statusFor(code components.Code) int {
switch code {
case components.CodeRateLimited:
return http.StatusTooManyRequests
case components.CodeUsageExceeded:
return http.StatusPaymentRequired
case components.CodeForbidden, components.CodeInsufficientPermissions:
return http.StatusForbidden
default:
return http.StatusUnauthorized
}
}
func requireKey(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
key := strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer ")
if key == "" || key == r.Header.Get("Authorization") {
http.Error(w, "missing API key", http.StatusUnauthorized)
return
}
res, err := client.Keys.VerifyKey(r.Context(), components.V2KeysVerifyKeyRequestBody{
Key: key,
Tags: []string{"endpoint=" + r.URL.Path, "method=" + r.Method},
})
if err != nil {
log.Printf("unkey: %v", err)
http.Error(w, "authentication unavailable", http.StatusServiceUnavailable)
return
}
if res.V2KeysVerifyKeyResponseBody == nil {
log.Print("unkey: empty response body")
http.Error(w, "authentication unavailable", http.StatusServiceUnavailable)
return
}
data := res.V2KeysVerifyKeyResponseBody.Data
if !data.Valid {
http.Error(w, string(data.Code), statusFor(data.Code))
return
}
ctx := context.WithValue(r.Context(), verificationKey{}, data)
next(w, r.WithContext(ctx))
}
}
type verificationKey struct{}
func main() {
http.HandleFunc("/items", requireKey(func(w http.ResponseWriter, r *http.Request) {
data := r.Context().Value(verificationKey{}).(components.V2KeysVerifyKeyResponseData)
owner := ""
if data.Identity != nil {
owner = data.Identity.ExternalID
}
json.NewEncoder(w).Encode(map[string]any{"items": []string{}, "owner": owner})
}))
log.Fatal(http.ListenAndServe(":3000", nil))
}
```
```bash theme={"system"}
go run . &
curl http://localhost:3000/items -H "Authorization: Bearer sk_live_..."
```
`keys.verifyKey` returns HTTP 200 for every outcome. Check `data.valid`, then `data.code` for the reason, and return the matching status from your API:
| `data.code` | Meaning | Return |
| - | - | - |
| `VALID` | Every check passed. | continue |
| `NOT_FOUND` | No such key, or your root key may not verify this keyspace. | 401 |
| `DISABLED` | The key was disabled. | 401 |
| `EXPIRED` | The key's expiry passed. | 401 |
| `FORBIDDEN` | Client IP not on the keyspace allow list, or the workspace is disabled. | 403 |
| `INSUFFICIENT_PERMISSIONS` | The `permissions` query was not satisfied. | 403 |
| `RATE_LIMITED` | A rate limit on the key or its identity was exceeded. | 429 |
| `USAGE_EXCEEDED` | The key has no credits left. | 402 or 429 |
Remaining credits are in `data.credits`, and each checked rate limit is in `data.ratelimits` with `remaining` and `reset`. The call only fails with an HTTP error (which the SDKs throw) when the call itself is wrong, such as a bad root key or a malformed body. See [Verifying keys](/docs/api-management/keys/verifying-keys) for every field.
Every request field, the order of checks, and every response field.
Meter usage per key and refill balances on a schedule.
Copy-ready recipes for rate limits, billing, and subscription tiers.
# Hono guide
Source: https://www.unkey.com/docs/api-management/guides/hono
Protect Hono routes with API keys.
Protect Hono routes with API keys. The code works on Node, Bun, Deno, and Cloudflare Workers. Only how you read the root key changes. Use the `@unkey/hono` middleware to have verification handled for you, or write a few lines with `@unkey/api` to control every response.
You need a root key with the permissions listed on this page. Create one in the dashboard under **Settings > Root Keys**. See [Permission reference](/docs/platform/root-keys/permissions-legacy) for every permission.
In the dashboard, go to **Settings > Root Keys** and create a root key with `api.*.create_api`, `api.*.create_key`, and `api.*.verify_key`. Put it in your server's `UNKEY_ROOT_KEY` environment variable. Never send it to a browser or mobile app.
Then create a keyspace, which holds your keys. Use **Keyspaces (APIs)** in the dashboard, or `apis.createApi`:
```bash create a keyspace theme={"system"}
curl -X POST https://api.unkey.com/v2/apis.createApi \
-H "Authorization: Bearer $UNKEY_ROOT_KEY" \
-H "Content-Type: application/json" \
-d '{ "name": "my-api" }'
```
Keep the `data.apiId` from the response. You need it to create keys. See [Root keys](/docs/platform/root-keys/overview) for more on root keys.
`@unkey/hono` (version ) is the middleware. `@unkey/api` (version ) is the client for creating keys and for the hand-written version.
```bash npm theme={"system"}
npm install hono @unkey/hono @unkey/api
```
```bash bun theme={"system"}
bun add hono @unkey/hono @unkey/api
```
Create a key from your backend when a user signs up or asks for one. `externalId` links the key to the user, so verification tells you who's calling. `meta` comes back on every verification. You get `data.key` only once: show it to the user and store only `data.keyId`.
```bash curl theme={"system"}
curl -X POST https://api.unkey.com/v2/keys.createKey \
-H "Authorization: Bearer $UNKEY_ROOT_KEY" \
-H "Content-Type: application/json" \
-d '{ "apiId": "api_...", "prefix": "sk_live", "externalId": "user_123", "meta": { "plan": "free" } }'
```
```ts TypeScript theme={"system"}
import { Unkey } from "@unkey/api";
const unkey = new Unkey({ rootKey: process.env.UNKEY_ROOT_KEY ?? "" });
const created = await unkey.keys.createKey({
apiId: process.env.UNKEY_API_ID ?? "",
prefix: "sk_live",
externalId: "user_123",
meta: { plan: "free" },
});
// created.data.key is the plaintext, created.data.keyId the handle you keep
```
Every field is described in [Creating keys](/docs/api-management/keys/creating-keys).
With `@unkey/hono`, the `unkey()` middleware reads the bearer token, verifies it, and stores the result in the `unkey` context variable. `handleInvalidKey` decides the response for a bad key.
`onError` only runs for unexpected responses, such as a 502 from a proxy. A rejected root key, a throttled request, a 500, or a network failure is thrown instead, so catch those in Hono's `app.onError` if you want one response for every authentication failure.
```ts src/index.ts (@unkey/hono) theme={"system"}
import { Hono } from "hono";
import { type UnkeyContext, unkey } from "@unkey/hono";
const app = new Hono<{ Variables: { unkey: UnkeyContext } }>();
app.use(
"/items/*",
unkey({
rootKey: process.env.UNKEY_ROOT_KEY ?? "",
handleInvalidKey: (c) => c.json({ error: "invalid API key" }, 401),
onError: (c, err) => {
console.error("unkey", err.message);
return c.json({ error: "authentication unavailable" }, 503);
},
}),
);
app.get("/items/list", (c) => {
const verification = c.get("unkey");
return c.json({ items: [], owner: verification.data.identity?.externalId });
});
export default app;
```
```ts src/index.ts (@unkey/api) theme={"system"}
import { Hono } from "hono";
import { Unkey } from "@unkey/api";
import * as errors from "@unkey/api/models/errors";
import type {
V2KeysVerifyKeyResponseBody,
V2KeysVerifyKeyResponseData,
} from "@unkey/api/models/components";
const unkey = new Unkey({ rootKey: process.env.UNKEY_ROOT_KEY ?? "" });
const app = new Hono<{ Variables: { unkey: V2KeysVerifyKeyResponseData } }>();
app.use("/items/*", async (c, next) => {
const auth = c.req.header("authorization") ?? "";
const key = auth.startsWith("Bearer ") ? auth.slice("Bearer ".length) : "";
if (!key) return c.json({ error: "missing API key" }, 401);
let result: V2KeysVerifyKeyResponseBody;
try {
result = await unkey.keys.verifyKey({ key });
} catch (err) {
if (err instanceof errors.UnkeyError) console.error("unkey", err.statusCode, err.message);
return c.json({ error: "authentication unavailable" }, 503);
}
if (!result.data.valid) {
return c.json({ error: result.data.code }, statusFor(result.data.code));
}
c.set("unkey", result.data);
await next();
});
app.get("/items/list", (c) => c.json({ items: [], owner: c.get("unkey").identity?.externalId }));
function statusFor(code: string): 401 | 402 | 403 | 429 {
switch (code) {
case "RATE_LIMITED":
return 429;
case "USAGE_EXCEEDED":
return 402;
case "FORBIDDEN":
case "INSUFFICIENT_PERMISSIONS":
return 403;
default:
return 401;
}
}
export default app;
```
Keep `next()` outside the `try`. Otherwise an error in a route handler is caught here and answered as an authentication failure.
The middleware also takes `permissions` (a permission query every key must pass), `tags`, and `getKey` (for keys sent somewhere other than the `Authorization` header).
`keys.verifyKey` returns HTTP 200 for every outcome. Check `data.valid`, then `data.code` for the reason, and return the matching status from your API:
| `data.code` | Meaning | Return |
| - | - | - |
| `VALID` | Every check passed. | continue |
| `NOT_FOUND` | No such key, or your root key may not verify this keyspace. | 401 |
| `DISABLED` | The key was disabled. | 401 |
| `EXPIRED` | The key's expiry passed. | 401 |
| `FORBIDDEN` | Client IP not on the keyspace allow list, or the workspace is disabled. | 403 |
| `INSUFFICIENT_PERMISSIONS` | The `permissions` query was not satisfied. | 403 |
| `RATE_LIMITED` | A rate limit on the key or its identity was exceeded. | 429 |
| `USAGE_EXCEEDED` | The key has no credits left. | 402 or 429 |
Remaining credits are in `data.credits`, and each checked rate limit is in `data.ratelimits` with `remaining` and `reset`. The call only fails with an HTTP error (which the SDKs throw) when the call itself is wrong, such as a bad root key or a malformed body. See [Verifying keys](/docs/api-management/keys/verifying-keys) for every field.
Every request field, the order of checks, and every response field.
Meter usage per key and refill balances on a schedule.
Copy-ready recipes for rate limits, billing, and subscription tiers.
# Next.js guide
Source: https://www.unkey.com/docs/api-management/guides/nextjs
Protect a Next.js route handler with an API key.
Protect a Next.js App Router route handler with API keys. Your server creates keys for users, and every request to the route is checked with `keys.verifyKey` before your handler runs. Verification needs your root key, so do it in route handlers or server actions, never in client components.
You need a root key with the permissions listed on this page. Create one in the dashboard under **Settings > Root Keys**. See [Permission reference](/docs/platform/root-keys/permissions-legacy) for every permission.
In the dashboard, go to **Settings > Root Keys** and create a root key with `api.*.create_api`, `api.*.create_key`, and `api.*.verify_key`. Put it in your server's `UNKEY_ROOT_KEY` environment variable. Never send it to a browser or mobile app.
Then create a keyspace, which holds your keys. Use **Keyspaces (APIs)** in the dashboard, or `apis.createApi`:
```bash create a keyspace theme={"system"}
curl -X POST https://api.unkey.com/v2/apis.createApi \
-H "Authorization: Bearer $UNKEY_ROOT_KEY" \
-H "Content-Type: application/json" \
-d '{ "name": "my-api" }'
```
Keep the `data.apiId` from the response. You need it to create keys. See [Root keys](/docs/platform/root-keys/overview) for more on root keys.
`@unkey/api` (version ) is the client for the whole API. `@unkey/nextjs` (version ) wraps a route handler and verifies for you. You only need it for the wrapper in step 4.
```bash npm theme={"system"}
npm install @unkey/api @unkey/nextjs
```
```bash pnpm theme={"system"}
pnpm add @unkey/api @unkey/nextjs
```
```bash bun theme={"system"}
bun add @unkey/api @unkey/nextjs
```
Create a key from your backend when a user signs up or asks for one. `externalId` links the key to the user, so verification tells you who's calling. `meta` comes back on every verification. You get `data.key` only once: show it to the user and store only `data.keyId`.
```bash curl theme={"system"}
curl -X POST https://api.unkey.com/v2/keys.createKey \
-H "Authorization: Bearer $UNKEY_ROOT_KEY" \
-H "Content-Type: application/json" \
-d '{ "apiId": "api_...", "prefix": "sk_live", "externalId": "user_123", "meta": { "plan": "free" } }'
```
```ts TypeScript theme={"system"}
import { Unkey } from "@unkey/api";
const unkey = new Unkey({ rootKey: process.env.UNKEY_ROOT_KEY ?? "" });
const created = await unkey.keys.createKey({
apiId: process.env.UNKEY_API_ID ?? "",
prefix: "sk_live",
externalId: "user_123",
meta: { plan: "free" },
});
// created.data.key is the plaintext, created.data.keyId the handle you keep
```
Every field is described in [Creating keys](/docs/api-management/keys/creating-keys).
The first handler calls `keys.verifyKey` directly, so you control the response. The second uses `withUnkey`, which reads the bearer token from the `Authorization` header, verifies it, and puts the result in `req.unkey`. Pass `handleInvalidKey` to choose the response for a bad key.
```ts app/api/items/route.ts (@unkey/api) theme={"system"}
import { Unkey } from "@unkey/api";
import * as errors from "@unkey/api/models/errors";
import { NextResponse } from "next/server";
const unkey = new Unkey({ rootKey: process.env.UNKEY_ROOT_KEY ?? "" });
export async function GET(req: Request) {
const auth = req.headers.get("authorization") ?? "";
const key = auth.startsWith("Bearer ") ? auth.slice("Bearer ".length) : "";
if (!key) {
return NextResponse.json({ error: "missing API key" }, { status: 401 });
}
try {
const result = await unkey.keys.verifyKey({
key,
tags: ["endpoint=/api/items", "method=GET"],
});
if (!result.data.valid) {
return NextResponse.json({ error: result.data.code }, { status: statusFor(result.data.code) });
}
return NextResponse.json({ items: [], owner: result.data.identity?.externalId });
} catch (err) {
if (err instanceof errors.UnkeyError) {
// The verification call itself failed (bad root key, throttle, outage), not the user's key.
console.error("unkey", err.statusCode, err.message);
}
return NextResponse.json({ error: "authentication unavailable" }, { status: 503 });
}
}
function statusFor(code: string): number {
switch (code) {
case "RATE_LIMITED":
return 429;
case "USAGE_EXCEEDED":
return 402;
case "FORBIDDEN":
case "INSUFFICIENT_PERMISSIONS":
return 403;
default:
return 401;
}
}
```
```ts app/api/items/route.ts (@unkey/nextjs) theme={"system"}
import { withUnkey } from "@unkey/nextjs";
import { NextResponse } from "next/server";
export const GET = withUnkey(
async (req) => {
// req.unkey holds the verification response; the key is already valid here
return NextResponse.json({ items: [], keyId: req.unkey?.data.keyId });
},
{
rootKey: process.env.UNKEY_ROOT_KEY ?? "",
handleInvalidKey: () => NextResponse.json({ error: "invalid API key" }, { status: 401 }),
onError: (_req, err) => {
console.error("unkey", err.message);
return NextResponse.json({ error: "authentication unavailable" }, { status: 503 });
},
},
);
```
`withUnkey` only calls `onError` for unexpected responses, such as a 502 from a proxy. A rejected root key, a throttled request, a 500, or a network failure is thrown instead, so your route fails with an unhandled error rather than a 503. The hand-written version catches all of these and returns 503.
Test it with the key from step 3:
```bash theme={"system"}
curl http://localhost:3000/api/items -H "Authorization: Bearer sk_live_..."
```
`keys.verifyKey` returns HTTP 200 for every outcome. Check `data.valid`, then `data.code` for the reason, and return the matching status from your API:
| `data.code` | Meaning | Return |
| - | - | - |
| `VALID` | Every check passed. | continue |
| `NOT_FOUND` | No such key, or your root key may not verify this keyspace. | 401 |
| `DISABLED` | The key was disabled. | 401 |
| `EXPIRED` | The key's expiry passed. | 401 |
| `FORBIDDEN` | Client IP not on the keyspace allow list, or the workspace is disabled. | 403 |
| `INSUFFICIENT_PERMISSIONS` | The `permissions` query was not satisfied. | 403 |
| `RATE_LIMITED` | A rate limit on the key or its identity was exceeded. | 429 |
| `USAGE_EXCEEDED` | The key has no credits left. | 402 or 429 |
Remaining credits are in `data.credits`, and each checked rate limit is in `data.ratelimits` with `remaining` and `reset`. The call only fails with an HTTP error (which the SDKs throw) when the call itself is wrong, such as a bad root key or a malformed body. See [Verifying keys](/docs/api-management/keys/verifying-keys) for every field.
Every request field, the order of checks, and every response field.
Meter usage per key and refill balances on a schedule.
Copy-ready recipes for rate limits, billing, and subscription tiers.
# Python guide
Source: https://www.unkey.com/docs/api-management/guides/python
Verify API keys from Python with unkey.py.
Protect a Python API with API keys, using the official SDK, `unkey.py` (version ). The examples use FastAPI, but the client calls are the same in Flask or Django. For a fuller FastAPI setup, see [FastAPI authentication](/docs/api-management/cookbook/fastapi-auth).
You need a root key with the permissions listed on this page. Create one in the dashboard under **Settings > Root Keys**. See [Permission reference](/docs/platform/root-keys/permissions-legacy) for every permission.
In the dashboard, go to **Settings > Root Keys** and create a root key with `api.*.create_api`, `api.*.create_key`, and `api.*.verify_key`. Put it in your server's `UNKEY_ROOT_KEY` environment variable. Never send it to a browser or mobile app.
Then create a keyspace, which holds your keys. Use **Keyspaces (APIs)** in the dashboard, or `apis.createApi`:
```bash create a keyspace theme={"system"}
curl -X POST https://api.unkey.com/v2/apis.createApi \
-H "Authorization: Bearer $UNKEY_ROOT_KEY" \
-H "Content-Type: application/json" \
-d '{ "name": "my-api" }'
```
Keep the `data.apiId` from the response. You need it to create keys. See [Root keys](/docs/platform/root-keys/overview) for more on root keys.
```bash pip theme={"system"}
pip install unkey.py fastapi uvicorn
```
```bash uv theme={"system"}
uv add unkey.py fastapi uvicorn
```
Create the client with `Unkey(root_key=...)`. It works as a context manager, and every method has an `_async` version for `asyncio`.
Create a key from your backend when a user signs up or asks for one. `externalId` links the key to the user, so verification tells you who's calling. `meta` comes back on every verification. You get `data.key` only once: show it to the user and store only `data.keyId`.
```bash curl theme={"system"}
curl -X POST https://api.unkey.com/v2/keys.createKey \
-H "Authorization: Bearer $UNKEY_ROOT_KEY" \
-H "Content-Type: application/json" \
-d '{ "apiId": "api_...", "prefix": "sk_live", "externalId": "user_123", "meta": { "plan": "free" } }'
```
```python Python theme={"system"}
import os
from unkey.py import Unkey
with Unkey(root_key=os.environ["UNKEY_ROOT_KEY"]) as unkey:
created = unkey.keys.create_key(
api_id=os.environ["UNKEY_API_ID"],
prefix="sk_live",
external_id="user_123",
meta={"plan": "free"},
)
# created.data.key is the plaintext, created.data.key_id the handle you keep
print(created.data.key_id)
```
Every field is described in [Creating keys](/docs/api-management/keys/creating-keys).
The dependency reads the bearer token, calls `keys.verify_key`, and raises `HTTPException` with the right status when `data.valid` is false. If the call itself fails, it raises `errors.UnkeyError`, or an `httpx` error for a timeout or connection failure. Catch both and return 503, so callers can tell an outage from a rejected key.
```python main.py theme={"system"}
import os
import httpx
from fastapi import Depends, FastAPI, HTTPException, Request
from unkey.py import Unkey, errors
app = FastAPI()
unkey = Unkey(root_key=os.environ["UNKEY_ROOT_KEY"])
STATUS = {
"RATE_LIMITED": 429,
"USAGE_EXCEEDED": 402,
"FORBIDDEN": 403,
"INSUFFICIENT_PERMISSIONS": 403,
}
def require_key(request: Request):
auth = request.headers.get("authorization", "")
if not auth.startswith("Bearer "):
raise HTTPException(status_code=401, detail="missing API key")
key = auth[len("Bearer "):]
try:
res = unkey.keys.verify_key(
key=key,
tags=[f"endpoint={request.url.path}", f"method={request.method}"],
)
except (errors.UnkeyError, httpx.HTTPError) as e:
# The call failed (bad root key, throttle, outage), not the user's key.
raise HTTPException(status_code=503, detail="authentication unavailable") from e
if not res.data.valid:
code = res.data.code
raise HTTPException(status_code=STATUS.get(code, 401), detail=code)
return res.data
@app.get("/items")
def list_items(verification=Depends(require_key)):
owner = verification.identity.external_id if verification.identity else None
return {"items": [], "owner": owner}
```
For an async app, create the client with `async with Unkey(...)` and call `await unkey.keys.verify_key_async(key=key)`.
```bash theme={"system"}
uvicorn main:app --port 3000
curl http://localhost:3000/items -H "Authorization: Bearer sk_live_..."
```
`keys.verifyKey` returns HTTP 200 for every outcome. Check `data.valid`, then `data.code` for the reason, and return the matching status from your API:
| `data.code` | Meaning | Return |
| - | - | - |
| `VALID` | Every check passed. | continue |
| `NOT_FOUND` | No such key, or your root key may not verify this keyspace. | 401 |
| `DISABLED` | The key was disabled. | 401 |
| `EXPIRED` | The key's expiry passed. | 401 |
| `FORBIDDEN` | Client IP not on the keyspace allow list, or the workspace is disabled. | 403 |
| `INSUFFICIENT_PERMISSIONS` | The `permissions` query was not satisfied. | 403 |
| `RATE_LIMITED` | A rate limit on the key or its identity was exceeded. | 429 |
| `USAGE_EXCEEDED` | The key has no credits left. | 402 or 429 |
Remaining credits are in `data.credits`, and each checked rate limit is in `data.ratelimits` with `remaining` and `reset`. The call only fails with an HTTP error (which the SDKs throw) when the call itself is wrong, such as a bad root key or a malformed body. See [Verifying keys](/docs/api-management/keys/verifying-keys) for every field.
Every request field, the order of checks, and every response field.
Meter usage per key and refill balances on a schedule.
Copy-ready recipes for rate limits, billing, and subscription tiers.
# Identities
Source: https://www.unkey.com/docs/api-management/identities/overview
Tie a user's keys together so they share metadata and rate limits.
An identity is a user, organization, or service account in your system that owns one or more keys. You name it with an `externalId`, usually that user's ID in your own database. Keys linked to an identity share its metadata and its rate limits, so a customer can't get around a limit by creating another key.
## What an identity holds
Unkey's ID, `id_...`. You can use it anywhere you name an identity.
Your ID, 1 to 255 characters matching `^[a-zA-Z0-9_.-]+$`, unique within the workspace. You can also use it anywhere you name an identity, so you rarely need to store `id`.
JSON with at most 100 top-level properties and at most 1 MB. Returned as `identity.meta` when any of the identity's keys is verified.
Up to 50 named limits, each with `id`, `name`, `limit`, `duration`, and `autoApply`, shared by every key of the identity. See [Shared rate limits across keys](/docs/api-management/identities/shared-rate-limits).
## Create an identity by issuing a key
The easiest way to create an identity is to pass `externalId` when you create a key (with `keys.createKey` or `keys.migrateKeys`). Unkey creates the identity if it doesn't exist and links the key. Most apps never call `identities.createIdentity`. Call it when you want metadata or shared limits in place before the first key exists, or when you manage identities from somewhere that doesn't issue keys, such as a billing webhook.
```bash theme={"system"}
curl -X POST https://api.unkey.com/v2/keys.createKey \
-H "Authorization: Bearer $UNKEY_ROOT_KEY" \
-H "Content-Type: application/json" \
-d '{ "apiId": "api_...", "externalId": "user_123" }'
```
Verifying that key now returns the identity:
```json theme={"system"}
{
"meta": { "requestId": "req_..." },
"data": {
"valid": true,
"code": "VALID",
"keyId": "key_...",
"identity": { "id": "id_...", "externalId": "user_123", "meta": {} }
}
}
```
To move a key to another identity, set `externalId` on `keys.updateKey`. Send `null` to unlink it.
You need a root key with the permissions listed on this page. Create one in the dashboard under **Settings > Root Keys**. See [Permission reference](/docs/platform/root-keys/permissions-legacy) for every permission.
You need `identity.*.create_identity`, `identity.*.read_identity`, `identity.*.update_identity`, or `identity.*.delete_identity` for those calls.
## Create an identity
1 to 255 characters, `^[a-zA-Z0-9_.-]+$`. A second identity with the same `externalId` fails with HTTP 409 [`err:unkey:data:identity_already_exists`](/docs/errors/unkey/data/identity_already_exists).
At most 100 top-level properties, and at most 1 MB once serialized to JSON. A larger object is rejected with HTTP 400 [`err:unkey:application:invalid_input`](/docs/errors/unkey/application/invalid_input).
Up to 50 limits, each with a unique `name`. Every entry needs `name` (3 to 128 characters), `limit` (1 or more), `duration` in milliseconds (1000 or more), and `autoApply` (required).
```bash theme={"system"}
curl -X POST https://api.unkey.com/v2/identities.createIdentity \
-H "Authorization: Bearer $UNKEY_ROOT_KEY" \
-H "Content-Type: application/json" \
-d '{
"externalId": "org_acme",
"meta": { "plan": "enterprise", "billingEmail": "ops@acme.example" },
"ratelimits": [ { "name": "requests", "limit": 1000, "duration": 60000, "autoApply": true } ]
}'
```
The response carries `identityId`.
## Find identities
`identities.getIdentity` takes `identity` (the `id` or `externalId`) and returns the fields above. An unknown value returns HTTP 404 `err:unkey:data:identity_not_found`.
`identities.listIdentities` takes `limit` (1 to 100, default 100), `cursor`, and an optional `search`. Search matches any part of the `id` or `externalId`, ignoring case. The response has `data` and `pagination`.
## Update an identity
`identities.updateIdentity` takes `identity` (id or externalId) plus any of:
Replaces all metadata. Omit it to keep the current metadata, or send `{}` to clear it. The same 100-property and 1 MB limits apply.
Replaces the whole list. Limits you leave out are deleted, matching names are updated, and new names are added. The same bounds as create apply. A repeated name fails with HTTP 400 `err:unkey:application:invalid_input`. Leave it out to keep the current limits.
You can't rename an identity's `externalId`. Create a new identity and move the keys with `keys.updateKey` instead.
## Delete an identity
`identities.deleteIdentity` takes `identity` (id or externalId). The keys aren't deleted, but they stop returning `identity` on verification and stop sharing its rate limits. You can reuse the `externalId` for a new identity.
## From the dashboard
Open **Identities** in the sidebar to search, create, edit, or delete identities. Each identity's page shows the verification history of all its keys together.
# Shared rate limits across keys
Source: https://www.unkey.com/docs/api-management/identities/shared-rate-limits
Put one rate limit on a user so all of their keys share it.
A rate limit on a key counts that key alone. If a user has three keys at 100 requests per minute each, they can make 300. Put the limit on the user's identity instead, and all their keys share the same 100. Here's how to set it up.
You need a root key with the permissions listed on this page. Create one in the dashboard under **Settings > Root Keys**. See [Permission reference](/docs/platform/root-keys/permissions-legacy) for every permission.
The root key needs `identity.*.create_identity` (or `identity.*.update_identity` for an existing identity), `api.*.create_key`, and `api.*.verify_key`.
`autoApply: true` makes the limit count on every verification without the caller naming it.
```bash cURL theme={"system"}
curl -X POST https://api.unkey.com/v2/identities.createIdentity \
-H "Authorization: Bearer $UNKEY_ROOT_KEY" \
-H "Content-Type: application/json" \
-d '{
"externalId": "user_123",
"ratelimits": [
{ "name": "requests", "limit": 100, "duration": 60000, "autoApply": true }
]
}'
```
```typescript TypeScript theme={"system"}
const { data } = await unkey.identities.createIdentity({
externalId: "user_123",
ratelimits: [
{ name: "requests", limit: 100, duration: 60_000, autoApply: true },
],
});
console.log(data.identityId);
```
If the identity already exists, for example because a key was created with this `externalId` earlier, the call returns HTTP 409 `err:unkey:data:identity_already_exists`. Use `identities.updateIdentity` with the same `ratelimits` array instead.
Any key created with the same `externalId` is linked to the identity.
```bash cURL theme={"system"}
curl -X POST https://api.unkey.com/v2/keys.createKey \
-H "Authorization: Bearer $UNKEY_ROOT_KEY" \
-H "Content-Type: application/json" \
-d '{ "apiId": "api_...", "externalId": "user_123", "name": "Production" }'
```
```typescript TypeScript theme={"system"}
await unkey.keys.createKey({ apiId: "api_...", externalId: "user_123", name: "Production" });
await unkey.keys.createKey({ apiId: "api_...", externalId: "user_123", name: "Staging" });
```
The identity limit is auto-applied, so a plain verification enforces it. Requests through the production key and the staging key both draw down the same 100 per minute.
```bash cURL theme={"system"}
curl -X POST https://api.unkey.com/v2/keys.verifyKey \
-H "Authorization: Bearer $UNKEY_ROOT_KEY" \
-H "Content-Type: application/json" \
-d '{ "key": "sk_..." }'
```
```typescript TypeScript theme={"system"}
const { data } = await unkey.keys.verifyKey({ key: incomingKey });
if (!data.valid && data.code === "RATE_LIMITED") {
// the user exceeded 100 requests per minute across all their keys
}
console.log(data.identity?.externalId); // "user_123"
```
`data.ratelimits` shows the limits that were checked, with `remaining` and `reset`.
## Several limits on one identity
Add more than one limit when different operations need different budgets. A limit with `autoApply: false` is only checked when a verification names it, so an expensive endpoint can have its own budget.
```bash theme={"system"}
curl -X POST https://api.unkey.com/v2/identities.updateIdentity \
-H "Authorization: Bearer $UNKEY_ROOT_KEY" \
-H "Content-Type: application/json" \
-d '{
"identity": "user_123",
"ratelimits": [
{ "name": "requests", "limit": 500, "duration": 3600000, "autoApply": true },
{ "name": "tokens", "limit": 20000, "duration": 86400000, "autoApply": false }
]
}'
```
Then spend 150 tokens on one call:
```bash theme={"system"}
curl -X POST https://api.unkey.com/v2/keys.verifyKey \
-H "Authorization: Bearer $UNKEY_ROOT_KEY" \
-H "Content-Type: application/json" \
-d '{ "key": "sk_...", "ratelimits": [ { "name": "tokens", "cost": 150 } ] }'
```
Both `requests` (auto-applied) and `tokens` are checked together. If either is exceeded, the verification returns `RATE_LIMITED` and neither is consumed.
## Exceptions for one key
If a key has a limit with the same `name` as one on its identity, the key's limit wins for that key. Use this to give one integration a higher limit than the user's other keys. Details are in [Key and identity rate limits](/docs/api-management/ratelimiting/key-and-identity-limits).
# API management
Source: https://www.unkey.com/docs/api-management/index
Put API keys in front of an app you host anywhere.
Use API Management to issue API keys for a public API and to verify them on each request. Call `keys.createKey` to issue a key, and call `keys.verifyKey` from your backend to check it. Your app can stay on the host you already use. Compute is the other product, and you don't need it for this.
Unkey stores only a hash of each key. Verification runs the checks you configure (expiry, credits, rate limits, permissions, IP allow lists) and returns one verdict. If you already have keys, [Migrating keys](/docs/api-management/keys/migrating-keys) explains how to import their hashes.
Create a keyspace, create a key, and verify it with curl or an SDK in a few minutes.
The four objects you work with and how they fit together.
The container for keys: settings, defaults, and listing keys.
Every field on `keys.createKey`, with bounds and defaults.
What `keys.verifyKey` checks, in which order, and what it returns.
Meter total usage per key and refill it daily or monthly.
Per-key and shared rate limits, identities, roles, and permission queries.
Query verification and rate-limit data with SQL.
See who changed what in the workspace, and stream it out with a log drain.
## How this ties to Compute and Platform
Root keys authenticate your calls to the Unkey API, and both products share the CLI. You'll find both documented in [Platform](/docs/platform). If you also host your app on Unkey, the Compute gateway can verify keys for you with its key-auth policy. See [API key authentication](/docs/compute/gateway/api-key-auth).
# Creating keys
Source: https://www.unkey.com/docs/api-management/keys/creating-keys
Every field you can set when you create a key, with its limits.
Create a key with `keys.createKey`. The only required field is the keyspace's API ID. Everything else is optional and you can change it later with `keys.updateKey`. You get the key back once, in the response.
You need a root key with the permissions listed on this page. Create one in the dashboard under **Settings > Root Keys**. See [Permission reference](/docs/platform/root-keys/permissions-legacy) for every permission.
The root key needs `api.*.create_key` or `api..create_key`. Creating a recoverable key also needs `api.*.encrypt_key` or `api..encrypt_key`. Without it you get HTTP 404 [`err:unkey:data:api_not_found`](/docs/errors/unkey/data/api_not_found), not a permission error, so a missing grant looks like a missing API. See [Root key permissions](/docs/platform/root-keys/permissions).
## Request fields
The keyspace that owns the key, by API ID (`api_...`). 3 to 255 characters matching `^[a-zA-Z0-9_]+$`. You can't move a key to another keyspace later.
1 to 16 characters matching `^[a-zA-Z0-9_]+$`. Becomes the start of the key as `_`, so users can tell keys apart in logs. When omitted, the key uses the keyspace's default prefix if one is set, and otherwise has no prefix. Don't put secrets or customer names in a prefix. It's visible wherever the key is.
1 to 255 characters. An internal label returned by `keys.getKey`, `apis.listKeys`, and `keys.verifyKey`. Unkey never shows it to your users.
16 to 255 random bytes, base58-encoded into the key. When omitted, the key uses the keyspace's default bytes value, or 16 if that's unset. 16 bytes gives 2^128 possibilities, and 32 is plenty for the most sensitive uses.
1 to 255 characters matching `^[a-zA-Z0-9_.-]+$`: your identifier for the user, organization, or tenant that owns the key. Unkey creates an identity with this `externalId` if none exists and links the key to it, so every verification returns `identity.externalId` and any metadata or shared rate limits set on the identity. See [Identities](/docs/api-management/identities/overview).
Arbitrary JSON returned in full on every verification. At most 100 top-level properties. Keep it small: it travels with every `keys.verifyKey` response, and the whole request body is capped at 10 MiB. Don't store secrets here. See [Metadata and tags](/docs/api-management/keys/metadata-and-tags).
Up to 100 role names, each 1 to 128 characters. Every role must already exist in the workspace or the request fails. Roles bundle permissions, and the key gains all of them.
Up to 1000 permission slugs, each 1 to 128 characters matching `^[a-zA-Z0-9_:\-\.\*]+$`. Added to the key on top of what its roles grant. Unlike roles, a slug that doesn't exist yet is created for you. An asterisk is a literal character, not a wildcard.
Unix timestamp in milliseconds, at most `4102444800000` (1 January 2100). After this instant verification returns `code: EXPIRED`. Omit for a key that never expires. See [Key expiration](/docs/api-management/keys/expiration).
Usage metering. `credits.remaining` (integer, 0 or more) is the number of verifications the key can still afford. `credits.refill` optionally restores it on a schedule with `interval` (`daily` or `monthly`), `amount` (1 or more), and `refillDay` (1 to 31, required for `monthly`). `remaining` must be set whenever `refill` is set. Omit the whole object for unlimited usage. See [Credits and refill](/docs/api-management/keys/credits-and-refill).
Up to 50 named rate limits, each with `name` (3 to 128 characters), `limit` (1 or more), `duration` in milliseconds (1000 or more), and `autoApply` (boolean, required). Limits with `autoApply: true` are checked on every verification. The others are checked only when a verification names them. See [Key and identity rate limits](/docs/api-management/ratelimiting/key-and-identity-limits).
Set to `false` to create the key in a disabled state. Verification returns `code: DISABLED` until you enable it. See [Disabling and deleting keys](/docs/api-management/keys/enable-disable-delete).
Store an encrypted copy of the key so `keys.getKey` and `apis.listKeys` can show it later with `decrypt: true`. If the keyspace doesn't have encrypted storage turned on, you get HTTP 412 [`err:unkey:application:precondition_failed`](/docs/errors/unkey/application/precondition_failed). If the root key lacks the encrypt permission, you get HTTP 404 [`err:unkey:data:api_not_found`](/docs/errors/unkey/data/api_not_found). See [Recoverable keys](/docs/api-management/keys/recoverable-keys).
## Example
```bash theme={"system"}
curl -X POST https://api.unkey.com/v2/keys.createKey \
-H "Authorization: Bearer $UNKEY_ROOT_KEY" \
-H "Content-Type: application/json" \
-d '{
"apiId": "api_...",
"prefix": "sk_live",
"name": "Acme production",
"externalId": "org_acme",
"meta": { "plan": "pro" },
"expires": 1767225600000,
"credits": {
"remaining": 10000,
"refill": { "interval": "monthly", "amount": 10000, "refillDay": 1 }
},
"ratelimits": [
{ "name": "requests", "limit": 100, "duration": 60000, "autoApply": true }
],
"permissions": ["documents.read", "documents.write"]
}'
```
## Response
```json theme={"system"}
{
"meta": { "requestId": "req_..." },
"data": {
"keyId": "key_...",
"key": "sk_live_..."
}
}
```
`keyId` is the identifier you use for every later management call. `key` is the key itself, and this is the only time it's returned. Unkey keeps only a hash, plus an encrypted copy if you asked for a recoverable key. Send it to your user over a secure channel and don't log it.
## Key format
A key looks like `_`, or just `` without a prefix. The random part is `byteLength` random bytes in base58. `keys.getKey` and `apis.listKeys` return a `start` field, `_`, so you can recognize a key without seeing all of it.
## From the dashboard
The keyspace's **Create key** dialog has the same fields in six steps: general setup, rate limits, credits, expiration, permissions, and metadata. Two things differ from the API. Expiry must be at least two minutes in the future, and the general step has an environment field, a free-text label of up to 256 characters. The API can't set it and verification doesn't return it, so use `meta` if your backend needs the label.
There's no `environment` field on `keys.createKey`, and the v1 names `remaining`, `refill`, and `ratelimit` aren't accepted. Credits live under `credits.remaining` and `credits.refill`, and rate limits under `ratelimits`.
# Credits and refill
Source: https://www.unkey.com/docs/api-management/keys/credits-and-refill
Give keys a spendable balance that refills on a schedule.
Credits cap how much a key can be used in total. (Rate limits cap how often.) A key starts with a balance, and each verification spends some of it. When the balance can't cover a request, the key stops working until you add more or a scheduled refill restores it. Use credits for "1,000 requests included" plans, trials, and pay-per-use APIs.
## How spending works
A key has credits when `credits.remaining` is a number. If it's `null` or missing, the key is unlimited. Each `keys.verifyKey` call costs 1 unless the request sets `credits.cost`, any integer from 0 to 1,000,000,000,000.
Credits are checked last, so a key that fails any other check (disabled, expired, rate limited, missing permissions) doesn't lose credits. What happens next depends on the balance:
* **Enough credits.** The cost is deducted and the response's `credits` field shows the new balance.
* **Not enough.** Nothing is deducted, the verification fails with `code: USAGE_EXCEEDED`, and `credits` shows the unchanged balance.
The balance never goes below zero. A cost of `0` checks the key without spending anything, so you can check a key or read its metadata for free.
```bash theme={"system"}
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_...", "credits": { "cost": 10 } }'
```
```json theme={"system"}
{
"meta": { "requestId": "req_..." },
"data": { "valid": true, "code": "VALID", "keyId": "key_...", "credits": 990 }
}
```
Verifications in every region spend from the same balance, so no spend is lost.
## Add credits to a key
Set `credits.remaining` when creating the key, or later with `keys.updateKey`.
```bash theme={"system"}
curl -X POST https://api.unkey.com/v2/keys.createKey \
-H "Authorization: Bearer $UNKEY_ROOT_KEY" \
-H "Content-Type: application/json" \
-d '{ "apiId": "api_...", "credits": { "remaining": 1000 } }'
```
Starting balance, 0 or more. On `keys.updateKey`, `null` removes the limit entirely and also clears any refill schedule. On `keys.createKey`, `null` isn't allowed, so omit `credits` instead.
## Refill on a schedule
A refill resets the balance to a fixed amount on a schedule. It replaces the balance rather than adding to it: a key with 50 credits left and a refill of 1000 has 1000 afterward, not 1050. You must set `credits.remaining` in the same request.
`daily` or `monthly`.
The balance after each refill, 1 or more.
1 to 31. Required when `interval` is `monthly`. Leaving it out fails with HTTP 400 `err:unkey:application:invalid_input`. With `daily`, `keys.updateKey` rejects it with the same error and `keys.createKey` ignores it. If the month is shorter, the refill runs on its last day, so `31` works in February.
```bash theme={"system"}
curl -X POST https://api.unkey.com/v2/keys.createKey \
-H "Authorization: Bearer $UNKEY_ROOT_KEY" \
-H "Content-Type: application/json" \
-d '{
"apiId": "api_...",
"credits": {
"remaining": 10000,
"refill": { "interval": "monthly", "amount": 10000, "refillDay": 1 }
}
}'
```
Refills run once a day at 00:00 UTC, whatever your customer's time zone. Daily keys refill every day, and monthly keys refill on their `refillDay`. A key that's already at or above its `amount` is left alone.
To stop refilling but keep the current balance, send `credits.refill: null` on `keys.updateKey`. To change the schedule, send the whole `refill` object again.
## Change a balance directly
Use `keys.updateCredits` for top-ups, purchases, and refunds. It changes the balance and leaves the refill schedule alone.
The key to change.
`set` replaces the balance with `value`, `increment` adds `value`, and `decrement` subtracts `value` and clamps at zero.
0 or more. Required for `increment` and `decrement`. With `set`, `null` makes the key unlimited.
On a key with unlimited credits, `increment` and `decrement` fail with HTTP 400 [`err:unkey:application:invalid_input`](/docs/errors/unkey/application/invalid_input). To give an unlimited key a quota, send `set` first.
```bash theme={"system"}
curl -X POST https://api.unkey.com/v2/keys.updateCredits \
-H "Authorization: Bearer $UNKEY_ROOT_KEY" \
-H "Content-Type: application/json" \
-d '{ "keyId": "key_...", "operation": "increment", "value": 5000 }'
```
The root key needs `api.*.update_key` or `api..update_key` for both `keys.updateKey` and `keys.updateCredits`. See [Root key permissions](/docs/platform/root-keys/permissions).
## Credits vs rate limits
| | Credits | Rate limits |
| - | - | - |
| Controls | Total usage | Usage per time window |
| Resets | Only by refill or `updateCredits` | Automatically as the window slides |
| Failure code | `USAGE_EXCEEDED` | `RATE_LIMITED` |
| Checked | Last, and spent only on success | Before credits |
Use both when you sell a quota and also want burst protection. The rate limit stops a client from burning the quota in a second, and credits enforce the quota. See [Key and identity rate limits](/docs/api-management/ratelimiting/key-and-identity-limits).
# Disabling and deleting keys
Source: https://www.unkey.com/docs/api-management/keys/enable-disable-delete
Suspend a key for now or delete it so it never works again.
To stop a key from working, disable it or delete it. Disable when it might be temporary, like a failed payment or an investigation. The key keeps its settings and works again when you enable it. Delete when the key should never work again.
You need a root key with the permissions listed on this page. Create one in the dashboard under **Settings > Root Keys**. See [Permission reference](/docs/platform/root-keys/permissions-legacy) for every permission.
Disabling and enabling use `keys.updateKey` and need `api.*.update_key` or `api..update_key`. Deleting uses `keys.deleteKey` and needs `api.*.delete_key` or `api..delete_key`. See [Root key permissions](/docs/platform/root-keys/permissions).
## Disable or enable a key
In the dashboard, open the actions menu on the key's row or on its detail page and choose **Disable Key** or **Enable Key**. From the API, set `enabled` on `keys.updateKey`:
`false` suspends the key and `true` restores it. Omitting the field leaves the state unchanged.
```bash theme={"system"}
curl -X POST https://api.unkey.com/v2/keys.updateKey \
-H "Authorization: Bearer $UNKEY_ROOT_KEY" \
-H "Content-Type: application/json" \
-d '{ "keyId": "key_...", "enabled": false }'
```
While a key is disabled, every verification returns `valid: false` with `code: DISABLED`, and it doesn't use up rate limits or credits.
```json theme={"system"}
{
"meta": { "requestId": "req_..." },
"data": { "valid": false, "code": "DISABLED", "keyId": "key_...", "enabled": false }
}
```
Send `"enabled": true` to restore the key with all its settings as they were. You can also create a key disabled, with `"enabled": false` on `keys.createKey`, for example when access needs approval first.
## Delete a key
In the dashboard, choose **Delete key** from the same actions menu. From the API, call `keys.deleteKey`:
The key to delete.
`false`, the default, deletes the key but keeps a record of it for your audit trail. `true` erases it completely, so the same key string could be issued again later. Use `true` for regulatory erasure requests. The dashboard always uses the default.
```bash theme={"system"}
curl -X POST https://api.unkey.com/v2/keys.deleteKey \
-H "Authorization: Bearer $UNKEY_ROOT_KEY" \
-H "Content-Type: application/json" \
-d '{ "keyId": "key_..." }'
```
After deletion, verification returns `code: NOT_FOUND` and the key disappears from listings. The `key.delete` audit log event says whether it was permanent. **Deletion can't be undone.**
## How quickly it takes effect
A disable, enable, or delete takes about 10 seconds to reach verification, and a few verifications just after that can still see the old state. You can't speed it up. If a key must stop working the instant you say so, your own service has to stop accepting it too.
## Revoke everything a user owns
To find the keys linked to an identity, filter `apis.listKeys` by `externalId`, then delete them one by one.
```typescript revoke-user-keys.ts theme={"system"}
let cursor: string | undefined;
do {
const { data, pagination } = await unkey.apis.listKeys({
apiId: "api_...",
externalId: "user_123",
cursor,
});
for (const key of data) {
await unkey.keys.deleteKey({ keyId: key.keyId });
}
cursor = pagination.hasMore ? pagination.cursor : undefined;
} while (cursor);
```
## Disable, delete, or reroll
| Situation | Use |
| - | - |
| Billing problem, investigation, pause | Disable, then enable later. |
| User left, key no longer needed | Delete. |
| Key leaked but the user should keep working | [Reroll](/docs/api-management/keys/rerolling-keys): a new key with the same configuration and a grace period for the old one. |
| Erasure request, or you must reuse the exact key string | Delete with `permanent: true`. |
# Key expiration
Source: https://www.unkey.com/docs/api-management/keys/expiration
Give a key an expiry so it stops working at a moment you choose.
Set an expiry to make a key stop working at a time you choose. After that time, every verification returns `code: EXPIRED`. Nothing else about the key changes, so you can extend an expired trial key and it keeps its metadata, credits, and permissions.
## Set an expiry
Unix timestamp in milliseconds. The maximum is `4102444800000` (1 January 2100). On `keys.updateKey`, omitting the field keeps the current expiry and `null` removes it. A timestamp in the past is accepted and makes the key expired at once.
```bash theme={"system"}
# expire 24 hours from now
EXPIRES=$(( $(date +%s) * 1000 + 86400000 ))
curl -X POST https://api.unkey.com/v2/keys.createKey \
-H "Authorization: Bearer $UNKEY_ROOT_KEY" \
-H "Content-Type: application/json" \
-d "{ \"apiId\": \"api_...\", \"expires\": $EXPIRES }"
```
In the dashboard's **Create key** dialog, the expiry must be at least two minutes in the future.
## What happens at expiry
Expiry is checked against Unkey's clock, not your server's. An expired key uses no rate limits or credits. The response looks like this:
```json theme={"system"}
{
"meta": { "requestId": "req_..." },
"data": {
"valid": false,
"code": "EXPIRED",
"keyId": "key_...",
"enabled": true,
"expires": 1704067200000
}
}
```
The `expires` value is also returned on valid verifications, so your backend can warn a user that their key is about to lapse.
## Expired keys are kept
Expired keys aren't deleted. They stay in the keyspace, show up in `apis.listKeys` and the dashboard, and keep returning `EXPIRED`. You can extend one and keep its analytics history. To remove expired keys, delete them yourself. See [Disabling and deleting keys](/docs/api-management/keys/enable-disable-delete).
## Extend or remove an expiry
```bash theme={"system"}
# extend by 7 days from now
EXPIRES=$(( $(date +%s) * 1000 + 604800000 ))
curl -X POST https://api.unkey.com/v2/keys.updateKey \
-H "Authorization: Bearer $UNKEY_ROOT_KEY" \
-H "Content-Type: application/json" \
-d "{ \"keyId\": \"key_...\", \"expires\": $EXPIRES }"
```
```bash theme={"system"}
# make the key permanent
curl -X POST https://api.unkey.com/v2/keys.updateKey \
-H "Authorization: Bearer $UNKEY_ROOT_KEY" \
-H "Content-Type: application/json" \
-d '{ "keyId": "key_...", "expires": null }'
```
Extending an expired key takes about 10 seconds to apply, and a few verifications just after that can still return `EXPIRED`. [Verifying keys](/docs/api-management/keys/verifying-keys#how-quickly-changes-take-effect) explains the timing.
## Expiry and rerolling
Rerolling a key copies its expiry to the new key and sets the old key to expire after the grace period you choose. In the dashboard you can't reroll an expired key, and the grace period can't go past the original expiry. The API applies the grace period as requested. See [Rerolling keys](/docs/api-management/keys/rerolling-keys).
## Expiry compared with credits
An expiry ends access at a time. Credits end it after an amount of use. For a trial that ends after seven days or 1,000 requests, whichever comes first, set both. See [Credits and refill](/docs/api-management/keys/credits-and-refill).
# Looking up keys
Source: https://www.unkey.com/docs/api-management/keys/looking-up-keys
Read a key's configuration by its id or from the key itself.
Look up a key's settings without verifying it or spending anything. Use `keys.getKey` when you have the key's ID, for example on a management screen. Use `keys.whoami` when you have the key itself, for example when a user pastes it into a support form. Both return the same fields, except `keys.whoami` can't return the decrypted key.
You need a root key with the permissions listed on this page. Create one in the dashboard under **Settings > Root Keys**. See [Permission reference](/docs/platform/root-keys/permissions-legacy) for every permission.
Both endpoints need `api.*.read_key` or `api..read_key`. Adding `decrypt: true` to `keys.getKey` additionally needs `api.*.decrypt_key` or `api..decrypt_key`. See [Root key permissions](/docs/platform/root-keys/permissions).
## By identifier
The `key_...` identifier from `keys.createKey`, a verification response, a listing, or the dashboard. 3 to 255 characters matching `^[a-zA-Z0-9_]+$`.
Include the plaintext for a key created as recoverable. See [Recoverable keys](/docs/api-management/keys/recoverable-keys).
```bash theme={"system"}
curl -X POST https://api.unkey.com/v2/keys.getKey \
-H "Authorization: Bearer $UNKEY_ROOT_KEY" \
-H "Content-Type: application/json" \
-d '{ "keyId": "key_..." }'
```
A key that doesn't exist, is deleted, or belongs to another workspace returns HTTP 404 `err:unkey:data:key_not_found`.
## By plaintext
The full key including its prefix, 1 to 512 characters. Any change to it gives a not-found error.
```bash theme={"system"}
curl -X POST https://api.unkey.com/v2/keys.whoami \
-H "Authorization: Bearer $UNKEY_ROOT_KEY" \
-H "Content-Type: application/json" \
-d '{ "key": "sk_live_..." }'
```
`keys.whoami` doesn't check whether the key is enabled, expired, or within its limits, and it doesn't record a verification. Use `keys.verifyKey` when you need a verdict.
## Response fields
The key's identifier.
The prefix and first characters of the key, for display in lists.
Whether the key is enabled.
Creation time as Unix milliseconds.
Last update as Unix milliseconds, when the key has been updated.
Last successful verification as Unix milliseconds. It updates about once a minute, so it can lag real usage by up to a minute.
The internal name.
The key's metadata.
Expiry as Unix milliseconds, when set.
`remaining` (integer or `null` for unlimited) and, when configured, `refill` with `interval`, `amount`, and `refillDay`.
Each key-level rate limit: `id`, `name`, `limit`, `duration`, `autoApply`.
Permission slugs the key holds directly or through roles.
Role names assigned to the key.
When linked: `id`, `externalId`, the identity's `meta`, and its `ratelimits`.
The decrypted key. Returned only by `keys.getKey` with `decrypt: true` on a recoverable key. `keys.whoami` never returns it.
```json theme={"system"}
{
"meta": { "requestId": "req_..." },
"data": {
"keyId": "key_...",
"start": "sk_live_abc1",
"enabled": true,
"name": "Acme production",
"createdAt": 1704067200000,
"lastUsedAt": 1704153600000,
"expires": 1767225600000,
"meta": { "plan": "pro" },
"credits": {
"remaining": 9420,
"refill": { "interval": "monthly", "amount": 10000, "refillDay": 1 }
},
"ratelimits": [
{ "id": "rl_...", "name": "requests", "limit": 100, "duration": 60000, "autoApply": true }
],
"permissions": ["documents.read", "documents.write"],
"roles": ["editor"],
"identity": { "id": "id_...", "externalId": "org_acme", "meta": {} }
}
}
```
To page through many keys at once, use `apis.listKeys`, which returns the same objects. See [Listing keys](/docs/api-management/keyspaces/listing-keys).
# Metadata and tags
Source: https://www.unkey.com/docs/api-management/keys/metadata-and-tags
Attach data to a key and to each verification of it.
Metadata is data you store on a key, like a plan tier. It comes back on every verification, so your backend doesn't need a database lookup. Tags are labels you send with each verification, like the endpoint called. They go only to analytics, so you can break usage down later.
## Metadata
A JSON object with at most 100 top-level properties. Nesting is allowed. Set it on `keys.createKey` or replace it on `keys.updateKey`, where leaving it out keeps the current value and `null` removes it. Every `keys.verifyKey` response returns it as `data.meta`.
```bash theme={"system"}
curl -X POST https://api.unkey.com/v2/keys.createKey \
-H "Authorization: Bearer $UNKEY_ROOT_KEY" \
-H "Content-Type: application/json" \
-d '{
"apiId": "api_...",
"meta": { "plan": "pro", "features": ["exports", "webhooks"], "billingEmail": "ops@example.com" }
}'
```
Keep metadata to what your request handler needs right away, like a plan tier, feature flags, or a tenant ID. Anything large slows every verification, and any service that calls `keys.verifyKey` can see it, so don't store secrets. To change one property, send the whole object again.
Metadata on an identity comes back separately as `data.identity.meta`. Put facts shared by all of a user's keys there, and key-specific facts on the key.
The dashboard's **Create key** dialog also has an environment field that sets a label on the key. That label isn't part of `keys.createKey`, `keys.updateKey`, or the verification response. Store such labels in `meta` when your backend must read them.
## Tags
On `keys.verifyKey`. Up to 20 strings of 1 to 512 characters each. They're recorded with the verification and don't change the result.
```bash theme={"system"}
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_...",
"tags": ["endpoint=/documents", "method=GET", "region=eu-west-1"]
}'
```
Tags land in the `tags` column of the verifications table in analytics, so you can break usage down by endpoint, client version, or anything else. A `key=value` format makes them easy to filter. Don't put anything sensitive in a tag, because tags show up in analytics and the dashboard's verification logs. The [key verifications table](/docs/api-management/analytics/tables/key-verifications) shows how to query them.
## Which one to use
| Need | Use |
| - | - |
| Know the caller's plan or flags while handling the request | `meta` on the key, or `meta` on the identity if it is shared by all of a user's keys |
| Break usage reports down by endpoint, version, or region | `tags` on each verification |
| Tell live keys from test keys in your code | `meta`, since the dashboard environment label is not returned by verification |
# Migrating existing keys
Source: https://www.unkey.com/docs/api-management/keys/migrating-keys
Bring existing keys from another system into Unkey without rotating them.
Move homegrown API keys to Unkey by sending the hash of each key, never the key itself. Users keep the keys they already have. After the import, your backend calls `keys.verifyKey` instead of looking keys up in your own table.
After the move, you no longer store plaintext keys or write the hash comparison. If you switch those checks over too, you also stop running your own rate-limit counters and monthly usage counters. You still run your API, unless you deploy it on Compute, and you still return 401 and 429 to your users. Unkey stores a SHA-256 hash of the full key, base64 encoded, and compares that on verification. See [How Unkey stores API keys](/docs/platform/security/overview#how-unkey-stores-api-keys).
Agree the hash format with Unkey before the import. The two formats are below.
## Get a migration ID
Email [support@unkey.com](mailto:support@unkey.com) with your workspace ID, the system the keys come from, and how they're hashed. Support sends a `migrationId`. Two hash formats work today.
| Algorithm | What you send as `hash` | Verification |
| - | - | - |
| `sha256` | SHA-256 of the full key string, **base64** encoded. This is Unkey's native format. | Works immediately with a plain `keys.verifyKey`; no `migrationId` needed at verify time. |
| `github.com/seamapi/prefixed-api-key` | SHA-256 of the key's long token only, **hex** encoded, as that library stores it. Keys look like `prefix_shortToken_longToken`. | Send `migrationId` on `keys.verifyKey` until each key has been verified once. After that the key verifies without it. |
Keys hashed any other way, such as bcrypt or a salted hash, can't be imported from the hash alone. Tell support, and the migration needs another plan.
### Hash the keys yourself
For `sha256`, this is the same function Unkey uses, so a key hashed this way works exactly like one Unkey created.
```typescript hash.ts theme={"system"}
import { createHash } from "node:crypto";
function hashKey(plaintext: string): string {
return createHash("sha256").update(plaintext).digest("base64");
}
hashKey("sk_live_abc"); // "5Q7m..." base64, not hex
```
Never include plaintext keys in the export or the migration request.
You need a root key with the permissions listed on this page. Create one in the dashboard under **Settings > Root Keys**. See [Permission reference](/docs/platform/root-keys/permissions-legacy) for every permission.
`keys.migrateKeys` needs `api.*.create_key` or `api..create_key`. Verifying afterwards needs the usual `api.*.verify_key`. See [Root key permissions](/docs/platform/root-keys/permissions).
## Import the hashes
The identifier from support, 3 to 255 characters. An unknown ID returns HTTP 404 `err:unkey:data:migration_not_found`.
The keyspace to import into.
At least one entry. Each has a required `hash` (3 or more characters, in your migration's format) and the same optional fields as `keys.createKey`: `name`, `externalId`, `meta`, `roles` (up to 100), `permissions` (up to 1000), `expires`, `enabled` (default `true`), `credits`, and `ratelimits` (up to 50). The rules match [Creating keys](/docs/api-management/keys/creating-keys).
```bash theme={"system"}
curl -X POST https://api.unkey.com/v2/keys.migrateKeys \
-H "Authorization: Bearer $UNKEY_ROOT_KEY" \
-H "Content-Type: application/json" \
-d '{
"migrationId": "",
"apiId": "api_...",
"keys": [
{
"hash": "xfV3...base64...=",
"name": "Acme production",
"externalId": "org_acme",
"meta": { "migratedFrom": "legacy" },
"ratelimits": [
{ "name": "requests", "limit": 1000, "duration": 60000, "autoApply": true }
]
}
]
}'
```
## Response
The response is HTTP 200 even when some hashes fail. A hash that already exists anywhere in Unkey, not just in your workspace, is skipped and listed under `failed`. Every other key is created and returned with its new `keyId`. Store it next to your own record of the key.
```json theme={"system"}
{
"meta": { "requestId": "req_..." },
"data": {
"migrated": [
{ "hash": "xfV3...base64...=", "keyId": "key_..." }
],
"failed": [
"duplicate...hash...="
]
}
}
```
Send large migrations in batches and reconcile `migrated` against your export after each one.
## Switch verification over
Point your backend at `keys.verifyKey`. For the `sha256` algorithm nothing else changes. For the prefixed-api-key algorithm, include the `migrationId` in every verification during the roll-out:
```bash theme={"system"}
curl -X POST https://api.unkey.com/v2/keys.verifyKey \
-H "Authorization: Bearer $UNKEY_ROOT_KEY" \
-H "Content-Type: application/json" \
-d '{ "key": "prefix_short_long", "migrationId": "" }'
```
After a key's first verification with `migrationId`, it works with or without the field. A key that hasn't been verified yet still needs it and returns `NOT_FOUND` without it, so keep sending `migrationId` until the fallback below goes quiet.
## Run both systems during the switch
Verify against Unkey first. If a key comes back `NOT_FOUND`, check your old system and log it. Remove the fallback when the log goes quiet. Create new keys with `keys.createKey` so new users are never in the old system.
# Recoverable keys
Source: https://www.unkey.com/docs/api-management/keys/recoverable-keys
Store an encrypted copy of a key so you can show it again later.
A recoverable key is one you can show again after it's created. By default Unkey keeps only a hash of each key, so once `keys.createKey` returns it, nobody (including Unkey) can read it again. That's the safest setup and right for most apps: a user who loses a key gets a new one.
Use recoverable keys when showing a key again is worth the extra risk, such as an API playground that needs a working key, or a settings page where users expect to reveal their key. We store an encrypted copy and decrypt it only when you ask. [Key storage](/docs/platform/security/key-storage) explains how that copy is protected.
## Opt the keyspace in
Recoverable keys need the keyspace's **Store encrypted keys** setting. It's off for new keyspaces and there's no dashboard toggle, so email support with the keyspace's API ID and we'll turn it on. Keys created before that can't be recovered.
You need a root key with the permissions listed on this page. Create one in the dashboard under **Settings > Root Keys**. See [Permission reference](/docs/platform/root-keys/permissions-legacy) for every permission.
Creating a recoverable key needs `api.*.encrypt_key` or `api..encrypt_key` in addition to the create permission. Reading one back needs `api.*.decrypt_key` or `api..decrypt_key` in addition to the read permission. Grant these only to root keys that need them. See [Root key permissions](/docs/platform/root-keys/permissions).
A missing encrypt permission doesn't look like a permission problem. `keys.createKey` returns HTTP 404 [`err:unkey:data:api_not_found`](/docs/errors/unkey/data/api_not_found), "The specified API was not found." If you see this for an API ID you know is correct, check the root key's `encrypt_key` permission first.
## Create a recoverable key
On `keys.createKey`. When `true`, an encrypted copy of the key is stored. Fails with HTTP 412 [`err:unkey:application:precondition_failed`](/docs/errors/unkey/application/precondition_failed) when the keyspace doesn't store encrypted keys ("This API does not support key encryption."), and with HTTP 404 `err:unkey:data:api_not_found` when the root key lacks `encrypt_key`.
```bash theme={"system"}
curl -X POST https://api.unkey.com/v2/keys.createKey \
-H "Authorization: Bearer $UNKEY_ROOT_KEY" \
-H "Content-Type: application/json" \
-d '{ "apiId": "api_...", "recoverable": true }'
```
Rerolling a recoverable key produces a recoverable key, so the reroll needs the encrypt permission too.
## Read the plaintext back
Both `keys.getKey` and `apis.listKeys` accept `decrypt`.
When `true`, every returned key that's recoverable has a `plaintext` field. Other keys come back without it, and the request doesn't fail.
```bash theme={"system"}
curl -X POST https://api.unkey.com/v2/keys.getKey \
-H "Authorization: Bearer $UNKEY_ROOT_KEY" \
-H "Content-Type: application/json" \
-d '{ "keyId": "key_...", "decrypt": true }'
```
```json theme={"system"}
{
"meta": { "requestId": "req_..." },
"data": {
"keyId": "key_...",
"start": "sk_live_abc1",
"enabled": true,
"createdAt": 1704067200000,
"plaintext": "sk_live_..."
}
}
```
`decrypt: true` fails with HTTP 412 when the key's keyspace doesn't store encrypted keys, and with a permission error when the root key lacks `decrypt_key`. Treat any response with `plaintext` like the original create response: send it over a secure channel, and never log or cache it.
## Recoverable keys and rotation
If a key may have leaked, reroll it. Being able to read it again doesn't make it safe. See [Rerolling keys](/docs/api-management/keys/rerolling-keys).
# Rerolling keys
Source: https://www.unkey.com/docs/api-management/keys/rerolling-keys
Replace a key's secret while keeping its configuration and a grace period.
Rerolling gives a key a new secret and keeps its settings. Unkey creates a new key with the same settings, and the old key keeps working for a grace period you choose. Use it for scheduled rotation, after a suspected leak, or when a user asks for a fresh key.
You need a root key with the permissions listed on this page. Create one in the dashboard under **Settings > Root Keys**. See [Permission reference](/docs/platform/root-keys/permissions-legacy) for every permission.
The root key needs `api.*.create_key` or `api..create_key`. When the original key is recoverable, it also needs `api.*.encrypt_key` or `api..encrypt_key`. See [Root key permissions](/docs/platform/root-keys/permissions).
## Request
The identifier of the key to reroll (`key_...`), not the key string itself.
Milliseconds from now until the old key stops working. The maximum is `4102444800000`. The time is rounded up to the next whole minute, so `expiration: 1` can leave the old key working for up to a minute. Only `0` revokes it immediately. Set it to `null` to keep the old key working; it keeps its current expiry, if it has one.
```bash theme={"system"}
curl -X POST https://api.unkey.com/v2/keys.rerollKey \
-H "Authorization: Bearer $UNKEY_ROOT_KEY" \
-H "Content-Type: application/json" \
-d '{ "keyId": "key_...", "expiration": 86400000 }'
```
## Response
```json theme={"system"}
{
"meta": { "requestId": "req_..." },
"data": {
"keyId": "key_...",
"key": "sk_live_..."
}
}
```
`key` is the new key, and this is the only time it's returned. `keyId` is the new key's ID. The old key keeps its own `keyId` until it expires.
## What the new key copies
The new key copies the old key's enabled state, name, metadata, identity, expiry, credits and refill schedule, roles, permissions, and rate limits. If the old key was recoverable, so is the new one.
The new key gets a new `keyId` and secret. It keeps the old key's prefix, or uses the keyspace's default prefix if it had none. Its length is the keyspace's current default, or 16 bytes if none is set, so a key created with a custom `byteLength` can change length.
Analytics by identity carry on across the reroll. Analytics by `keyId` start fresh for the new key.
## What happens to the original
The old key's expiry becomes now plus `expiration`, rounded up to the next minute. With `null`, the old key's expiry does not change. With `0`, its next verification returns `code: EXPIRED`. The API applies the grace period even if the old key was due to expire sooner, so a reroll can extend its life. Check the old key's `expires` first if that matters.
The event is recorded in the audit log as `key.reroll`.
## From the dashboard
Choose **Rotate key** from the key's actions menu. Pick a grace period: revoke immediately, 15 minutes, 1 hour, 6 hours, 24 hours (the default), or **Do not expire original key**. That last option keeps the old key working with its current expiry. The dashboard is stricter than the API: you can't rotate an expired key, and the grace period can't go past the key's original expiry. The new key is shown once.
## Reroll or reissue
Reroll when the user should keep working without a break. `keys.rerollKey` can't change settings, so to change them too, update the new key with `keys.updateKey` afterward, or create a new key and delete the old one.
# Updating keys
Source: https://www.unkey.com/docs/api-management/keys/updating-keys
Change a key in place, and clear a field rather than recreating the key.
Use `keys.updateKey` to change what a key can do without issuing a new one. The key string stays the same, so your users keep working. Only `keyId` is required.
For every other field:
* Leave it out to keep the current value.
* Send a value to replace it.
* Send `null` to clear it, for example to remove an expiry, a refill schedule, or an identity link.
You need a root key with the permissions listed on this page. Create one in the dashboard under **Settings > Root Keys**. See [Permission reference](/docs/platform/root-keys/permissions-legacy) for every permission.
The root key needs `api.*.update_key` or `api..update_key`. Without it you get HTTP 403 [`err:unkey:authorization:insufficient_permissions`](/docs/errors/unkey/authorization/insufficient_permissions). A `keyId` that doesn't exist or belongs to another workspace returns HTTP 404 [`err:unkey:data:key_not_found`](/docs/errors/unkey/data/key_not_found). See [Root key permissions](/docs/platform/root-keys/permissions).
## Request fields
The key's ID (`key_...`), not the key string. 3 to 255 characters matching `^[a-zA-Z0-9_]+$`.
1 to 255 characters. `null` removes the name.
1 to 255 characters matching `^[a-zA-Z0-9_.-]+$`. Moves the key to the identity with that `externalId`, creating the identity in the keyspace's project if it doesn't exist yet. An existing identity in a different project is rejected with HTTP 404 [`err:unkey:data:identity_not_found`](/docs/errors/unkey/data/identity_not_found). `null` unlinks the key from its identity. See [Identities](/docs/api-management/identities/overview).
JSON with at most 100 top-level properties. It replaces the old metadata rather than merging. `null` removes it. See [Metadata and tags](/docs/api-management/keys/metadata-and-tags).
Unix timestamp in milliseconds, at most `4102444800000` (1 January 2100). `null` makes the key permanent. A timestamp in the past is accepted and expires the key at once. See [Key expiration](/docs/api-management/keys/expiration).
`credits.remaining` (integer or `null`, 0 or more) and `credits.refill` (object or `null`). Unlike `keys.createKey`, neither is required, so you can change a schedule without resending the balance. See [Credits and refill on update](#credits-and-refill-on-update). See [Credits and refill](/docs/api-management/keys/credits-and-refill).
Up to 50 limits, each with `name` (3 to 128 characters), `limit` (1 or more), `duration` in milliseconds (1000 or more), and `autoApply`. The list replaces the key's limits rather than adding to them. `null` or `[]` removes them all. See [Key and identity rate limits](/docs/api-management/ratelimiting/key-and-identity-limits).
`false` suspends the key and `true` restores it. Not nullable. See [Disabling and deleting keys](/docs/api-management/keys/enable-disable-delete).
Up to 100 role names, each 1 to 128 characters. The list replaces the key's roles. Not nullable, so send `[]` to detach them all.
Up to 1000 permission slugs, each 1 to 128 characters matching `^[a-zA-Z0-9_:\-\.\*]+$`. The list replaces the key's direct permissions. Not nullable, so send `[]` to detach them all.
## Credits and refill on update
The balance is `credits.remaining` and the schedule is `credits.refill`. Clearing one sometimes clears the other:
| Sent | Balance | Refill schedule |
| - | - | - |
| `credits` omitted | unchanged | unchanged |
| `credits: null` | cleared, key becomes unlimited | cleared |
| `credits.remaining: null` | cleared, key becomes unlimited | cleared |
| `credits.remaining: 500` | set to 500 | unchanged |
| `credits.refill: null` | unchanged | cleared |
| `credits.refill: { ... }` | unchanged | replaced |
Removing the balance removes the schedule too. Removing the schedule keeps the balance, so you can stop a subscription from renewing without taking away credits the customer already paid for.
A `refill` object has `interval` (`daily` or `monthly`) and `amount` (1 or more). `refillDay` (1 to 31) is for `monthly` only. Sending it with `daily`, or leaving it out with `monthly`, fails with HTTP 400 [`err:unkey:application:invalid_input`](/docs/errors/unkey/application/invalid_input). (`keys.createKey` ignores `refillDay` with `daily`, so a body that works for create can fail here.)
To change only the balance, use `keys.updateCredits`.
## Lists replace, they don't merge
`ratelimits`, `roles`, and `permissions` are each the full list you want the key to end up with. Send every entry you want to keep, not just the new ones.
* **Rate limits** are matched by `name`. A limit you send keeps its ID and gets the new values. A limit you leave out is deleted.
* **Roles** must already exist. The first one that doesn't fails the request with HTTP 404 [`err:unkey:data:role_not_found`](/docs/errors/unkey/data/role_not_found).
* **Permissions** that don't exist yet are created for you. This list only replaces the key's direct permissions, not what its roles grant.
To add or remove one entry without resending the list, use `keys.addPermissions`, `keys.removePermissions`, `keys.addRoles`, or `keys.removeRoles` instead. [Managing key permissions](/docs/api-management/authorization/managing-key-permissions) compares them.
## Example
Move a key to the paid plan, extend it, and stop it expiring:
```bash theme={"system"}
curl -X POST https://api.unkey.com/v2/keys.updateKey \
-H "Authorization: Bearer $UNKEY_ROOT_KEY" \
-H "Content-Type: application/json" \
-d '{
"keyId": "key_...",
"meta": { "plan": "pro" },
"expires": null,
"credits": {
"remaining": 10000,
"refill": { "interval": "monthly", "amount": 10000, "refillDay": 1 }
},
"ratelimits": [
{ "name": "requests", "limit": 1000, "duration": 60000, "autoApply": true }
]
}'
```
## Response
```json theme={"system"}
{
"meta": { "requestId": "req_..." },
"data": {}
}
```
The response has no key data. Use `keys.getKey` to read the result.
## When the change takes effect
An update applies in full or not at all, so an error leaves the key as it was.
Changes take about 10 seconds to reach verification, and a few verifications just after that can still see the old settings. A new credit balance applies right away. [Verifying keys](/docs/api-management/keys/verifying-keys#how-quickly-changes-take-effect) has the details.
Each update writes a `key.update` [audit event](/docs/platform/workspace/audit-logs/event-types), plus a `permission.create` event for each permission it created.
## What you can't change
You can't change `apiId`, `prefix`, `byteLength`, or `recoverable` after a key is created. To replace the key string while keeping the same configuration, see [Rerolling keys](/docs/api-management/keys/rerolling-keys). To start over, [create a new key](/docs/api-management/keys/creating-keys) and delete the old one.
`credits.remaining` is optional on `keys.updateKey` although it's required inside `credits` on `keys.createKey`. Omitting a field isn't the same as sending `null`: omitting keeps the stored value and `null` clears it. `enabled`, `roles`, and `permissions` don't accept `null`.
# Verify API keys with Unkey
Source: https://www.unkey.com/docs/api-management/keys/verifying-keys
Unkey checks an API key for validity, rate limits, and credits, and Compute runs that check before your API.
Unkey checks the key your user sent by calling `keys.verifyKey` from your own server. One call runs expiry, an IP allow list when the keyspace has one, permissions, rate limits, and credits, then returns `data.valid` and a `code`. The HTTP status stays 200 when the key fails, and your server returns 401 or 429 to the caller. On Compute, the API key authentication policy runs the same checks at the gateway and returns 401, 403 or 429 itself.
## What the check tells you
Read `data.code`. These are the outcomes you usually branch on. The full list, including the status to return, is under [Code values](#code-values).
| You need to know | What Unkey returns |
| - | - |
| Is the key allowed? | `valid: true` and `code: VALID` |
| Is it missing, disabled, or expired? | `NOT_FOUND`, `DISABLED`, or `EXPIRED` |
| Is the caller blocked? | `FORBIDDEN` when the client IP is off the keyspace allow list, or the workspace is disabled |
| Does it have permission? | `INSUFFICIENT_PERMISSIONS` when the `permissions` query fails |
| Is it over a rate limit? | `RATE_LIMITED`. That request doesn't spend credits |
| Has it used its quota? | `USAGE_EXCEEDED` when credits can't cover the cost |
| Should the gateway check the key? | The [API key authentication policy](/docs/compute/gateway/api-key-auth) runs the same checks and returns 401, 403, or 429 itself |
Verify an API key for your public API by calling `keys.verifyKey` with the key your user sent. This is the API Management endpoint. Use it when your own server, not the Compute gateway, decides whether the request is allowed.
Send the key exactly as your user sent it. Unkey runs the checks set on the key, plus any you add to the request, and returns one answer. The response is HTTP 200 for every outcome, including an invalid key, a rate limit, and a used-up credit balance. Read `data.valid` and `data.code`, not the status code. Your server then returns 401 or 429 to the caller. See [Rate limiting](/docs/api-management/ratelimiting/overview#what-a-limited-request-looks-like) for `Retry-After`.
You need a root key with the permissions listed on this page. Create one in the dashboard under **Settings > Root Keys**. See [Permission reference](/docs/platform/root-keys/permissions-legacy) for every permission.
The root key needs `api.*.verify_key` or `api..verify_key` for the key's keyspace. Without it you get a 200 with `code: NOT_FOUND`, not a permission error. A missing or invalid root key gets HTTP 401. See [Root key permissions](/docs/platform/root-keys/permissions).
## Request
The key your user sent, 1 to 512 characters, including its prefix. Any change to the string gives `NOT_FOUND`. You don't pass an API ID because the key already belongs to one keyspace.
1 to 5 keyspace IDs (`ks_...`), each up to 100 characters, matched exactly. The key must belong to one of them or the result is `NOT_FOUND`. Use it when one root key verifies several keyspaces but an endpoint should only accept keys from some of them. An empty array is HTTP 400.
A permission query, 1 to 1000 characters, such as `documents.read AND (billing.read OR billing.write)`. `AND` binds tighter than `OR`, and parentheses group. Fails the verification with `INSUFFICIENT_PERMISSIONS` when the key's direct and role-derived permissions don't satisfy it. A malformed query is HTTP 400 `err:user:bad_request:permissions_query_syntax_error`. Asterisks inside slugs are matched literally. See [Permission queries](/docs/api-management/authorization/permission-queries).
`credits.cost` (integer, 0 to 1,000,000,000,000) is how many credits this verification spends. Omitting the object spends 1 on keys that have credits configured and nothing on unlimited keys. `cost: 0` checks the key without spending. See [Credits and refill](/docs/api-management/keys/credits-and-refill).
Rate limits to check on this call. Each entry names a limit configured on the key or its identity (`name`, 3 to 255 characters) and may pass `cost` (default 1). Naming a limit that doesn't exist on the key or its identity fails the whole request with HTTP 412 `err:unkey:application:precondition_failed`. An entry that also carries both `limit` and `duration` defines an inline, key-scoped limit for this call instead. Limits with `autoApply: true` are checked whether or not you list them.
An inline `limit` must be 1 or more and `duration` at least 1000 milliseconds. Values below that aren't rejected with an error. Instead the verification skips rate limiting entirely, including the auto-applied limits, and can still return `VALID`. Check these two values in your own code before you send them.
Up to 20 strings of up to 512 characters, recorded with the verification for analytics only. They never affect the verdict. See [Metadata and tags](/docs/api-management/keys/metadata-and-tags).
Up to 256 characters. Lets Unkey match a key imported from another provider's hash format on its first use and convert it to the native hash. See [Migrating existing keys](/docs/api-management/keys/migrating-keys).
```bash theme={"system"}
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": "documents.read",
"credits": { "cost": 2 },
"ratelimits": [{ "name": "requests" }],
"tags": ["endpoint=/documents", "method=GET"]
}'
```
## Order of checks
Checks run in this order and stop at the first failure. The order decides which `code` you get, and whether a rate limit or credits get used up.
1. **Existence.** The key must exist, its workspace and keyspace must not be deleted, and your root key must be allowed to verify keys in that keyspace. Anything else is `NOT_FOUND`.
2. **Keyspace restriction.** If the request lists `keyspaces` and the key's keyspace isn't one of them, the result is `NOT_FOUND` with no key details.
3. **Workspace.** A disabled workspace produces `FORBIDDEN`.
4. **Enabled.** A key with `enabled: false` produces `DISABLED`.
5. **Expiration.** A key whose `expires` is in the past produces `EXPIRED`.
6. **IP allow list.** If the keyspace has one, the request's client IP must be on it, or the result is `FORBIDDEN`.
7. **Permissions.** If the request carries a `permissions` query, the key must satisfy it, or the result is `INSUFFICIENT_PERMISSIONS`.
8. **Rate limits.** Auto-applied limits and limits named in the request are checked together. If any is exceeded, the result is `RATE_LIMITED`, and the other limits aren't used up.
9. **Credits.** Only if everything above passed, the cost is deducted. Not enough credits gives `USAGE_EXCEEDED` and nothing is deducted.
So a rate-limited request doesn't spend credits, but a request that runs out of credits still counts against its rate limits.
## Response
`true` only when every check passed. Read this first.
One of `VALID`, `NOT_FOUND`, `FORBIDDEN`, `INSUFFICIENT_PERMISSIONS`, `USAGE_EXCEEDED`, `RATE_LIMITED`, `DISABLED`, `EXPIRED`. Code values explains each one.
The key's identifier. Present for every outcome except `NOT_FOUND`.
The keyspace the key belongs to (`ks_...`). Present for every outcome except `NOT_FOUND`.
The key's internal name, when set.
Whether the key is enabled.
Expiry as Unix milliseconds, when set.
Credits remaining after this call. Omitted for unlimited keys. After `USAGE_EXCEEDED` it's the unchanged balance.
The key's metadata, when set.
Every permission slug the key holds, directly or through roles.
Every role name assigned to the key.
When the key is linked to an identity: `id`, `externalId`, the identity's `meta`, and its `ratelimits` (each with `id`, `name`, `limit`, `duration`, `autoApply`).
One entry per rate limit that was checked: `id` (empty for inline limits), `name`, `limit`, `duration`, `remaining`, `reset` (Unix milliseconds), `exceeded`, and `autoApply`.
### Code values
| `code` | Meaning | Typical HTTP status to return to your user |
| - | - | - |
| `VALID` | All checks passed. | proceed |
| `NOT_FOUND` | No such key; the key belongs to a deleted keyspace or a workspace your root key cannot verify for; or the request's `keyspaces` list excluded it. | 401 |
| `DISABLED` | The key has `enabled: false`. | 401 or 403 |
| `EXPIRED` | The key's `expires` has passed. | 401 |
| `FORBIDDEN` | Client IP not on the keyspace's allow list, or the workspace is disabled. | 403 |
| `INSUFFICIENT_PERMISSIONS` | The `permissions` query was not satisfied. | 403 |
| `RATE_LIMITED` | At least one checked rate limit is exceeded. | 429 |
| `USAGE_EXCEEDED` | Not enough credits for the requested cost. | 402 or 429 |
## Why some failures say NOT\_FOUND
Unkey never tells a caller that a key exists unless they're allowed to verify it. So you get a 200 with `code: NOT_FOUND` when the key belongs to another workspace, its keyspace was deleted, your root key has no verify permission for its keyspace, or your `keyspaces` list leaves it out. The response looks the same in every case. If you get `NOT_FOUND` for a key you know exists, check your root key's permissions first.
## How quickly changes take effect
A change to a key (disable, delete, new expiry, new permissions or roles) takes about 10 seconds to reach verification, and a few verifications just after that can still see the old state. Plan incident runbooks around a little more than 10 seconds, not an instant cutoff.
Credit balances change instantly. Rate limit counts sync across regions within moments, so `remaining` is approximate. See [How rate limiting works](/docs/api-management/ratelimiting/how-it-works).
## What gets recorded
Every verification, whatever its outcome, is recorded in analytics with its outcome, key, identity, tags, credits spent, and latency. Verifications made with a root key also produce a `key.verify` audit log event. See [Analytics](/docs/api-management/analytics/overview) and [Audit logs](/docs/platform/workspace/audit-logs/overview).
# Listing keys
Source: https://www.unkey.com/docs/api-management/keyspaces/listing-keys
Page through the keys in a keyspace or find the ones a user owns.
Use `apis.listKeys` to list the keys in a keyspace, up to 100 per page. It's handy for a "your API keys" screen, for finding every key a user owns, or for auditing a keyspace. It only returns the full key for recoverable keys, and only when you ask.
By default, keys are read from a replica, which may not reflect recent changes. Set `revalidateKeysCache: true` to read keys and their identities from the primary database instead, including when you filter by `externalId`. Primary reads can have higher latency, depending on your region.
You need a root key with the permissions listed on this page. Create one in the dashboard under **Settings > Root Keys**. See [Permission reference](/docs/platform/root-keys/permissions-legacy) for every permission.
The root key needs two permissions: `api.*.read_key` or `api..read_key`, **and** `api.*.read_api` or `api..read_api`. Adding `decrypt: true` additionally needs `api.*.decrypt_key` or `api.