> ## 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. Edit role dialog showing the keys assigned to the role and its assigned permissions # 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. Create keyspace dialog with a name entered and the Create Keyspace button 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. Identity page with a requests chart and a verification log table, both empty for an identity with no verifications yet # 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. Create key dialog on the General Setup step with Name, Prefix, and External ID fields, and the other steps listed on the left 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 Keys tab with a key's actions menu open, including Disable Key, Rotate key, and Delete 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. Rotate key dialog with the grace period dropdown open, from Revoke immediately to 24 hours ## 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..decrypt_key`. See [Root key permissions](/docs/platform/root-keys/permissions). A missing permission isn't a 403. You get HTTP 404 [`err:unkey:data:api_not_found`](/docs/errors/unkey/data/api_not_found), "The requested API does not exist or has been deleted." If you see this for an ID you know is correct, check both permissions on the root key. ## Request The keyspace to list, by API ID. Keys per page, 1 to 100. The `pagination.cursor` from the previous response. Omit for the first page. Only keys linked to the identity with this exact `externalId`. Use it to list one user's keys. Include `plaintext` for keys created with `recoverable: true`. Requires the decrypt permission and a keyspace with encrypted storage enabled. See [Recoverable keys](/docs/api-management/keys/recoverable-keys). Read from the primary database to avoid replica lag. This can increase latency. ```bash theme={"system"} curl -X POST https://api.unkey.com/v2/apis.listKeys \ -H "Authorization: Bearer $UNKEY_ROOT_KEY" \ -H "Content-Type: application/json" \ -d '{ "apiId": "api_...", "externalId": "user_123", "limit": 50 }' ``` ## Response `data` is an array of key objects, and `pagination` tells you whether to continue. ```json theme={"system"} { "meta": { "requestId": "req_..." }, "data": [ { "keyId": "key_...", "start": "sk_prod_abc1", "enabled": true, "name": "Production key", "createdAt": 1704067200000, "expires": 1735689600000, "meta": { "plan": "premium" }, "credits": { "remaining": 950 }, "identity": { "id": "id_...", "externalId": "user_123" } } ], "pagination": { "cursor": "...", "hasMore": true } } ``` Each item has the same fields `keys.getKey` returns. `start` is the prefix and first few characters, so a user can recognize the key. `plaintext` appears only when `decrypt: true` worked for that key. [Looking up keys](/docs/api-management/keys/looking-up-keys) lists every field. ## Paging Send `pagination.cursor` back until `hasMore` is false. Cursors expire, so don't store them. ```typescript list-all-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) { console.log(key.keyId, key.start); } cursor = pagination.hasMore ? pagination.cursor : undefined; } while (cursor); ``` ## In the dashboard The keyspace's **Keys** tab shows the same list. You can filter by key ID, name, identity, and tags (is, contains, starts with, ends with). Select several rows to change their external ID, enable or disable them, or delete them in bulk. Keys tab with the filter menu open, offering Name, Identity, Key ID, and Tags Keys tab with two keys selected and the bulk action bar offering Change External ID, Disable key, and Delete key # Keyspaces Source: https://www.unkey.com/docs/api-management/keyspaces/overview Group related keys in a keyspace and decide when you need more than one. A keyspace is a group of related API keys. Every key belongs to one keyspace, and you name it every time you create a key. It's not a running service and has no URL. The dashboard lists keyspaces under **Keyspaces (APIs)**, and API endpoints call them APIs, as in `apis.createApi`. ## When to create more than one One keyspace is enough when all your keys share the same defaults and you tell them apart by metadata or identity. Create more when you want a hard boundary: different key prefixes per product, separate analytics per environment such as production and staging, or root keys that can only touch one product's keys. Root key permissions such as `api..create_key` can be limited to one keyspace. ## Two identifiers Each keyspace has two IDs, used in different places. | Identifier | Format | Used by | | - | - | - | | API ID | `api_...` | `keys.createKey`, `apis.listKeys`, `apis.getApi`, `apis.deleteApi`, and root key permissions such as `api..verify_key` | | Keyspace ID | `ks_...` | Analytics tables (`key_space_id`), the `keyspaces` filter on `keys.verifyKey`, and the customer portal | Both appear on the keyspace's **Settings** page with a copy button. ## Settings that apply to every key These settings apply to every key in the keyspace. [Keyspace settings](/docs/api-management/keyspaces/settings) covers each one. | Setting | Effect | | - | - | | Default prefix | Prepended to new keys that do not pass their own `prefix`. | | Default bytes | Random bytes used for new keys that do not pass their own `byteLength`; 16 unless changed. | | Store encrypted keys | Allows keys to be created as recoverable; opt-in through support. | | IP allow list | Verification rejects requests from any other client IP; exact match, not editable in the dashboard. | | Delete protection | Blocks `apis.deleteApi` and the dashboard delete button while on. | ## Create a 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. Open **Keyspaces (APIs)** in the sidebar, click **Create keyspace**, and enter a name. Both IDs are on the new keyspace's settings page. Keyspaces page showing keyspace cards with their key counts and the Create keyspace button `apis.createApi` takes a `name` (3 to 256 characters, just a label) and needs `api.*.create_api` on the root key. The response has the new API ID. ```bash 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": "payment-service-prod" }' ``` `apis.getApi` returns the `id` and `name` for an API ID you already have. ## Delete a keyspace Deleting a keyspace makes every key in it verify as `NOT_FOUND`. In the dashboard, use the **Delete Keyspace** card in the settings danger zone and type the keyspace name and the confirmation phrase. From the API, call `apis.deleteApi` with the `apiId`. It needs `api.*.delete_api` or `api..delete_api`. If delete protection is on, both fail with HTTP 412 `err:unkey:application:protected_resource`. Turn protection off on the settings page first. See [Delete protection](/docs/platform/security/delete-protection). ## Next steps Defaults, encrypted storage, IP allow list, and delete protection. Page through the keys in a keyspace and filter by owner. # Keyspace settings Source: https://www.unkey.com/docs/api-management/keyspaces/settings Set the defaults and protections that apply to every key in a keyspace. These settings apply to every key in a keyspace. Change them on the keyspace's **Settings** page in the dashboard. There's no API for this. For encrypted key storage and the IP allow list, contact support. Keyspace settings page with the name, API ID, keyspace ID, key defaults, and a Danger Zone holding the Delete Protection card and the Delete Keyspace button ## Name and identifiers The label shown in the dashboard. You can change it any time. Read-only, `api_...`. Pass it as `apiId` when creating or listing keys. Read-only, `ks_...`. Used by analytics (`key_space_id`) and the customer portal. ## Key generation defaults These apply when a `keys.createKey` request leaves out `prefix` or `byteLength`. Up to 16 characters. New keys without their own `prefix` look like `_`. Rerolling a key with no prefix also uses it. Random bytes in a new key when the request leaves out `byteLength`. The API accepts 16 to 255. Rerolled keys always use this value (or 16 if unset), not the original key's length. ## Store encrypted keys When on, you can create recoverable keys and read them back with `decrypt: true`. When off, `keys.createKey` with `recoverable: true` fails with HTTP 412 "This API does not support key encryption." There's no dashboard toggle. Contact support to turn it on. See [Recoverable keys](/docs/api-management/keys/recoverable-keys). ## IP allow list A comma-separated list of IP addresses, up to 512 characters. When set, a verification of any key in this keyspace fails with `code: FORBIDDEN` unless it comes from an IP on the list. Matching is exact, for IPv4 and IPv6. CIDR ranges such as `10.0.0.0/8` don't work. A request with no client IP is also rejected with `FORBIDDEN`. New keyspaces have no allow list. Contact support to set or change one. You can't see the list in the dashboard or the API, so if verifications return `FORBIDDEN` and you don't know why, ask support whether an allow list is set. ## Delete protection When on, **Delete Keyspace** in the dashboard and `apis.deleteApi` both fail with HTTP 412 `err:unkey:application:protected_resource`. Switch it on the **Delete Protection** card and type the keyspace name to confirm. The change shows in the audit log as `api.update`. Protection covers the keyspace only. You can still delete keys inside it. [Delete protection](/docs/platform/security/delete-protection) describes the same flag on the other resource types. ## Delete keyspace The **Delete Keyspace** card in the danger zone deletes the keyspace after you type its name and the confirmation word. Every key in it then verifies as `NOT_FOUND`. You can't undo this from the dashboard. # What portal users can do Source: https://www.unkey.com/docs/api-management/portal/end-user-experience What your users see and can do inside the portal. Here's what your users see in the portal. The scopes on their [session](/docs/api-management/portal/sessions) decide what they can do, and they only ever see their own keys and usage. ## One page, shaped by scope The portal is one Keys page. The scopes decide what's on it: * **`keys:read`** shows the key table. Without it the page is empty. * **`keys:reroll`** adds a roll-over action to each key. * **`analytics:read`** adds a time filter, usage numbers, and an outcomes chart above the table, plus a request count on each key. If the token expires while the user is on the page, the portal shows "Your session has expired" with a link to the session's `returnUrl`. ## Keys The key table lists every key the user owns, with its name, status, creation date, and expiry. The full key is never shown. With `keys:reroll`, the user can roll a key over. They pick a grace period, copy the new key (shown once), and the old key keeps working until the grace period ends. ## Usage With `analytics:read`, the page shows verification counts over a time range, broken down by outcome, for all the user's keys or one of them. The time ranges on offer fit your workspace's log retention. ## Building your own portal front end To build your own interface instead of using the hosted portal, call `portal.listKeys`, `portal.rerollKey`, and `portal.getVerifications` with the session's access token as a bearer token. They only return the session user's data. ### List keys `portal.listKeys` returns the user's keys, up to 100 per page (`limit`, default 100, with `cursor` and `hasMore` as on every [list endpoint](/docs/platform/api/overview)). There are no filters. ### Roll a key over `portal.rerollKey` takes the same body as `keys.rerollKey`: the key and an `expiration` grace period for the old key. Another user's key returns not found. ### Read usage `portal.getVerifications` takes `startTime` and `endTime` (Unix milliseconds, start included, end not) and an optional `keyId` for one key. A window longer than your log retention fails with `err:user:bad_request:query_range_exceeds_retention`. The response has one series per key: * `bucketMillis` is the bucket size (a minute, hour, or day, based on the window). * `keys` has one entry per key with traffic in the window, with every bucket filled in (zeros included), in time order. Sum them for an account total. * Keys with no traffic are left out. An entry can be for a key that's since been deleted, so don't assume you can look it up in `portal.listKeys`. If more than 150 keys had traffic in the window, the request fails with [`per_key_breakout_too_large`](/docs/errors/user/bad_request/per_key_breakout_too_large). Narrow the window or pass a `keyId`. Each entry's `data` holds one point per bucket: Bucket start in Unix milliseconds. All verifications in the bucket. Verifications with outcome `VALID`. Rejected because a rate limit was exceeded. Rejected for missing permissions. Rejected as forbidden. Rejected because the key was disabled. Rejected because the key had expired. Rejected because the key's credits were exhausted. Asking for another user's key returns nothing. ### Create and delete keys `keys.createKey` and `keys.deleteKey` also accept a portal token, so your interface can let users create and delete their own keys. A new key always belongs to the session's user, whatever the request says. Deleting another user's key returns not found. These actions show in the [audit log](/docs/platform/workspace/audit-logs/overview) with a `portalEndUser` actor. # Developer portal Source: https://www.unkey.com/docs/api-management/portal/overview A hosted page where your users manage their own API keys. The developer portal is a hosted page where your users see their API keys, roll them over, and check their usage, without you building any of it. You keep the sign-in: your backend creates a short-lived session for a user who's already signed in, and redirects them to the portal. Each user only sees their own keys. The developer portal is in beta. You can manage a portal for a keyspace in the dashboard, with the API, or with the CLI ([create-portal](/docs/api-management/cli/portal/create-portal), [get-portal](/docs/api-management/cli/portal/get-portal), [update-portal](/docs/api-management/cli/portal/update-portal), [delete-portal](/docs/api-management/cli/portal/delete-portal), [create-session](/docs/api-management/cli/portal/create-session)). A portal for a deployed app can only be managed with the API or the CLI for now. ## What a portal is bound to A portal shows keys from one place. Set `keyspaceId` to show the keys in a keyspace. Or set `appId` to a Compute app to show keys from every keyspace the gateway accepts for that app. Send one or the other, not both. You can change it later. A user only sees keys whose identity `externalId` matches the one in their session. Keys without an identity never show up in the portal, so attach identities to keys you want users to manage. ## Fields A handle for the portal in API calls, unique in your workspace. 3 to 64 lowercase letters, digits, and hyphens, not starting or ending with a hyphen. Your users never see it. You can use the slug or the `pc_` ID wherever you name a portal. A taken slug, or a second portal for the same keyspace or app, fails with HTTP 409 `err:unkey:data:portal_already_exists`. 1 to 64 characters shown in the portal header and page titles. Change it any time. The keyspace to serve. Mutually exclusive with `appId`. The app to serve. Mutually exclusive with `keyspaceId`. Whether new sessions can be created. Disabling also ends sessions that are already live. `https://` URL of the logo in the portal header, up to 500 characters. Your users' browsers load it directly, so that host sees their IP address. Set `null` on update to remove it. Six-digit hex color (`#6366f1`) for buttons and accents. Set `null` on update to go back to the default. When you read a portal, `logoUrl` and `primaryColor` come back under `branding`, and `createdAt` and `updatedAt` are Unix milliseconds. ## Create a portal 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 portal needs `portal.*.create_portal`. Reading, updating, and deleting need `read_portal`, `update_portal`, or `delete_portal`, either as `portal.*.` or scoped to one portal as `portal..`. Creating sessions needs a separate permission, covered on [Portal sessions](/docs/api-management/portal/sessions). ```bash theme={"system"} curl -X POST https://api.unkey.com/v2/portal.createPortal \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "slug": "acme-portal", "displayName": "Acme", "keyspaceId": "ks_1234abcd", "primaryColor": "#6366f1" }' ``` You get the portal back with its `pc_` ID. Next, [create a session](/docs/api-management/portal/sessions) for a signed-in user and redirect them to the URL you get back. ## Change or remove a portal `portal.updatePortal` takes the same fields, all optional. Leave a field out to keep it. Pointing the portal at a different keyspace or app, or disabling it, ends every live session. `portal.deletePortal` deletes the portal, ends all its live sessions, and frees the slug. Both write an [audit log](/docs/platform/workspace/audit-logs/overview) entry (`portal.update` or `portal.delete`). ## Next steps Create a session for a signed-in user, redirect them to the portal, and set the permissions your root key needs. The keys and analytics tabs, what each scope unlocks, and the endpoints behind them. # Portal sessions Source: https://www.unkey.com/docs/api-management/portal/sessions Sign a user into the portal from your backend. Sign a user into the [developer portal](/docs/api-management/portal/overview) from your backend, with no second login. After they sign in to your app, ask Unkey for a session for that user and redirect their browser to the URL you get back. Your root key never reaches the browser. ## Sign a user in Call `portal.createSession` with the portal, the user's `externalId`, and the scopes they should have. You get `{ "id": "ps_...", "url": "https://portal.unkey.com/?code=pec_..." }`. The URL works **once**, within **15 minutes**. Send the browser to `url`. The portal swaps the code for an access token that lasts **24 hours**. If the code is expired, already used, or unknown, the user sees `err:unkey:authentication:portal_session_not_found`, "Session is invalid, expired, or has already been used." The user only sees their own keys. When the token expires, or they click the return link, the portal sends them to the `returnUrl` you set. Without one, there's no return link. To let them back in, create a new session. ## 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 portal's `pc_` ID or slug. The user's ID in your system, 1 to 256 characters. A session for `user_123` shows the keys whose identity is `user_123`. One or more of `keys:read`, `keys:reroll`, and `analytics:read`. Scopes decide what the user can see and do. See [What portal users can do](/docs/api-management/portal/end-user-experience). `keys:reroll` and `analytics:read` each need `keys:read` too. Without it, the request fails with 400 and `err:unkey:application:invalid_input`. Full URL, up to 500 characters, to send the user back to when they leave or the session expires. You set it per session, so different users can go back to different pages. Create a preview session to try the portal yourself. ## Permissions your root key needs Your root key needs `portal.*.create_portal_session` or `portal..create_portal_session`. Without it, the request fails with **404**, not 403. It also needs a matching permission for each scope, on every keyspace the portal shows, either as `*` or for each `api_` ID: | Scope | Required on the root key | | - | - | | `keys:read` | `read_key` and `read_api` | | `keys:reroll` | `create_key`, plus `encrypt_key` if the keyspace stores encrypted keys | | `analytics:read` | `read_analytics` | If the root key is missing one, the whole request fails with **403**. A disabled portal can't create sessions at all. ## What ends a session A session ends when its 24 hour token expires, or when the portal is disabled, deleted, or pointed at a different keyspace or app. That usually takes effect within about 10 seconds, but can take up to 5 minutes. Re-enabling a disabled portal doesn't bring its sessions back. To log one end user out early, call `portal.revokeSession` with the portal and their `externalId`. It ends every live session they have on that portal. To see who is signed in, call `portal.listSessions` with the portal. It returns one entry per end user with a session that has not expired or been revoked, including sessions whose portal URL was not opened yet, and each end user's sessions and scopes. Results are ordered by `externalId`, paginated with `cursor`, and can be filtered with a case-sensitive `search` prefix. It needs a grant on the portal's sessions: `portal.*.create_portal_session`, `portal..create_portal_session`, or the portal's `sessions/*` with `read` or `write`. Reading the portal itself is not enough. Without it the request fails with **404**. Creating, exchanging, and revoking sessions write `portal.session.create`, `portal.session.exchange`, and `portal.session.revoke` [audit log](/docs/platform/workspace/audit-logs/event-types) entries. What the user does in the portal is logged under a `portalEndUser` actor. ## Errors | Status | Code | Meaning | | - | - | - | | 404 | `err:unkey:data:portal_not_found` | The portal does not exist, is not in your workspace, or your root key lacks `create_portal_session` for it. | | 403 | `err:unkey:authorization:insufficient_permissions` | A requested scope needs a keyspace permission the root key lacks. | | 401 | `err:unkey:authentication:portal_session_not_found` | On exchange: the code is expired, used, or unknown. On end-user endpoints: the token is expired or revoked. | | 400 | `err:unkey:application:invalid_input` | `keys:reroll` or `analytics:read` was requested without `keys:read`. | | 400 | `err:unkey:authentication:portal_token_missing` | An end-user endpoint was called without a portal token. | # How rate limiting works Source: https://www.unkey.com/docs/api-management/ratelimiting/how-it-works Know how requests are counted, what a rate limit response tells you, and how exact the counts are across regions. Unkey counts requests per identifier for you, so there's no counter store to run. Call `ratelimit.limit` with a user id or customer id, or put the limit on an API key and read it from `keys.verifyKey`. Both use the same sliding window. When the identifier is over the limit, Unkey still returns HTTP 200. `ratelimit.limit` sets `data.success` to `false`. `keys.verifyKey` sets `data.code` to `RATE_LIMITED`. Your server returns `429 Too Many Requests` to the caller, with `Retry-After` in seconds. `reset` in the body is Unix milliseconds. Header names Unkey itself sends are on [Rate limiting](/docs/api-management/ratelimiting/overview#what-a-limited-request-looks-like). ```typescript theme={"system"} import { Unkey } from "@unkey/api"; const unkey = new Unkey({ rootKey: process.env.UNKEY_ROOT_KEY }); export async function handler(request: Request) { const userId = request.headers.get("x-user-id") ?? "anonymous"; const result = await unkey.ratelimit.limit({ namespace: "api.requests", identifier: userId, limit: 100, duration: 60_000, }); if (!result.data.success) { const retryAfter = Math.max(1, Math.ceil((result.data.reset - Date.now()) / 1000)); return new Response("Too many requests", { status: 429, headers: { "Retry-After": String(retryAfter) }, }); } return new Response("ok"); } ``` A monthly total on an API key is credits, not this window. See [Credits and refill](/docs/api-management/keys/credits-and-refill). The rest of this page explains how the count is computed. ## How requests are counted A limit is `limit` requests per `duration` milliseconds. Unkey uses a sliding window, so a caller can't use a full limit at the end of one window and another full limit right after. It counts the current window plus part of the previous one, based on how much of the current window is left. ```text theme={"system"} previous window current window |-------------------------------|-------------------------------| ^ now, 25% into the window effective count = current + previous * (1 - 0.25) allowed if effective count + cost <= limit ``` For example, with a limit of 100 per minute, a caller who made 100 requests at the end of one minute still counts as 75 fifteen seconds into the next (100 × 0.75), so the burst can't repeat right away. Denied requests don't count, so retrying while limited doesn't push the reset further away. ## Make some requests cost more Each check spends `cost` tokens, 1 by default, so a heavy operation can use more of the limit than a light one. With `limit: 100`, a caller can make 100 checks at cost 1, 20 at cost 5, or any mix. A cost of 0 reads the current state without spending anything. When one call checks several limits, a request denied by one limit doesn't use up the others. ## How exact the counts are across regions Each region decides quickly using the traffic it has seen, and regions share counts within moments. In practice: | Situation | What to expect | | - | - | | One region receives most of an identifier's traffic | Enforcement is tight. | | Traffic is split across regions | A short burst can get through in more than one region before the counts catch up. | | Part of our infrastructure is down | Limits stay on in each region, but a limit can let through more than usual until counts catch up. | Rate limits protect you from bursts. They aren't an exact count. Use credits when you need an exact total. See [Credits and refill](/docs/api-management/keys/credits-and-refill). ## What you get back | Field | Meaning | | - | - | | `success` | Whether this request fit within the limit. In `ratelimit.multiLimit` the per-check field is `passed`. | | `limit` | The limit that applied: yours, or an override's if one matched. | | `remaining` | Tokens left in the current window, as an estimate; 0 when the check failed. | | `reset` | Unix milliseconds when the current window ends. Capacity comes back gradually after this, not all at once. | | `overrideId` | The override that supplied `limit` and `duration`, when one matched; empty otherwise. | Trust `success` as the answer. Treat `remaining` as a close estimate, because another region may have accepted requests that aren't counted yet. ## What happens on errors `ratelimit.limit` and `ratelimit.multiLimit` reject an empty identifier, a limit below 1, or a duration under 1000 milliseconds. If the check can't run, they return an error, and your code decides whether to allow or deny. `keys.verifyKey` is different. If the rate limit check can't run, the verification carries on as if the limits passed, so an outage can't lock your users out. The same happens with an inline limit below 1 or a duration under 1000 milliseconds, so check those values before you send them. See [Verifying keys](/docs/api-management/keys/verifying-keys). # Key and identity rate limits Source: https://www.unkey.com/docs/api-management/ratelimiting/key-and-identity-limits Enforce named rate limits on a key or an identity during verification. Put a rate limit and a monthly request quota on each customer's API key, and `keys.verifyKey` enforces both. One call checks the key, the sliding-window limit, and the credit balance, with no Redis to run. To change a customer's numbers, call `keys.updateKey`. No deploy needed. The rate limit is a named window on the key, such as 100 requests per 60,000 milliseconds. The monthly quota is credits. Set `credits.refill.interval` to `monthly` and the balance refills on `refillDay`. When the window is used up, `data.code` is `RATE_LIMITED`. When credits can't cover the request, `data.code` is `USAGE_EXCEEDED`. Unkey still returns HTTP 200, and your server returns the 429. ```typescript theme={"system"} import { Unkey } from "@unkey/api"; const unkey = new Unkey({ rootKey: process.env.UNKEY_ROOT_KEY }); export async function handler(request: Request) { const header = request.headers.get("authorization") ?? ""; const key = header.startsWith("Bearer ") ? header.slice("Bearer ".length) : header; const { data } = await unkey.keys.verifyKey({ key }); if (!data.valid && data.code === "RATE_LIMITED") { const resetMs = data.ratelimits?.find((limit) => limit.exceeded)?.reset ?? Date.now(); const retryAfter = Math.max(1, Math.ceil((resetMs - Date.now()) / 1000)); return new Response("Too many requests", { status: 429, headers: { "Retry-After": String(retryAfter) }, }); } if (!data.valid && data.code === "USAGE_EXCEEDED") { return new Response("Monthly quota exceeded", { status: 429 }); } return data.valid ? new Response("ok") : new Response("Unauthorized", { status: 401 }); } ``` `Retry-After` is in seconds, and RFC 6585 defines both 429 and that header. `reset` on each checked limit is Unix milliseconds. `keys.verifyKey` doesn't set `Retry-After` or `X-RateLimit-*`. Which headers Unkey does send, including on the Compute gateway, is on [Rate limiting](/docs/api-management/ratelimiting/overview#what-a-limited-request-looks-like). A limit on an identity counts every key linked to that customer together. A limit on a key counts that key alone. ## Define limits You set limits with `ratelimits` on `keys.createKey`, `keys.updateKey`, `identities.createIdentity`, and `identities.updateIdentity`, up to 50 per key or identity. Each entry has: 3 to 128 characters, unique within the key or identity. You refer to the limit by this name at verification time. Tokens per window, 1 or more. Window length in milliseconds, 1000 or more. `true` checks this limit on every verification. `false` checks it only when a verification names it. Required: leaving it out fails with HTTP 400 and `missing property 'autoApply'`. ```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_...", "ratelimits": [ { "name": "requests", "limit": 100, "duration": 60000, "autoApply": true }, { "name": "exports", "limit": 5, "duration": 3600000, "autoApply": false } ] }' ``` On `keys.updateKey` and `identities.updateIdentity`, `ratelimits` replaces the whole list. Limits you leave out are deleted and matching names are updated. `null` on `keys.updateKey` removes them all. Leave the field out to keep limits as they are. ## Check limits at verification Auto-applied limits are always checked. To check any other limit, or to spend a custom cost, list it in the verification's `ratelimits` array. The name of a limit on the key or its identity, 3 to 255 characters (saved limit names stop at 128; longer names only work for inline limits). Tokens to spend on this limit for this verification, 0 or more. With `duration`, defines an inline limit instead of referring to a configured one. See below. Window in milliseconds for an inline limit. Send it together with `limit`. ```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_...", "ratelimits": [ { "name": "exports" }, { "name": "requests", "cost": 3 } ] }' ``` All listed limits are checked together. If any is exceeded, none is used up, the verification fails with `code: RATE_LIMITED`, and no credits are spent. Naming a limit that isn't on the key or its identity fails the whole request with HTTP 412 [`err:unkey:application:precondition_failed`](/docs/errors/unkey/application/precondition_failed). If the rate limit check can't run, the verification carries on as if the limits passed, so an outage never blocks valid keys. ### Inline limits An entry with both `limit` and `duration` is an inline limit, used for this call only. It counts against the key (never the identity), isn't saved, and has an empty `id` in the response. Use it when your code decides the numbers per endpoint. An inline limit needs a `limit` of 1 or more and a `duration` of 1000 milliseconds or more. Below that you get no error. Instead the verification skips all rate limits, including auto-applied ones, and `data.ratelimits` comes back empty. Check these values in your code before you send them. ```json theme={"system"} { "key": "sk_live_...", "ratelimits": [ { "name": "search", "limit": 20, "duration": 10000 } ] } ``` ## What the response contains Every checked limit appears in `data.ratelimits` with `id`, `name`, `limit`, `duration`, `remaining`, `reset` (Unix milliseconds), `exceeded`, and `autoApply`. If the key has an identity, `data.identity.ratelimits` lists all of the identity's limits, checked or not. ```json theme={"system"} { "meta": { "requestId": "req_..." }, "data": { "valid": false, "code": "RATE_LIMITED", "keyId": "key_...", "ratelimits": [ { "id": "rl_...", "name": "requests", "limit": 100, "duration": 60000, "remaining": 0, "reset": 1704067260000, "exceeded": true, "autoApply": true } ] } } ``` ## Key limits and identity limits A limit on a key counts that key alone. A limit on an identity counts all its keys together, so five keys share one budget. If a key and its identity both have a limit with the same name, the key's limit wins, so you can give one key an exception to a shared limit. [Shared rate limits across keys](/docs/api-management/identities/shared-rate-limits) walks through the identity side. ## From the dashboard Set the same four fields in the **Create key** dialog's rate limit step, or with **Edit ratelimit** on a key or identity. Create key dialog on the Ratelimit step with the ratelimit toggle and the name, limit, and refill interval fields # ratelimit.limit and multiLimit Source: https://www.unkey.com/docs/api-management/ratelimiting/limit-and-multi-limit Rate limit any identifier with one call, or several limits at once. Call `ratelimit.limit` to rate limit a public API by user id, customer id, or IP address. Unkey keeps the counters, so you don't run Redis. You send the limit and the window on each call, and you return HTTP 429 when `data.success` is `false`. `ratelimit.multiLimit` runs up to 100 checks in one call, and counts them only if they all pass. The rules live in your code. To change the rule for particular identifiers without a deploy, use [overrides](/docs/api-management/ratelimiting/overrides). A monthly total on an API key is credits, not this window. See [Key and identity rate limits](/docs/api-management/ratelimiting/key-and-identity-limits). ```typescript theme={"system"} import { Unkey } from "@unkey/api"; const unkey = new Unkey({ rootKey: process.env.UNKEY_ROOT_KEY }); export async function handler(request: Request) { const userId = request.headers.get("x-user-id") ?? "anonymous"; const { data } = await unkey.ratelimit.limit({ namespace: "api.requests", identifier: userId, limit: 100, duration: 60_000, }); if (!data.success) { const retryAfter = Math.max(1, Math.ceil((data.reset - Date.now()) / 1000)); return new Response("Too many requests", { status: 429, headers: { "Retry-After": String(retryAfter) }, }); } return new Response("ok"); } ``` The endpoint returns HTTP 200 either way. `data.reset` is Unix milliseconds. `Retry-After` is in seconds, which is what RFC 6585 allows on a 429. Unkey doesn't set `Retry-After` or `X-RateLimit-*` on this endpoint. The full header list, including what the Compute gateway sends, is on [Rate limiting](/docs/api-management/ratelimiting/overview#what-a-limited-request-looks-like). 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 `ratelimit.*.limit` or `ratelimit..limit` for every namespace checked. The first call with a new namespace name creates it, which also needs `ratelimit.*.create_namespace`. ## Namespaces A namespace names what you're limiting, such as `email.send` or `api.requests`. Identifiers are counted separately in each namespace. Refer to a namespace by name (1 to 512 characters, case-sensitive) or ID (`rlns_...`). A new name is created on first use. Rename or delete namespaces in the dashboard under **Ratelimit**. A check against a deleted namespace fails with HTTP 410 `err:unkey:data:ratelimit_namespace_gone`. ## ratelimit.limit Namespace name or ID, 1 to 512 characters. Who or what you're counting, 1 to 512 characters: a user ID, IP address, tenant, or any stable string. Maximum tokens per window, 1 or more. A matching override replaces it. Window length in milliseconds, 1000 (one second) to 2592000000 (30 days). A matching override replaces it. Tokens this request spends, 0 or more. 0 checks without counting. ```bash 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, "cost": 5 }' ``` ### Response Whether the request fit within the limit. The endpoint returns HTTP 200 either way, so read this field. The limit that applied, from the request or from a matching override. Tokens left in the current window, as a close estimate. 0 on denial. Unix milliseconds when the current window ends. The override that was applied, when one matched. ```json theme={"system"} { "meta": { "requestId": "req_..." }, "data": { "success": true, "limit": 100, "remaining": 95, "reset": 1704067260000 } } ``` Every check is recorded in rate limit analytics and writes a `ratelimit.limit` audit event. To keep a `ratelimit.limit` call out of analytics and your API request logs, send the header `X-Unkey-Metrics: disabled`. `ratelimit.multiLimit` ignores this header. ## ratelimit.multiLimit Send an array of 1 to 100 objects with the same fields as `ratelimit.limit`. Use it when one action must pass several limits, for example a per-user and a per-organization limit, or a request count and a token budget. ```bash theme={"system"} curl -X POST https://api.unkey.com/v2/ratelimit.multiLimit \ -H "Authorization: Bearer $UNKEY_ROOT_KEY" \ -H "Content-Type: application/json" \ -d '[ { "namespace": "api.requests", "identifier": "user_123", "limit": 100, "duration": 60000 }, { "namespace": "api.requests", "identifier": "org_acme", "limit": 1000, "duration": 60000 }, { "namespace": "llm.tokens", "identifier": "user_123", "limit": 50000, "duration": 3600000, "cost": 1200 } ]' ``` If any check fails, none of them count. A request that passes counts against every limit. Overrides apply per check. A deleted namespace in any check fails the whole call with `err:unkey:data:ratelimit_namespace_gone`. ### Response `true` only when every check passed. One result per check, in request order, each with `namespace`, `identifier`, `passed`, `limit`, `remaining`, `reset`, and `overrideId`. ```json theme={"system"} { "meta": { "requestId": "req_..." }, "data": { "passed": false, "limits": [ { "namespace": "api.requests", "identifier": "user_123", "passed": true, "limit": 100, "remaining": 99, "reset": 1704067260000 }, { "namespace": "api.requests", "identifier": "org_acme", "passed": true, "limit": 1000, "remaining": 999, "reset": 1704067260000 }, { "namespace": "llm.tokens", "identifier": "user_123", "passed": false, "limit": 50000, "remaining": 0, "reset": 1704070800000 } ] } } ``` Nothing was counted, so `remaining` on the passing checks doesn't include this request. ## Choosing limits * **Pick the window for the job.** Short windows, such as 20 per 10 seconds, stop bursts. Longer windows, such as 1,000 per hour, work as quotas. Windows of a minute or more are more accurate across regions. * **Use `cost` for expensive operations** instead of a second namespace. * **Keep identifiers stable.** An identifier with a timestamp or request ID in it is never seen twice, so it limits nothing. * **Use an [override](/docs/api-management/ratelimiting/overrides)** when one customer needs a different number, instead of a code path. # Rate limit overrides Source: https://www.unkey.com/docs/api-management/ratelimiting/overrides Give one identifier or a pattern its own limit without changing code. An override gives particular identifiers a different limit from the one your code sends. Your app keeps calling `ratelimit.limit` with its default numbers, and when the identifier matches an override, Unkey uses the override's numbers instead. Use it to raise a partner's limit or throttle an abuser without deploying. 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. Setting an override needs `ratelimit.*.set_override` or `ratelimit..set_override`. Reading and listing need `ratelimit.*.read_override`, and deleting needs `ratelimit.*.delete_override`. ## Set an override Namespace name or ID, 1 to 512 characters. Unlike `ratelimit.limit`, this doesn't create a missing namespace. It returns HTTP 404 `err:unkey:data:ratelimit_namespace_not_found`. The exact identifier, or a pattern containing `*`, 1 to 512 characters. Case-sensitive. Tokens per window for matching identifiers, 1 or more. Don't use `0` to ban someone: the API accepts it, but checks against it fail with a server error. Use `1` with a long duration instead. Window length in milliseconds, 1000 or more. It may differ from the duration your code sends. ```bash 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": "org_acme", "limit": 10000, "duration": 60000 }' ``` ```json theme={"system"} { "meta": { "requestId": "req_..." }, "data": { "overrideId": "rlor_..." } } ``` Setting an override on an identifier that already has one replaces it. The change shows in the audit log as `ratelimit.set_override`. It takes up to about a minute to apply everywhere. The endpoint is `/v2/ratelimit.setOverride` (singular `ratelimit`) and the namespace is passed as `namespace`, by name or ID. The request has no separate namespace-id or namespace-name field and no asynchronous mode. ## How a match is chosen An override on the exact identifier always wins. If there isn't one, a matching wildcard override applies. In a pattern, `*` matches any run of characters (including none), and the pattern must match the whole identifier. | Override | Matches | Does not match | | - | - | - | | `*@acme.com` | `alice@acme.com`, `@acme.com` | `alice@acme.com.evil.io` | | `enterprise:*` | `enterprise:123`, `enterprise:` | `pro:123` | | `user_*_prod` | `user_1_prod`, `user__prod` | `user_1_staging` | | `*suspicious*` | anything containing `suspicious` | | With `*@acme.com` at 500 per minute and `ceo@acme.com` at 10,000, `ceo@acme.com` gets 10,000, everyone else at `acme.com` gets 500, and `user@other.com` gets the numbers your code sent. If two wildcards match the same identifier, there's no rule for which wins, so avoid overlapping wildcards in one namespace. ## Read, list, and delete * **`ratelimit.getOverride`** takes `namespace` and `identifier` and returns the override that would apply, using the same matching rules: `overrideId`, `identifier` (the stored pattern), `limit`, and `duration`. If nothing matches, it returns HTTP 404 `err:unkey:data:ratelimit_override_not_found`. * **`ratelimit.listOverrides`** takes `namespace`, `limit` (1 to 100, default 50), and `cursor`, and returns the same objects with pagination. * **`ratelimit.deleteOverride`** takes `namespace` and `identifier`. Matching identifiers go back to your code's numbers (or another matching wildcard) within about a minute. ```bash theme={"system"} curl -X POST https://api.unkey.com/v2/ratelimit.deleteOverride \ -H "Authorization: Bearer $UNKEY_ROOT_KEY" \ -H "Content-Type: application/json" \ -d '{ "namespace": "api.requests", "identifier": "org_acme" }' ``` ## From the dashboard Under **Ratelimit**, open the namespace and its **Overrides** tab to add, edit, or delete overrides. You can also choose **Delete Override** on a row in the namespace's logs. In the dashboard, the identifier must be at least 2 characters and the limit can't go over 10,000. Overrides set through the API outside those bounds still work. Ratelimit namespace Overrides page listing overrides by identifier with their limits, and an Override Identifier button Override Identifier dialog with Identifier, Limit, and Duration fields ## Patterns that work well * **Per-plan limits.** Build identifiers as `${plan}:${userId}` and set one wildcard override per plan (`free:*`, `pro:*`, `enterprise:*`). Update overrides from your billing webhooks when customers change plans. * **Stop an attack.** Set the identifier to a limit of `1` over a long duration, and delete the override when the incident ends. # Rate limiting with Unkey Source: https://www.unkey.com/docs/api-management/ratelimiting/overview Unkey rate limits your API per customer, key, endpoint, or IP, and Compute limits it at the gateway. Per customer, Unkey counts on an identity, so extra keys don't add budget. You can also limit per key, per endpoint with a named limit or a `cost`, and per IP with `ratelimit.limit`. Monthly quotas are separate, as credits on the key. On Compute, a gateway policy applies these limits before the request reaches your API. ## Which one do I use Use the row that matches what you want to count. | You want to limit | What Unkey uses | Read more | | - | - | - | | Per customer | A rate limit on the identity, shared by every key with that `externalId` | [Shared rate limits](/docs/api-management/identities/shared-rate-limits) | | Per key | A named rate limit on the key, checked during `keys.verifyKey` | [Key and identity rate limits](/docs/api-management/ratelimiting/key-and-identity-limits) | | Per endpoint | A named limit the route opts into, or a `cost` on that call. With no key, one namespace per route on `ratelimit.limit` | [Per-endpoint rate limits](/docs/api-management/cookbook/endpoint-rate-limit) | | Per IP | The IP as the identifier on `ratelimit.limit`. On Compute, a gateway policy can count by remote IP | [ratelimit.limit](/docs/api-management/ratelimiting/limit-and-multi-limit), [Rate limit policy](/docs/compute/gateway/rate-limiting) | | Monthly quota | Credits on the key, with `credits.refill.interval` set to `monthly` | [Credits and refill](/docs/api-management/keys/credits-and-refill) | ## Limit per customer Create the identity with the shared limit, then create each key with the same `externalId`. `keys.verifyKey` counts all of those keys together. The client is `@unkey/api` . ```typescript theme={"system"} import { Unkey } from "@unkey/api"; const unkey = new Unkey({ rootKey: process.env.UNKEY_ROOT_KEY }); await unkey.identities.createIdentity({ externalId: "cus_acme", ratelimits: [ { name: "requests", limit: 100, duration: 60000, autoApply: true }, ], }); await unkey.keys.createKey({ apiId: "api_...", externalId: "cus_acme" }); export async function handler(key: string) { const { data } = await unkey.keys.verifyKey({ key }); if (data.valid) { return new Response("ok"); } if (data.code === "RATE_LIMITED") { return new Response("Too many requests", { status: 429 }); } return new Response("Unauthorized", { status: 401 }); } ``` To override that customer's limit, call `identities.updateIdentity` with `identity` set to the same `externalId` and a new `ratelimits` array. The array replaces the whole list, so send every limit the customer should keep. See [Per-user rate limits](/docs/api-management/cookbook/per-user-rate-limit). Unkey enforces per-user and per-customer rate limits, and a monthly request quota, on an API you host yourself. You don't run Redis or any other counter store. Attach both to an API key, or call `ratelimit.limit` with a user id, a customer id, or any other stable string. A rate limit is a sliding window, such as 100 requests per minute. A monthly quota is credits on the key: set `credits.refill.interval` to `monthly`, and each successful `keys.verifyKey` spends from that balance. Credits are an exact total. Rate limit counts are close, and they can run a little high when traffic is split across regions. See [Credits and refill](/docs/api-management/keys/credits-and-refill). ## Add a per-key limit Create the key with a per-minute limit and a monthly credit refill, then verify it on each request. This limit counts that key alone. `externalId` still ties the key to one customer. ```typescript theme={"system"} import { Unkey } from "@unkey/api"; const unkey = new Unkey({ rootKey: process.env.UNKEY_ROOT_KEY }); await unkey.keys.createKey({ apiId: "api_...", externalId: "cus_acme", credits: { remaining: 10000, refill: { interval: "monthly", amount: 10000, refillDay: 1 }, }, ratelimits: [ { name: "requests", limit: 100, duration: 60000, autoApply: true }, ], }); export async function handler(request: Request) { const header = request.headers.get("authorization") ?? ""; const key = header.startsWith("Bearer ") ? header.slice("Bearer ".length) : header; const { data } = await unkey.keys.verifyKey({ key }); if (data.valid) { return new Response("ok"); } if (data.code === "RATE_LIMITED") { const resetMs = data.ratelimits?.find((limit) => limit.exceeded)?.reset ?? Date.now(); const retryAfter = Math.max(1, Math.ceil((resetMs - Date.now()) / 1000)); return new Response("Too many requests", { status: 429, headers: { "Retry-After": String(retryAfter) }, }); } if (data.code === "USAGE_EXCEEDED") { return new Response("Monthly quota exceeded", { status: 429 }); } return new Response("Unauthorized", { status: 401 }); } ``` `duration` is the window in milliseconds. `autoApply: true` checks that limit on every verification. The monthly refill replaces the balance on `refillDay` at 00:00 UTC. It doesn't add to what's left. For an endpoint with no API key, call `ratelimit.limit` with your own identifier instead. The same pattern is on [ratelimit.limit and multiLimit](/docs/api-management/ratelimiting/limit-and-multi-limit). ## What a limited request looks like `keys.verifyKey` and `ratelimit.limit` return HTTP 200 when your user is over their limit. Read the body. `keys.verifyKey` sets `data.valid` to `false` and `data.code` to `RATE_LIMITED` (or `USAGE_EXCEEDED` when credits run out). Each checked limit is in `data.ratelimits`, with `limit`, `remaining`, `reset`, and `exceeded`. `ratelimit.limit` sets `data.success` to `false` and puts `limit`, `remaining`, and `reset` on `data`. `reset` is Unix milliseconds, when the current window ends. Your server is the one that returns `429 Too Many Requests` to the caller. RFC 6585 defines that status and says the response may include `Retry-After`, a number of seconds to wait. Set it from `reset`, and use at least 1 second. Unkey doesn't send `Retry-After` or `X-RateLimit-*` on these two endpoints for your user's limit, so copy `limit`, `remaining`, and `reset` onto your own response if you want those headers. `X-RateLimit-Reset` on the Compute gateway is Unix seconds, so divide `reset` by 1000 if you want to match it. If the API runs on Compute, the gateway returns 429 itself. The response has `Retry-After` (seconds, never under 1), `X-RateLimit-Limit`, `X-RateLimit-Remaining`, and `X-RateLimit-Reset` (Unix seconds). See [rate\_limited](/docs/errors/frontline/client/rate_limited). Your server's own calls to `api.unkey.com` have a separate workspace limit. If you exceed it, Unkey returns HTTP 429 with `RateLimit-Limit`, `RateLimit-Remaining`, `RateLimit-Reset` (seconds until the window ends), and `Retry-After`. Those headers describe your workspace's budget for the Unkey API, not your users' limits. Rate limiting in API Management has two forms. A standalone API takes any identifier you choose. Limits on a key or identity are checked when the key is verified. The Compute gateway's rate limit policy is separate. See the [glossary](/docs/platform/glossary). ## Choose a rate limit | | Standalone `ratelimit.limit` | Key and identity rate limits | | - | - | - | | What you limit | Any string: a user ID, an IP address, an email domain, a tenant | A key, or every key of an identity together | | Where it runs | Its own endpoint, whenever you call it | Inside `keys.verifyKey`, as one of its checks | | Configuration | You send the limit and duration with every call | Named limits stored on the key or identity | | Per-caller exceptions | Overrides on an identifier or wildcard pattern | Different limits per key or identity | | Failure signal | `success: false` in the response | `code: RATE_LIMITED` in the verification | Use the standalone API when the endpoint has no key (sign-up, password reset) or when you want to limit something other than the key holder. Use key and identity limits when the request already carries a key, so the check happens during the verification you already make. Both count requests the same way. See [How rate limiting works](/docs/api-management/ratelimiting/how-it-works). ## A first 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. The root key needs `ratelimit.*.limit`, plus `ratelimit.*.create_namespace` the first time a namespace name is used. ```bash 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": "email.send", "identifier": "user_123", "limit": 10, "duration": 60000 }' ``` ```json theme={"system"} { "meta": { "requestId": "req_..." }, "data": { "success": true, "limit": 10, "remaining": 9, "reset": 1704067260000 } } ``` The namespace `email.send` names what you're limiting. The identifier `user_123` is who's being counted. Each user gets ten calls per minute, and the eleventh returns `success: false` until the window moves on. In the TypeScript SDK, the same call is `unkey.ratelimit.limit({ namespace, identifier, limit, duration })`. ## Next steps Sliding windows, regional counters, cross-region convergence, and what `remaining` and `reset` mean. Request and response fields, bounds, namespaces, cost, and atomic multi-limit checks. Give one identifier or a wildcard pattern a different limit without changing code. Named limits on keys and identities, auto-apply, cost, and inline limits at verification. # Nuxt module (community) Source: https://www.unkey.com/docs/api-management/sdks/community/nuxt A community-maintained Nuxt module for Unkey, and what to use for a supported setup. `@unkey/nuxt` (version on npm) is a community-maintained Nuxt integration for Unkey. We don't maintain or test it. ## What the registry says It's built on an old version of `@unkey/api` (`0.20`), so it uses older response shapes, not the `Unkey` client documented in [@unkey/api](/docs/api-management/sdks/typescript/api). Check the [npm page](https://www.npmjs.com/package/@unkey/nuxt) for its options. ## Install ```bash theme={"system"} npm install @unkey/nuxt ``` ## Supported alternative For a setup we support, use [@unkey/api](/docs/api-management/sdks/typescript/api) in a Nuxt server route or middleware. Create the client with a root key from runtime config, and call `keys.verifyKey` with the bearer token from the request. It's the same few lines the [Hono](/docs/api-management/sdks/typescript/hono) and [Next.js](/docs/api-management/sdks/typescript/nextjs) packages run for you. # Go SDK Source: https://www.unkey.com/docs/api-management/sdks/go Manage keys, rate limits, and deployments from Go with the v3 client. Use the Go SDK, `github.com/unkeyed/sdks/api/go/v3` (current release ), to call the Unkey API from Go. The older `v1` and `v2` module paths are no longer updated. 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. ## Install ```bash theme={"system"} go get github.com/unkeyed/sdks/api/go/v3 ``` It needs a recent Go toolchain (currently Go 1.25). ## Construct the client ```go theme={"system"} import ( unkey "github.com/unkeyed/sdks/api/go/v3" ) client := unkey.New( unkey.WithSecurity(os.Getenv("UNKEY_ROOT_KEY")), ) ``` `WithSecurity` sets your root key. To change retries for every call, add `unkey.WithRetryConfig(retry.Config{...})`. For one call, pass `operations.WithRetries(...)` as the last argument. Request and response types are in `github.com/unkeyed/sdks/api/go/v3/models/components`, and typed errors in `.../models/apierrors`. ## Verify a key ```go theme={"system"} import ( "errors" "github.com/unkeyed/sdks/api/go/v3/models/components" ) res, err := client.Keys.VerifyKey(ctx, components.V2KeysVerifyKeyRequestBody{ Key: "prod_abc123", Permissions: unkey.String("documents.read"), // optional permission query }) if err != nil { return err } if res.V2KeysVerifyKeyResponseBody == nil { return errors.New("unkey: empty response body") } data := res.V2KeysVerifyKeyResponseBody.Data if !data.Valid { // data.Code says why: NOT_FOUND, DISABLED, EXPIRED, RATE_LIMITED, ... } ``` The response field, here `V2KeysVerifyKeyResponseBody`, is a pointer, so check it for `nil` even when `err` is `nil`. Optional request fields are pointers or slices. Use helpers such as `unkey.String` and `unkey.Bool` for pointer values. An invalid key is a successful call with `Valid` false, not an `error`. The meaning of each field is covered in [Verifying keys](/docs/api-management/keys/verifying-keys) and the schema in [keys.verifyKey](/docs/api-management/api-reference/keys/verify-api-key). ## Create a key ```go theme={"system"} res, err := client.Keys.CreateKey(ctx, components.V2KeysCreateKeyRequestBody{ APIID: "api_1234abcd", Prefix: unkey.String("prod"), Name: unkey.String("Payment service"), ExternalID: unkey.String("user_42"), Permissions: []string{"documents.read"}, Enabled: unkey.Bool(true), }) if err != nil { return err } created := res.V2KeysCreateKeyResponseBody.Data fmt.Println(created.KeyID) fmt.Println(created.Key) // returned once; store it or hand it to the user now ``` `ByteLength`, `Meta`, `Roles`, `Expires`, `Credits`, `Ratelimits`, and `Recoverable` are the remaining fields. [Creating keys](/docs/api-management/keys/creating-keys) explains them and [keys.createKey](/docs/api-management/api-reference/keys/create-api-key) lists the schema. ## Apply a rate limit ```go theme={"system"} res, err := client.Ratelimit.Limit(ctx, components.V2RatelimitLimitRequestBody{ Namespace: "email.send", Identifier: "user_42", Limit: 10, Duration: 60_000, // milliseconds }) if err != nil { return err } if !res.V2RatelimitLimitResponseBody.Data.Success { // Data.Remaining and Data.Reset (Unix ms) tell the caller when to retry } ``` The endpoint is [ratelimit.limit](/docs/api-management/api-reference/ratelimit/apply-rate-limiting). ## Create a deployment The client covers Compute too. Name the project, app, and environment, and optionally set the source with one of `Oci`, `Git`, or `Deployment`. Leave all three out to use the app's default: ```go theme={"system"} res, err := client.Deployments.CreateDeploymentV3(ctx, components.V3DeploymentsCreateDeploymentRequestBody{ Project: "my-project", App: "api", Environment: "production", Oci: &components.DeploymentSourceOCI{Image: "ghcr.io/acme/api:v1.0.0"}, }) if err != nil { return err } fmt.Println(res.V3DeploymentsCreateDeploymentResponseBody.Data.DeploymentID) ``` The endpoint is [deployments.createDeploymentV3](/docs/compute/api-reference/deployments/create-deployment). Don't use the `CreateDeployment` method. It calls the deprecated v2 endpoint. For projects, apps, and environments, see [Projects, apps, and environments](/docs/compute/concepts/projects-apps-environments). ## Handle errors Every method returns a response or an error, never both. API errors have a type per status that you can match with `errors.As`. Anything else is an `*apierrors.APIError`. ```go theme={"system"} import ( "errors" "github.com/unkeyed/sdks/api/go/v3/models/apierrors" ) res, err := client.Keys.GetKey(ctx, components.V2KeysGetKeyRequestBody{KeyID: "key_1234abcd"}) if err != nil { var notFound *apierrors.NotFoundErrorResponse var unauthorized *apierrors.UnauthorizedErrorResponse var apiErr *apierrors.APIError switch { case errors.As(err, ¬Found): fmt.Println("not found:", notFound.Error_.GetDetail()) case errors.As(err, &unauthorized): fmt.Println("check the root key:", unauthorized.Error_.GetDetail()) case errors.As(err, &apiErr): fmt.Println(apiErr.StatusCode, apiErr.Message) default: return err // transport failure } } ``` The typed errors are `BadRequestErrorResponse` (400, whose `Error_` is a `components.BadRequestErrorDetails` with a per-field `GetErrors()` list), `UnauthorizedErrorResponse` (401), `ForbiddenErrorResponse` (403), `NotFoundErrorResponse` (404), `ConflictErrorResponse` (409), `GoneErrorResponse` (410), `PreconditionFailedErrorResponse` (412), `UnprocessableEntityErrorResponse` (422), `TooManyRequestsErrorResponse` (429), `InternalServerErrorResponse` (500), and `ServiceUnavailableErrorResponse` (503). Each endpoint page in the API reference, for example [keys.getKey](/docs/api-management/api-reference/keys/get-api-key), lists which ones apply. ## Next steps The same calls from a shell, built on this module. Request and response fields for every method. # SDKs Source: https://www.unkey.com/docs/api-management/sdks/overview Official libraries for TypeScript, Go, and Python, plus a community Nuxt module. Use an SDK to call the Unkey API from your code. We publish clients for TypeScript, Go, and Python that cover every endpoint, plus smaller TypeScript packages for one job each, such as verifying keys in a Hono route. The community maintains a Nuxt module. Every package that calls the API needs a root 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. ## Packages | Package | Language | Current version | What it is | | - | - | - | - | | [@unkey/api](/docs/api-management/sdks/typescript/api) | TypeScript | Generated client for the whole API | | | [@unkey/ratelimit](/docs/api-management/sdks/typescript/ratelimit) | TypeScript | Standalone rate limiting with timeouts and fallbacks | | | [@unkey/hono](/docs/api-management/sdks/typescript/hono) | TypeScript | Hono middleware that verifies the bearer key | | | [@unkey/nextjs](/docs/api-management/sdks/typescript/nextjs) | TypeScript | `withUnkey` wrapper for Next.js route handlers | | | [@unkey/cache](/docs/api-management/sdks/typescript/cache) | TypeScript | Typed tiered cache with stale-while-revalidate | | | [Go SDK](/docs/api-management/sdks/go) | Go | Generated client, module `github.com/unkeyed/sdks/api/go/v3` | | | [Python SDK](/docs/api-management/sdks/python) | Python | Generated client `unkey.py`, sync and async | | | [Nuxt module](/docs/api-management/sdks/community/nuxt) | TypeScript | Community-maintained Nuxt integration | | The TypeScript, Go, and Python clients match the [API reference](/docs/api-management/api-reference/keys/verify-api-key), so method names and fields line up across languages. The Python client () covers API Management only. The TypeScript and Go clients also cover Compute. ## Which package to use * **`@unkey/api`, the Go SDK, or the Python SDK** when you call several endpoints or want full types. * **`@unkey/hono` or `@unkey/nextjs`** when you only need to check incoming requests for a valid key. They call `keys.verifyKey` and give your handler the result. * **`@unkey/ratelimit`** to rate limit user IDs or IP addresses without issuing keys. It adds a timeout and fallback so a network problem can't block your function. * **`@unkey/cache`** for typed caching in your own services. It doesn't call the Unkey API. ## One client, both products The TypeScript and Go clients cover Compute too, so the same `Unkey` instance that verifies a key can create a deployment for a Compute app. The [Go SDK](/docs/api-management/sdks/go) page shows the same pair in Go. From Python, call Compute endpoints over HTTP. The endpoints are [keys.verifyKey](/docs/api-management/api-reference/keys/verify-api-key) and [deployments.createDeploymentV3](/docs/compute/api-reference/deployments/create-deployment). ```typescript Verify a key theme={"system"} import { Unkey } from "@unkey/api"; const unkey = new Unkey({ rootKey: process.env.UNKEY_ROOT_KEY ?? "" }); const result = await unkey.keys.verifyKey({ key: "prod_abc123" }); if (!result.data.valid) { // result.data.code says why: NOT_FOUND, DISABLED, EXPIRED, RATE_LIMITED, ... } ``` ```typescript Create a deployment theme={"system"} import { Unkey } from "@unkey/api"; const unkey = new Unkey({ rootKey: process.env.UNKEY_ROOT_KEY ?? "" }); const result = await unkey.deployments.createDeploymentV3({ project: "my-project", app: "api", environment: "production", oci: { image: "ghcr.io/acme/api:v1.0.0" }, }); console.log(result.data.deploymentId); ``` For Compute concepts such as projects, apps, and environments, see [Projects, apps, and environments](/docs/compute/concepts/projects-apps-environments). ## Response shape Every successful response has `meta.requestId` (quote it when you contact support) and `data`, the result. Errors are thrown or returned as typed errors per HTTP status. Each SDK page shows how to catch them. See [API overview](/docs/platform/api/overview) for the raw format. ## Next steps Install, construct, verify and create keys, handle errors. `go get` the v3 module the CLI uses. `unkey.py` with sync and async clients. # Python SDK Source: https://www.unkey.com/docs/api-management/sdks/python Manage keys and rate limits from Python with unkey.py. Use `unkey.py` to call the Unkey API from Python. The current release is , and it needs Python 3.10 or newer. Every method has a regular and an `_async` version. Methods are grouped as `unkey.keys`, `unkey.apis`, `unkey.identities`, `unkey.permissions`, `unkey.ratelimit`, `unkey.analytics`, and `unkey.portal`. 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. ## Install ```bash uv theme={"system"} uv add unkey.py ``` ```bash pip theme={"system"} pip install unkey.py ``` ```bash poetry theme={"system"} poetry add unkey.py ``` ## Construct the client ```python theme={"system"} import os from unkey.py import Unkey with Unkey(root_key=os.environ["UNKEY_ROOT_KEY"]) as unkey: ... ``` `root_key` is your root key. The client is a context manager that closes its connections on exit. In a long-running service, create one client and reuse it. To change retries, pass `retry_config=RetryConfig(...)` from `unkey.py.utils` to the client or to one call. ## Verify a key ```python Sync theme={"system"} res = unkey.keys.verify_key(key="prod_abc123", permissions="documents.read") if not res.data.valid: print(res.data.code) # NOT_FOUND, FORBIDDEN, INSUFFICIENT_PERMISSIONS, USAGE_EXCEEDED, RATE_LIMITED, DISABLED, EXPIRED ``` ```python Async theme={"system"} import asyncio import os from unkey.py import Unkey async def main(): async with Unkey(root_key=os.environ["UNKEY_ROOT_KEY"]) as unkey: res = await unkey.keys.verify_key_async(key="prod_abc123") print(res.data.valid) asyncio.run(main()) ``` Request fields are `snake_case` keyword arguments (`tags`, `permissions`, `credits`, `ratelimits`). An invalid key isn't an exception: `data.valid` is false and `data.code` says why. See [Verifying keys](/docs/api-management/keys/verifying-keys) for what each check means and [keys.verifyKey](/docs/api-management/api-reference/keys/verify-api-key) for the schema. ## Create a key ```python theme={"system"} res = unkey.keys.create_key( api_id="api_1234abcd", prefix="prod", name="Payment service", external_id="user_42", permissions=["documents.read"], enabled=True, ) print(res.data.key_id) print(res.data.key) # returned once; store it or hand it to the user now ``` The remaining fields (`byte_length`, `meta`, `roles`, `expires`, `credits`, `ratelimits`, `recoverable`) are described in [Creating keys](/docs/api-management/keys/creating-keys), and the endpoint is [keys.createKey](/docs/api-management/api-reference/keys/create-api-key). ## Apply a rate limit ```python theme={"system"} res = unkey.ratelimit.limit( namespace="email.send", identifier="user_42", limit=10, duration=60_000, # milliseconds ) if not res.data.success: ... # res.data.remaining and res.data.reset (Unix ms) tell the caller when to retry ``` The endpoint is [ratelimit.limit](/docs/api-management/api-reference/ratelimit/apply-rate-limiting). ## Handle errors HTTP errors raise an `errors.UnkeyError`, with `message`, `status_code`, `headers`, `body`, and `raw_response`. Each status has its own subclass with a typed `data` attribute holding the API's `meta` and `error` fields. ```python theme={"system"} from unkey.py import Unkey, errors try: res = unkey.keys.get_key(key_id="key_1234abcd") except errors.NotFoundErrorResponse as e: print(e.data.meta.request_id, e.data.error.detail) except errors.UnkeyError as e: print(e.status_code, e.message) ``` The subclasses are `BadRequestErrorResponse` (400), `UnauthorizedErrorResponse` (401), `ForbiddenErrorResponse` (403), `NotFoundErrorResponse` (404), `ConflictErrorResponse` (409), `GoneErrorResponse` (410), `PreconditionFailedErrorResponse` (412), `UnprocessableEntityErrorResponse` (422), `TooManyRequestsErrorResponse` (429), `InternalServerErrorResponse` (500), and `ServiceUnavailableErrorResponse` (503). Network failures raise `httpx.RequestError` subclasses such as `httpx.ConnectError` and `httpx.TimeoutException`. Each endpoint page in the API reference, for example [keys.getKey](/docs/api-management/api-reference/keys/get-api-key), lists which errors it can return. ## Next steps Every field a key can carry. Request and response fields for every method. # @unkey/api Source: https://www.unkey.com/docs/api-management/sdks/typescript/api The generated TypeScript client for the whole Unkey API. Use `@unkey/api` to call the whole Unkey API from TypeScript. The current version is . Create one `Unkey` client. Its properties (`keys`, `apis`, `ratelimit`, `identities`, `permissions`, `deployments`, and so on) group the methods the same way as the [API reference](/docs/api-management/api-reference/keys/verify-api-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. ## Install ```bash npm theme={"system"} npm add @unkey/api ``` ```bash pnpm theme={"system"} pnpm add @unkey/api ``` ```bash bun theme={"system"} bun add @unkey/api ``` ```bash yarn theme={"system"} yarn add @unkey/api ``` It works with CommonJS and ES modules. Supported runtimes are listed in [RUNTIMES.md](https://github.com/unkeyed/sdks/blob/main/api/ts/RUNTIMES.md). ## Construct the client ```typescript theme={"system"} import { Unkey } from "@unkey/api"; const unkey = new Unkey({ rootKey: process.env.UNKEY_ROOT_KEY ?? "", }); ``` `rootKey` is your root key. Methods that use a different credential, such as the portal endpoints, take it as an argument on each call. ## Verify a key ```typescript theme={"system"} const result = await unkey.keys.verifyKey({ key: "prod_abc123", permissions: "documents.read", // optional permission query tags: ["endpoint=/documents"], // optional, for analytics filtering }); if (result.data.valid) { console.log(result.data.keyId, result.data.permissions); } else { console.log(result.data.code); // NOT_FOUND | FORBIDDEN | INSUFFICIENT_PERMISSIONS | USAGE_EXCEEDED | RATE_LIMITED | DISABLED | EXPIRED } ``` An invalid key isn't an error: `data.valid` is `false` and `data.code` says why. You can also send `credits` for a custom cost and `ratelimits` to check named limits. See [Verifying keys](/docs/api-management/keys/verifying-keys), and the endpoint is [keys.verifyKey](/docs/api-management/api-reference/keys/verify-api-key). ## Create a key ```typescript theme={"system"} const created = await unkey.keys.createKey({ apiId: "api_1234abcd", prefix: "prod", name: "Payment service", externalId: "user_42", permissions: ["documents.read"], enabled: true, }); console.log(created.data.keyId); console.log(created.data.key); // shown once; store or hand it to the user now ``` Optional fields include `byteLength`, `meta`, `roles`, `expires` (Unix milliseconds), `credits`, `ratelimits`, and `recoverable`. See [Creating keys](/docs/api-management/keys/creating-keys) for what they do and [keys.createKey](/docs/api-management/api-reference/keys/create-api-key) for the full schema. ## Apply a rate limit ```typescript theme={"system"} const rl = await unkey.ratelimit.limit({ namespace: "email.send", identifier: "user_42", limit: 10, duration: 60_000, // milliseconds cost: 1, }); if (!rl.data.success) { // rl.data.remaining and rl.data.reset (Unix ms) tell the caller when to retry } ``` The endpoint is [ratelimit.limit](/docs/api-management/api-reference/ratelimit/apply-rate-limiting). For rate limiting without a client instance and with a built-in timeout fallback, use [@unkey/ratelimit](/docs/api-management/sdks/typescript/ratelimit), which wraps this call. ## Handle errors HTTP errors throw an `UnkeyError`, with `message`, `statusCode`, `headers`, `body`, and `rawResponse`. Each status has its own subclass with a typed `data$` holding the API's `meta` and `error` fields. ```typescript theme={"system"} import * as errors from "@unkey/api/models/errors"; try { await unkey.keys.getKey({ keyId: "key_1234abcd" }); } catch (err) { if (err instanceof errors.NotFoundErrorResponse) { console.log(err.data$.meta.requestId, err.data$.error.detail); } else if (err instanceof errors.UnkeyError) { console.log(err.statusCode, err.message); } else { throw err; // ConnectionError, RequestTimeoutError, and other client-side failures } } ``` The subclasses are `BadRequestErrorResponse` (400), `UnauthorizedErrorResponse` (401), `ForbiddenErrorResponse` (403), `NotFoundErrorResponse` (404), `ConflictErrorResponse` (409), `GoneErrorResponse` (410), `PreconditionFailedErrorResponse` (412), `UnprocessableEntityErrorResponse` (422), `TooManyRequestsErrorResponse` (429), `InternalServerErrorResponse` (500), and `ServiceUnavailableErrorResponse` (503). Each method's page in the API reference, for example [keys.getKey](/docs/api-management/api-reference/keys/get-api-key), lists which ones it can throw. See also [API errors](/docs/platform/api/errors). ## Retries and standalone functions Calls retry 5xx responses and connection errors by default, waiting 50 ms at first and growing 1.5 times each retry, up to 1 s per wait and 10 s in total. To change this, pass `{ retries: { strategy: "backoff", ... } }` as the second argument of a call, or `retryConfig` to the constructor. To turn it off, pass `{ retries: { strategy: "none" } }`. To keep your bundle small, every method is also a standalone function (`keysVerifyKey`, `keysCreateKey`, `ratelimitLimit`, and so on) that takes the client as its first argument. See [FUNCTIONS.md](https://github.com/unkeyed/sdks/blob/main/api/ts/FUNCTIONS.md). ## Next steps Let middleware call `verifyKey` for you. Every method, request field, and error per endpoint. # @unkey/cache Source: https://www.unkey.com/docs/api-management/sdks/typescript/cache Add typed, tiered caching with stale-while-revalidate to your own TypeScript services. Use `@unkey/cache` (version ) to add a typed cache to your own services, with several layers such as memory in front of Redis. It doesn't call the Unkey API and needs no root key. You define typed namespaces, give each a list of stores, and get `get`, `set`, `remove`, and `swr` with fresh and stale times handled for you. ## Install ```bash theme={"system"} npm install @unkey/cache ``` ## Quickstart ```typescript theme={"system"} import { createCache, DefaultStatefulContext, Namespace } from "@unkey/cache"; import { MemoryStore } from "@unkey/cache/stores"; type User = { id: string; email: string }; // Cloudflare Workers and Vercel hand you an execution context; elsewhere use this one. const ctx = new DefaultStatefulContext(); const memory = new MemoryStore({ persistentMap: new Map() }); const cache = createCache({ user: new Namespace(ctx, { stores: [memory], fresh: 60_000, // serve without revalidating for 60 s stale: 900_000, // serve stale and revalidate in the background for up to 15 min }), }); await cache.user.set("user_42", { id: "user_42", email: "ada@example.com" }); const { val: user, err } = await cache.user.get("user_42"); if (err) { // a CacheError from a store; decide whether to fall through to the origin } ``` Reads and writes return `{ val, err }` instead of throwing, so a failing store acts like a cache miss instead of breaking your request. ## Concepts ### Namespaces A namespace holds one type: `Namespace` only accepts and returns `User`. `createCache` returns an object with one entry per namespace, each with `get(key)`, `set(key, value, opts?)`, `remove(key)` (which also takes an array), and `swr(key, loadFromOrigin)`. ### Tiered stores `stores` is a list in order. Reads try each store and return the first hit, copying it back to the earlier stores. Writes go to every store. A common setup is memory first, then a shared store such as Cloudflare or Redis. ### Fresh and stale `fresh` and `stale` are in milliseconds. Before `fresh`, a value is returned as is. Between `fresh` and `stale`, it's still returned, and `swr` reloads it in the background through `ctx.waitUntil` (that's why a context is required). After `stale`, it counts as a miss. ### Stale-while-revalidate ```typescript theme={"system"} const { val: user } = await cache.user.swr("user_42", async (id) => { return await db.users.findById(id); }); ``` `swr` returns the cached value if there is one, refreshes it in the background if it's past `fresh`, and only waits for `loadFromOrigin` on a miss. Several misses for the same key at once load the origin only once. ## Stores and middlewares The `@unkey/cache/stores` entry point exports `MemoryStore`, `CloudflareStore` (the Cloudflare Cache API, configured with an API key, zone ID, and domain), `UpstashRedisStore`, and `LibSQLStore` for Turso and other libSQL databases. Any object implementing the exported `Store` interface (`name`, `get`, `set`, `remove`) can be used as a tier. `@unkey/cache/middleware` exports `withEncryption(base64Key)`, which encrypts values before they reach a store, and `withMetrics(metrics)`, which reports hits, misses, and latency. Each returns an object with a `wrap(store)` method. Put the wrapped store in `stores`, not the middleware. `withEncryption` is async and takes 32 random bytes in base64 (`openssl rand -base64 32`). Changing the key makes every entry it wrote unreadable, so they become misses. ```typescript theme={"system"} import { withEncryption } from "@unkey/cache/middleware"; const encrypted = await withEncryption(process.env.CACHE_ENCRYPTION_KEY ?? ""); const cache = createCache({ user: new Namespace(ctx, { stores: [encrypted.wrap(memory)], fresh: 60_000, stale: 900_000, }), }); ``` ## Source The package is in the [unkeyed/sdks](https://github.com/unkeyed/sdks/tree/main/cache) repository. `src/examples/` has runnable samples. # @unkey/hono Source: https://www.unkey.com/docs/api-management/sdks/typescript/hono Hono middleware that verifies API keys for you. `@unkey/hono` (version ) is a [Hono](https://hono.dev) middleware that verifies the API key on each request and puts the result on the context. It needs `hono` 4.6 or later. 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. ## Install ```bash theme={"system"} npm install @unkey/hono ``` ## Protect routes ```typescript theme={"system"} import { Hono } from "hono"; import { type UnkeyContext, unkey } from "@unkey/hono"; const app = new Hono<{ Variables: { unkey: UnkeyContext } }>(); app.use("*", unkey({ rootKey: process.env.UNKEY_ROOT_KEY ?? "" })); app.get("/protected", (c) => { const verification = c.get("unkey"); if (!verification.data.valid) { return c.json({ error: "unauthorized" }, 401); } return c.json({ keyId: verification.data.keyId, permissions: verification.data.permissions }); }); ``` The middleware reads the bearer token from the `Authorization` header, calls `keys.verifyKey`, and puts the full result in `c.get("unkey")`, so your handler can read `data.valid`, `data.code`, `data.keyId`, `data.meta`, and the rest. A request with no key gets `401 {"error":"unauthorized"}`. Unless you pass `handleInvalidKey`, an invalid key doesn't stop the request: the middleware stores the failed verification and calls the next handler. Check `data.valid` in your handler, or set `handleInvalidKey` to reject centrally. ## Options Root key used to call `keys.verifyKey`. A permission query the key must satisfy for `data.valid` to be `true`, for example `"documents.read"`. Tags recorded with the verification for later filtering in analytics. Read the key from somewhere else, such as a query parameter. Return a `Response` to stop there. Return nothing to get a 401. Called when the key is present but `data.valid` is `false`. Return the response the client should get. Called only for unexpected responses from the verify call, such as a 502 from a proxy. Return the response the client should get. Without it, these become a Hono `HTTPException` with status 500. ## Reject invalid keys centrally ```typescript theme={"system"} app.use( "*", unkey({ rootKey: process.env.UNKEY_ROOT_KEY ?? "", permissions: "documents.read", getKey: (c) => c.req.query("api_key"), handleInvalidKey: (c, result) => c.json({ error: "invalid key", code: result.data.code }, 401), onError: (c, err) => { console.error("unkey error", err.message); return c.json({ error: "authentication unavailable" }, 503); }, }), ); ``` `onError` doesn't catch everything. A rejected root key (401), a throttled request (429), a 500, or a connection failure or timeout is thrown instead. Catch those in Hono's `app.onError` if you want one response for every authentication failure. ## Next steps What `valid`, `code`, permissions, and rate limits mean in the result. Call any other endpoint from the same app. # @unkey/nextjs Source: https://www.unkey.com/docs/api-management/sdks/typescript/nextjs Wrap a Next.js route handler so only valid API keys reach it. `@unkey/nextjs` (version ) exports `withUnkey`, a wrapper for App Router route handlers that verifies the API key before your code runs. Unlike the Hono middleware, it rejects invalid keys by default, so your handler only runs for valid 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. ## Install ```bash theme={"system"} npm install @unkey/nextjs ``` ## Protect a route handler ```typescript app/api/hello/route.ts theme={"system"} import { type NextRequestWithUnkeyContext, withUnkey } from "@unkey/nextjs"; export const POST = withUnkey( async (req: NextRequestWithUnkeyContext) => { const { keyId, meta } = req.unkey.data; return Response.json({ message: "your key is valid", keyId, meta }); }, { rootKey: process.env.UNKEY_ROOT_KEY ?? "" }, ); ``` The wrapper reads the bearer token from the `authorization` header and calls `keys.verifyKey`: * **No key:** it responds `401 {"error":"unauthorized"}`. * **Invalid key:** it responds `401 Unauthorized`. * **Valid key:** it puts the full result in `req.unkey` and runs your handler. The second `context` argument (route params) works as usual. If the verify call gets an unexpected response, such as a 502 from a proxy, the wrapper logs it and responds `500 Internal Server Error`. Other failures (a rejected root key, a throttled request, a 500, a connection failure or timeout) aren't caught, so Next.js returns its own 500. To control that response, verify with [@unkey/api](/docs/api-management/sdks/typescript/api) directly. ## Options Root key used to call `keys.verifyKey`. A permission query the key must satisfy for the verification to be valid. Tags recorded with the verification for analytics filtering. Read the key yourself, for example `new URL(req.url).searchParams.get("key")`. Return a response to send it instead. Return `null` for the default 401. Replace the default `401 Unauthorized` for keys that verify as invalid. `result.data.code` says why. Replace the default logged error and `500`. Only called for unexpected responses, not for the other failures described above. ## Customize the responses ```typescript theme={"system"} export const GET = withUnkey( async (req) => Response.json({ ok: true }), { rootKey: process.env.UNKEY_ROOT_KEY ?? "", permissions: "reports.read", getKey: (req) => new URL(req.url).searchParams.get("key"), handleInvalidKey: (req, result) => Response.json({ error: "invalid key", code: result.data.code }, { status: 401 }), onError: (req, err) => { console.error(`unkey error ${err.statusCode}: ${err.message}`); return Response.json({ error: "authentication unavailable" }, { status: 503 }); }, }, ); ``` ## Next steps What the verification result contains. Create keys and call other endpoints from server code. # @unkey/ratelimit Source: https://www.unkey.com/docs/api-management/sdks/typescript/ratelimit Rate limit from serverless functions with a timeout fallback built in. Use `@unkey/ratelimit` (version ) to rate limit a Next.js route, an Express handler, or any other TypeScript server. You pass a user id, a customer id, or an IP address, and Unkey keeps the counters, so there's no Redis to run. The old Next.js and Express rate limit quickstarts point here. The package wraps `ratelimit.limit`. You set the namespace and limit once, so each check is one line. It answers from memory for identifiers that are already blocked, and it returns a fallback answer if the API takes too long. It doesn't issue API keys. For a limit stored on a key, including a monthly credit quota, see [Key and identity rate limits](/docs/api-management/ratelimiting/key-and-identity-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. ## Install ```bash theme={"system"} npm install @unkey/ratelimit ``` ## Configure ```typescript theme={"system"} import { Ratelimit } from "@unkey/ratelimit"; const limiter = new Ratelimit({ rootKey: process.env.UNKEY_ROOT_KEY ?? "", namespace: "email.send", limit: 10, duration: "30s", }); ``` Root key used to call the API. Rate limit namespace, created on first use. Identifiers are counted separately in each namespace. Requests allowed per window. Window length as milliseconds or a string such as `"30s"`, `"5 m"`, `"1h"`, or `"1d"` (units `ms`, `s`, `m`, `h`, `d`). How long to wait for the API before returning `fallback`. Defaults to 5 seconds with a fallback that denies the request. Set `false` to wait indefinitely. Called when the request fails, and its return value becomes the result. Without it the error is thrown. Alternative API URL. You don't normally need to set it. Storage for the local memory of blocked identifiers. Defaults to a new `Map`. Pass your own to share it across instances. Opt out of the library's telemetry. ## Use it The same `limiter` works in a Next.js route handler and in Express. `limit` returns `{ success, limit, remaining, reset }`. Unkey's HTTP response to your server is 200 even when `success` is `false`, so your handler is the one that returns `429 Too Many Requests`. RFC 6585 defines that status and allows `Retry-After` in seconds. `reset` is Unix milliseconds. This package doesn't set rate limit headers for you. ```typescript app/api/search/route.ts theme={"system"} export async function POST(request: Request) { const userId = request.headers.get("x-user-id") ?? "anonymous"; const result = await limiter.limit(userId); if (!result.success) { const retryAfter = Math.max(1, Math.ceil((result.reset - Date.now()) / 1000)); return new Response("Too many requests", { status: 429, headers: { "Retry-After": String(retryAfter), "X-RateLimit-Limit": String(result.limit), "X-RateLimit-Remaining": String(result.remaining), "X-RateLimit-Reset": String(Math.floor(result.reset / 1000)), }, }); } return new Response("ok"); } ``` ```typescript middleware.ts theme={"system"} import { Ratelimit } from "@unkey/ratelimit"; import type { NextFunction, Request, Response } from "express"; function rateLimit(limiter: Ratelimit) { return async (req: Request, res: Response, next: NextFunction) => { const userId = req.header("x-user-id") ?? req.ip ?? "anonymous"; const result = await limiter.limit(userId); if (result.success) { next(); return; } const retryAfter = Math.max(1, Math.ceil((result.reset - Date.now()) / 1000)); res.set("Retry-After", String(retryAfter)); res.status(429).send("Too many requests"); }; } ``` `X-RateLimit-Reset` in the route handler is Unix seconds, matching the Compute gateway. The JSON field `reset` stays in milliseconds. Which headers Unkey itself sends is on [Rate limiting](/docs/api-management/ratelimiting/overview#what-a-limited-request-looks-like). `overrideId` is set when an override applied. The optional second argument takes `cost` (default 1) and `limit: { limit, duration }` to use a different limit for this call: ```typescript theme={"system"} await limiter.limit(identifier, { cost: 5 }); await limiter.limit(identifier, { limit: { limit: 100, duration: "1h" } }); ``` Once an identifier is blocked, the library answers further calls for it from memory until `reset`, so a client hammering a blocked identifier doesn't cause more API calls. ## Make it fail safe If the API can't be reached and your rate limiter denies everything, your endpoint goes down too. Set a timeout and an error handler to decide what happens: ```typescript theme={"system"} const fallback = (identifier: string) => ({ success: true, // let the request through when Unkey is unreachable limit: 0, remaining: 0, reset: 0, }); const limiter = new Ratelimit({ rootKey: process.env.UNKEY_ROOT_KEY ?? "", namespace: "email.send", limit: 10, duration: "30s", timeout: { ms: 3000, fallback }, onError: (err, identifier) => { console.error(`ratelimit failed for ${identifier}: ${err.message}`); return fallback(identifier); }, }); ``` Allowing keeps your service up during an outage. Denying protects an expensive downstream service. ## Scope `Ratelimit` wraps [ratelimit.limit](/docs/api-management/api-reference/ratelimit/apply-rate-limiting). The package also has: * **`Overrides`**, which takes the same `rootKey` and `baseUrl` and has `getOverride`, `setOverride`, `deleteOverride`, and `listOverrides`. See [ratelimit.setOverride](/docs/api-management/api-reference/ratelimit/set-ratelimit-override). * **`NoopRatelimit`**, which works the same way without any network calls. Use it in tests. For [ratelimit.multiLimit](/docs/api-management/api-reference/ratelimit/apply-multiple-rate-limit-checks) and analytics, use [@unkey/api](/docs/api-management/sdks/typescript/api). For rate limits on keys, see [Key and identity limits](/docs/api-management/ratelimiting/key-and-identity-limits). # API key security Source: https://www.unkey.com/docs/api-management/security/overview How the keys you issue are protected, from storage to leak detection. Here's how the API keys you issue are protected, and what you can do to keep them safe. For workspace security, such as how your team signs in, see [Security](/docs/platform/security/overview). ## Keys are stored as hashes When you create a key, you get it back once and we keep only a hash. A copy of our database wouldn't give anyone a working key, and we can't show a key again. So show the key when it's created and let users roll it over. Don't promise to show it later. See [Key storage](/docs/platform/security/key-storage). ## Recovery is opt-in and separately permissioned If your product must show a key more than once, we can turn on encrypted storage for a keyspace. Keys created with `recoverable: true` are then also stored encrypted with keys unique to your workspace. It's off by default. Creating a recoverable key needs the `encrypt_key` permission, and reading one back needs `decrypt_key`, so you can grant recovery narrowly. See [Recovering keys](/docs/api-management/security/recovering-keys) for the trade-offs and [Recoverable keys](/docs/api-management/keys/recoverable-keys) for the steps. ## Verification doesn't leak `keys.verifyKey` returns `NOT_FOUND` for several failures, including a root key without permission for the keyspace, so nobody can use it to find out which keys exist. Every verification is recorded in [analytics](/docs/api-management/analytics/overview), and verifications made with a root key are also in the [audit log](/docs/platform/workspace/audit-logs/overview). ## IP allow lists A keyspace can have a list of IP addresses its keys can be verified from. Verification from anywhere else fails with `FORBIDDEN`. Use it for keys that should only be used from your own servers. See [Keyspace settings](/docs/api-management/keyspaces/settings). ## Delete protection With delete protection on, you can't delete a keyspace until you turn protection off, so one wrong call can't wipe out a production keyspace and all its keys. See [Delete protection](/docs/platform/security/delete-protection). ## Root keys are scoped A root key has only the permissions you give it, down to one keyspace. For example, `api..verify_key` lets a service verify keys in one keyspace and nothing else. Give each service its own root key with as few permissions as it needs, so a leaked key does little damage and is easy to replace. See [Root key permissions](/docs/platform/root-keys/permissions). ## Leaked credentials GitHub's secret scanning recognizes root keys by their `unkey_` prefix. If one is pushed to a public repository, every member of your workspace gets an email. The keys you issue to your users use your own prefixes, and GitHub doesn't scan for them, so delete or reroll a user's key yourself when you learn it leaked. Details are on [GitHub secret scanning](/docs/platform/security/github-secret-scanning). ## Everything is audited Every change to keyspaces, keys, identities, roles, permissions, and portals is in the audit log, with who did it and from where. You can stream it to your own systems. See [Audit logs](/docs/platform/workspace/audit-logs/overview). # Recovering keys Source: https://www.unkey.com/docs/api-management/security/recovering-keys What you give up when a keyspace lets plaintext keys be read back. Unkey stores every API key as a SHA-256 hash of the full key string, standard base64, with no salt. Verification hashes the presented key and compares the hashes. Unkey doesn't keep the plaintext unless you turn on recovery for that key. For background, see the OWASP Secrets Management Cheat Sheet and the OWASP REST Security Cheat Sheet. SHA-256 here isn't a password hash. Recoverable keys let you show a key again after it's created. That comes with extra risk, so turn recovery on only when you need it: a dashboard that must show a key again, a migration that moves keys, or a support flow that reads a key back for a customer. By default you see a key only once, when it's created, and that's the right setup for most products. For the steps, see [Recoverable keys](/docs/api-management/keys/recoverable-keys). ## How the encrypted copy is stored A recoverable key is still verified by its hash. Beside the hash, Unkey stores an AES-256-GCM ciphertext of the key. The data key is 32 bytes, the nonce is 12 bytes, and the keyring is the workspace, so another workspace can't decrypt it. `keys.getKey` and `apis.listKeys` decrypt it only when you pass `decrypt: true`, and only with a root key that has `decrypt_key`. A key is only recoverable if its keyspace has **Store encrypted keys** turned on (ask [support@unkey.com](mailto:support@unkey.com)) and the key was created with `recoverable: true` after that. Older keys can't be recovered. ## Two separate permissions Creating a recoverable key needs `api.*.encrypt_key` or `api..encrypt_key` on top of `create_key`. Reading a key back needs `api.*.decrypt_key` or `api..decrypt_key` on top of `read_key`. Without `decrypt_key`, a root key can create and list keys but never read them back. Give `decrypt_key` only to the one service that needs to show keys, for the one keyspace that needs it. ## Limiting the exposure With recovery on, a copy of our database is no longer useless on its own. Someone who also got your workspace's encryption keys, or a leaked root key with `decrypt_key`, could read your keys. To limit that: * Only turn on recovery for keyspaces that need it. * Never pass `decrypt: true` in code that serves end users. * Use the [developer portal](/docs/api-management/portal/overview), where users roll their own keys instead of reading them back. * Watch the [audit log](/docs/platform/workspace/audit-logs/overview) for unexpected `key.create` entries in keyspaces with encryption on. If [rerolling](/docs/api-management/keys/rerolling-keys) meets your need, use it instead. The user gets a fresh key and nobody reads the old one. # Query gateway request data Source: https://www.unkey.com/docs/compute/api-reference/analytics/query-gateway-request-data https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v2/analytics.getGatewayRequests A query can use only the public alias `gateway_requests_v1`. CTEs, subqueries, UNION, and EXCEPT are permitted. The root key must have the `project.*.read_gateway_requests` permission. Unkey limits each query to the workspace of the root key. To get the data for one project, app, or environment, add a filter on `project_id`, `app_id`, or `environment_id`. The workspace retention period and the workspace query limits apply. For the columns and more query examples, see the gateway request analytics documentation. # Query runtime log data Source: https://www.unkey.com/docs/compute/api-reference/analytics/query-runtime-log-data https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v2/analytics.getRuntimeLogs A query can use only the public alias `runtime_logs_v1`. CTEs, subqueries, UNION, and EXCEPT are permitted. The root key must have the `project.*.read_runtime_logs` permission. Unkey limits each query to the workspace of the root key. To get the logs of one project, app, environment, or deployment, add a filter on `project_id`, `app_id`, `environment_id`, or `deployment_id`. The workspace retention period and the workspace query limits apply. For the table, the columns, and more query examples, see [Query runtime logs](/platform/analytics/get-runtime-logs). # Create app Source: https://www.unkey.com/docs/compute/api-reference/apps/create-app https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v2/apps.createApp Create an app within a project. The app is created with default `production` and `preview` environments. Set exactly one source. Use `git` to create a GitHub-sourced app and connect its repository. Use `oci` to create an app from a prebuilt OCI image. The slug you provide is the stable, caller-defined handle used to reference this app. It must be unique within the project. **Important**: The slug cannot collide with an existing app in the same project. A duplicate slug returns a 409 conflict. **Required Permissions** Your root key must have one of the following permissions: - `project.*.create_app` (to create apps in any project) - `project..create_app` (to create apps in a specific project) # Delete app Source: https://www.unkey.com/docs/compute/api-reference/apps/delete-app https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v2/apps.deleteApp Delete an existing app, identified by its id. Deletion is asynchronous and eventually consistent. The app and all of its associated resources (environments, deployments, custom domains) are torn down by a background workflow. A successful response indicates the deletion was enqueued, not that every resource has already been removed. Apps with delete protection enabled cannot be deleted until protection is disabled. **Required Permissions** Your root key must have one of the following permissions: - `app.*.delete_app` (to delete any app) - `app..delete_app` (to delete a specific app) # Get app Source: https://www.unkey.com/docs/compute/api-reference/apps/get-app https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v2/apps.getApp Retrieve a single app by its id or slug within a project. Use this to fetch app details after creation or to verify an app exists before performing operations. **Required Permissions** Your root key must have one of the following permissions: - `app.*.read_app` (to read any app) - `app..read_app` (to read a specific app) # List apps Source: https://www.unkey.com/docs/compute/api-reference/apps/list-apps https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v2/apps.listApps Retrieve a paginated list of apps within a project. Use this to enumerate every app in a project. Results are ordered by app id and paginated; when `hasMore` is true, pass the returned `cursor` to fetch the next page. **Required Permissions** Your root key must have the following permission: - `app.*.read_app` (to read apps in any project) # Update app Source: https://www.unkey.com/docs/compute/api-reference/apps/update-app https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v2/apps.updateApp Update an existing app, identified by its id. The app name, slug, default branch, and delete protection setting can be changed. Omitted fields are left unchanged. Changing the slug affects the deployment domains generated for this app. **Important**: The slug cannot collide with an existing app in the same project. A duplicate slug returns a 409 conflict. **Required Permissions** Your root key must have one of the following permissions: - `app.*.update_app` (to update any app) - `app..update_app` (to update a specific app) # Create deployment Source: https://www.unkey.com/docs/compute/api-reference/deployments/create-deployment https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v3/deployments.createDeployment Create a deployment for an app in a project. Omit the source to use the app's configured default. A Git app builds its default branch. An OCI app deploys its default image. Optionally provide one source override: - `oci`: deploy a prebuilt OCI image without a build. Mutable tags are resolved to immutable digests before rollout. - `git`: build and deploy from the app's connected GitHub repository, a branch, a specific commit, or a fork commit. Requires the app to have a repository connected. - `deployment`: re-run an existing deployment by its id. Git deployments rebuild from the recorded commit; OCI deployments reuse the recorded resolved image. Returns immediately with a `deploymentId`. The build and rollout run asynchronously. Poll `deployments.getDeployment` to watch status until it is ready. **Authentication**: requires a root key with permission to create deployments. # Get deployment Source: https://www.unkey.com/docs/compute/api-reference/deployments/get-deployment https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v2/deployments.getDeployment Retrieve a single deployment by its id. Use this to check a deployment's status after creating it, or to inspect the runtime configuration of an existing deployment. **Required Permissions** Your root key must have this permission: - `unkey:v1::projects//apps//environments//deployments/#read` (use `*` for any level) # List deployments Source: https://www.unkey.com/docs/compute/api-reference/deployments/list-deployments https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v2/deployments.listDeployments Retrieve a paginated list of deployments within a workspace, newest first. Filter by project, app, environment, lifecycle status, git branch, and creation time. All filters are optional; with none set, every deployment in the workspace is returned. Filters nest: `app` requires `project`, and `environment` and `branch` require both `project` and `app`. Results are paginated; when `hasMore` is true, pass the returned `cursor` to fetch the next page. **Required Permissions** Your root key must have this permission: - `unkey:v1::projects//apps//environments//deployments/*#read` (use `*` for every level you do not filter by) The permission must cover every deployment the filters select: a grant on one environment is not sufficient for a request that spans more than that environment. # Promote deployment Source: https://www.unkey.com/docs/compute/api-reference/deployments/promote-deployment https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v2/deployments.promoteDeployment Promote a deployment to become the current deployment for its environment. All sticky domains are reassigned from the current deployment to the promoted one, and the previous deployment is scheduled for standby. The deployment must be ready, not already shutting down, belong to the production environment, and its app must already have a current deployment. Promoting the deployment that is already current fails, unless the app is in a rolled-back state, in which case promoting the current deployment confirms the rollback and re-enables automatic promotion of future deployments. Promotion runs as a durable workflow: this endpoint returns once the promotion is accepted. Poll `getDeployment` or `listDeployments` to observe the result. **Required Permissions** Your root key must have one of the following permissions: - `environment.*.promote_deployment` (to promote deployments in any environment) - `environment..promote_deployment` (to promote deployments in a specific environment) # Rollback deployment Source: https://www.unkey.com/docs/compute/api-reference/deployments/rollback-deployment https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v2/deployments.rollbackDeployment Roll live traffic back to a previous deployment. `deploymentId` is the deployment to roll back TO; the app's current deployment is used as the rollback source automatically. The target deployment must be ready, not already shutting down, belong to the production environment, and must not itself be the current deployment. After a rollback the app is marked as rolled back, which prevents new deployments from automatically taking over live traffic. Promote the rolled-back deployment (or a newer one) to clear this state. Rollback runs as a durable workflow: this endpoint returns once the rollback is accepted. Poll `getDeployment` or `listDeployments` to observe the result. **Required Permissions** Your root key must have one of the following permissions: - `environment.*.rollback_deployment` (to roll back deployments in any environment) - `environment..rollback_deployment` (to roll back deployments in a specific environment) # Start deployment Source: https://www.unkey.com/docs/compute/api-reference/deployments/start-deployment https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v2/deployments.startDeployment Start a deployment that was previously stopped with `stopDeployment`, so it serves traffic again. The deployment keeps the configuration it had when it was stopped; nothing is rebuilt or redeployed. The deployment must currently be stopped, and must belong to a non-production environment. Production deployments are never stopped, so they cannot be started. Starting is asynchronous: this endpoint only enqueues the start and returns immediately. Poll `getDeployment` until the status reaches `ready`. **Required Permissions** Your root key must have one of the following permissions: - `environment.*.start_deployment` (to start deployments in any environment) - `environment..start_deployment` (to start deployments in a specific environment) # Stop deployment Source: https://www.unkey.com/docs/compute/api-reference/deployments/stop-deployment https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v2/deployments.stopDeployment Stop a running preview deployment. Stopped deployments keep their configuration and can be resumed later with `startDeployment`. The deployment must be ready and running, and must belong to a non-production environment; production deployments cannot be stopped. A deployment that is already draining from a previous stop is rejected with a precondition error. Stopping is asynchronous: this endpoint only enqueues the stop and returns immediately. Poll `getDeployment` until the status reaches `stopped`. **Required Permissions** Your root key must have one of the following permissions: - `environment.*.stop_deployment` (to stop deployments in any environment) - `environment..stop_deployment` (to stop deployments in a specific environment) # Create domain Source: https://www.unkey.com/docs/compute/api-reference/domains/create-domain https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v2/domains.createDomain Attach a custom domain to an environment and start verifying it. The domain is created in the `pending` state and does not serve traffic until verification succeeds. Verification runs in the background and polls DNS, so it is eventually consistent. The response returns `dnsRecords`: every record needed to finish setup, already resolved for whether this domain is an apex or a subdomain. Create every entry exactly as given. One record establishes routing and one proves ownership, and both are needed: whether ownership can be inferred from the routing record depends on how your provider publishes it, and a name another workspace has already verified can only be claimed through the ownership record. Neither is knowable before the records exist. When your DNS provider supports Domain Connect, the response also carries a `domainConnect` object; opening its `url` applies the same records at the provider in one step. The object is absent when the shortcut is unavailable. Domains are unique per workspace, so the same name cannot be attached to two environments. Attaching a domain that already exists in your workspace returns a 409 conflict. How many domains you may attach is set by your plan. Attaching one beyond that allowance returns a 403; upgrade the plan or remove a domain you no longer need. **Important**: verification stops after 24 hours without the required DNS records, and the domain moves to `failed`. **Required Permissions** Your root key must have one of the following permissions: - `environment.*.create_domain` (to attach domains to any environment) - `environment..create_domain` (to attach domains to a specific environment) # Delete domain Source: https://www.unkey.com/docs/compute/api-reference/domains/delete-domain https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v2/domains.deleteDomain Delete a custom domain from your workspace. Address the domain by its id or by its name. Names are unique per workspace, so `api.acme.com` is enough. Unkey stops serving the domain. Later requests fail with a certificate error. The DNS records at your provider stay in place. **Required Permissions** Your root key must have one of the following permissions: - `environment.*.delete_domain` (to delete domains in any environment) - `environment..delete_domain` (to delete domains in a specific environment) # Get domain Source: https://www.unkey.com/docs/compute/api-reference/domains/get-domain https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v2/domains.getDomain Retrieve a custom domain and its verification status. Address the domain by its id or by its name. Names are unique per workspace, so `api.acme.com` is sufficient. You do not need to supply a project, app, or environment. Use this endpoint to poll after `domains.createDomain`. Verification runs in the background and checks DNS approximately each minute. `status: verified` means the domain is verified. Unkey has configured routing and requested a certificate. Each entry in `dnsRecords` has a `verified` flag. The flag shows which records Unkey has read back, so you can see which records are still missing. Some providers hide a record from DNS lookups, for example a proxied or flattened routing record. Such a record stays `false` while it serves traffic. `verificationError` gives the reason for the last failed attempt. `dnsRecords` contains the same values that `domains.createDomain` returned. Use it to recover the values without creating the domain again. **Important**: verification stops 24 hours after the domain was created, and the status becomes `failed`. The window starts at `createdAt`, not at the last attempt. **Required Permissions** Your root key must have one of the following permissions: - `environment.*.read_domain` (to read domains in any environment) - `environment..read_domain` (to read domains in a specific environment) # List domains Source: https://www.unkey.com/docs/compute/api-reference/domains/list-domains https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v2/domains.listDomains List your custom domains with their verification status and DNS records. Filter by project, app, or environment using IDs or slugs, or send `{}` to list domains across your workspace. Use any filter on its own or combine filters to narrow the results. Results match all supplied filters. Omitting `environment` includes all matching environments. Results include only domains you have permission to read, sorted by ID. When `hasMore` is true, send the returned `cursor` to get the next page. `status: verified` means the domain is verified. Unkey has configured routing and requested a certificate. Each domain includes its full `dnsRecords`. Each record has a `verified` flag. The flag shows which records Unkey has read back, so you can see which records are still missing without a second call. Some providers hide a record from DNS lookups, for example a proxied or flattened routing record. Such a record stays `false` while it serves traffic. **Required Permissions** Use a root key with the `environment.*.read_domain` permission. A successful request returns an empty list if no matching domains are readable by your key. # Verify domain Source: https://www.unkey.com/docs/compute/api-reference/domains/verify-domain https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v2/domains.verifyDomain Restart verification for a custom domain. Address the domain by its ID or by its name. Names are unique per workspace, so `api.acme.com` is enough. Call this after you correct the DNS records of a domain that shows `failed`, or to give a `pending` domain a new 24-hour verification period. The domain goes back to `pending` and the 24-hour period starts again. The endpoint returns when Unkey accepts the retry. Poll `domains.getDomain` for the result. A domain that is already `verified` returns a 412. **Required Permissions** Your root key must have one of the following permissions: - `environment.*.verify_domain` (to verify domains in any environment) - `environment..verify_domain` (to verify domains in a specific environment) # Get environment Source: https://www.unkey.com/docs/compute/api-reference/environments/get-environment https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v2/environments.getEnvironment Retrieve a single environment by its id. Use this to fetch environment details after creation or to verify an environment exists before performing operations. **Required Permissions** Your root key must have one of the following permissions: - `environment.*.read_environment` (to read any environment) - `environment..read_environment` (to read a specific environment) # List environment variables Source: https://www.unkey.com/docs/compute/api-reference/environments/list-environment-variables https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v2/environments.listEnvironmentVariables Retrieve the environment variables for an environment. Identify the environment by its project, app, and environment identifiers. Results are ordered by variable id and paginated; when `hasMore` is true, pass the returned `cursor` to fetch the next page. `recoverable` variables are returned with their decrypted plaintext value. `writeonly` variables never expose a value: only the key, kind, and description are returned. **Required Permissions** Your root key must have one of the following permissions: - `environment.*.read_environment_variables` (for any environment) - `environment..read_environment_variables` (for a specific environment) # List environments Source: https://www.unkey.com/docs/compute/api-reference/environments/list-environments https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v2/environments.listEnvironments Retrieve the environments within an app. Use this to enumerate every environment in an app. Identify the app by its project slug and app slug. Results are ordered by environment id. An app has only a handful of environments, so all of them are returned in a single response. **Required Permissions** Your root key must have the following permission: - `environment.*.read_environment` (to read environments in any app) # Remove environment variables Source: https://www.unkey.com/docs/compute/api-reference/environments/remove-environment-variables https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v2/environments.removeEnvironmentVariables Remove environment variables from an environment in a single atomic request. This operation only deletes keys by name. Keys in the payload that exist are removed; keys that are not present are ignored, since their absence already matches the requested state. To replace or update values, use `setEnvironmentVariables` instead. The whole operation is atomic: if any part fails the environment is left unchanged. Duplicate keys in the payload collapse to a single removal. Values are never read or returned. **Required Permissions** Your root key must have one of the following permissions: - `environment.*.remove_environment_variables` (for any environment) - `environment..remove_environment_variables` (for a specific environment) # Set environment variables Source: https://www.unkey.com/docs/compute/api-reference/environments/set-environment-variables https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v2/environments.setEnvironmentVariables Create or update environment variables for an environment in a single atomic request. By default this is an upsert: each variable in the payload is created if new or fully overwritten if the key already exists, and any variable not in the payload is left untouched. This lets you change one variable without re-sending the others, which matters for write-only secrets you can no longer read back. Set `prune: true` to make it a full replace instead: after upserting, every variable not in the payload is deleted. Sending `prune: true` with an empty `variables` list resets the environment by deleting every variable. Each variable is written exactly as sent, never merged, so omitted optional fields fall back to their defaults rather than the previous value. Values are always encrypted at rest. Set `kind: recoverable` to allow a value to be read back; it defaults to `writeonly`, which can never be read back through the API. **Required Permissions** Your root key must have one of the following permissions: - `environment.*.set_environment_variables` (for any environment) - `environment..set_environment_variables` (for a specific environment) # Update environment settings Source: https://www.unkey.com/docs/compute/api-reference/environments/update-environment-settings https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v2/environments.updateSettings Update the build, runtime, and regional settings for an environment. All settings fields are optional. Omit a field to leave it unchanged. For nullable fields (`dockerfile`, `healthcheck`, `openapiSpecPath`), send null to clear the value. When `regions` is present it replaces the full set of regions for the environment. **Required Permissions** Your root key must have one of the following permissions: - `environment.*.update_environment` (to update any environment) - `environment..update_environment` (to update a specific environment) # List policies Source: https://www.unkey.com/docs/compute/api-reference/gateway/list-policies https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v2/gateway.listPolicies Retrieve an environment's gateway policies in evaluation order: the gateway evaluates them top to bottom and the first rejection short-circuits the request. The full policy list is returned in a single response. **Required Permissions** Your root key must have one of the following permissions: - `environment.*.read_policies` (for any environment) - `environment..read_policies` (for a specific environment) # Set policies Source: https://www.unkey.com/docs/compute/api-reference/gateway/set-policies https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v2/gateway.setPolicies Replace an environment's gateway policies in a single atomic request. Policies run at the edge before requests reach your app: verify API keys, rate limit, block requests outright, or validate them against your OpenAPI spec. Policies are an ordered list: the gateway evaluates them top to bottom and the first rejection short-circuits the request. Each policy sets exactly one of `keyauth`, `ratelimit`, `firewall` or `openapi`, plus optional `match` expressions restricting which requests it applies to. Every call is a full replace: the environment's policies become exactly the request list in the given order, and the server generates a fresh id for each one. An empty list removes all policies. The operation is atomic: if any policy is invalid, nothing is written. **Required Permissions** Your root key must have one of the following permissions: - `environment.*.set_policies` (for any environment) - `environment..set_policies` (for a specific environment) # Update policy Source: https://www.unkey.com/docs/compute/api-reference/gateway/update-policy https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v2/gateway.updatePolicy Update a single policy in place without resending the environment's full policy list. The policy keeps its id and its position in the evaluation order, and all other policies are untouched. Omitted fields keep their stored values; at least one updatable field must be provided. Setting `match` to null removes all match expressions so the policy applies to every request. Providing one of `keyauth`, `ratelimit`, `firewall` or `openapi` replaces the policy's rule entirely, including switching its type; at most one may be set. Policy ids are regenerated whenever `gateway.setPolicies` replaces the list, so fetch current ids via `gateway.listPolicies` first. **Required Permissions** Your root key must have one of the following permissions: - `environment.*.update_policy` (for any environment) - `environment..update_policy` (for a specific environment) # Install GitHub App Source: https://www.unkey.com/docs/compute/api-reference/github/install-github-app https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v2/github.installApp Start installing the Unkey GitHub App for your workspace. Returns a GitHub App install URL: open it in a browser to install the app and grant repository access. After installation GitHub returns to Unkey, which binds the installation to your workspace and lands you in the workspace settings. Installation is workspace-wide and takes no parameters. Once installed, link repositories to individual apps with the `git` field on `apps.createApp` and `apps.updateApp`. **Required Permissions** Your root key must have the following permission: - `workspace.*.install_github` # Create project Source: https://www.unkey.com/docs/compute/api-reference/projects/create-project https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v2/projects.createProject Create a project to group deployments and applications under a workspace-scoped slug. The slug you provide is the stable, caller-defined handle used to reference this project in subsequent operations (get, update, delete). It must be unique within your workspace. **Important**: The slug cannot collide with an existing project in your workspace. A duplicate slug returns a 409 conflict. **Required Permissions** Your root key must have the following permission: - `project.*.create_project` (to create projects in your workspace) # Delete project Source: https://www.unkey.com/docs/compute/api-reference/projects/delete-project https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v2/projects.deleteProject Delete an existing project in your workspace, identified by its id. Deletion is asynchronous and eventually consistent. The project and all of its associated resources (apps, environments, deployments, custom domains) are torn down by a background workflow. A successful response indicates the deletion was enqueued, not that every resource has already been removed. Projects with delete protection enabled cannot be deleted until protection is disabled. **Required Permissions** Your root key must have one of the following permissions: - `project.*.delete_project` (to delete any project) - `project..delete_project` (to delete a specific project) # Get project Source: https://www.unkey.com/docs/compute/api-reference/projects/get-project https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v2/projects.getProject Retrieve a single project in your workspace by its id. Use this to fetch project details after creation, verify a project exists before performing operations, or resolve a project's metadata from its id. **Required Permissions** Your root key must have one of the following permissions: - `project.*.read_project` (to read any project) - `project..read_project` (to read a specific project) # List projects Source: https://www.unkey.com/docs/compute/api-reference/projects/list-projects https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v2/projects.listProjects Retrieve a paginated list of projects in your workspace. Use this to build project management dashboards or to enumerate projects for administrative purposes. Results are ordered by project id and returned in pages. When `hasMore` is true, pass the returned `cursor` to fetch the next page. **Required Permissions** Your root key must have the following permission: - `project.*.read_project` (to read projects in your workspace) # Update project Source: https://www.unkey.com/docs/compute/api-reference/projects/update-project https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v2/projects.updateProject Update an existing project in your workspace, identified by its id. The project name, slug, and delete protection setting can be changed. Omitted fields are left unchanged. Changing the slug affects the deployment domains generated for this project. **Required Permissions** Your root key must have one of the following permissions: - `project.*.update_project` (to update any project) - `project..update_project` (to update a specific project) # Build-time secrets Source: https://www.unkey.com/docs/compute/build/build-secrets Use your environment variables during a build without baking them into the image. Your build can read every [environment variable](/docs/compute/configure/environment-variables) of the environment you're deploying, and none of them end up in the image. Automatic builds need no setup. Dockerfile builds need one extra line in each `RUN` step that uses them. This is safer than Docker's `ARG` and `ENV`, which are saved in the image history where anyone with the image can read them. ## Automatic builds When the environment builds without a Dockerfile, every variable is set as an environment variable of the same name for your install and build commands, the way it would be in CI. Change a value and the next deployment rebuilds the steps that use it. ## Dockerfile builds In a Dockerfile build, your variables arrive as a `.env` file. Mount it in each `RUN` step that needs it: ```dockerfile Dockerfile theme={"system"} FROM node:lts-alpine AS builder ARG UNKEY_SECRETS_ID WORKDIR /app COPY . . RUN npm ci RUN --mount=type=secret,id=${UNKEY_SECRETS_ID},target=/run/secrets/.env \ set -a && . /run/secrets/.env && set +a && \ npm run build FROM node:lts-alpine WORKDIR /app COPY --from=builder /app . CMD ["node", "dist/index.js"] ``` * **Declare `ARG UNKEY_SECRETS_ID` in every stage that mounts the file.** Docker doesn't carry `ARG` values across `FROM` lines. * **`set -a && . /run/secrets/.env && set +a`** loads the variables for that one `RUN` step only. The file isn't saved in the layer. * **Changing a variable reruns that step** and every step after it in the stage. Steps before it stay cached. The file uses standard `.env` syntax, one `KEY=value` per line. Values with spaces, quotes, `$`, or newlines are single-quoted, so PEM keys and JSON come through unchanged. If your framework reads `.env` files itself, point it at `/run/secrets/.env` and make sure its parser handles quoted multi-line values. ## Troubleshooting ### A build step keeps using an old value Older Dockerfiles mount the secret with the fixed id `env`. That still builds, but changing a variable doesn't rerun the step, so the old value can stay cached. Change the mount to `id=${UNKEY_SECRETS_ID}`. ### Cloning a private repository or installing a private package fails We fetch only the repository being built, with read-only access. A `RUN git clone` of another private repository, a private submodule, or a private package registry isn't authenticated. Add the token it needs as an environment variable and use it in the step that needs it. ## Next steps Kinds, limits, import, and how values reach running instances. Build context, the runtime contract, and explained errors. # Dockerfile builds Source: https://www.unkey.com/docs/compute/build/dockerfile Build with your own Dockerfile when automatic detection is not enough. By default, we [build your app](/docs/compute/build/overview) without a Dockerfile. Use your own Dockerfile when you need something automatic detection can't do, such as system packages, custom build stages, an unusual toolchain, or full control of the image. ## Switch an environment to a Dockerfile 1. Open the app, go to **App settings** > **Build**, and find the **Dockerfile** row. 2. Pick a Dockerfile from the list of ones we found in your repository, or type its path. The path is relative to the root directory and can be 1 to 500 characters. If the file isn't on the tracked branch, you'll see "File not found on this branch". 3. Deploy. The setting applies from the next deployment. With the API, set `dockerfile` on `environments.updateSettings`. Production and preview have separate build settings, so one environment can use a Dockerfile while the other builds automatically. The dashboard's **Build** page saves to both environments, so set a different value per environment with the API. To go back to automatic builds, choose **Automatic (no Dockerfile)** in the dashboard, or set `dockerfile` to `null` in the API. ## Set the build context The root directory is the build context. `COPY` and `ADD` paths, and the Dockerfile path, are resolved from it. The default `.` is the repository root. For a service in `services/api`, you have two options: * Set the root directory to `services/api` and the Dockerfile path to `Dockerfile`. * Keep the root at `.` and set the Dockerfile path to `services/api/Dockerfile`, if the build needs files from elsewhere in the repository. The root directory must be a relative path like `services/api`. It can't start with `/` or `./`, or contain `..`, backslashes, or spaces. ## What your container must do * **Listen on `PORT`.** We set this environment variable to the environment's port (8080 by default), and it overrides any `ENV PORT` in your image. * **Exit cleanly on the shutdown signal**, `SIGTERM` unless you change it, so instances can drain when they're replaced. * **Keep local writes under 128 MiB.** That's the cap on the container's own filesystem. Use the optional disk at `/data` for anything larger. If your image's default command isn't what you want, set the **Start command** in [runtime settings](/docs/compute/configure/runtime-settings) instead of keeping a second Dockerfile. ## Why your Dockerfile build failed A failed build ends the deployment as `failed` with the code `build_failed`. The deployment page shows a plain message for common causes, with the full build log underneath: | What went wrong | Message you see | | - | - | | The Dockerfile path points at an empty file | The Dockerfile appears to be empty. Please verify the file path in settings. | | The Dockerfile could not be read at the configured path | Dockerfile could not be read. Please check that the file path is correct in settings. | | A file the Dockerfile references is missing from the build context | A file or directory referenced in the build was not found. Please check the root directory in settings. | | A `COPY` or `ADD` source is missing | A file referenced in the Dockerfile was not found. Please check the root directory in settings. | | The requested build target stage doesn't exist | The specified build target stage was not found. Please check the target name in settings. | | The Dockerfile has a syntax error | Dockerfile has a syntax error. Please check the Dockerfile for typos. | | There is no `FROM` instruction | Dockerfile has no valid build stage. Please add a FROM instruction. | | A `FROM` image reference is malformed | A Docker image reference is invalid. Please check your FROM lines. | | The base image has no build for the target platform | The base image does not support the target platform. Please use a multi-platform image or change the platform. | | An undeclared `ARG` is referenced | Dockerfile references an undefined variable. Please check your ARG declarations. | | A `RUN` command exited non-zero | A build command failed. Please check the build logs for details. | Errors we don't recognize show the raw build error. If your Dockerfile clones another private repository or a private submodule, it gets a 404 from GitHub. We only have read access to the repository being built. Vendor those dependencies, or fetch them with credentials passed as [build-time secrets](/docs/compute/build/build-secrets). ## Next steps Mount your environment variables into `RUN` steps safely. Port, command, shutdown signal, CPU, memory, and disk. # GitHub integration Source: https://www.unkey.com/docs/compute/build/github Connect a GitHub repository so every push becomes a deployment. Connect a GitHub repository to an app and every push becomes a deployment. You install the Unkey GitHub App once, connect a repository, and push. ## Install the GitHub App You install the Unkey GitHub App once per workspace, on a GitHub organization or personal account, and choose which repositories it can see. 1. Start from any of these places in the dashboard: **Connect GitHub** when you create an app, the **Repository** row on an app's **App settings** > **Build** page, or [workspace settings](/docs/platform/workspace/settings). 2. Approve the installation on GitHub. GitHub sends you back to Unkey. To do it with the API, call `github.installApp` with a root key that has `workspace.*.install_github`, and open the URL it returns. You can install the app on more than one GitHub account. An installation belongs to one workspace only, so a second workspace can't use it. ## Connect a repository to an app Pick the repository when you create the app, or later under **App settings** > **Build**. From the CLI, pass `--git` to [create-app](/docs/compute/cli/apps/create-app) or [update-app](/docs/compute/cli/apps/update-app). You connect a repository per app, not per project. In a monorepo, two apps can connect to the same repository and use different build settings. The app's default branch deploys to production. It starts as the repository's default branch on GitHub, and you can change it with `apps.updateApp`. To stop deploying from pushes, click **Disconnect repository** in the danger zone of the app's **App settings** > **General** page. The dashboard warns that "Unkey will stop building pushes to this repository. Running deployments stay live." ## What deploys when you push Two things on GitHub create a deployment: * **A push to a branch.** The first push of a new branch counts, which is how a branch gets its first preview. Pushing a tag or deleting a branch doesn't deploy. * **A pull request from a fork**, when it's opened or gets new commits. Pull requests from branches in your own repository don't deploy again, because the push already did. The branch picks the environment. The app's default branch (or `main` if none is set) deploys to production. Every other branch, and every fork pull request, deploys to preview. If several apps are connected to the repository, one push creates one deployment per app. ## Skip deploys with auto deploy and watch paths Two [build settings](/docs/compute/configure/build-settings) can stop a push from deploying. We check them in this order: 1. **Auto deploy off.** The push is recorded as `skipped` with "Auto deploy is disabled for this environment." Watch paths aren't checked. 2. **Watch paths set, and no changed file matches.** The push is skipped with "Watch paths did not match any changed files." If a pattern isn't a valid glob, it's skipped with a message naming the pattern. Otherwise, we create a deployment. Watch paths also apply to fork pull requests. Patterns use `**` globbing. `src/**` matches everything under `src`, `**/*.go` matches every Go file, and a pattern with no wildcard matches only that exact path. ## Approve pull requests from forks A fork pull request runs code from someone without write access to your repository, with your environment variables. So we never build it automatically. 1. The deployment waits in `awaiting_approval`. On the pull request, a failing check named **Unkey Deploy Authorization** says "Awaiting authorization from a project member" and links to the deployment. 2. A project member approves it in the dashboard. The build starts. Pushes from people with write access to the repository don't need approval. Fork previews get their own hostname prefix, `[-]-fork--...`, so you can't confuse them with your own branches. You can also deploy a fork yourself from the **Create Deployment** dialog, or with `deployments.createDeployment` by passing `git.repository` as `owner/repo` and a `commitSha`. ## What you'll see on GitHub Each deployment of a connected app reports back to GitHub: * **A GitHub Deployment** on the commit, labeled `project - env` (or `project/app - env` when the app isn't `default`), linking to the environment's `unkey.app` domain. Its status goes from "Deployment queued" to "Deploying to regions...", "Configuring routing...", and "Deployment is live", or to a failure that names the step that failed. * **A pull request comment**, when the branch has an open pull request or the deployment came from a fork. We keep one comment per pull request up to date, with a row per app and environment: status, a **Visit Preview** link, an **Inspect** link to the dashboard, and when it last changed. * **A commit status**, only for fork deployments waiting for approval. ## Troubleshooting: a push didn't deploy Check these in order: 1. The Unkey GitHub App is installed on the account that owns the repository, and has access to it. 2. The repository is connected to the app under **App settings** > **Build**. 3. Auto deploy is on for the environment the branch maps to (the **Auto-deploy** switch on the environment's page under **App settings** > **Environments**). 4. The push changed a file that matches the watch paths, if any are set. 5. The app's deployments list shows a `skipped` or `failed` row with the reason. ## Next steps Root directory, Dockerfile, build command, watch paths, auto deploy. What happens after a deployment on either branch kind reaches ready. # Builds Source: https://www.unkey.com/docs/compute/build/overview Choose how your app is built, and work out why a build is waiting or has failed. A build turns the source of a git-connected app into the container image your deployment runs. Builds happen on our infrastructure, so you don't need Docker installed to deploy. Only git-connected apps build. If you deploy a prebuilt image, there is nothing to build: the deployment records the image you named and runs it, and none of the settings on this page apply. ## Choose how your app is built There is one decision, and it is the Dockerfile path in your environment's [build settings](/docs/compute/configure/build-settings). Leave it empty, the default the dashboard calls **Automatic (no Dockerfile)**, and we detect your language and toolchain from the root directory and build it for you. Set a path and we run a normal Docker build with that file, using the root directory as the build context. See [Dockerfile builds](/docs/compute/build/dockerfile). To get an automatic build right: * **Pin your tool versions** in the files your ecosystem already uses. Detection reads them to decide which toolchain to install, so a project that pins nothing can move versions underneath you between builds. * **In a monorepo, set the root directory** to the app's own directory, so detection looks at the right project rather than the repository root. * **Set a build command** when the detected one is wrong. It can be up to 1000 characters, and it is ignored when a Dockerfile is set. If detection can't work out how to build your app, the deployment fails with "Unkey could not build this app automatically. For a monorepo, set the root directory to your app and a custom build command in settings, or review the build logs for details." The fix is one of the three above. ## Why your build is waiting A workspace runs one build at a time on every plan today, so a second deployment sits in `pending` until the first finishes. Deployments from a prebuilt image queue too, even though they don't build. Production deployments jump ahead of preview deployments in the queue, so a hot-fix doesn't wait behind a backlog of pull request builds. A deployment that has waited an hour for its turn fails rather than waiting longer, so a jammed queue surfaces instead of hanging. Deploy again once the queue has drained. ## Why your build failed A failed build ends the deployment as `failed` with the error code `build_failed`. Read the build output on the deployment's detail page in the dashboard, where it streams live and stays afterwards. Common causes are rewritten in plain language above the log, for example a Dockerfile that couldn't be read or a file the build referenced that doesn't exist, and the full log is always underneath. Problems in your own source, such as a Dockerfile syntax error or a build command that exits non-zero, fail immediately. Retrying them without changing anything gives the same result. Infrastructure blips are retried for you, up to five attempts and no longer than 30 minutes in total, whichever comes first. A build that takes unusually long and then fails has normally spent that time in retries. ## Make builds faster, or skip them Build layers are cached per project, up to 25 GB, with layers older than 7 days evicted. Repeat builds of a project whose dependencies haven't changed reuse that cache. A project you haven't deployed in over a week builds from scratch again. Because every deployment produces its own image, promoting and rolling back never rebuild. They run an image that already exists, which is why a rollback is fast. Two settings stop a push from building at all: * **Watch paths** builds only when a changed file matches one of the environment's glob patterns, so a documentation-only commit doesn't start a build. * **Auto-deploy**, turned off, stops pushes from deploying entirely, leaving you to deploy from the dashboard or the API when you choose. A push that isn't built still appears as a `skipped` deployment with the reason, so you can tell the difference between a push we ignored and one we never saw. Both settings live on [Build settings](/docs/compute/configure/build-settings), and the order they're evaluated in is on [GitHub integration](/docs/compute/build/github). Your environment variables are available while the build runs without being baked into the image. See [Build-time secrets](/docs/compute/build/build-secrets). ## Next steps Take control of the image when detection isn't enough. Use environment variables during the build without baking them into layers. Root directory, Dockerfile, build command, watch paths, auto deploy. Where the build sits in the deployment lifecycle. # unkey api analytics get-gateway-requests Source: https://www.unkey.com/docs/compute/cli/analytics/get-gateway-requests Run a SQL query against the gateway request logs of your workspace. 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. Run a SQL query against the requests your gateway handled. Query the `gateway_requests_v1` table. CTEs, subqueries, `UNION`, and `EXCEPT` all work. You only see data from the root key's workspace. To narrow it to one project, app, or environment, filter on `project_id`, `app_id`, or `environment_id`. Your plan's log query range limits how far back a query can reach, and analytics query quotas apply. See [Limits](/docs/platform/billing/limits). ## Usage ```bash theme={"system"} unkey api analytics get-gateway-requests --query= ``` ## Flags SQL query to run against `gateway_requests_v1`. ### Shared flags Every `unkey api` command accepts these; [CLI output and shared flags](/docs/platform/cli/output-and-flags) describes them in full. A JSON document sent as the request body instead of building it from the flags above. It is mutually exclusive with the request-building flags, and unknown fields are rejected locally. See [Send a raw body](/docs/platform/cli/output-and-flags#send-a-raw-body). Root key for the request. Falls back to `UNKEY_ROOT_KEY`, then to the config file written by `unkey auth login`. See [CLI authentication](/docs/platform/cli/authentication). Base URL of the API. Falls back to `UNKEY_API_BASE_URL`. You don't normally need to set it. Path of the TOML file that `unkey auth login` writes. Falls back to `UNKEY_CONFIG`. Output format. Falls back to `UNKEY_OUTPUT`. Set `json` to print the full response envelope (`meta` and `data`) for piping; any other value prints the request ID followed by `data`. ## Required permissions Your root key needs `project.*.read_gateway_requests`. A grant for one project, such as `project..read_gateway_requests`, doesn't work here. Without a matching permission the API answers 403 and the CLI prints `Permission denied:` followed by the detail. See [Root key permissions](/docs/platform/root-keys/permissions) for the full catalog. ## Examples Count requests per project: ```bash theme={"system"} unkey api analytics get-gateway-requests --query="SELECT project_id, COUNT(*) AS total FROM gateway_requests_v1 GROUP BY project_id" ``` Recent requests of one environment: ```bash theme={"system"} unkey api analytics get-gateway-requests --query="SELECT time, response_status, total_latency FROM gateway_requests_v1 WHERE environment_id = 'env_1234abcd' ORDER BY time DESC LIMIT 100" ``` Send the request body as JSON: ```bash theme={"system"} unkey api analytics get-gateway-requests --body='{"query":"SELECT COUNT(*) AS total FROM gateway_requests_v1"}' --output=json ``` ## API endpoint The command calls [`POST /v2/analytics.getGatewayRequests`](/docs/compute/api-reference/analytics/query-gateway-request-data) and prints its response. The request fields carry the same names as the flags in camelCase, which is the shape `--body` expects. ## Related Column reference for gateway request and runtime log analytics. # unkey api analytics get-runtime-logs Source: https://www.unkey.com/docs/compute/cli/analytics/get-runtime-logs Run a SQL query against the runtime logs of your deployments. 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. Run a SQL query against the logs your deployments wrote. Query the `runtime_logs_v1` table. CTEs, subqueries, `UNION`, and `EXCEPT` all work. You only see data from the root key's workspace. To narrow it to one project, app, environment, or deployment, filter on `project_id`, `app_id`, `environment_id`, or `deployment_id`. Your plan's log query range limits how far back a query can reach, and analytics query quotas apply. See [Limits](/docs/platform/billing/limits). ## Usage ```bash theme={"system"} unkey api analytics get-runtime-logs --query= ``` ## Flags SQL query to run against `runtime_logs_v1`. ### Shared flags Every `unkey api` command accepts these; [CLI output and shared flags](/docs/platform/cli/output-and-flags) describes them in full. A JSON document sent as the request body instead of building it from the flags above. It is mutually exclusive with the request-building flags, and unknown fields are rejected locally. See [Send a raw body](/docs/platform/cli/output-and-flags#send-a-raw-body). Root key for the request. Falls back to `UNKEY_ROOT_KEY`, then to the config file written by `unkey auth login`. See [CLI authentication](/docs/platform/cli/authentication). Base URL of the API. Falls back to `UNKEY_API_BASE_URL`. You don't normally need to set it. Path of the TOML file that `unkey auth login` writes. Falls back to `UNKEY_CONFIG`. Output format. Falls back to `UNKEY_OUTPUT`. Set `json` to print the full response envelope (`meta` and `data`) for piping; any other value prints the request ID followed by `data`. ## Required permissions Your root key needs `project.*.read_runtime_logs`. A grant for one project, such as `project..read_runtime_logs`, doesn't work here. Without a matching permission the API answers 403 and the CLI prints `Permission denied:` followed by the detail. See [Root key permissions](/docs/platform/root-keys/permissions) for the full catalog. ## Examples Latest log lines of one deployment: ```bash theme={"system"} unkey api analytics get-runtime-logs --query="SELECT time, message FROM runtime_logs_v1 WHERE deployment_id = 'd_1234abcd' ORDER BY time DESC LIMIT 100" ``` Count lines by severity: ```bash theme={"system"} unkey api analytics get-runtime-logs --query="SELECT severity, COUNT(*) AS total FROM runtime_logs_v1 WHERE environment_id = 'env_1234abcd' GROUP BY severity" ``` Send the request body as JSON: ```bash theme={"system"} unkey api analytics get-runtime-logs --body='{"query":"SELECT COUNT(*) AS total FROM runtime_logs_v1"}' --output=json ``` ## API endpoint The command calls [`POST /v2/analytics.getRuntimeLogs`](/docs/compute/api-reference/analytics/query-runtime-log-data) and prints its response. The request fields carry the same names as the flags in camelCase, which is the shape `--body` expects. ## Related Column reference for gateway request and runtime log analytics. # unkey api apps create-app Source: https://www.unkey.com/docs/compute/cli/apps/create-app Create an app in a project, optionally connected to a GitHub repository. 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 app inside a project. The app is created with a `production` and a `preview` environment. The slug is what you pass to later `--app` flags. It must be unique within the project, or you get a 409 conflict. Pass exactly one source: * `--git` for a GitHub repository. Pass a `repository` to connect it now (the [Unkey GitHub App](/docs/compute/cli/github/install-app) must be installed first), or pass `--git='{}'` and connect one later with [update-app](/docs/compute/cli/apps/update-app). * `--oci` for a prebuilt image, if you already build images elsewhere. ## Usage ```bash theme={"system"} unkey api apps create-app --project= --name= --slug= (--git= | --oci=) ``` ## Flags GitHub repository connection as a JSON object, for example `{"repository":"unkeyed/api","defaultBranch":"main"}`. Both fields are optional, so `{}` creates a Git app with no repository yet; `defaultBranch` requires `repository`. Mutually exclusive with `--oci`. OCI image source as a JSON object with a required `image` field, for example `{"image":"ghcr.io/acme/payments:v1.2.3"}`. The reference needs an explicit tag or digest and is at most 256 characters. Mutually exclusive with `--git`. Human-readable name for the app, 1 to 256 characters. Project ID or slug. Both forms resolve to the same project. Stable app slug, unique within the project. 3 to 255 characters matching `^[a-zA-Z0-9_-]+$`. Used in `--app` flags and in generated deployment hostnames. ### Shared flags Every `unkey api` command accepts these; [CLI output and shared flags](/docs/platform/cli/output-and-flags) describes them in full. A JSON document sent as the request body instead of building it from the flags above. It is mutually exclusive with the request-building flags, and unknown fields are rejected locally. See [Send a raw body](/docs/platform/cli/output-and-flags#send-a-raw-body). Root key for the request. Falls back to `UNKEY_ROOT_KEY`, then to the config file written by `unkey auth login`. See [CLI authentication](/docs/platform/cli/authentication). Base URL of the API. Falls back to `UNKEY_API_BASE_URL`. You don't normally need to set it. Path of the TOML file that `unkey auth login` writes. Falls back to `UNKEY_CONFIG`. Output format. Falls back to `UNKEY_OUTPUT`. Set `json` to print the full response envelope (`meta` and `data`) for piping; any other value prints the request ID followed by `data`. ## Required permissions Your root key needs one of: * `project.*.create_app` (apps in any project) * `project..create_app` (apps in a specific project) If `--git` includes a `repository`, your key also needs `app.*.connect_repository`. `--git='{}'` doesn't. Without a matching permission the API answers 403 and the CLI prints `Permission denied:` followed by the detail. See [Root key permissions](/docs/platform/root-keys/permissions) for the full catalog. ## Examples Create an app connected to a repository: ```bash theme={"system"} unkey api apps create-app --project=payments --name='Payments API' --slug=payments-api --git='{"repository":"unkeyed/api","defaultBranch":"main"}' ``` Create a Git app now and connect the repository later: ```bash theme={"system"} unkey api apps create-app --project=payments --name='Payments API' --slug=payments-api --git='{}' ``` Create an app from a prebuilt image: ```bash theme={"system"} unkey api apps create-app --project=payments --name='Payments API' --slug=payments-api --oci='{"image":"ghcr.io/acme/payments:v1.2.3"}' ``` Send the request body as JSON: ```bash theme={"system"} unkey api apps create-app --body='{"project":"payments","name":"Payments API","slug":"payments-api","oci":{"image":"ghcr.io/acme/payments:v1.2.3"}}' --output=json ``` ## API endpoint The command calls [`POST /v2/apps.createApp`](/docs/compute/api-reference/apps/create-app) and prints its response. The request fields carry the same names as the flags in camelCase, which is the shape `--body` expects. ## Related How the three objects nest and how slugs and IDs are resolved. Connect a repository and ship the first deployment. # unkey api apps delete-app Source: https://www.unkey.com/docs/compute/cli/apps/delete-app Delete an app together with everything deployed under 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 an app together with its environments, deployments, and custom domains. Deletion runs in the background: a successful response means it has started, not that everything is gone yet. An app with delete protection enabled is refused until you disable it with `update-app`. ## Usage ```bash theme={"system"} unkey api apps delete-app --project= --app= ``` ## Flags App ID or slug. Project ID or slug. Both forms resolve to the same project. ### Shared flags Every `unkey api` command accepts these; [CLI output and shared flags](/docs/platform/cli/output-and-flags) describes them in full. A JSON document sent as the request body instead of building it from the flags above. It is mutually exclusive with the request-building flags, and unknown fields are rejected locally. See [Send a raw body](/docs/platform/cli/output-and-flags#send-a-raw-body). Root key for the request. Falls back to `UNKEY_ROOT_KEY`, then to the config file written by `unkey auth login`. See [CLI authentication](/docs/platform/cli/authentication). Base URL of the API. Falls back to `UNKEY_API_BASE_URL`. You don't normally need to set it. Path of the TOML file that `unkey auth login` writes. Falls back to `UNKEY_CONFIG`. Output format. Falls back to `UNKEY_OUTPUT`. Set `json` to print the full response envelope (`meta` and `data`) for piping; any other value prints the request ID followed by `data`. ## Required permissions Your root key needs one of: * `app.*.delete_app` (any app) * `app..delete_app` (a specific app) Without a matching permission the API answers 403 and the CLI prints `Permission denied:` followed by the detail. See [Root key permissions](/docs/platform/root-keys/permissions) for the full catalog. ## Examples Delete an app: ```bash theme={"system"} unkey api apps delete-app --project=payments --app=app_1234abcd ``` Send the request body as JSON: ```bash theme={"system"} unkey api apps delete-app --body='{"project":"payments","app":"payments-api"}' ``` ## API endpoint The command calls [`POST /v2/apps.deleteApp`](/docs/compute/api-reference/apps/delete-app) and prints its response. The request fields carry the same names as the flags in camelCase, which is the shape `--body` expects. # unkey api apps get-app Source: https://www.unkey.com/docs/compute/cli/apps/get-app Look up one app in a project by 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. Retrieve one app by its ID or slug within a project. ## Usage ```bash theme={"system"} unkey api apps get-app --project= --app= ``` ## Flags App ID or slug. Project ID or slug. Both forms resolve to the same project. ### Shared flags Every `unkey api` command accepts these; [CLI output and shared flags](/docs/platform/cli/output-and-flags) describes them in full. A JSON document sent as the request body instead of building it from the flags above. It is mutually exclusive with the request-building flags, and unknown fields are rejected locally. See [Send a raw body](/docs/platform/cli/output-and-flags#send-a-raw-body). Root key for the request. Falls back to `UNKEY_ROOT_KEY`, then to the config file written by `unkey auth login`. See [CLI authentication](/docs/platform/cli/authentication). Base URL of the API. Falls back to `UNKEY_API_BASE_URL`. You don't normally need to set it. Path of the TOML file that `unkey auth login` writes. Falls back to `UNKEY_CONFIG`. Output format. Falls back to `UNKEY_OUTPUT`. Set `json` to print the full response envelope (`meta` and `data`) for piping; any other value prints the request ID followed by `data`. ## Required permissions Your root key needs one of: * `app.*.read_app` (any app) * `app..read_app` (a specific app) If the app doesn't exist or your key doesn't have the permission, you get the same 404: `The requested app does not exist.` See [Root key permissions](/docs/platform/root-keys/permissions) for the full catalog. ## Examples Get an app: ```bash theme={"system"} unkey api apps get-app --project=payments --app=app_1234abcd ``` Send the request body as JSON: ```bash theme={"system"} unkey api apps get-app --body='{"project":"payments","app":"payments-api"}' --output=json ``` ## API endpoint The command calls [`POST /v2/apps.getApp`](/docs/compute/api-reference/apps/get-app) and prints its response. The request fields carry the same names as the flags in camelCase, which is the shape `--body` expects. # unkey api apps list-apps Source: https://www.unkey.com/docs/compute/cli/apps/list-apps List the apps in a project 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 apps in a project. Results are ordered by app ID. When `pagination.hasMore` is true, pass `pagination.cursor` back with `--cursor`. ## Usage ```bash theme={"system"} unkey api apps list-apps --project= [--limit=] [--cursor=] [--search=] ``` ## Flags Pagination cursor from a previous response. Maximum number of apps per page, between 1 and 100. Project ID or slug. Both forms resolve to the same project. Free-form text, at most 256 characters. Returns apps whose ID, name, or slug contains it, matched case-insensitively. ### Shared flags Every `unkey api` command accepts these; [CLI output and shared flags](/docs/platform/cli/output-and-flags) describes them in full. A JSON document sent as the request body instead of building it from the flags above. It is mutually exclusive with the request-building flags, and unknown fields are rejected locally. See [Send a raw body](/docs/platform/cli/output-and-flags#send-a-raw-body). Root key for the request. Falls back to `UNKEY_ROOT_KEY`, then to the config file written by `unkey auth login`. See [CLI authentication](/docs/platform/cli/authentication). Base URL of the API. Falls back to `UNKEY_API_BASE_URL`. You don't normally need to set it. Path of the TOML file that `unkey auth login` writes. Falls back to `UNKEY_CONFIG`. Output format. Falls back to `UNKEY_OUTPUT`. Set `json` to print the full response envelope (`meta` and `data`) for piping; any other value prints the request ID followed by `data`. ## Required permissions Your root key needs `app.*.read_app`. If the project doesn't exist or your key doesn't have the permission, you get the same 404: `The requested project does not exist.` See [Root key permissions](/docs/platform/root-keys/permissions) for the full catalog. ## Examples List the apps in a project: ```bash theme={"system"} unkey api apps list-apps --project=payments ``` Filter and page: ```bash theme={"system"} unkey api apps list-apps --project=payments --limit=25 --search=checkout ``` ## API endpoint The command calls [`POST /v2/apps.listApps`](/docs/compute/api-reference/apps/list-apps) and prints its response. The request fields carry the same names as the flags in camelCase, which is the shape `--body` expects. # unkey api apps update-app Source: https://www.unkey.com/docs/compute/cli/apps/update-app Rename an app or change its slug, repository, image, or delete protection. 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 app's name, slug, GitHub repository connection, image, or delete protection. Flags you omit leave the stored value unchanged. * Changing the slug changes the hostnames of the app's deployments. * `--git` with a JSON object connects or changes the repository, and `--git=null` disconnects it. It only works on a GitHub app. * `--oci` changes the image of an image app. It only works on an image app. * A request with nothing to update returns `Provide at least one field to update.` ## Usage ```bash theme={"system"} unkey api apps update-app --project= --app= [--name=] [--slug=] [--git=|null] [--oci=] [--delete-protection[=false]] ``` ## Flags App ID or slug. Enable (`--delete-protection`) or disable (`--delete-protection=false`) delete protection. Omit the flag to leave the current setting unchanged. GitHub repository update as a JSON object, or `null` to disconnect the repository. OCI image update as a JSON object with an `image` field. The reference needs an explicit tag or digest. New human-readable name, 1 to 256 characters. Project ID or slug. Both forms resolve to the same project. New app slug, 3 to 255 characters matching `^[a-zA-Z0-9_-]+$`. Must stay unique in the project; a duplicate returns a 409 conflict. ### Shared flags Every `unkey api` command accepts these; [CLI output and shared flags](/docs/platform/cli/output-and-flags) describes them in full. A JSON document sent as the request body instead of building it from the flags above. It is mutually exclusive with the request-building flags, and unknown fields are rejected locally. See [Send a raw body](/docs/platform/cli/output-and-flags#send-a-raw-body). Root key for the request. Falls back to `UNKEY_ROOT_KEY`, then to the config file written by `unkey auth login`. See [CLI authentication](/docs/platform/cli/authentication). Base URL of the API. Falls back to `UNKEY_API_BASE_URL`. You don't normally need to set it. Path of the TOML file that `unkey auth login` writes. Falls back to `UNKEY_CONFIG`. Output format. Falls back to `UNKEY_OUTPUT`. Set `json` to print the full response envelope (`meta` and `data`) for piping; any other value prints the request ID followed by `data`. ## Required permissions Your root key needs one of: * `app.*.update_app` (any app) * `app..update_app` (a specific app) When you pass `--git`, your key also needs `app.*.connect_repository` or `app..connect_repository`. Without a matching permission the API answers 403 and the CLI prints `Permission denied:` followed by the detail. See [Root key permissions](/docs/platform/root-keys/permissions) for the full catalog. ## Examples Rename an app: ```bash theme={"system"} unkey api apps update-app --project=payments --app=app_1234abcd --name='Payments API' ``` Connect a repository: ```bash theme={"system"} unkey api apps update-app --project=payments --app=payments-api --git='{"repository":"unkeyed/api","defaultBranch":"main"}' ``` Disconnect the repository: ```bash theme={"system"} unkey api apps update-app --project=payments --app=payments-api --git=null ``` Point the app at a different image: ```bash theme={"system"} unkey api apps update-app --project=payments --app=payments-api --oci='{"image":"ghcr.io/acme/payments:v1.3.0"}' ``` Send the request body as JSON: ```bash theme={"system"} unkey api apps update-app --body='{"project":"payments","app":"payments-api","deleteProtection":true}' ``` ## API endpoint The command calls [`POST /v2/apps.updateApp`](/docs/compute/api-reference/apps/update-app) and prints its response. The request fields carry the same names as the flags in camelCase, which is the shape `--body` expects. # unkey deploy Source: https://www.unkey.com/docs/compute/cli/deploy Deploy a prebuilt container image to a project with one command. 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. Deploy a prebuilt container image to an app and wait until it's live or has failed. It's a shortcut for [create-deployment](/docs/compute/cli/deployments/create-deployment) followed by polling [get-deployment](/docs/compute/cli/deployments/get-deployment). `unkey deploy` relies on endpoints that aren't part of the public API reference and can change without notice. For scripts that must not break, use the `unkey api deployments` commands, which call the stable `deployments.*` endpoints. ## Usage ```bash theme={"system"} unkey deploy --project= --root-key= [flags] ``` The image is required. Without it, the command exits with `docker image is required`. The image must already be in a registry we can pull from. Nothing is built. ## Flags Project slug the app belongs to. Falls back to `UNKEY_PROJECT`. App slug within the project. Falls back to `UNKEY_APP`. Slug of the environment to deploy to. Pass `production` to deploy to production. Keyspace whose keys the gateway accepts for this deployment. Falls back to `UNKEY_KEYSPACE_ID`. See [API key authentication](/docs/compute/gateway/api-key-auth). Git branch recorded on the deployment. If you leave the default and you're in a Git checkout on another branch, the CLI uses that branch. Commit SHA recorded on the deployment. Defaults to the HEAD of your Git checkout. Outside a Git repository, no commit is recorded unless you pass this. If you pass a SHA other than HEAD, only the SHA is recorded, without the commit message or author. Root key. Falls back to `UNKEY_ROOT_KEY`. Unlike the `unkey api` commands, `unkey deploy` doesn't read `~/.unkey/config.toml`, so a key saved with `unkey auth login` doesn't work here. API base URL. Falls back to `UNKEY_API_BASE_URL`. When empty, `https://api.unkey.com` is used. You don't normally need to set it. The command doesn't accept `--output`, `--config`, or `--body`. ## Required permissions `project.*.create_deployment` or `project..create_deployment` to create the deployment, and `project.*.read_deployment` or `project..read_deployment` to follow it. These are project permissions. The `unkey api deployments` commands use different permissions, listed on each command's page. See [Root key permissions](/docs/platform/root-keys/permissions). ## What you'll see The command prints a `Deployment Progress` header with the branch, commit, and image. Inside a Git repository, it records the branch, commit, message, author, and commit time on the deployment so the dashboard can show them, and marks the commit as dirty if you have uncommitted changes. If the deployment can't be created, the command prints the error and exits 1. Common causes are a `--project` or `--app` that doesn't exist, an `--env` that isn't an environment of that app, a root key without `create_deployment`, an image reference we can't parse, or a `--keyspace-id` that isn't in your workspace. Otherwise it follows the deployment until it finishes: * **Ready:** prints `Deployment completed successfully`, the deployment ID, the environment, and its hostnames, and exits 0. * **Failed:** prints the error message (or `Unknown deployment error`) and exits 1, so a CI job fails. * **Still going after 5 minutes:** exits with `deployment timeout after 5 minutes`. The deployment keeps going. Follow it with `unkey api deployments get-deployment`. ## Examples Deploy to the preview environment of the default app: ```bash theme={"system"} unkey deploy ghcr.io/acme/payments:1.4.2 --project=payments --root-key=unkey_xxx ``` Deploy to production with the key from the environment, as in CI: ```bash theme={"system"} export UNKEY_ROOT_KEY=unkey_xxx unkey deploy myregistry.io/payments-api:latest --project=payments --app=payments-api --env=production ``` Deploy behind API key authentication: ```bash theme={"system"} unkey deploy ghcr.io/acme/payments:1.4.2 --project=payments --app=payments-api --env=production --keyspace-id=ks_1234abcd ``` ## Related The guided walkthrough from first image to first request. Statuses, the build queue, and why a deployment fails. Where the root key comes from and why `unkey deploy` differs. # unkey api deployments create-deployment Source: https://www.unkey.com/docs/compute/cli/deployments/create-deployment Start a deployment from a Git ref, an image, or an earlier deployment. 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 deployment for an app in one of its environments. With no source flag, the app deploys its usual source: a Git app builds its default branch, and an image app deploys its default image. That's what you want in CI for a routine redeploy. To deploy something else, pass one source flag: * `--oci` deploys a prebuilt image, with no build. A tag like `latest` is pinned to the exact image it points to when you deploy. * `--git` builds a branch, commit, or fork commit from the app's connected repository. * `--deployment` redeploys an existing deployment. A Git deployment rebuilds from its commit, and an image deployment reuses its image. You can pass only one of them. The command returns a `deploymentId` right away, while the build and rollout keep running. Poll [get-deployment](/docs/compute/cli/deployments/get-deployment) to follow the status. Deploying needs an active Compute plan. Without one the API answers 412 with `The workspace has no active Compute plan.` A workspace over its Compute spend cap gets `The workspace is suspended by its Compute spend cap. Raise the spend limit to resume.` See [Compute plans](/docs/compute/get-started/plans). ## Usage ```bash theme={"system"} unkey api deployments create-deployment --project= --app= --environment= [--git= | --oci= | --deployment=] ``` ## Flags App ID or slug. Existing deployment to re-run, as a JSON object with a `deploymentId` field. It must be in the same app and environment. Any other ID returns `The specified deployment does not exist.` Environment ID or slug the deployment lands in. Git source as a JSON object with `branch`, `commitSha`, or a fork's `repository` plus `commitSha`, for example `{"branch":"main"}`. Requires the app to have a repository connected. OCI image source as a JSON object with a required `image` field. A tag is pinned to the exact image it points to when you deploy. Project ID or slug. Both forms resolve to the same project. ### Shared flags Every `unkey api` command accepts these; [CLI output and shared flags](/docs/platform/cli/output-and-flags) describes them in full. A JSON document sent as the request body instead of building it from the flags above. It is mutually exclusive with the request-building flags, and unknown fields are rejected locally. See [Send a raw body](/docs/platform/cli/output-and-flags#send-a-raw-body). Root key for the request. Falls back to `UNKEY_ROOT_KEY`, then to the config file written by `unkey auth login`. See [CLI authentication](/docs/platform/cli/authentication). Base URL of the API. Falls back to `UNKEY_API_BASE_URL`. You don't normally need to set it. Path of the TOML file that `unkey auth login` writes. Falls back to `UNKEY_CONFIG`. Output format. Falls back to `UNKEY_OUTPUT`. Set `json` to print the full response envelope (`meta` and `data`) for piping; any other value prints the request ID followed by `data`. ## Required permissions Your root key needs one of: * `environment.*.create_deployment` (any environment) * `environment..create_deployment` (a specific environment) Without a matching permission the API answers 403 and the CLI prints `Permission denied:` followed by the detail. See [Root key permissions](/docs/platform/root-keys/permissions) for the full catalog. ## Examples Deploy the app's configured source: ```bash theme={"system"} unkey api deployments create-deployment --project=payments --app=payments-api --environment=production ``` Build and deploy a specific branch: ```bash theme={"system"} unkey api deployments create-deployment --project=payments --app=payments-api --environment=production --git='{"branch":"main"}' ``` Deploy a prebuilt image: ```bash theme={"system"} unkey api deployments create-deployment --project=payments --app=payments-api --environment=preview --oci='{"image":"ghcr.io/acme/payments:1.4.2"}' ``` Send the request body as JSON and capture the ID: ```bash theme={"system"} unkey api deployments create-deployment --body='{"project":"payments","app":"payments-api","environment":"production","git":{"branch":"main"}}' --output=json | jq -r '.data.deploymentId' ``` ## API endpoint The command calls [`POST /v3/deployments.createDeployment`](/docs/compute/api-reference/deployments/create-deployment) and prints its response. The request fields carry the same names as the flags in camelCase, which is the shape `--body` expects. ## Related Statuses, the build queue, and why a deployment fails. The one-shot command that deploys an image and waits for it. # unkey api deployments get-deployment Source: https://www.unkey.com/docs/compute/cli/deployments/get-deployment Look up the status and details of a deployment. 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. Retrieve one deployment by ID: its lifecycle status, build steps, error messages, and runtime configuration. Use it to check progress after `create-deployment`, `promote-deployment`, `rollback-deployment`, `start-deployment`, or `stop-deployment`, which all return before the work is done. The statuses are explained in [Deployments](/docs/compute/concepts/deployments#statuses). ## Usage ```bash theme={"system"} unkey api deployments get-deployment --deployment-id= ``` ## Flags Deployment ID, for example `d_1234abcd`. ### Shared flags Every `unkey api` command accepts these; [CLI output and shared flags](/docs/platform/cli/output-and-flags) describes them in full. A JSON document sent as the request body instead of building it from the flags above. It is mutually exclusive with the request-building flags, and unknown fields are rejected locally. See [Send a raw body](/docs/platform/cli/output-and-flags#send-a-raw-body). Root key for the request. Falls back to `UNKEY_ROOT_KEY`, then to the config file written by `unkey auth login`. See [CLI authentication](/docs/platform/cli/authentication). Base URL of the API. Falls back to `UNKEY_API_BASE_URL`. You don't normally need to set it. Path of the TOML file that `unkey auth login` writes. Falls back to `UNKEY_CONFIG`. Output format. Falls back to `UNKEY_OUTPUT`. Set `json` to print the full response envelope (`meta` and `data`) for piping; any other value prints the request ID followed by `data`. ## Required permissions Your root key needs `unkey:v1::projects//apps//environments//deployments/#read`. Use `*` for any level to cover more deployments. If the deployment doesn't exist or your key doesn't have the permission, you get the same 404: `The requested deployment does not exist.` See [Root key permissions](/docs/platform/root-keys/permissions) for the full catalog. ## Examples Get a deployment: ```bash theme={"system"} unkey api deployments get-deployment --deployment-id=d_1234abcd ``` Poll until it is ready: ```bash theme={"system"} until [ "$(unkey api deployments get-deployment --deployment-id=d_1234abcd --output=json | jq -r '.data.status')" = ready ]; do sleep 5; done ``` ## API endpoint The command calls [`POST /v2/deployments.getDeployment`](/docs/compute/api-reference/deployments/get-deployment) and prints its response. The request fields carry the same names as the flags in camelCase, which is the shape `--body` expects. # unkey api deployments list-deployments Source: https://www.unkey.com/docs/compute/cli/deployments/list-deployments List deployments newest first, filtered by app or status. 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 deployments in the workspace, newest first. All filters are optional. With none set, every deployment in the workspace is returned. Filters nest: `--app` requires `--project`, and `--environment` requires both. When `pagination.hasMore` is true, pass `pagination.cursor` back with `--cursor`. `--status` takes a comma-separated list of statuses from `pending`, `starting`, `building`, `deploying`, `network`, `finalizing`, `ready`, `failed`, `skipped`, `awaiting_approval`, `stopped`, `superseded`, and `cancelled`. ## Usage ```bash theme={"system"} unkey api deployments list-deployments [--project= [--app= [--environment=]]] [--status=,...] [--limit=] [--cursor=] ``` ## Flags App ID or slug to filter by. Requires `--project`. Pagination cursor from a previous response. Environment ID or slug to filter by. Requires `--project` and `--app`. Maximum deployments per page, between 1 and 100. The API uses 100 when the flag is omitted. Project ID or slug to filter by. Comma-separated lifecycle statuses to include. ### Shared flags Every `unkey api` command accepts these; [CLI output and shared flags](/docs/platform/cli/output-and-flags) describes them in full. A JSON document sent as the request body instead of building it from the flags above. It is mutually exclusive with the request-building flags, and unknown fields are rejected locally. See [Send a raw body](/docs/platform/cli/output-and-flags#send-a-raw-body). Root key for the request. Falls back to `UNKEY_ROOT_KEY`, then to the config file written by `unkey auth login`. See [CLI authentication](/docs/platform/cli/authentication). Base URL of the API. Falls back to `UNKEY_API_BASE_URL`. You don't normally need to set it. Path of the TOML file that `unkey auth login` writes. Falls back to `UNKEY_CONFIG`. Output format. Falls back to `UNKEY_OUTPUT`. Set `json` to print the full response envelope (`meta` and `data`) for piping; any other value prints the request ID followed by `data`. ## Required permissions Your root key needs `unkey:v1::projects//apps//environments//deployments/*#read`, with `*` for every level you don't filter by. The permission must cover every deployment the filters select. A grant for one environment isn't enough for a list that spans more environments. Without a matching permission the API answers 403 and the CLI prints `Permission denied:` followed by the detail. See [Root key permissions](/docs/platform/root-keys/permissions) for the full catalog. ## Examples Deployments of one environment: ```bash theme={"system"} unkey api deployments list-deployments --project=payments --app=payments-api --environment=production ``` Recent successes and failures across the workspace: ```bash theme={"system"} unkey api deployments list-deployments --status=ready,failed --limit=25 ``` ## API endpoint The command calls [`POST /v2/deployments.listDeployments`](/docs/compute/api-reference/deployments/list-deployments) and prints its response. The request fields carry the same names as the flags in camelCase, which is the shape `--body` expects. # unkey api deployments promote-deployment Source: https://www.unkey.com/docs/compute/cli/deployments/promote-deployment Make a ready production deployment the live one. 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. Promote a deployment so it becomes the current deployment of its production environment. The live domains move to it, and the previous deployment stops after 30 minutes on standby. The deployment must be `ready`, in the production environment, and not shutting down, and its app must already have a current deployment. You can't promote the deployment that's already current, except after a rollback. Then promoting it ends the rolled-back state and turns automatic promotion back on. The command returns right away. Poll `get-deployment` or `list-deployments` for the result. Promoting needs an active Compute plan. Without one the API answers 412 with `The workspace has no active Compute plan.` A workspace over its Compute spend cap gets `The workspace is suspended by its Compute spend cap. Raise the spend limit to resume.` See [Compute plans](/docs/compute/get-started/plans). ## Usage ```bash theme={"system"} unkey api deployments promote-deployment --deployment-id= ``` ## Flags ID of the deployment to promote. ### Shared flags Every `unkey api` command accepts these; [CLI output and shared flags](/docs/platform/cli/output-and-flags) describes them in full. A JSON document sent as the request body instead of building it from the flags above. It is mutually exclusive with the request-building flags, and unknown fields are rejected locally. See [Send a raw body](/docs/platform/cli/output-and-flags#send-a-raw-body). Root key for the request. Falls back to `UNKEY_ROOT_KEY`, then to the config file written by `unkey auth login`. See [CLI authentication](/docs/platform/cli/authentication). Base URL of the API. Falls back to `UNKEY_API_BASE_URL`. You don't normally need to set it. Path of the TOML file that `unkey auth login` writes. Falls back to `UNKEY_CONFIG`. Output format. Falls back to `UNKEY_OUTPUT`. Set `json` to print the full response envelope (`meta` and `data`) for piping; any other value prints the request ID followed by `data`. ## Required permissions Your root key needs one of: * `environment.*.promote_deployment` (any environment) * `environment..promote_deployment` (a specific environment) If the deployment doesn't exist or your key doesn't have the permission, you get the same 404: `The requested deployment does not exist.` See [Root key permissions](/docs/platform/root-keys/permissions) for the full catalog. ## Examples Promote a deployment: ```bash theme={"system"} unkey api deployments promote-deployment --deployment-id=d_1234abcd ``` Send the request body as JSON: ```bash theme={"system"} unkey api deployments promote-deployment --body='{"deploymentId":"d_1234abcd"}' ``` ## API endpoint The command calls [`POST /v2/deployments.promoteDeployment`](/docs/compute/api-reference/deployments/promote-deployment) and prints its response. The request fields carry the same names as the flags in camelCase, which is the shape `--body` expects. ## Related What promote and rollback change and when traffic switches. # unkey api deployments rollback-deployment Source: https://www.unkey.com/docs/compute/cli/deployments/rollback-deployment Send production traffic back to an earlier deployment. 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. Move live traffic back to an earlier deployment of an app. `--deployment-id` is the deployment you want to go back to. The target must be `ready`, in the production environment, not shutting down, and not already current. After a rollback, new deployments stop taking over live traffic automatically until you promote the rolled-back deployment or a newer one. The command returns right away. Poll `get-deployment` or `list-deployments` for the result. Rolling back needs an active Compute plan. Without one the API answers 412 with `The workspace has no active Compute plan.` A workspace over its Compute spend cap gets `The workspace is suspended by its Compute spend cap. Raise the spend limit to resume.` See [Compute plans](/docs/compute/get-started/plans). ## Usage ```bash theme={"system"} unkey api deployments rollback-deployment --deployment-id= ``` ## Flags ID of the deployment to roll back to. ### Shared flags Every `unkey api` command accepts these; [CLI output and shared flags](/docs/platform/cli/output-and-flags) describes them in full. A JSON document sent as the request body instead of building it from the flags above. It is mutually exclusive with the request-building flags, and unknown fields are rejected locally. See [Send a raw body](/docs/platform/cli/output-and-flags#send-a-raw-body). Root key for the request. Falls back to `UNKEY_ROOT_KEY`, then to the config file written by `unkey auth login`. See [CLI authentication](/docs/platform/cli/authentication). Base URL of the API. Falls back to `UNKEY_API_BASE_URL`. You don't normally need to set it. Path of the TOML file that `unkey auth login` writes. Falls back to `UNKEY_CONFIG`. Output format. Falls back to `UNKEY_OUTPUT`. Set `json` to print the full response envelope (`meta` and `data`) for piping; any other value prints the request ID followed by `data`. ## Required permissions Your root key needs one of: * `environment.*.rollback_deployment` (any environment) * `environment..rollback_deployment` (a specific environment) If the deployment doesn't exist or your key doesn't have the permission, you get the same 404: `The requested deployment does not exist.` See [Root key permissions](/docs/platform/root-keys/permissions) for the full catalog. ## Examples Roll back to a known good deployment: ```bash theme={"system"} unkey api deployments rollback-deployment --deployment-id=d_1234abcd ``` Send the request body as JSON: ```bash theme={"system"} unkey api deployments rollback-deployment --body='{"deploymentId":"d_1234abcd"}' ``` ## API endpoint The command calls [`POST /v2/deployments.rollbackDeployment`](/docs/compute/api-reference/deployments/rollback-deployment) and prints its response. The request fields carry the same names as the flags in camelCase, which is the shape `--body` expects. ## Related What a rollback changes and how to clear the rolled-back state. # unkey api deployments start-deployment Source: https://www.unkey.com/docs/compute/cli/deployments/start-deployment Resume a stopped preview deployment. 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. Start a preview deployment that was stopped with `stop-deployment`, so it serves traffic again. Nothing is rebuilt, and it keeps the configuration it had. It must be in a preview environment, because production deployments can't be stopped. You don't need to wait for `stopped`: a deployment that's still shutting down can be started right away. The command returns before the deployment is back. Poll `get-deployment` until the status is `ready`. Starting a deployment needs an active Compute plan. Without one the API answers 412 with `The workspace has no active Compute plan.` A workspace over its Compute spend cap gets `The workspace is suspended by its Compute spend cap. Raise the spend limit to resume.` See [Compute plans](/docs/compute/get-started/plans). ## Usage ```bash theme={"system"} unkey api deployments start-deployment --deployment-id= ``` ## Flags ID of the stopped deployment to start. ### Shared flags Every `unkey api` command accepts these; [CLI output and shared flags](/docs/platform/cli/output-and-flags) describes them in full. A JSON document sent as the request body instead of building it from the flags above. It is mutually exclusive with the request-building flags, and unknown fields are rejected locally. See [Send a raw body](/docs/platform/cli/output-and-flags#send-a-raw-body). Root key for the request. Falls back to `UNKEY_ROOT_KEY`, then to the config file written by `unkey auth login`. See [CLI authentication](/docs/platform/cli/authentication). Base URL of the API. Falls back to `UNKEY_API_BASE_URL`. You don't normally need to set it. Path of the TOML file that `unkey auth login` writes. Falls back to `UNKEY_CONFIG`. Output format. Falls back to `UNKEY_OUTPUT`. Set `json` to print the full response envelope (`meta` and `data`) for piping; any other value prints the request ID followed by `data`. ## Required permissions Your root key needs one of: * `environment.*.start_deployment` (any environment) * `environment..start_deployment` (a specific environment) If the deployment doesn't exist or your key doesn't have the permission, you get the same 404: `The requested deployment does not exist.` See [Root key permissions](/docs/platform/root-keys/permissions) for the full catalog. ## Examples Start a stopped deployment: ```bash theme={"system"} unkey api deployments start-deployment --deployment-id=d_1234abcd ``` Send the request body as JSON: ```bash theme={"system"} unkey api deployments start-deployment --body='{"deploymentId":"d_1234abcd"}' ``` ## API endpoint The command calls [`POST /v2/deployments.startDeployment`](/docs/compute/api-reference/deployments/start-deployment) and prints its response. The request fields carry the same names as the flags in camelCase, which is the shape `--body` expects. ## Related Stop and start in preview environments and the automatic stop timers. # unkey api deployments stop-deployment Source: https://www.unkey.com/docs/compute/cli/deployments/stop-deployment Stop a running preview deployment and release its compute. 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. Stop a running preview deployment and free its compute resources. It keeps its configuration, and you can bring it back with `start-deployment`. It must be `ready` and in a preview environment. Production deployments can't be stopped. Stopping a deployment that's still shutting down from an earlier stop returns a precondition error. The command returns before the instances are gone. Poll `get-deployment` until the status is `stopped`. ## Usage ```bash theme={"system"} unkey api deployments stop-deployment --deployment-id= ``` ## Flags ID of the running deployment to stop. ### Shared flags Every `unkey api` command accepts these; [CLI output and shared flags](/docs/platform/cli/output-and-flags) describes them in full. A JSON document sent as the request body instead of building it from the flags above. It is mutually exclusive with the request-building flags, and unknown fields are rejected locally. See [Send a raw body](/docs/platform/cli/output-and-flags#send-a-raw-body). Root key for the request. Falls back to `UNKEY_ROOT_KEY`, then to the config file written by `unkey auth login`. See [CLI authentication](/docs/platform/cli/authentication). Base URL of the API. Falls back to `UNKEY_API_BASE_URL`. You don't normally need to set it. Path of the TOML file that `unkey auth login` writes. Falls back to `UNKEY_CONFIG`. Output format. Falls back to `UNKEY_OUTPUT`. Set `json` to print the full response envelope (`meta` and `data`) for piping; any other value prints the request ID followed by `data`. ## Required permissions Your root key needs one of: * `environment.*.stop_deployment` (any environment) * `environment..stop_deployment` (a specific environment) If the deployment doesn't exist or your key doesn't have the permission, you get the same 404: `The requested deployment does not exist.` See [Root key permissions](/docs/platform/root-keys/permissions) for the full catalog. ## Examples Stop a preview deployment: ```bash theme={"system"} unkey api deployments stop-deployment --deployment-id=d_1234abcd ``` Send the request body as JSON: ```bash theme={"system"} unkey api deployments stop-deployment --body='{"deploymentId":"d_1234abcd"}' ``` ## API endpoint The command calls [`POST /v2/deployments.stopDeployment`](/docs/compute/api-reference/deployments/stop-deployment) and prints its response. The request fields carry the same names as the flags in camelCase, which is the shape `--body` expects. ## Related Stop and start in preview environments and the automatic stop timers. # unkey api domains create-domain Source: https://www.unkey.com/docs/compute/cli/domains/create-domain Attach a custom domain to an environment and get its DNS records. 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. Attach a custom domain to an environment and start verifying it. The domain starts as `pending` and serves no traffic until verification succeeds. We check DNS about once a minute, so expect a short delay after you add your records. The response's `dnsRecords` lists every record you need. Create each one exactly as given: one routes traffic and one proves you own the domain, and you need both. If your DNS provider supports Domain Connect, the response also has a `domainConnect.url` that adds the records for you in one step. * A name that already exists in your workspace returns a 409. * Going over your plan's domain limit returns a 403. See [Limits](/docs/platform/billing/limits). * If the records aren't found within 24 hours, the domain moves to `failed`. Use [verify-domain](/docs/compute/cli/domains/verify-domain) to try again. ## Usage ```bash theme={"system"} unkey api domains create-domain --project= --app= --environment= --domain= ``` ## Flags App ID or slug. Fully qualified domain name, for example `api.acme.com`. Wildcards (`*.acme.com`), public suffixes (`co.uk`, `github.io`), IP addresses, and anything with a scheme, port, or path return a 400. Names are stored lowercase, with Unicode converted to Punycode, so `MÜNCHEN.DE` and `xn--mnchen-3ya.de` are the same domain. Environment ID or slug the domain routes to. Project ID or slug. Both forms resolve to the same project. ### Shared flags Every `unkey api` command accepts these; [CLI output and shared flags](/docs/platform/cli/output-and-flags) describes them in full. A JSON document sent as the request body instead of building it from the flags above. It is mutually exclusive with the request-building flags, and unknown fields are rejected locally. See [Send a raw body](/docs/platform/cli/output-and-flags#send-a-raw-body). Root key for the request. Falls back to `UNKEY_ROOT_KEY`, then to the config file written by `unkey auth login`. See [CLI authentication](/docs/platform/cli/authentication). Base URL of the API. Falls back to `UNKEY_API_BASE_URL`. You don't normally need to set it. Path of the TOML file that `unkey auth login` writes. Falls back to `UNKEY_CONFIG`. Output format. Falls back to `UNKEY_OUTPUT`. Set `json` to print the full response envelope (`meta` and `data`) for piping; any other value prints the request ID followed by `data`. ## Required permissions Your root key needs one of: * `environment.*.create_domain` (any environment) * `environment..create_domain` (a specific environment) If the environment doesn't exist or your key doesn't have the permission, you get the same 404: `The requested environment does not exist.` See [Root key permissions](/docs/platform/root-keys/permissions) for the full catalog. ## Examples Attach a domain and print the DNS records: ```bash theme={"system"} unkey api domains create-domain --project=payments --app=api --environment=production --domain=api.acme.com --output=json | jq '.data.dnsRecords' ``` Send the request body as JSON: ```bash theme={"system"} unkey api domains create-domain --body='{"project":"payments","app":"api","environment":"production","domain":"api.acme.com"}' ``` ## API endpoint The command calls [`POST /v2/domains.createDomain`](/docs/compute/api-reference/domains/create-domain) and prints its response. The request fields carry the same names as the flags in camelCase, which is the shape `--body` expects. ## Related DNS records, verification, and certificates for your own hostnames. # unkey api domains delete-domain Source: https://www.unkey.com/docs/compute/cli/domains/delete-domain Detach a custom domain so Unkey stops serving 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 custom domain from your workspace, addressed by ID or name. We stop serving the name, and requests to it fail with a certificate error. Your DNS records stay in place, so remove them yourself. ## Usage ```bash theme={"system"} unkey api domains delete-domain --domain= ``` ## Flags Domain ID or fully qualified name. ### Shared flags Every `unkey api` command accepts these; [CLI output and shared flags](/docs/platform/cli/output-and-flags) describes them in full. A JSON document sent as the request body instead of building it from the flags above. It is mutually exclusive with the request-building flags, and unknown fields are rejected locally. See [Send a raw body](/docs/platform/cli/output-and-flags#send-a-raw-body). Root key for the request. Falls back to `UNKEY_ROOT_KEY`, then to the config file written by `unkey auth login`. See [CLI authentication](/docs/platform/cli/authentication). Base URL of the API. Falls back to `UNKEY_API_BASE_URL`. You don't normally need to set it. Path of the TOML file that `unkey auth login` writes. Falls back to `UNKEY_CONFIG`. Output format. Falls back to `UNKEY_OUTPUT`. Set `json` to print the full response envelope (`meta` and `data`) for piping; any other value prints the request ID followed by `data`. ## Required permissions Your root key needs one of: * `environment.*.delete_domain` (any environment) * `environment..delete_domain` (a specific environment) If the domain doesn't exist or your key doesn't have the permission, you get the same 404: `The requested domain does not exist.` See [Root key permissions](/docs/platform/root-keys/permissions) for the full catalog. ## Examples Delete a domain: ```bash theme={"system"} unkey api domains delete-domain --domain=api.acme.com ``` Send the request body as JSON: ```bash theme={"system"} unkey api domains delete-domain --body='{"domain":"api.acme.com"}' ``` ## API endpoint The command calls [`POST /v2/domains.deleteDomain`](/docs/compute/api-reference/domains/delete-domain) and prints its response. The request fields carry the same names as the flags in camelCase, which is the shape `--body` expects. ## Related DNS records, verification, and certificates for your own hostnames. # unkey api domains get-domain Source: https://www.unkey.com/docs/compute/cli/domains/get-domain Look up a custom domain and its verification status. 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. Retrieve a custom domain by ID or name and its verification status. Names are unique per workspace, so `api.acme.com` is enough. No project, app, or environment is needed. Use it to check progress after `create-domain`. We check DNS about once a minute. * `status: verified` means routing is set up and a certificate has been requested. * Each entry in `dnsRecords` has a `verified` flag, so you can see which records are still missing. A routing record your provider proxies or flattens can stay `false` even while it serves traffic. * `verificationError` explains the last failed check. * Verification stops 24 hours after `createdAt`, and the status becomes `failed`. ## Usage ```bash theme={"system"} unkey api domains get-domain --domain= ``` ## Flags Domain ID or fully qualified name. ### Shared flags Every `unkey api` command accepts these; [CLI output and shared flags](/docs/platform/cli/output-and-flags) describes them in full. A JSON document sent as the request body instead of building it from the flags above. It is mutually exclusive with the request-building flags, and unknown fields are rejected locally. See [Send a raw body](/docs/platform/cli/output-and-flags#send-a-raw-body). Root key for the request. Falls back to `UNKEY_ROOT_KEY`, then to the config file written by `unkey auth login`. See [CLI authentication](/docs/platform/cli/authentication). Base URL of the API. Falls back to `UNKEY_API_BASE_URL`. You don't normally need to set it. Path of the TOML file that `unkey auth login` writes. Falls back to `UNKEY_CONFIG`. Output format. Falls back to `UNKEY_OUTPUT`. Set `json` to print the full response envelope (`meta` and `data`) for piping; any other value prints the request ID followed by `data`. ## Required permissions Your root key needs one of: * `environment.*.read_domain` (any environment) * `environment..read_domain` (a specific environment) If the domain doesn't exist or your key doesn't have the permission, you get the same 404: `The requested domain does not exist.` See [Root key permissions](/docs/platform/root-keys/permissions) for the full catalog. ## Examples Check a domain: ```bash theme={"system"} unkey api domains get-domain --domain=api.acme.com ``` Print only the status: ```bash theme={"system"} unkey api domains get-domain --domain=api.acme.com --output=json | jq -r '.data.status' ``` ## API endpoint The command calls [`POST /v2/domains.getDomain`](/docs/compute/api-reference/domains/get-domain) and prints its response. The request fields carry the same names as the flags in camelCase, which is the shape `--body` expects. ## Related DNS records, verification, and certificates for your own hostnames. # unkey api domains list-domains Source: https://www.unkey.com/docs/compute/cli/domains/list-domains List the custom domains in your workspace with their status. 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 custom domains in your workspace with their verification status and `dnsRecords`. Results are sorted by ID and paginated. When `pagination.hasMore` is true, pass `pagination.cursor` back with `--cursor`. Add `--output=json` to see the `pagination` object. Every filter is optional. With none, you get every domain in the workspace. `--project`, `--app`, and `--environment` each narrow the list, and the most specific one wins. A filter that matches nothing returns an empty list, not a 404. In large workspaces, use filters: if a page would need to look through more than 10,000 domains, the call fails with 503 and `The domain scan limit was reached. Narrow the project, app, environment, or search filters and retry.` ## Usage ```bash theme={"system"} unkey api domains list-domains [--project=] [--app=] [--environment=] [--limit=] [--cursor=] [--search=] ``` ## Flags App ID or slug to filter by. Pagination cursor from a previous response. Environment ID or slug to filter by. Maximum domains per page, from 1 to 100. Project ID or slug to filter by. Both forms resolve to the same project. Case-insensitive filter on domain ID or name. ### Shared flags Every `unkey api` command accepts these; [CLI output and shared flags](/docs/platform/cli/output-and-flags) describes them in full. A JSON document sent as the request body instead of building it from the flags above. It is mutually exclusive with the request-building flags, and unknown fields are rejected locally. See [Send a raw body](/docs/platform/cli/output-and-flags#send-a-raw-body). Root key for the request. Falls back to `UNKEY_ROOT_KEY`, then to the config file written by `unkey auth login`. See [CLI authentication](/docs/platform/cli/authentication). Base URL of the API. Falls back to `UNKEY_API_BASE_URL`. You don't normally need to set it. Path of the TOML file that `unkey auth login` writes. Falls back to `UNKEY_CONFIG`. Output format. Falls back to `UNKEY_OUTPUT`. Set `json` to print the full response envelope (`meta` and `data`) for piping; any other value prints the request ID followed by `data`. ## Required permissions Your root key needs the workspace-wide `environment.*.read_domain`. A grant for one environment doesn't work here. A key without the permission isn't rejected. It gets an empty list instead of a 403, so an empty result doesn't always mean you have no domains. See [Root key permissions](/docs/platform/root-keys/permissions) for the full catalog. ## Examples List every domain in the workspace: ```bash theme={"system"} unkey api domains list-domains ``` List the domains of one environment: ```bash theme={"system"} unkey api domains list-domains --project=payments --app=api --environment=production ``` Filter and page: ```bash theme={"system"} unkey api domains list-domains --project=payments --app=api --environment=production --limit=25 --search=acme.com ``` ## API endpoint The command calls [`POST /v2/domains.listDomains`](/docs/compute/api-reference/domains/list-domains) and prints its response. The request fields carry the same names as the flags in camelCase, which is the shape `--body` expects. ## Related DNS records, verification, and certificates for your own hostnames. # unkey api domains verify-domain Source: https://www.unkey.com/docs/compute/cli/domains/verify-domain Restart verification of a pending or failed custom domain. 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. Restart verification of a custom domain, addressed by ID or name. Call it after you correct the DNS records of a domain that shows `failed`, or to give a `pending` domain a new 24-hour window. The domain goes back to `pending` and the 24 hours start again. The command returns right away. Poll `get-domain` for the result. A domain that is already `verified` returns a 412. ## Usage ```bash theme={"system"} unkey api domains verify-domain --domain= ``` ## Flags Domain ID or fully qualified name. ### Shared flags Every `unkey api` command accepts these; [CLI output and shared flags](/docs/platform/cli/output-and-flags) describes them in full. A JSON document sent as the request body instead of building it from the flags above. It is mutually exclusive with the request-building flags, and unknown fields are rejected locally. See [Send a raw body](/docs/platform/cli/output-and-flags#send-a-raw-body). Root key for the request. Falls back to `UNKEY_ROOT_KEY`, then to the config file written by `unkey auth login`. See [CLI authentication](/docs/platform/cli/authentication). Base URL of the API. Falls back to `UNKEY_API_BASE_URL`. You don't normally need to set it. Path of the TOML file that `unkey auth login` writes. Falls back to `UNKEY_CONFIG`. Output format. Falls back to `UNKEY_OUTPUT`. Set `json` to print the full response envelope (`meta` and `data`) for piping; any other value prints the request ID followed by `data`. ## Required permissions Your root key needs one of: * `environment.*.verify_domain` (any environment) * `environment..verify_domain` (a specific environment) If the domain doesn't exist or your key doesn't have the permission, you get the same 404: `The requested domain does not exist.` See [Root key permissions](/docs/platform/root-keys/permissions) for the full catalog. ## Examples Retry verification: ```bash theme={"system"} unkey api domains verify-domain --domain=api.acme.com ``` Send the request body as JSON: ```bash theme={"system"} unkey api domains verify-domain --body='{"domain":"api.acme.com"}' ``` ## API endpoint The command calls [`POST /v2/domains.verifyDomain`](/docs/compute/api-reference/domains/verify-domain) and prints its response. The request fields carry the same names as the flags in camelCase, which is the shape `--body` expects. ## Related DNS records, verification, and certificates for your own hostnames. # unkey api environments get-environment Source: https://www.unkey.com/docs/compute/cli/environments/get-environment Look up an environment with its build and runtime settings. 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. Retrieve one environment of an app, including its build, runtime, health check, and region settings. Each of `--project`, `--app`, and `--environment` takes an ID or a slug. ## Usage ```bash theme={"system"} unkey api environments get-environment --project= --app= --environment= ``` ## Flags App ID or slug. Environment ID or slug, for example `production` or `preview`. Project ID or slug. Both forms resolve to the same project. ### Shared flags Every `unkey api` command accepts these; [CLI output and shared flags](/docs/platform/cli/output-and-flags) describes them in full. A JSON document sent as the request body instead of building it from the flags above. It is mutually exclusive with the request-building flags, and unknown fields are rejected locally. See [Send a raw body](/docs/platform/cli/output-and-flags#send-a-raw-body). Root key for the request. Falls back to `UNKEY_ROOT_KEY`, then to the config file written by `unkey auth login`. See [CLI authentication](/docs/platform/cli/authentication). Base URL of the API. Falls back to `UNKEY_API_BASE_URL`. You don't normally need to set it. Path of the TOML file that `unkey auth login` writes. Falls back to `UNKEY_CONFIG`. Output format. Falls back to `UNKEY_OUTPUT`. Set `json` to print the full response envelope (`meta` and `data`) for piping; any other value prints the request ID followed by `data`. ## Required permissions Your root key needs one of: * `environment.*.read_environment` (any environment) * `environment..read_environment` (a specific environment) If the environment doesn't exist or your key doesn't have the permission, you get the same 404: `The requested environment does not exist.` See [Root key permissions](/docs/platform/root-keys/permissions) for the full catalog. ## Examples Get the production environment: ```bash theme={"system"} unkey api environments get-environment --project=payments --app=payments-api --environment=production ``` Print only the runtime settings: ```bash theme={"system"} unkey api environments get-environment --project=payments --app=payments-api --environment=production --output=json | jq '.data.runtime' ``` ## API endpoint The command calls [`POST /v2/environments.getEnvironment`](/docs/compute/api-reference/environments/get-environment) and prints its response. The request fields carry the same names as the flags in camelCase, which is the shape `--body` expects. # unkey api environments list-environment-variables Source: https://www.unkey.com/docs/compute/cli/environments/list-environment-variables List the variables of an environment 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 variables of an environment. Results are ordered by variable ID and paginated. When `pagination.hasMore` is true, pass `pagination.cursor` back with `--cursor`. `recoverable` variables include their value. `writeonly` variables never do: their `value` is an empty string. ## Usage ```bash theme={"system"} unkey api environments list-environment-variables --project= --app= --environment= [--limit=] [--cursor=] ``` ## Flags App ID or slug. Pagination cursor from a previous response. Environment ID or slug. Maximum variables per page, between 1 and 100. The API uses 100 when the flag is omitted. Project ID or slug. Both forms resolve to the same project. ### Shared flags Every `unkey api` command accepts these; [CLI output and shared flags](/docs/platform/cli/output-and-flags) describes them in full. A JSON document sent as the request body instead of building it from the flags above. It is mutually exclusive with the request-building flags, and unknown fields are rejected locally. See [Send a raw body](/docs/platform/cli/output-and-flags#send-a-raw-body). Root key for the request. Falls back to `UNKEY_ROOT_KEY`, then to the config file written by `unkey auth login`. See [CLI authentication](/docs/platform/cli/authentication). Base URL of the API. Falls back to `UNKEY_API_BASE_URL`. You don't normally need to set it. Path of the TOML file that `unkey auth login` writes. Falls back to `UNKEY_CONFIG`. Output format. Falls back to `UNKEY_OUTPUT`. Set `json` to print the full response envelope (`meta` and `data`) for piping; any other value prints the request ID followed by `data`. ## Required permissions Your root key needs one of: * `environment.*.read_environment_variables` (any environment) * `environment..read_environment_variables` (a specific environment) Without a matching permission the API answers 403 and the CLI prints `Permission denied:` followed by the detail. See [Root key permissions](/docs/platform/root-keys/permissions) for the full catalog. ## Examples List the variables of production: ```bash theme={"system"} unkey api environments list-environment-variables --project=payments --app=payments-api --environment=production ``` Smaller pages: ```bash theme={"system"} unkey api environments list-environment-variables --project=payments --app=payments-api --environment=production --limit=25 ``` ## API endpoint The command calls [`POST /v2/environments.listEnvironmentVariables`](/docs/compute/api-reference/environments/list-environment-variables) and prints its response. The request fields carry the same names as the flags in camelCase, which is the shape `--body` expects. ## Related Kinds, encryption, and how variables reach a deployment. # unkey api environments list-environments Source: https://www.unkey.com/docs/compute/cli/environments/list-environments List the production and preview environments of an app. 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 environments of an app. They all come back in one response, ordered by ID, with no pagination. ## Usage ```bash theme={"system"} unkey api environments list-environments --project= --app= ``` ## Flags App ID or slug. Project ID or slug. Both forms resolve to the same project. ### Shared flags Every `unkey api` command accepts these; [CLI output and shared flags](/docs/platform/cli/output-and-flags) describes them in full. A JSON document sent as the request body instead of building it from the flags above. It is mutually exclusive with the request-building flags, and unknown fields are rejected locally. See [Send a raw body](/docs/platform/cli/output-and-flags#send-a-raw-body). Root key for the request. Falls back to `UNKEY_ROOT_KEY`, then to the config file written by `unkey auth login`. See [CLI authentication](/docs/platform/cli/authentication). Base URL of the API. Falls back to `UNKEY_API_BASE_URL`. You don't normally need to set it. Path of the TOML file that `unkey auth login` writes. Falls back to `UNKEY_CONFIG`. Output format. Falls back to `UNKEY_OUTPUT`. Set `json` to print the full response envelope (`meta` and `data`) for piping; any other value prints the request ID followed by `data`. ## Required permissions Your root key needs `environment.*.read_environment`. If the app doesn't exist or your key doesn't have the permission, you get the same 404: `The requested app does not exist.` See [Root key permissions](/docs/platform/root-keys/permissions) for the full catalog. ## Examples List the environments of an app: ```bash theme={"system"} unkey api environments list-environments --project=payments --app=payments-api ``` ## API endpoint The command calls [`POST /v2/environments.listEnvironments`](/docs/compute/api-reference/environments/list-environments) and prints its response. The request fields carry the same names as the flags in camelCase, which is the shape `--body` expects. ## Related What differs between the two environments. # unkey api environments remove-environment-variables Source: https://www.unkey.com/docs/compute/cli/environments/remove-environment-variables Delete named variables from an environment in one atomic call. 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 variables from an environment by name, all in one atomic request. Names that don't exist are ignored. ## Usage ```bash theme={"system"} unkey api environments remove-environment-variables --project= --app= --environment= --variables=[,...] ``` ## Flags App ID or slug. Environment ID or slug. Project ID or slug. Both forms resolve to the same project. Comma-separated variable names to remove, 1 to 50 per request. Each must be a POSIX shell name matching `^[A-Za-z_][A-Za-z0-9_]*$`. ### Shared flags Every `unkey api` command accepts these; [CLI output and shared flags](/docs/platform/cli/output-and-flags) describes them in full. A JSON document sent as the request body instead of building it from the flags above. It is mutually exclusive with the request-building flags, and unknown fields are rejected locally. See [Send a raw body](/docs/platform/cli/output-and-flags#send-a-raw-body). Root key for the request. Falls back to `UNKEY_ROOT_KEY`, then to the config file written by `unkey auth login`. See [CLI authentication](/docs/platform/cli/authentication). Base URL of the API. Falls back to `UNKEY_API_BASE_URL`. You don't normally need to set it. Path of the TOML file that `unkey auth login` writes. Falls back to `UNKEY_CONFIG`. Output format. Falls back to `UNKEY_OUTPUT`. Set `json` to print the full response envelope (`meta` and `data`) for piping; any other value prints the request ID followed by `data`. ## Required permissions Your root key needs one of: * `environment.*.remove_environment_variables` (any environment) * `environment..remove_environment_variables` (a specific environment) Without a matching permission the API answers 403 and the CLI prints `Permission denied:` followed by the detail. See [Root key permissions](/docs/platform/root-keys/permissions) for the full catalog. ## Examples Remove two variables: ```bash theme={"system"} unkey api environments remove-environment-variables --project=payments --app=payments-api --environment=production --variables=OLD_TOKEN,LEGACY_URL ``` Send the request body as JSON: ```bash theme={"system"} unkey api environments remove-environment-variables --body='{"project":"payments","app":"payments-api","environment":"production","variables":["OLD_TOKEN"]}' ``` ## API endpoint The command calls [`POST /v2/environments.removeEnvironmentVariables`](/docs/compute/api-reference/environments/remove-environment-variables) and prints its response. The request fields carry the same names as the flags in camelCase, which is the shape `--body` expects. ## Related Kinds, encryption, and how variables reach a deployment. # unkey api environments set-environment-variables Source: https://www.unkey.com/docs/compute/cli/environments/set-environment-variables Create or update the variables of an environment in one atomic call. 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 or update variables of an environment in one atomic request. By default, each variable you send is created or overwritten, and variables you don't mention stay as they are. So you can change one secret without resending the others. * `--prune` deletes every variable you didn't send. `--prune --variables='[]'` removes them all. * `kind` defaults to `writeonly`, which can never be read back. Set `"kind":"recoverable"` so `list-environment-variables` can return the value. Values are encrypted at rest. ## Usage ```bash theme={"system"} unkey api environments set-environment-variables --project= --app= --environment= --variables= [--prune] ``` ## Flags App ID or slug. Environment ID or slug. Project ID or slug. Both forms resolve to the same project. Delete every variable not present in `--variables` after the upsert. JSON array of at most 50 variables, each with `key` and `value` and optionally `kind` and `description`, for example `[{"key":"TOKEN","value":"secret"}]`. A key must be a POSIX shell name of at most 256 characters and may appear only once. A value is capped at 16384 UTF-8 bytes, and a description at 255 characters. ### Shared flags Every `unkey api` command accepts these; [CLI output and shared flags](/docs/platform/cli/output-and-flags) describes them in full. A JSON document sent as the request body instead of building it from the flags above. It is mutually exclusive with the request-building flags, and unknown fields are rejected locally. See [Send a raw body](/docs/platform/cli/output-and-flags#send-a-raw-body). Root key for the request. Falls back to `UNKEY_ROOT_KEY`, then to the config file written by `unkey auth login`. See [CLI authentication](/docs/platform/cli/authentication). Base URL of the API. Falls back to `UNKEY_API_BASE_URL`. You don't normally need to set it. Path of the TOML file that `unkey auth login` writes. Falls back to `UNKEY_CONFIG`. Output format. Falls back to `UNKEY_OUTPUT`. Set `json` to print the full response envelope (`meta` and `data`) for piping; any other value prints the request ID followed by `data`. ## Required permissions Your root key needs one of: * `environment.*.set_environment_variables` (any environment) * `environment..set_environment_variables` (a specific environment) Without a matching permission the API answers 403 and the CLI prints `Permission denied:` followed by the detail. See [Root key permissions](/docs/platform/root-keys/permissions) for the full catalog. ## Examples Set one variable: ```bash theme={"system"} unkey api environments set-environment-variables --project=payments --app=payments-api --environment=production --variables='[{"key":"TOKEN","value":"secret"}]' ``` Replace the whole set: ```bash theme={"system"} unkey api environments set-environment-variables --project=payments --app=payments-api --environment=production --prune --variables='[{"key":"DATABASE_URL","value":"mysql://...","kind":"recoverable"}]' ``` Send the request body as JSON: ```bash theme={"system"} unkey api environments set-environment-variables --body='{"project":"payments","app":"payments-api","environment":"production","variables":[{"key":"TOKEN","value":"secret"}]}' ``` ## API endpoint The command calls [`POST /v2/environments.setEnvironmentVariables`](/docs/compute/api-reference/environments/set-environment-variables) and prints its response. The request fields carry the same names as the flags in camelCase, which is the shape `--body` expects. ## Related Kinds, encryption, and how variables reach a deployment. # unkey api environments update-settings Source: https://www.unkey.com/docs/compute/cli/environments/update-settings Change how an environment builds and runs its next deployment. 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 selected build, runtime, health check, and region settings of an environment. Every setting flag is optional, and settings you leave out don't change. Changes apply from the next deployment. To clear `dockerfile`, `buildCommand`, `healthcheck`, or `openapiSpecPath`, send `null` with `--body`, because the flags can't send `null`. `--regions` replaces the whole region list and can't be empty. A value outside the limits below returns 400. `--v-cpus`, `--memory-mib`, and `--storage-mib` are capped by your workspace's per-instance quota. If your workspace has no resource limits set, you get `Resource limits are not configured for this workspace. Contact support@unkey.com.` ## Usage ```bash theme={"system"} unkey api environments update-settings --project= --app= --environment= [setting flags] ``` ## Flags App ID or slug. Whether pushes to the connected repository deploy automatically (`--auto-deploy` or `--auto-deploy=false`). Omit the flag to leave the current setting unchanged. Build command. Comma-separated container command that overrides the image's command. At most 10 entries, each at most 4096 characters. Path of the Dockerfile inside the root directory. Environment ID or slug. Health check configuration as a JSON object, for example `{"method":"GET","path":"/health"}`. `method` is `GET` or `POST`, and `path` must start with a slash. The optional fields are `intervalSeconds` (default 10), `timeoutSeconds` (default 5), `failureThreshold` (default 3), and `initialDelaySeconds` (default 0). Memory allocation in MiB. At least 256, in steps of 256, up to your workspace's per-instance quota. Path of the OpenAPI specification served by the app, used by the gateway's OpenAPI validation policy. It must start with a slash and match `^(/[\w\-]+)+(\.[\w]+)?$`, for example `/openapi.yaml`. Container port the app listens on, from 1 to 65535. Project ID or slug. Both forms resolve to the same project. Region configuration as a JSON array, for example `[{"name":"us-east-1","replicas":{"min":1,"max":2}}]`. Replaces the full region list, which must hold 1 to 5 regions. Directory of the repository the build runs in, as a relative path. Use `.` for the repository root. An absolute path is rejected with `Root directory must be a relative path like 'api' or 'services/api'.` Signal sent to the container on shutdown. One of `SIGTERM`, `SIGINT`, `SIGQUIT`, or `SIGKILL`. Ephemeral storage allocation in MiB, in steps of 512, up to your workspace's per-instance quota. `0` allocates none. Protocol the gateway uses to reach the container. Either `http1` or `h2c`. CPU allocation in vCPUs. At least 0.25, in steps of 0.25, up to your workspace's per-instance quota. Comma-separated glob patterns, at most 10. A push deploys only if a changed file matches one. Use `src/**` for everything under a directory and `**/*.go` for a file type. Don't start a pattern with `/` or `./`: it's accepted but never matches. ### Shared flags Every `unkey api` command accepts these; [CLI output and shared flags](/docs/platform/cli/output-and-flags) describes them in full. A JSON document sent as the request body instead of building it from the flags above. It is mutually exclusive with the request-building flags, and unknown fields are rejected locally. See [Send a raw body](/docs/platform/cli/output-and-flags#send-a-raw-body). Root key for the request. Falls back to `UNKEY_ROOT_KEY`, then to the config file written by `unkey auth login`. See [CLI authentication](/docs/platform/cli/authentication). Base URL of the API. Falls back to `UNKEY_API_BASE_URL`. You don't normally need to set it. Path of the TOML file that `unkey auth login` writes. Falls back to `UNKEY_CONFIG`. Output format. Falls back to `UNKEY_OUTPUT`. Set `json` to print the full response envelope (`meta` and `data`) for piping; any other value prints the request ID followed by `data`. ## Required permissions Your root key needs one of: * `environment.*.update_environment` (any environment) * `environment..update_environment` (a specific environment) Without a matching permission the API answers 403 and the CLI prints `Permission denied:` followed by the detail. See [Root key permissions](/docs/platform/root-keys/permissions) for the full catalog. ## Examples Set a health check and scale a region: ```bash theme={"system"} unkey api environments update-settings --project=payments --app=payments-api --environment=production --healthcheck='{"method":"GET","path":"/health"}' --regions='[{"name":"us-east-1","replicas":{"min":1,"max":2}}]' ``` Change the container size: ```bash theme={"system"} unkey api environments update-settings --project=payments --app=payments-api --environment=production --v-cpus=1 --memory-mib=1024 ``` Clear the Dockerfile path with a raw body: ```bash theme={"system"} unkey api environments update-settings --body='{"project":"payments","app":"payments-api","environment":"production","dockerfile":null}' ``` ## API endpoint The command calls [`POST /v2/environments.updateSettings`](/docs/compute/api-reference/environments/update-environment-settings) and prints its response. The request fields carry the same names as the flags in camelCase, which is the shape `--body` expects. ## Related Port, command, resources, shutdown signal, and regions. Dockerfile, root directory, build command, and watch paths. What the health check probes and how failures are handled. # unkey api gateway list-policies Source: https://www.unkey.com/docs/compute/cli/gateway/list-policies List the gateway policies of an environment in evaluation order. 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. Get the gateway policies of an environment, in the order they run. The gateway runs them top to bottom and stops at the first one that rejects the request. The whole list comes back in one response. Use it to get current policy IDs before `update-policy`, because `set-policies` gives every policy a new ID. ## Usage ```bash theme={"system"} unkey api gateway list-policies --project= --app= --environment= ``` ## Flags App ID or slug. Environment ID or slug. Project ID or slug. Both forms resolve to the same project. ### Shared flags Every `unkey api` command accepts these; [CLI output and shared flags](/docs/platform/cli/output-and-flags) describes them in full. A JSON document sent as the request body instead of building it from the flags above. It is mutually exclusive with the request-building flags, and unknown fields are rejected locally. See [Send a raw body](/docs/platform/cli/output-and-flags#send-a-raw-body). Root key for the request. Falls back to `UNKEY_ROOT_KEY`, then to the config file written by `unkey auth login`. See [CLI authentication](/docs/platform/cli/authentication). Base URL of the API. Falls back to `UNKEY_API_BASE_URL`. You don't normally need to set it. Path of the TOML file that `unkey auth login` writes. Falls back to `UNKEY_CONFIG`. Output format. Falls back to `UNKEY_OUTPUT`. Set `json` to print the full response envelope (`meta` and `data`) for piping; any other value prints the request ID followed by `data`. ## Required permissions Your root key needs one of: * `environment.*.read_policies` (any environment) * `environment..read_policies` (a specific environment) Without a matching permission the API answers 403 and the CLI prints `Permission denied:` followed by the detail. See [Root key permissions](/docs/platform/root-keys/permissions) for the full catalog. ## Examples List the policies of production: ```bash theme={"system"} unkey api gateway list-policies --project=payments --app=payments-api --environment=production ``` Print policy IDs and names: ```bash theme={"system"} unkey api gateway list-policies --project=payments --app=payments-api --environment=production --output=json | jq -r '.data[] | "\(.id) \(.name)"' ``` ## API endpoint The command calls [`POST /v2/gateway.listPolicies`](/docs/compute/api-reference/gateway/list-policies) and prints its response. The request fields carry the same names as the flags in camelCase, which is the shape `--body` expects. ## Related How policies are ordered, matched, and evaluated at the edge. # unkey api gateway set-policies Source: https://www.unkey.com/docs/compute/cli/gateway/set-policies Replace the whole ordered list of gateway policies for an environment. 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 the gateway policies of an environment in one atomic request. Policies run at the edge before a request reaches your app: they verify API keys, rate limit, block requests outright, or validate them against your OpenAPI spec. Each policy sets exactly one of `keyauth`, `ratelimit`, `firewall`, `openapi`, or `logging`, plus up to ten optional `match` expressions that restrict which requests it applies to. Every call replaces the whole list. The environment's policies become exactly the array you send, in that order, and each one gets a new ID. An empty array removes all policies. An environment can have up to 50 policies. If any policy is invalid, nothing is saved. ## Usage ```bash theme={"system"} unkey api gateway set-policies --project= --app= --environment= --policies= ``` ## Flags App ID or slug. Environment ID or slug. JSON array containing the complete ordered policy list. Project ID or slug. Both forms resolve to the same project. ### Shared flags Every `unkey api` command accepts these; [CLI output and shared flags](/docs/platform/cli/output-and-flags) describes them in full. A JSON document sent as the request body instead of building it from the flags above. It is mutually exclusive with the request-building flags, and unknown fields are rejected locally. See [Send a raw body](/docs/platform/cli/output-and-flags#send-a-raw-body). Root key for the request. Falls back to `UNKEY_ROOT_KEY`, then to the config file written by `unkey auth login`. See [CLI authentication](/docs/platform/cli/authentication). Base URL of the API. Falls back to `UNKEY_API_BASE_URL`. You don't normally need to set it. Path of the TOML file that `unkey auth login` writes. Falls back to `UNKEY_CONFIG`. Output format. Falls back to `UNKEY_OUTPUT`. Set `json` to print the full response envelope (`meta` and `data`) for piping; any other value prints the request ID followed by `data`. ## Required permissions Your root key needs one of: * `environment.*.set_policies` (any environment) * `environment..set_policies` (a specific environment) Without a matching permission the API answers 403 and the CLI prints `Permission denied:` followed by the detail. See [Root key permissions](/docs/platform/root-keys/permissions) for the full catalog. ## Examples Require an API key from one keyspace. A `keyauth` rule names one to five keyspaces, and each one must exist in your workspace. Otherwise the call fails with `Keyspace "" does not exist.` and nothing is saved. ```bash theme={"system"} unkey api gateway set-policies --project=payments --app=payments-api --environment=production --policies='[{"name":"Require API key","enabled":true,"keyauth":{"keyspaces":["ks_1234abcd"]}}]' ``` Remove every policy: ```bash theme={"system"} unkey api gateway set-policies --project=payments --app=payments-api --environment=production --policies='[]' ``` Send the request body as JSON: ```bash theme={"system"} unkey api gateway set-policies --body='{"project":"payments","app":"payments-api","environment":"production","policies":[]}' ``` ## API endpoint The command calls [`POST /v2/gateway.setPolicies`](/docs/compute/api-reference/gateway/set-policies) and prints its response. The request fields carry the same names as the flags in camelCase, which is the shape `--body` expects. ## Related How policies are ordered, matched, and evaluated at the edge. # unkey api gateway update-policy Source: https://www.unkey.com/docs/compute/cli/gateway/update-policy Change one gateway policy in place without resending the list. 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 one gateway policy of an environment in place. The policy keeps its ID and position, and the other policies are untouched. Pass the fields to change as a JSON object in `--policy`. Fields you leave out keep their values. * `"match": null` or an empty array removes all match expressions, so the policy applies to every request. * Passing one of `keyauth`, `ratelimit`, `firewall`, `openapi`, or `logging` replaces the rule, and can change its type. Pass at most one. Policy IDs change every time `set-policies` replaces the list, so get them from `list-policies` first. ## Usage ```bash theme={"system"} unkey api gateway update-policy --project= --app= --environment= --policy-id= --policy= ``` ## Flags App ID or slug. Environment ID or slug. Policy fields to update as a JSON object. Accepted keys are `name`, `enabled`, `match`, `keyauth`, `ratelimit`, `firewall`, `openapi`, and `logging`. ID of the policy to update, from `list-policies`. Project ID or slug. Both forms resolve to the same project. ### Shared flags Every `unkey api` command accepts these; [CLI output and shared flags](/docs/platform/cli/output-and-flags) describes them in full. A JSON document sent as the request body instead of building it from the flags above. It is mutually exclusive with the request-building flags, and unknown fields are rejected locally. See [Send a raw body](/docs/platform/cli/output-and-flags#send-a-raw-body). Root key for the request. Falls back to `UNKEY_ROOT_KEY`, then to the config file written by `unkey auth login`. See [CLI authentication](/docs/platform/cli/authentication). Base URL of the API. Falls back to `UNKEY_API_BASE_URL`. You don't normally need to set it. Path of the TOML file that `unkey auth login` writes. Falls back to `UNKEY_CONFIG`. Output format. Falls back to `UNKEY_OUTPUT`. Set `json` to print the full response envelope (`meta` and `data`) for piping; any other value prints the request ID followed by `data`. ## Required permissions Your root key needs one of: * `environment.*.update_policy` (any environment) * `environment..update_policy` (a specific environment) Without a matching permission the API answers 403 and the CLI prints `Permission denied:` followed by the detail. See [Root key permissions](/docs/platform/root-keys/permissions) for the full catalog. ## Examples Disable a policy: ```bash theme={"system"} unkey api gateway update-policy --project=payments --app=payments-api --environment=production --policy-id=pol_123 --policy='{"enabled":false}' ``` Apply a policy to every request: ```bash theme={"system"} unkey api gateway update-policy --project=payments --app=payments-api --environment=production --policy-id=pol_123 --policy='{"match":null}' ``` Switch a rule to a firewall deny: ```bash theme={"system"} unkey api gateway update-policy --project=payments --app=payments-api --environment=production --policy-id=pol_123 --policy='{"firewall":{"action":"ACTION_DENY"}}' ``` Send the request body as JSON: ```bash theme={"system"} unkey api gateway update-policy --body='{"project":"payments","app":"payments-api","environment":"production","policyId":"pol_123","enabled":false}' ``` ## API endpoint The command calls [`POST /v2/gateway.updatePolicy`](/docs/compute/api-reference/gateway/update-policy) and prints its response. The request fields carry the same names as the flags in camelCase, which is the shape `--body` expects. ## Related How policies are ordered, matched, and evaluated at the edge. # unkey api github install-app Source: https://www.unkey.com/docs/compute/cli/github/install-app Get the URL that installs the Unkey GitHub App in your workspace. 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. Start installing the Unkey GitHub App for your workspace. The command returns a GitHub install URL. Open it in a browser, install the app, and choose which repositories it can see. GitHub then sends you back to your workspace settings in Unkey. The URL expires 15 minutes after the call (`expiresAt`, in Unix milliseconds). Run the command again for a new one. Once installed, connect repositories to individual apps with the `--git` flag of [create-app](/docs/compute/cli/apps/create-app) and [update-app](/docs/compute/cli/apps/update-app). ## Usage ```bash theme={"system"} unkey api github install-app ``` ## Flags The command has no request-building flags of its own. ### Shared flags Every `unkey api` command accepts these; [CLI output and shared flags](/docs/platform/cli/output-and-flags) describes them in full. This endpoint takes an empty request body, so `--body` accepts only `{}`; anything else is rejected locally. A JSON document sent as the request body instead of building it from the flags above. It is mutually exclusive with the request-building flags, and unknown fields are rejected locally. See [Send a raw body](/docs/platform/cli/output-and-flags#send-a-raw-body). Root key for the request. Falls back to `UNKEY_ROOT_KEY`, then to the config file written by `unkey auth login`. See [CLI authentication](/docs/platform/cli/authentication). Base URL of the API. Falls back to `UNKEY_API_BASE_URL`. You don't normally need to set it. Path of the TOML file that `unkey auth login` writes. Falls back to `UNKEY_CONFIG`. Output format. Falls back to `UNKEY_OUTPUT`. Set `json` to print the full response envelope (`meta` and `data`) for piping; any other value prints the request ID followed by `data`. ## Required permissions Your root key needs `workspace.*.install_github`. Without a matching permission the API answers 403 and the CLI prints `Permission denied:` followed by the detail. See [Root key permissions](/docs/platform/root-keys/permissions) for the full catalog. ## Examples Get the install URL: ```bash theme={"system"} unkey api github install-app ``` Print only the URL: ```bash theme={"system"} unkey api github install-app --output=json | jq -r '.data.url' ``` ## API endpoint The command calls [`POST /v2/github.installApp`](/docs/compute/api-reference/github/install-github-app) and prints its response. The endpoint takes no request fields. ## Related Connect a repository and ship the first deployment. # unkey healthcheck Source: https://www.unkey.com/docs/compute/cli/healthcheck Probe a URL and exit non-zero unless it answers 200, for scripts and CI. Send one HTTP GET request to a URL and exit 0 if the server answers 200, or exit 1 otherwise. The command needs no root key and never contacts the Unkey API. Use it as a small probe for CI pipelines, load balancer checks, and monitoring scripts, for example against the hostname of a deployment. ## Usage ```bash theme={"system"} unkey healthcheck ``` The URL is required. Without it, the command exits 1 with `you must provide a url like so: 'unkey healthcheck '`. The command has no flags. ## Behavior Redirects are followed and there's no request timeout, so a server that never answers keeps the command waiting. A 200 response prints `✓ Healthcheck passed: returned 200` and exits 0. Any other status exits 1 with `healthcheck failed with status code `. A connection error exits 1 with `failed to perform healthcheck:` followed by the underlying error. ## Examples Probe a deployment hostname: ```bash theme={"system"} unkey healthcheck https://payments-payments-api-production-acme.unkey.app/health ``` Alert when a service is down: ```bash theme={"system"} unkey healthcheck https://example.com/api/status || echo 'Service is down!' ``` Gate a pipeline step: ```bash theme={"system"} unkey healthcheck http://localhost:8080/health && npm test ``` # unkey api projects create-project Source: https://www.unkey.com/docs/compute/cli/projects/create-project Create a project to hold apps and their deployments. 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 project, the top-level Compute container that groups apps and their deployments under a slug that is unique in your workspace. The slug is what you pass to later `--project` flags. It must be unique in the workspace, or you get a 409 conflict. Projects are described in [Projects, apps, and environments](/docs/compute/concepts/projects-apps-environments). Creating a project needs an active Compute plan. Without one the API answers 412 with `The workspace has no active Compute plan.` See [Compute plans](/docs/compute/get-started/plans). ## Usage ```bash theme={"system"} unkey api projects create-project --name= --slug= ``` ## Flags Human-readable name for the project, shown in the dashboard. 1 to 256 characters. Stable project slug, unique within the workspace. 3 to 255 characters matching `^[a-zA-Z0-9_-]+$`. Used in `--project` flags and in generated deployment hostnames. ### Shared flags Every `unkey api` command accepts these; [CLI output and shared flags](/docs/platform/cli/output-and-flags) describes them in full. A JSON document sent as the request body instead of building it from the flags above. It is mutually exclusive with the request-building flags, and unknown fields are rejected locally. See [Send a raw body](/docs/platform/cli/output-and-flags#send-a-raw-body). Root key for the request. Falls back to `UNKEY_ROOT_KEY`, then to the config file written by `unkey auth login`. See [CLI authentication](/docs/platform/cli/authentication). Base URL of the API. Falls back to `UNKEY_API_BASE_URL`. You don't normally need to set it. Path of the TOML file that `unkey auth login` writes. Falls back to `UNKEY_CONFIG`. Output format. Falls back to `UNKEY_OUTPUT`. Set `json` to print the full response envelope (`meta` and `data`) for piping; any other value prints the request ID followed by `data`. ## Required permissions Your root key needs `project.*.create_project`. Without a matching permission the API answers 403 and the CLI prints `Permission denied:` followed by the detail. See [Root key permissions](/docs/platform/root-keys/permissions) for the full catalog. ## Examples Create a project: ```bash theme={"system"} unkey api projects create-project --name='Payments Service' --slug=payments-service ``` Send the request body as JSON: ```bash theme={"system"} unkey api projects create-project --body='{"name":"Payments Service","slug":"payments-service"}' --output=json ``` ## API endpoint The command calls [`POST /v2/projects.createProject`](/docs/compute/api-reference/projects/create-project) and prints its response. The request fields carry the same names as the flags in camelCase, which is the shape `--body` expects. # unkey api projects delete-project Source: https://www.unkey.com/docs/compute/cli/projects/delete-project Delete a project and everything 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 project and all of its apps, environments, deployments, and custom domains. Deletion runs in the background: a successful response means it has started, not that every resource is gone yet. A project with delete protection enabled is refused until you disable it with `update-project`. ## Usage ```bash theme={"system"} unkey api projects delete-project --project= ``` ## Flags Project ID or slug. Both forms resolve to the same project. ### Shared flags Every `unkey api` command accepts these; [CLI output and shared flags](/docs/platform/cli/output-and-flags) describes them in full. A JSON document sent as the request body instead of building it from the flags above. It is mutually exclusive with the request-building flags, and unknown fields are rejected locally. See [Send a raw body](/docs/platform/cli/output-and-flags#send-a-raw-body). Root key for the request. Falls back to `UNKEY_ROOT_KEY`, then to the config file written by `unkey auth login`. See [CLI authentication](/docs/platform/cli/authentication). Base URL of the API. Falls back to `UNKEY_API_BASE_URL`. You don't normally need to set it. Path of the TOML file that `unkey auth login` writes. Falls back to `UNKEY_CONFIG`. Output format. Falls back to `UNKEY_OUTPUT`. Set `json` to print the full response envelope (`meta` and `data`) for piping; any other value prints the request ID followed by `data`. ## Required permissions Your root key needs one of: * `project.*.delete_project` (any project) * `project..delete_project` (a specific project) Without a matching permission the API answers 403 and the CLI prints `Permission denied:` followed by the detail. See [Root key permissions](/docs/platform/root-keys/permissions) for the full catalog. ## Examples Delete a project: ```bash theme={"system"} unkey api projects delete-project --project=proj_1234abcd ``` Send the request body as JSON: ```bash theme={"system"} unkey api projects delete-project --body='{"project":"payments"}' ``` ## API endpoint The command calls [`POST /v2/projects.deleteProject`](/docs/compute/api-reference/projects/delete-project) and prints its response. The request fields carry the same names as the flags in camelCase, which is the shape `--body` expects. # unkey api projects get-project Source: https://www.unkey.com/docs/compute/cli/projects/get-project Look up one project by 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. Retrieve one project by its ID or slug. Use it to confirm a project exists or to look up its metadata before creating apps in it. ## Usage ```bash theme={"system"} unkey api projects get-project --project= ``` ## Flags Project ID or slug. Both forms resolve to the same project. ### Shared flags Every `unkey api` command accepts these; [CLI output and shared flags](/docs/platform/cli/output-and-flags) describes them in full. A JSON document sent as the request body instead of building it from the flags above. It is mutually exclusive with the request-building flags, and unknown fields are rejected locally. See [Send a raw body](/docs/platform/cli/output-and-flags#send-a-raw-body). Root key for the request. Falls back to `UNKEY_ROOT_KEY`, then to the config file written by `unkey auth login`. See [CLI authentication](/docs/platform/cli/authentication). Base URL of the API. Falls back to `UNKEY_API_BASE_URL`. You don't normally need to set it. Path of the TOML file that `unkey auth login` writes. Falls back to `UNKEY_CONFIG`. Output format. Falls back to `UNKEY_OUTPUT`. Set `json` to print the full response envelope (`meta` and `data`) for piping; any other value prints the request ID followed by `data`. ## Required permissions Your root key needs one of: * `project.*.read_project` (any project) * `project..read_project` (a specific project) If the project doesn't exist or your key doesn't have the permission, you get the same 404: `The requested project does not exist.` See [Root key permissions](/docs/platform/root-keys/permissions) for the full catalog. ## Examples Get a project by ID: ```bash theme={"system"} unkey api projects get-project --project=proj_1234abcd ``` Get a project by slug and extract one field: ```bash theme={"system"} unkey api projects get-project --project=payments --output=json | jq '.data.slug' ``` ## API endpoint The command calls [`POST /v2/projects.getProject`](/docs/compute/api-reference/projects/get-project) and prints its response. The request fields carry the same names as the flags in camelCase, which is the shape `--body` expects. # unkey api projects list-projects Source: https://www.unkey.com/docs/compute/cli/projects/list-projects List the projects 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 the projects in your workspace. Results are ordered by project ID and paginated. When `pagination.hasMore` is true, pass `pagination.cursor` back with `--cursor`. ## Usage ```bash theme={"system"} unkey api projects list-projects [--limit=] [--cursor=] [--search=] ``` ## Flags Pagination cursor from a previous response. Maximum number of projects per page, between 1 and 100. Free-form text, at most 256 characters. Returns projects whose ID, name, or slug contains it, matched case-insensitively. ### Shared flags Every `unkey api` command accepts these; [CLI output and shared flags](/docs/platform/cli/output-and-flags) describes them in full. A JSON document sent as the request body instead of building it from the flags above. It is mutually exclusive with the request-building flags, and unknown fields are rejected locally. See [Send a raw body](/docs/platform/cli/output-and-flags#send-a-raw-body). Root key for the request. Falls back to `UNKEY_ROOT_KEY`, then to the config file written by `unkey auth login`. See [CLI authentication](/docs/platform/cli/authentication). Base URL of the API. Falls back to `UNKEY_API_BASE_URL`. You don't normally need to set it. Path of the TOML file that `unkey auth login` writes. Falls back to `UNKEY_CONFIG`. Output format. Falls back to `UNKEY_OUTPUT`. Set `json` to print the full response envelope (`meta` and `data`) for piping; any other value prints the request ID followed by `data`. ## Required permissions Your root key needs `project.*.read_project`. Without a matching permission the API answers 403 and the CLI prints `Permission denied:` followed by the detail. See [Root key permissions](/docs/platform/root-keys/permissions) for the full catalog. ## Examples List every project: ```bash theme={"system"} unkey api projects list-projects ``` Filter and page: ```bash theme={"system"} unkey api projects list-projects --limit=25 --search=billing ``` Send the request body as JSON: ```bash theme={"system"} unkey api projects list-projects --body='{"limit":25}' --output=json ``` ## API endpoint The command calls [`POST /v2/projects.listProjects`](/docs/compute/api-reference/projects/list-projects) and prints its response. The request fields carry the same names as the flags in camelCase, which is the shape `--body` expects. # unkey api projects update-project Source: https://www.unkey.com/docs/compute/cli/projects/update-project Rename a project or change its slug or delete protection. 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 project's name, slug, or delete protection. Flags you omit leave the stored value unchanged. Changing the slug changes the hostnames of the project's deployments, which breaks anything that uses the old ones. Delete protection blocks `delete-project` until you turn it off again; see [Delete protection](/docs/platform/security/delete-protection). ## Usage ```bash theme={"system"} unkey api projects update-project --project= [--name=] [--slug=] [--delete-protection[=false]] ``` ## Flags Enable (`--delete-protection`) or disable (`--delete-protection=false`) delete protection. Omit the flag to leave the current setting unchanged. New human-readable name, 1 to 256 characters. Project ID or slug. Both forms resolve to the same project. New project slug. Must stay unique in the workspace, and must be 3 to 255 characters matching `^[a-zA-Z0-9_-]+$`. ### Shared flags Every `unkey api` command accepts these; [CLI output and shared flags](/docs/platform/cli/output-and-flags) describes them in full. A JSON document sent as the request body instead of building it from the flags above. It is mutually exclusive with the request-building flags, and unknown fields are rejected locally. See [Send a raw body](/docs/platform/cli/output-and-flags#send-a-raw-body). Root key for the request. Falls back to `UNKEY_ROOT_KEY`, then to the config file written by `unkey auth login`. See [CLI authentication](/docs/platform/cli/authentication). Base URL of the API. Falls back to `UNKEY_API_BASE_URL`. You don't normally need to set it. Path of the TOML file that `unkey auth login` writes. Falls back to `UNKEY_CONFIG`. Output format. Falls back to `UNKEY_OUTPUT`. Set `json` to print the full response envelope (`meta` and `data`) for piping; any other value prints the request ID followed by `data`. ## Required permissions Your root key needs one of: * `project.*.update_project` (any project) * `project..update_project` (a specific project) Without a matching permission the API answers 403 and the CLI prints `Permission denied:` followed by the detail. See [Root key permissions](/docs/platform/root-keys/permissions) for the full catalog. ## Examples Rename a project: ```bash theme={"system"} unkey api projects update-project --project=proj_1234abcd --name='Payments API' ``` Protect a project from deletion: ```bash theme={"system"} unkey api projects update-project --project=payments --delete-protection ``` Send the request body as JSON: ```bash theme={"system"} unkey api projects update-project --body='{"project":"payments","deleteProtection":false}' ``` ## API endpoint The command calls [`POST /v2/projects.updateProject`](/docs/compute/api-reference/projects/update-project) and prints its response. The request fields carry the same names as the flags in camelCase, which is the shape `--body` expects. # Deployments on Unkey Source: https://www.unkey.com/docs/compute/concepts/deployments Unkey builds a deployment from GitHub, an image, or the CLI. Unkey creates a deployment from a GitHub push, a container image, the dashboard, the CLI, or `deployments.createDeployment`. That deployment keeps the image, runtime settings, environment variables, and gateway policies it started with. With auto deploy on and no watch paths, the default branch becomes a production deployment and any other branch becomes a preview. The deployment can enforce API keys and rate limits from API management through its gateway policies. ## How a deployment starts The source you pass decides what gets built. For a GitHub push, auto deploy must be on. If you set watch paths, a changed file must match one of them, or Unkey records the push as `skipped`. | How you ship | What Unkey creates | | - | - | | Push to the connected default branch | A production deployment | | Push to any other branch | A preview deployment | | Pull request from a fork | A preview deployment that waits in `awaiting_approval` until a project member approves it | | Prebuilt image (`oci`) | A deployment of the tag or digest you set, or `latest` if you set neither. Later moves of that tag don't change it | | Dashboard, CLI, or API, with no source | A git app builds its default branch. An image app deploys its default image | A deployment is one built version of an app in one environment. You can't change a deployment once it's created. It keeps the image, runtime settings, environment variables, and gateway policies it started with, so a change to any of them applies from the next deployment. ## Create a deployment A push to a connected GitHub repository creates a deployment for you. With auto deploy on and no watch paths, Unkey deploys from GitHub on every push. The default branch becomes a production deployment, and any other branch becomes a preview deployment. Watch it on the deployment page. The status moves through `pending`, `building`, and `deploying` to `ready`, and the page lists the domains. See [Deploy from GitHub](/docs/compute/get-started/deploy-from-github) and [GitHub integration](/docs/compute/build/github). To deploy by hand, open the app's **Deployments** page and click **Create deployment**, or click **Redeploy** on an existing deployment. From the CLI or API, use [`unkey api deployments create-deployment`](/docs/compute/cli/deployments/create-deployment) or `deployments.createDeployment` with the project, app, and environment. With no source, a git app builds its default branch and an image app deploys its default image. To deploy something else, pass one source: * `git`: build from the connected repository. Set `branch` for the latest commit on a branch, `commitSha` for an exact commit, or `repository` with a `commitSha` to build a fork. * `oci`: run a prebuilt image as it is. Give it a tag or a digest, or you get `latest`. The deployment keeps the image the tag pointed to when you deployed, so moving the tag later doesn't change it. * `deployment`: run an existing deployment again. A git app rebuilds the same commit, and an image app reuses the same image. This is what **Redeploy** does. The dashboard shows a badge for how each deployment was started: GitHub, API, CLI, or dashboard. Deployments page listing deployments with status, environment, and branch, with one row's menu open showing Rollback, Promote, Redeploy, Cancel deployment, and links to requests, logs, and build logs ## Statuses A deployment moves through these statuses. Six of them are final: `ready`, `failed`, `skipped`, `stopped`, `superseded`, and `cancelled`. The other seven mean it's still in progress. ```mermaid theme={"system"} stateDiagram-v2 [*] --> pending : push, API, CLI, or dashboard [*] --> awaiting_approval : pull request from a fork [*] --> skipped : auto deploy off, or no watch path matched awaiting_approval --> pending : approved by a project member awaiting_approval --> superseded : newer commit on the same branch pending --> superseded : newer commit on the same branch pending --> starting : its turn in the queue state "In flight" as inflight { starting --> building building --> deploying : image ready deploying --> network : instances running network --> finalizing : domains assigned } finalizing --> ready inflight --> failed pending --> cancelled : cancel awaiting_approval --> cancelled : cancel inflight --> cancelled : cancel ready --> stopped : stop, idle preview, or spend cap stopped --> deploying : start a stopped preview deployment ``` | Status | What's happening | | - | - | | `pending` | Waiting for its turn. See [Why a deployment is waiting](#why-a-deployment-is-waiting). | | `awaiting_approval` | A pull request from a fork, held until a project member approves it. | | `starting` | Its turn has come and it's getting started. | | `building` | Building the image from your repository. A deployment from a prebuilt image has nothing to build, so it passes through quickly. | | `deploying` | Starting instances in each of the environment's regions and waiting for them to become healthy. | | `network` | Assigning the deployment's domains. The dashboard labels this **Assigning Domains**. | | `finalizing` | Finishing up and, for production, making it the live deployment. | A production deployment that stops being the live one also becomes `stopped`, after a 30-minute standby. See [Production and preview](/docs/compute/concepts/production-and-preview). The deployment's page shows each step with its start and end time as it runs. A deployment from a prebuilt image has no build log. See [Build logs](/docs/compute/observe/build-logs). ## Why a deployment is waiting A workspace runs one deployment at a time on every plan, from build through rollout. The next one sits in `pending` until the one ahead finishes. Deployments from a prebuilt image queue too, even though they don't build. Production deployments jump ahead of preview deployments, so a hot-fix doesn't wait behind a backlog of pull request builds. A deployment that has waited an hour for its turn fails rather than waiting longer. Deploy again once the queue has drained. See [Builds](/docs/compute/build/overview#why-your-build-is-waiting). ## Why a deployment was superseded or skipped If you push a second commit to a branch while the first commit's deployment is still `pending` or `awaiting_approval`, the first one becomes `superseded` and the newer commit builds instead. Once a deployment has left `pending`, it finishes even if newer commits arrive. `skipped` means we saw the push but didn't build it. Either auto deploy is off for the environment, or no changed file matched the environment's [watch paths](/docs/compute/configure/build-settings). The deployments list shows the reason. ## Awaiting approval A pull request from a fork runs outside code with your environment variables, so it never builds on its own. It waits in `awaiting_approval` until a project member approves it in the dashboard. GitHub shows a check named **Unkey Deploy Authorization** on the commit while it waits. Pushes from people with write access don't need approval. See [GitHub integration](/docs/compute/build/github). ## Cancel a deployment To cancel a deployment that's still in progress, open its menu in the deployments list and click **Cancel deployment**. The current step is marked "Cancelled by user", the deployment ends as `cancelled`, and the next deployment in the queue can start. A finished deployment can't be canceled. ## Rollouts across regions During `deploying`, we wait for each region to reach its minimum number of instances. The deployment goes live once all regions but one are healthy, so one region that fails to start doesn't block the release. A single-region environment waits for that one region. If enough regions aren't healthy within 15 minutes, the deployment fails. See [Regions](/docs/compute/concepts/regions). ## Why a deployment failed The deployment's page shows the step that failed and a message. In the API, a `failed` deployment has an `error` object with a `code`, the `step` that failed, and a `message`. The codes are: | Code | Meaning | | - | - | | `no_schedulable_regions` | The environment has no region that can currently be scheduled. Configure at least one region. | | `invalid_runtime_settings` | The port is outside 1 to 65535, the CPU is below 0.25 vCPU, or the memory is below 256 MiB. | | `cpu_quota_exceeded` | Starting this deployment would push the workspace past its CPU limit across all running deployments. | | `memory_quota_exceeded` | The same for memory. | | `storage_quota_exceeded` | The same for ephemeral disk. | | `build_failed` | The image build did not complete. The build log names the failing step. | | `unknown` | We couldn't classify the failure. Read `message`. | For the quota codes, we add what your running deployments already use to this deployment's CPU, memory, and disk at its maximum instance count in every region, and compare that to your workspace [limits](/docs/platform/billing/limits). So a wide autoscaling range counts at its maximum. Scale down or remove a deployment to make room. ## What you can do with a deployment You can promote and roll back production deployments, and stop and start preview deployments. See [Production and preview](/docs/compute/concepts/production-and-preview). The dashboard also offers **Redeploy** for any deployment that's `ready`, `stopped`, `failed`, `superseded`, or `cancelled`, and **Cancel deployment** for anything in progress. In the API, a deployment's `availableActions` field lists the actions that will work right now, so you can check before you call. ## When the deployment isn't serving What callers get depends on why: * **The deployment is stopped:** `503` with [`deployment_offline`](/docs/errors/frontline/capacity/deployment_offline). * **It should be running, but no instance is up yet:** `503` with [`no_running_instances`](/docs/errors/frontline/capacity/no_running_instances). * **The workspace hit its Compute spend budget with the stop option on:** every deployment returns `402` with [`spend_limit_reached`](/docs/errors/frontline/capacity/spend_limit_reached) until you raise or remove the budget. # Instances and autoscaling Source: https://www.unkey.com/docs/compute/concepts/instances-and-autoscaling What an instance is, how many you run, and when Unkey adds more. An instance is one running container of a deployment in one region. A deployment with two regions and two replicas each has four instances, all running the same image with the same variables. Instances are ephemeral: nothing written inside one survives its replacement. ## How many replicas you get Each environment sets, per region, a minimum and a maximum replica count. A new app created with the API or CLI starts with one replica in its default region. In the dashboard, the **Instances** field on the environment's **Compute** card sets the bounds as **Min** and **Max**, and both start at 1. With minimum and maximum equal, the count is pinned and autoscaling is off. Autoscaling starts only when you raise the maximum above the minimum. The minimum is at least 1, so you can't scale to zero. The maximum is capped by your plan's replicas-per-region limit: 4 on Starter, 8 on Pro, and 16 on Business. Every region uses the same minimum and maximum. One replica gives no availability guarantee within a region. Two replicas let the deployment keep serving while one instance is replaced, and Unkey keeps at most one instance of a deployment unavailable at a time during planned disruption. For a regional outage you need a second region. See [Regions](/docs/compute/concepts/regions). ## How autoscaling decides When the maximum exceeds the minimum, Unkey watches average CPU across the deployment's instances in each region. It adds replicas when average CPU exceeds 80 percent of the requested CPU and removes them when load drops, waiting 60 seconds after a drop before scaling in so short spikes don't cause flapping. ## CPU, memory, and disk [Runtime settings](/docs/compute/configure/runtime-settings) give each instance a CPU allocation in vCPUs (minimum 0.25, in steps of 0.25), a memory allocation in MiB (minimum 256, in steps of 256), and an optional ephemeral disk in MiB (steps of 512). The upper bounds are your workspace's per-instance limits on [Limits](/docs/platform/billing/limits), and the API returns `400` if you exceed them. The workspace-wide totals are checked when a deployment starts, as described under [Deployments](/docs/compute/concepts/deployments). The allocation you set is the hard limit for the container. Half of it is guaranteed, and an instance can burst to its full allocation when there's spare capacity. The container's own writable filesystem is capped at 128 MiB. That's scratch space for logs and small files, not storage. When you configure ephemeral disk, it's mounted at `/data` and `UNKEY_EPHEMERAL_DISK_PATH=/data` is set so your code can find it. The disk starts empty with each instance and is deleted when the instance stops. ## What the container sees Unkey injects `PORT` with the configured port (default 8080) and a set of `UNKEY_*` variables naming the deployment, environment slug, region, instance, and the git commit, branch, repository, and commit message when the deployment came from git. Your environment variables are added alongside, and the injected variables win if the names collide. The container receives the configured shutdown signal (`SIGTERM` by default) when an instance is being replaced or scaled in. ## Health probes A [health check](/docs/compute/configure/health-checks) is optional. When you configure one, Unkey probes the container over HTTP at the path you give with `GET` or `POST`, every `intervalSeconds` (default 10, at most 3600), with a `timeoutSeconds` (default 5), after an `initialDelaySeconds` (default 0). The same probe drives two decisions: an instance that hasn't passed yet receives no traffic, and an instance that fails `failureThreshold` consecutive probes (default 3) is restarted. A `GET` probe counts any status from 200 to 399 as a pass. Without a health check, an instance is treated as ready as soon as its container is running, and it's only restarted if the process exits. A probe that checks downstream dependencies can therefore take your whole deployment out when a dependency blips. Probe the process, not the database. ## How traffic reaches instances The gateway in each region sends a request to a random running instance of the deployment in that region. If it can't connect to one, it tries another. An error after the request was sent is returned as-is rather than retried, so your handlers never run twice for one request. If no instance in the region is running, the gateway forwards the request to the nearest region that has one, and if none does, it answers `503` with [`no_running_instances`](/docs/errors/frontline/capacity/no_running_instances). # Production and preview Source: https://www.unkey.com/docs/compute/concepts/production-and-preview What you can do with a deployment in production and in preview. Every app has a production environment and a preview environment. Production has one live deployment that you promote and roll back. Preview has many short-lived deployments that you can stop and start, and we stop them for you when they go quiet. ## Where a push lands For a git-connected app, a push to the default branch creates a production deployment. A push to any other branch, or a pull request from a fork, creates a preview deployment. When you deploy from the API, CLI, or dashboard, you pick the environment. ## Production ### The live deployment An app has one live production deployment. The environment domain, the shorter live domain, and any verified custom domains all point at it. When a new production deployment reaches `ready`, it goes live and those domains move to it, unless the app is rolled back (see [Rollback](#rollback)). The deployment it replaced keeps running for 30 minutes as a standby, then stops. ### Promote **Promote** in the dashboard, or `deployments.promoteDeployment` in the API, makes a `ready` production deployment the live one. Use it to ship a deployment that was built while the app was rolled back, or to move traffic to an earlier deployment on purpose. The deployment it replaces stops after 30 minutes. You can promote a deployment when it's `ready` and not stopped, and the app already has a live deployment. Promoting the deployment that's already live only works while the app is rolled back. That's a **confirm rollback**: nothing moves, but the rolled-back state ends and new pushes go live again. ### Rollback **Rollback** in the dashboard, or `deployments.rollbackDeployment` in the API, moves traffic back to the deployment you pick. The domains move in one step and the app is marked as rolled back. While an app is rolled back, new production deployments still build and reach `ready`, but they don't go live. So a push can't undo your rollback. To end the rolled-back state, promote the deployment you rolled back to (confirm rollback) or promote a newer one. You can roll back to a deployment that's `ready`, not stopped, and not the live one. A stopped deployment, including an old live one after its 30 minute standby, can't be a rollback target. To get back to it, click **Redeploy** on it, wait for `ready`, and promote it if the app is rolled back. Or roll forward with a fixed commit. You can't stop or start production deployments by hand, because stopping the live one would take your app offline. ## Preview ### Stop and start **Stop deployment** in the dashboard, or `deployments.stopDeployment` in the API, stops a `ready` preview deployment and frees its resources. It shows `stopped` once no instance is left. **Wake deployment** in the dashboard, or `deployments.startDeployment` in the API, starts a stopped preview deployment again. It goes through `deploying` and back to `ready`. You can't start one while the workspace is suspended by its Compute spend budget. ### Automatic stops We stop deployments for you in these cases. A stopped preview deployment can be started again at any time. | Trigger | What stops | When | | - | - | - | | New live production deployment, or a promote | The previous live deployment | 30 minutes later | | New preview deployment on a branch | The deployment that was on that branch domain | 1 minute later | | Preview deployment gets no requests | That deployment | After 1 hour idle | | You stop it | A `ready` preview deployment | Right away | ## What callers see during a switch Promote and rollback don't restart anything. The domains move to the other deployment, and the old one keeps serving until they have. The switch reaches every region within a few seconds, so a few requests can still reach the old deployment right after. A request to a stopped deployment gets `503` with [`deployment_offline`](/docs/errors/frontline/capacity/deployment_offline). # Projects, apps, and environments Source: https://www.unkey.com/docs/compute/concepts/projects-apps-environments How projects, apps, and environments fit together. Your workspace holds projects. A project holds apps. Every app has exactly two environments: production and preview. A deployment belongs to one environment. Almost everything you configure (environment variables, gateway policies, the container's CPU) is set per environment. ## Projects A project is how you group apps. It has a name and a slug. The slug is what you pass to the API and the CLI, so it must be unique in your workspace. The slug `default` is reserved, so the API will return an error if you use it. A project can also carry delete protection, which blocks deletion until you turn it off. See [Delete protection](/docs/platform/security/delete-protection). Creating a project creates nothing else. There's no default app. In the dashboard, creating a project opens the new-app flow. The API returns the project alone. Unlike platforms where a project is itself what you deploy, here the project is only a folder. In the dashboard, the project level holds things that span apps: runtime **Logs** and gateway **Requests** for every app in the project, plus **Project settings** (name and delete). ## Apps An app is what you deploy. Two apps in one project build and deploy on their own. A monorepo with an API and a background worker is two apps on the same repository with different root directories. The project page lists apps. **Create app** adds another. An app has a name, a slug unique within its project, and a source. The source is either a GitHub repository or a container image. The source kind is fixed when you create the app. For an image-sourced app, the dashboard's **Build** settings page shows only the image, because there's nothing to build. For a git-connected app the tracked branch is `defaultBranch`, which defaults to the repository's default branch on GitHub. Pushes to that branch deploy to production. Every other branch deploys to preview. You can retarget the branch, replace the repository, or disconnect. ## Environments Creating an app also creates its two environments: `production` with kind `production` and `preview` with kind `preview`. You can't add a third environment or delete either of these. The environment slug is what you pass to the API alongside the project and app slugs. In the dashboard, **App settings** > **Environments** lists both environments. Each one opens a page with its compute (regions, size, instances, and storage), its domains, and, for a git-connected app, its tracked branch and auto-deploy switch. The kind decides how deployments behave: * `production` deployments serve the live domains, can be promoted and rolled back, and can't be stopped. * `preview` deployments can be stopped and started and are stopped automatically when idle. ## Identifiers Every Compute endpoint accepts either the ID or the slug for a project, app, or environment. A request looks like `project: payments, app: api, environment: production`, or uses the `proj_`, `app_`, and `env_` IDs the list endpoints return. The dashboard URL carries the IDs. # Regions Source: https://www.unkey.com/docs/compute/concepts/regions Which regions you can run in, how to choose them, and what happens when one goes down. A region is a place Unkey runs your instances. Each environment of an app has its own region list, and every deployment in that environment runs in all of them. ## Available regions Unkey runs on AWS and uses AWS region names. Compute runs in four: | Region | Location | | - | - | | `us-west-2` | Oregon | | `us-east-1` | Northern Virginia | | `eu-central-1` | Frankfurt | | `ap-southeast-1` | Singapore | We add regions over time. A region can also be closed to new deployments for a while, and the dashboard grays it out. Picking one anyway fails with `Region 'us-east-1' is not available for scheduling.`, and a name that is not a region at all fails with `Region 'eu-west-1' does not exist.` ## Set the regions for an environment Production and preview have separate lists, so you can run production in three regions and preview in one. Configure compute panel for the production environment with Frankfurt and N. Virginia selected under Regions, plus Size, Instances, and Storage In the dashboard, open the app, go to **App settings** > **Environments**, and open the environment. Click **Configure** on the **Compute** card and pick regions under **Regions**. Unschedulable regions are visible but not selectable, and you can't remove the last region. From the CLI or the API: ```bash CLI theme={"system"} unkey api environments update-settings \ --project=payments --app=payments-api --environment=production \ --regions='[{"name":"us-east-1","replicas":{"min":1,"max":2}},{"name":"eu-central-1","replicas":{"min":1,"max":2}}]' ``` ```bash API theme={"system"} curl -X POST https://api.unkey.com/v2/environments.updateSettings \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "project": "payments", "app": "payments-api", "environment": "production", "regions": [ {"name": "us-east-1", "replicas": {"min": 1, "max": 2}}, {"name": "eu-central-1", "replicas": {"min": 1, "max": 2}} ] }' ``` The list you send replaces the whole set. A region you leave out is dropped, so send every region you want, not just the new one. The rules the API enforces: * Between 1 and 5 regions. An empty list is rejected, because an environment cannot have zero regions. * No duplicates: `Region 'us-east-1' is listed more than once.` * Every region needs the same `min` and `max`. Mixed bounds fail with `All regions must specify the same replica bounds; per-region autoscaling is not supported yet.` * `min` is at least 1 and `max` is at most your plan's replicas-per-region ceiling, which is on [Limits](/docs/platform/billing/limits). Outside that you get `Region 'us-east-1' replicas must be between 1 and 4.` ## How failover works Requests reach whichever Unkey gateway is nearest to the caller. That gateway then decides where to send the request: 1. If its own region has running instances of the deployment, it serves from there. This is the normal path and costs no extra hop. 2. If not, it forwards to the nearest region that does have one, following a fixed proximity order. 3. If none of the regions in that order has an instance, it forwards to any region that does. 4. If no region has one, the request fails with `503` and [`no_running_instances`](/docs/errors/frontline/capacity/no_running_instances). Only regions your environment is deployed to are candidates. Our gateway picks among the regions that have a running instance of that deployment, so a deployment that runs in one region never serves from another, and traffic never reaches a region you did not choose. When your environment does span several of them, step 2 resolves in this order: | Gateway region | Prefers, in order | | - | - | | `us-west-2` | `us-east-1`, `ap-southeast-1`, `eu-central-1` | | `us-east-1` | `us-west-2`, `eu-central-1`, `ap-southeast-1` | | `eu-central-1` | `us-east-1`, `us-west-2`, `ap-southeast-1` | | `ap-southeast-1` | `us-west-2`, `us-east-1`, `eu-central-1` | That ordering matters for data residency. `eu-central-1` is the only European region today, so an environment that must keep traffic in the EU has to run there alone, and it has no regional redundancy. If Frankfurt goes down the deployment returns `503` rather than serving from elsewhere. Adding a second region for availability means accepting that requests can be served outside the EU. A second region is what protects you from losing a region. Extra replicas in one region protect you from losing an instance, but if that region goes away, so does your app. Failover is per request and automatic. There is nothing to configure and no order you can set: adding a region to the environment is what makes it a target. ### Rollouts tolerate one bad region When a deployment starts, Unkey waits for each region to reach the environment's minimum replica count, and releases once all but one region is healthy. A single-region environment waits for that region. A three-region environment goes live once two are up, and the third joins when it recovers. If enough regions are not healthy within 15 minutes, the deployment fails. See [Deployments](/docs/compute/concepts/deployments). ## Choosing regions Put a region near your users and a region near the data your app talks to. A request served far from your database pays that round trip on every call, which usually outweighs the latency you saved by being close to the user. Two regions with a couple of replicas each is the minimum we advise for production. Preview environments rarely need more than one region, since they exist to be looked at rather than to survive an outage. # Build settings Source: https://www.unkey.com/docs/compute/configure/build-settings Every build setting an environment has, with defaults and validation rules. Build settings control how a git-connected app is built, and which pushes build at all. Each environment has its own, so production and preview can differ. Apps deployed from an image have no build settings. ## Change build settings Edit them under **App settings** > **Build** in the dashboard, with `environments.updateSettings` in the API, or with `unkey api environments update-settings` in the CLI. In the API, send only the fields you want to change. To read the current values, call `environments.getEnvironment` and look under `build`. The **Build** page shows the production values, and saving there writes to every environment of the app. To give production and preview different build settings, use the API or CLI. **Auto-deploy** is the exception: each environment has its own switch, on its page under **App settings** > **Environments**. App settings Build page showing the repository, root directory, Dockerfile, build command, and watch paths A change applies from the next deployment. Existing deployments keep the settings they were built with. ## Fields Path to the Dockerfile, relative to the root directory, 1 to 500 characters. When it's unset (the default), we detect your language and build without a Dockerfile. Send `null` to go back to automatic builds. In the dashboard, the **Dockerfile** row lists Dockerfiles in your repository and shows "File not found on this branch" if the path is missing. See [Dockerfile builds](/docs/compute/build/dockerfile). The directory we build from, 1 to 500 characters. Automatic detection looks here, and a Dockerfile build uses it as the build context. Use `.` or a relative path like `services/api` with letters, digits, `.`, `_`, and `-`. A path that starts with `/` or `./`, or contains `..`, a backslash, or spaces, returns "Root directory must be a relative path like 'api' or 'services/api'." The dashboard suggests app directories it finds in your repository. Replaces the build command automatic detection would run, 1 to 1000 characters, for example `pnpm --filter api build`. Ignored when a Dockerfile is set, and the dashboard marks the field "Not used with a Dockerfile." Send `null` to go back to the detected command. Glob patterns that decide whether a push builds. Up to 10 patterns, each up to 500 characters. When the list is empty, every push builds. Otherwise a push builds only if a changed file matches a pattern. `src/**` matches everything under `src`, and `**/*.go` matches every Go file. A pattern without a wildcard matches only that exact path, so `src` isn't the same as `src/**`. Don't start a pattern with `/` or `./`. An invalid pattern returns `400` when you save it. See [GitHub integration](/docs/compute/build/github). Whether GitHub pushes create deployments in this environment. In the dashboard, it's the **Auto-deploy** switch under **Branch tracking** on the environment's page. When it's off, a push shows up as a `skipped` deployment with "Auto deploy is disabled for this environment." A common setup is auto deploy on for preview and off for production: every branch gets a preview, and you release to production on purpose from the dashboard, API, or CLI. ## Example ```bash Set a Dockerfile and scope the build to a subdirectory theme={"system"} curl -X POST https://api.unkey.com/v2/environments.updateSettings \ -H "Authorization: Bearer $UNKEY_ROOT_KEY" \ -H "Content-Type: application/json" \ -d '{ "project": "payments", "app": "api", "environment": "production", "rootDirectory": "services/api", "dockerfile": "Dockerfile", "watchPaths": ["services/api/**", "packages/shared/**"] }' ``` ## Next steps How builds run, why they wait or fail, and caching. Port, CPU, memory, disk, command, protocol, regions. # Environment variables Source: https://www.unkey.com/docs/compute/configure/environment-variables Give your app configuration and secrets that are encrypted and scoped per environment. Environment variables give your app configuration and secrets, both while it builds and while it runs. Each variable belongs to one environment, so production and preview can have different values for `DATABASE_URL`. Every value is encrypted at rest. ## Add variables in the dashboard 1. Open the app and go to **Environment variables**. 2. Click **Add environment variable**. A panel opens with a table of keys and values. 3. Enter one or more key-value pairs. To import a `.env` file, drop it on the table, click **Import .env**, or paste its contents into a key cell. 4. Turn on **Sensitive** in a row for secrets. 5. Pick the target **Environment**, or keep **All environments** to set the same value everywhere. 6. Save, then redeploy. See [When a change takes effect](#when-a-change-takes-effect). Add environment variables panel with a LOG_LEVEL row and a sensitive STRIPE_SECRET_KEY row, the Add variable and Import .env actions, the Environment select set to All environments, and Cancel and Save buttons The `.env` import skips comments, strips surrounding quotes, and handles multi-line values such as PEM keys. If a key appears twice in the file, the last one wins. You can save up to 200 variables at once. If a key already exists in the target environment, nothing is saved, the row shows "A variable with this key already exists.", and the panel stays open. If you close the panel with unsaved rows, the dashboard asks before it discards them. The saved list has one row for each variable in each environment. You can filter it by environment, search it by key, and sort it by name or newest. Click a row to edit its key, value, note, or sensitive setting. ### While you set up a new app In the new-app flow, you can also change the value of a saved variable or delete it. Sensitive rows and rows marked "Varies by environment" can only be deleted. Changes apply to every selected environment. Environment variables panel in the new-app flow with saved DATABASE_URL and LOG_LEVEL values, a masked sensitive STRIPE_SECRET_KEY, an empty row for a new variable, and the Environment select set to All environments ## Recoverable or write-only Every variable is either `recoverable` or `writeonly`. Both are encrypted the same way. The difference is whether you can ever see the value again: * **`recoverable`**: shown in the dashboard and returned by `environments.listEnvironmentVariables`. Use it for configuration you want to check later. * **`writeonly`**: never shown or returned. You can only replace or delete it. Use it for API keys and other credentials. The dashboard's **Sensitive** switch makes a variable `writeonly`, with the note "Hidden in the dashboard and the API after you save. Unkey encrypts every value." In the API, `writeonly` is the default. ## Rename, mark sensitive, or delete These actions are only in the dashboard: * **Make Sensitive** turns selected `recoverable` variables into `writeonly`. This can't be undone. * **Rename** changes the key in the edit form of a row. It changes the key in that row's environment only, and fails if the new name is already used there. It works for sensitive variables too. * **Delete** removes a variable from the row's environment. Select several rows to delete them together. ## When a change takes effect A deployment keeps the variables it was created with, for its build and its instances. Editing a variable doesn't change anything that's already running. To apply a change, push or redeploy. The dashboard shows a redeploy banner after an edit. ## Set variables with the API `environments.setEnvironmentVariables` creates or overwrites the variables you send, in one atomic call: if any variable is invalid, nothing changes. Variables you don't send stay as they are. An existing key is fully overwritten. If you leave out `description` or `kind`, they reset to empty and `writeonly`. ```bash Change one variable and leave the rest theme={"system"} curl -X POST https://api.unkey.com/v2/environments.setEnvironmentVariables \ -H "Authorization: Bearer $UNKEY_ROOT_KEY" \ -H "Content-Type: application/json" \ -d '{ "project": "payments", "app": "api", "environment": "production", "variables": [ { "key": "LOG_LEVEL", "value": "info", "kind": "recoverable", "description": "Log verbosity" } ] }' ``` To replace the whole set, add `prune: true`. Every variable not in your list is deleted. `prune: true` with an empty list deletes them all. `environments.removeEnvironmentVariables` deletes the keys you name, and ignores keys that don't exist. `environments.listEnvironmentVariables` returns each variable's `key`, `kind`, `createdAt`, and `description`, plus `value` for `recoverable` ones only. ## Limits | Rule | Limit | | - | - | | Key | 1 to 256 characters matching `^[A-Za-z_][A-Za-z0-9_]*$`: letters, digits, and underscores, not starting with a digit. | | Value | 1 to 16384 bytes of UTF-8. Multibyte characters count as more than one byte. Newlines inside the value are kept. The dashboard trims surrounding whitespace and rejects carriage returns. | | Description | Up to 255 characters, optional. | | Variables per API call | Up to 50 in one `setEnvironmentVariables` or `removeEnvironmentVariables` call. | | Duplicate keys in one call | Rejected with `400`. | There's no limit on how many variables an environment can have. ## Variables Unkey injects At build time, your variables are available as described in [Build-time secrets](/docs/compute/build/build-secrets). At run time, they're set as environment variables in each instance, along with these: | Variable | Value | | - | - | | `PORT` | The environment's configured port. Your app must listen on it. | | `UNKEY_DEPLOYMENT_ID` | The deployment's ID. | | `UNKEY_ENVIRONMENT_SLUG` | `production` or `preview`. | | `UNKEY_REGION` | The region the instance runs in. | | `UNKEY_GIT_COMMIT_SHA` | The commit the deployment was built from. Empty for image deployments. | | `UNKEY_GIT_BRANCH` | The branch of that commit. Empty for image deployments. | | `UNKEY_GIT_REPO` | The repository as `owner/repo`. Empty for image deployments. | | `UNKEY_GIT_COMMIT_MESSAGE` | The commit message. Empty for image deployments. | | `UNKEY_INSTANCE_ID` | The instance's own ID. | | `UNKEY_EPHEMERAL_DISK_PATH` | `/data`, only when the environment has ephemeral disk. | If one of your variables has the same name as one of these, ours wins. `KUBERNETES_SERVICE_HOST` and the other `KUBERNETES_*` variables some libraries look for are set to empty strings. ## Next steps Use variables during the build without baking them into layers. The port and disk settings behind `PORT` and `UNKEY_EPHEMERAL_DISK_PATH`. # Health checks Source: https://www.unkey.com/docs/compute/configure/health-checks Tell Unkey how to probe your app and what happens when the probe fails. A health check tells us how to tell whether your app is ready and still working. We send an HTTP request to each instance on a schedule. It's optional, and you set it per environment. ## What the health check does * **An instance gets no traffic until it passes.** So a deployment can wait for your app to finish starting before the first request arrives. * **An instance that fails `failureThreshold` checks in a row is restarted.** A `GET` check passes on any status from 200 to 399. Anything else, including a timeout, is a failure. Without a health check, an instance is ready as soon as its container is running, and it's only restarted if the process exits. That's fine for apps that are ready as soon as they open the port. Add a check if startup takes a while, or if your process can be running but unable to serve. ## Choose what to check Check the process, not its dependencies. A failing check restarts the container, so a check that queries your database takes every instance down when the database has a bad minute. `GET /healthz` returning `200` is enough. ## Add a health check App settings Runtime page with the Process group (port and start command) and the Health check group (endpoint and interval) In the dashboard, go to **App settings** > **Runtime** and use the **Health check** group: pick `GET` or `POST`, enter the path under **Endpoint**, and set the **Interval**. Saving there applies the check to every environment of the app. In the API, set `healthcheck` in [runtime settings](/docs/compute/configure/runtime-settings) with `environments.updateSettings`: ```bash Add a check with a shorter timeout theme={"system"} curl -X POST https://api.unkey.com/v2/environments.updateSettings \ -H "Authorization: Bearer $UNKEY_ROOT_KEY" \ -H "Content-Type: application/json" \ -d '{ "project": "payments", "app": "api", "environment": "production", "healthcheck": { "method": "GET", "path": "/healthz", "intervalSeconds": 10, "timeoutSeconds": 3, "failureThreshold": 3, "initialDelaySeconds": 5 } }' ``` Like every runtime setting, it applies from the next deployment. To remove the check, clear the **Endpoint** path in the dashboard, or send `"healthcheck": null`. ## Fields Only `method` and `path` are required. The others use the defaults below. The HTTP method. `POST` sends an empty body using `wget` inside your container, so your image must include `wget`. The path to request on your app's port, 1 to 512 characters, starting with `/` and matching `^(/[\w\-]+)+(\.[\w]+)?$`, for example `/healthz`. Seconds between checks, 1 to 3600. The dashboard's **Interval** field takes whole seconds and starts at 10. Seconds to wait for a response before the check fails, 1 to 3600. Only settable in the API. Saving from the dashboard keeps the current value, or sets 5 for a new check. Failures in a row before the instance is restarted, 1 to 100. Only settable in the API. Saving from the dashboard keeps the current value, or sets 3 for a new check. Seconds after the container starts before the first check, 0 to 3600. Only settable in the API. Saving from the dashboard keeps the current value, or sets 0 for a new check. ## Next steps How readiness, restarts, and traffic distribution fit together. How a deployment waits for healthy instances across regions. # Compute limits Source: https://www.unkey.com/docs/compute/configure/limits The resource limits on your apps, per plan, and what happens when you hit one. Your Compute plan sets how much CPU, memory, and disk your apps can use. Here's each limit, when we check it, and what you see when you hit it. The full workspace table, including API Management, is on [Limits](/docs/platform/billing/limits). **Settings > Limits** shows these limits with your current usage once you have a Compute plan. A limit you've reached shows the banner "You've reached a limit". To read the same limits and usage from code, call `workspace.getLimits` with a root key that has `unkey:v1:{workspace_id}:limits#read`. Limits settings page showing API management, logs, and Compute limits with current usage bars ## Limits per plan | Limit | Starter | Pro | Business | Checked | | - | - | - | - | - | | CPU per instance | 2 vCPU | 8 vCPU | 16 vCPU | When you save runtime settings | | Memory per instance | 2 GiB | 8 GiB | 32 GiB | When you save runtime settings | | Ephemeral disk per instance | 10 GiB | 10 GiB | 10 GiB | When you save runtime settings | | Workspace CPU | 30 vCPU | 120 vCPU | 240 vCPU | When a deployment starts | | Workspace memory | 60 GiB | 240 GiB | 480 GiB | When a deployment starts | | Workspace ephemeral disk | 120 GiB | 480 GiB | 960 GiB | When a deployment starts | | Replicas per region | 4 | 8 | 16 | When you save regions | | Concurrent builds | 1 | 1 | 1 | While deployments are in flight | | Custom domains per workspace | 1 | Unlimited | Unlimited | When you add a domain | | Log query range | 3 days | 7 days | 14 days | On every analytics API query | Without a Compute plan you can't deploy. See [Compute plans](/docs/compute/get-started/plans). "Unlimited" custom domains has a very high cap to stop abuse. Need a higher limit on your plan? Ask [support@unkey.com](mailto:support@unkey.com). ## Saving a size over your plan's limit CPU, memory, and disk in [runtime settings](/docs/compute/configure/runtime-settings) are checked when you save them. Go over and you get `400` with a message like "Memory per instance cannot exceed 2048 MiB. Contact [support@unkey.com](mailto:support@unkey.com) to increase it." The dashboard only offers sizes your plan allows, so you'll only see this error from the API or CLI. ## A deployment fails with a quota error Workspace CPU, memory, and disk are checked when a deployment starts `deploying`, because they depend on what's already running. We add up what all your running instances use, plus this deployment's CPU, memory, and disk at its **maximum** replicas in each region. If the total is over a limit, the deployment fails with `cpu_quota_exceeded`, `memory_quota_exceeded`, or `storage_quota_exceeded` and the message "We are unable to deploy this application as you have exceeded your CPU quota" (or Memory, or Storage). Because the check uses maximum replicas, an environment with a wide autoscaling range counts at its maximum even while it runs at the minimum. To make room, lower an environment's maximum replicas or stop preview deployments. Replaced production deployments also stop on their own after 30 minutes, and idle previews after an hour. ## Replicas per region Each region's `max` replicas can't go above your plan's limit, and `min` is at least 1. Go over and you get `400` with, on Starter, "Region 'x' replicas must be between 1 and 4." On every plan, all regions in an environment must use the same bounds, and an environment can have at most 5 regions. ## Deployments wait their turn Every plan runs one deployment at a time per workspace. Others wait in `pending` for up to an hour, production ahead of preview, and fail if their turn doesn't come. See [Builds](/docs/compute/build/overview). ## Custom domain limit reached Adding a domain over your limit fails with `err:unkey:limits:custom_domain_limit_exceeded`. The dashboard shows "Custom domain limit reached" with links to your limits and to upgrade. Removing a domain frees up room. ## Log query range The log query range limits how far back an analytics API query can reach. It's separate from how long we keep the data, and you can read whichever is shorter: * Request log rows are stored for 7 days, so a Business workspace reads at most 7 days of them even though its query range allows 14. * Runtime logs are stored for about 90 days, so a Starter workspace reads at most 3 days of them even though the rows exist for far longer. A query that reaches further back than your plan allows fails with [`err:user:bad_request:query_range_exceeds_retention`](/docs/errors/user/bad_request/query_range_exceeds_retention) instead of returning partial results. This only applies to the [analytics API](/docs/compute/observe/analytics-api), not the dashboard's log views. See [Observability](/docs/compute/observe/overview) for how long data is kept. ## Limits that are the same on every plan | What | Limit | | - | - | | Regions per environment | 5 | | Watch paths | 10 patterns of up to 500 characters | | Build command | 1000 characters | | Container command | 10 arguments of up to 4096 characters | | Environment variables per request | 50; key up to 256 characters, value up to 16384 bytes | | Gateway policies per environment | 50, with up to 10 match expressions each | ## What has no limit There's no limit on the number of projects, apps, or deployments in a workspace, or on the number of environment variables in an environment. Every app has exactly two environments. Compute is billed by what runs, so the limits are on CPU, memory, and disk, not on how many things you create. ## Next steps The full workspace table, including API Management and log rows. Cap monthly Compute spend instead of resources. # Runtime settings Source: https://www.unkey.com/docs/compute/configure/runtime-settings Every runtime setting an environment has, with defaults, steps, and plan caps. Runtime settings control how big your container is, how it starts and stops, and where it runs. Each environment of an app has its own. ## Change runtime settings Edit them under **App settings** in the dashboard, with `environments.updateSettings` in the API, or with `unkey api environments update-settings` in the CLI. In the API, send only the fields you want to change. `environments.getEnvironment` returns the current values under `runtime` and `regions`. In the dashboard, the settings are in three places: * **Runtime** holds the port, start command, and health check. * **Advanced** holds the OpenAPI spec path and upstream protocol. * **Environments** opens a page for each environment. Click **Configure** on its **Compute** card to set regions, size (CPU and memory), instances, and storage. **Runtime** and **Advanced** show the production values, and saving there writes to every environment of the app. Compute is saved for one environment only. To give production and preview a different port, command, or protocol, use the API or CLI. App settings Runtime page with the Process group (port and start command) and the Health check group (endpoint and interval) A change applies from the next deployment, and the dashboard shows "Changes apply to the next deployment" on a group with unsaved edits. Running deployments keep what they started with, so to apply a change without new code, redeploy the current deployment. Your plan caps CPU, memory, and disk per instance. Going over returns `400` with a message like "CPU per instance cannot exceed 2 vCPU. Contact [support@unkey.com](mailto:support@unkey.com) to increase it." See [Compute limits](/docs/compute/configure/limits). ## Container The port your container listens on, 1 to 65535. We set it as the `PORT` environment variable, overriding any value in the image, and send traffic and health checks to it. A port outside the range fails the deployment with `invalid_runtime_settings`. CPU per instance in vCPUs, at least 0.25, in steps of 0.25, up to your plan's limit. An instance can use up to this much and is always guaranteed at least half. In the dashboard, set it with **Size** on the environment's **Compute** card. The presets are XS (1/4 vCPU, 256 MiB), S (1/2 vCPU, 1 GiB), M (1 vCPU, 2 GiB), L (2 vCPU, 4 GiB), XL (4 vCPU, 8 GiB), 2XL (8 vCPU, 16 GiB), and 4XL (16 vCPU, 32 GiB). A preset above your plan shows a lock and the plan that unlocks it. **Custom** sets CPU and memory on their own, in the same steps, up to what your plan allows. Memory per instance in MiB, at least 256, in steps of 256, up to your plan's limit. It's a hard limit: an instance that goes over is killed and restarted, and the dashboard shows `OOMKilled` with exit code 137. In the dashboard, it's part of **Size** on the environment's **Compute** card. Temporary disk per instance in MiB, in steps of 512, up to your plan's limit. `0` means none. When set, each instance gets its own disk at `/data`, and `UNKEY_EPHEMERAL_DISK_PATH` is set to `/data`. Nothing on it survives a restart or a new deployment. Without it, the container can write at most 128 MiB. The dashboard's **Storage** field offers None, 512 MiB, and 1, 2, 5, 10, 20, and 50 GiB, up to what your plan allows. A value set through the API that isn't a preset shows as an extra option. Replaces the image's command, as an array of up to 10 strings of up to 4096 characters each. An empty array runs the image's own command. The dashboard splits a single line on spaces and ignores quotes, so use the API when an argument contains spaces. The signal your container's main process gets when an instance is replaced or scaled in. Set it with the API or CLI. It isn't in the dashboard. How the gateway talks to your container. `http1` is HTTP/1.1 and works with every framework. `h2c` is HTTP/2 without TLS. Choose it only if your server supports h2c. The path where your app serves its OpenAPI document, for example `/openapi.yaml`. 1 to 512 characters, starting with `/` and matching `^(/[\w\-]+)+(\.[\w]+)?$`. When a deployment is `ready`, we fetch the document with a `GET` and save it with that deployment. Documents up to 10 MiB are accepted, and a `404` means the deployment has no spec. The spec is used by the gateway's OpenAPI validation policy and the dashboard's spec diff. Send `null`, or leave the dashboard field empty, to turn this off. An HTTP check we run against each instance. See [Health checks](/docs/compute/configure/health-checks). Send `null` to remove it. ## Regions and replicas The regions the environment runs in, 1 to 5 entries, each like `{ "name": "us-east-1", "replicas": { "min": 1, "max": 3 } }`. The list replaces the whole set, so regions you leave out are removed. In the dashboard, the **Regions** and **Instances** fields on the environment's **Compute** card edit this field. When you change **Instances** or add a region, the dashboard writes the same `min` and `max` to every region. * The region must be available, or you get "Region 'x' is not available for scheduling." * Each region can appear once. * `min` is at least 1. `max` is at least `min` and at most your plan's replicas-per-region limit. * Every region must use the same bounds, or you get "All regions must specify the same replica bounds; per-region autoscaling is not supported yet." A new app created with the API starts in one schedulable region, with `min` and `max` both 1, so autoscaling is off until you raise `max`. In the dashboard, a new app starts in every region available to the workspace. Pick your regions before you go live. See [Regions](/docs/compute/concepts/regions). ## Raise resources and add a region ```bash Raise resources and add a second region theme={"system"} curl -X POST https://api.unkey.com/v2/environments.updateSettings \ -H "Authorization: Bearer $UNKEY_ROOT_KEY" \ -H "Content-Type: application/json" \ -d '{ "project": "payments", "app": "api", "environment": "production", "vCpus": 1, "memoryMib": 1024, "regions": [ { "name": "us-east-1", "replicas": { "min": 2, "max": 4 } }, { "name": "eu-central-1", "replicas": { "min": 2, "max": 4 } } ] }' ``` The **Regions** field on the environment's **Compute** card lists the regions you can use. The change applies from the next deployment. ## Next steps Probe fields, defaults, and what a failing probe does. What the allocation and replica bounds mean at runtime. The per-instance and workspace ceilings behind the `400`s. How regions are discovered, chosen, and used for failover. # Spend budget Source: https://www.unkey.com/docs/compute/configure/spend-budget Cap what Compute can cost you in a month and choose whether it stops at the cap. A spend budget caps what Compute usage can cost you in a month. You get emails as you approach it, and you can choose to stop everything when you reach it. Set it under **Settings > Billing**. Only workspace admins can change it. See [Plans](/docs/platform/billing/plans) for how Compute billing works. ## Set a budget Enter a whole dollar amount up to \$10,000,000, or leave the field empty for no budget. The budget covers usage for the current calendar month across all five Compute meters: CPU, memory, disk, egress, and active keys. It counts usage before any included credit is subtracted, and doesn't include the plan fee. With a budget set, we email workspace admins the first time spend crosses 50, 75, and 100 percent of it, once per threshold per month. If you raise the budget, the thresholds move with it. Alerts already sent aren't sent again. Billing settings page showing the Compute and API management plans with Change and Upgrade buttons, and the Compute cost control budget with its email alerts ## Stop when you hit the budget **Stop workloads at the budget** is off by default, and you need a budget before you can turn it on. When it's on and spend reaches 100 percent, we suspend Compute for the rest of the month: * Every running deployment is stopped. We remember which one was live for each app so we can bring it back. * Every request to the workspace's domains gets `402` with `spend_limit_reached`. * You can't start, wake, or create deployments. * The dashboard shows the workspace as paused by its spend cap. This isn't cancellation. Your plan, settings, environment variables, domains, and deployment history stay. Nothing is billed while nothing runs. The 402 response body callers receive while a workspace is suspended, and how to tell it apart from a stopped deployment. ## Resume compute Do one of these, and each app's live deployment starts again. You don't need to redeploy. * **Remove the budget or turn off Stop workloads at the budget.** Compute resumes at the next check. * **Raise the budget.** Compute resumes once spend has been under the new budget for two checks in a row. * **Wait for the next month.** Compute resumes on its own when the new billing month starts. If someone promoted a different deployment while Compute was suspended, we don't wake the old one. ## Budget vs limits The budget is about money. [Compute limits](/docs/compute/configure/limits) are about resources: they stop a deployment from using more CPU, memory, or disk than your plan allows, before it runs. The budget acts on what you've actually used this month. Use the budget to control cost, and limits to control size. ## Next steps What the plan includes and what is metered above it. What your workspace consumed this month. # API key authentication policy Source: https://www.unkey.com/docs/compute/gateway/api-key-auth Have the gateway verify Unkey API keys before requests reach your app. This policy checks keys from an API Management [keyspace](/docs/api-management/keyspaces/overview). Create the keyspace and its keys there first. The API Management docs explain [permission queries](/docs/api-management/authorization/permission-queries), per-key rate limits, credits, and identities. Have the [gateway](/docs/compute/gateway/overview) verify an Unkey API key before a request reaches your app, so your code doesn't have to. If the key is good, your app gets the caller's details in the [principal header](/docs/compute/gateway/principal). If not, the caller gets a `401`, `403`, or `429` and your app never sees the request. A key behaves the same as it does with `keys.verifyKey`. To add one, open the app, go to **Policies**, click **Add policy**, and set **Type** to **Key Auth**. Or use the API or CLI as described in [Gateway policies](/docs/compute/gateway/policies). ## Settings 1 to 5 keyspace IDs. A key from any other keyspace is rejected as invalid. Each keyspace must be in the same workspace as the environment, or saving fails with [`err:unkey:data:key_space_not_found`](/docs/errors/unkey/data/key_space_not_found). Where to look for the key, tried in order until one has a value. Each entry sets exactly one of: * `bearer: {}`: the `Authorization: Bearer ` header. * `header: { name, stripPrefix? }`: a custom header. If you set `stripPrefix` and the value doesn't start with it, the header is ignored. * `queryParam: { name }`: a query parameter. When you leave it out, only `bearer` is checked. Permissions the key must have, up to 1000 characters, for example `documents.read AND documents.write`. It uses the same syntax as `keys.verifyKey`. A query that can't be parsed rejects every request with `422`. Up to 10 per-key rate limits, the same as the `ratelimits` parameter of `keys.verifyKey`. Each has a `name` (a limit on the key or its identity, or a new name), an optional `limit` (requests) and `duration` (milliseconds) that go together, and a `cost` (default 1). If you name a limit the key doesn't have and don't give a `limit` and `duration`, the request gets a `500`. Credits each matching request takes from the key. `0` checks the key without spending credits, which is useful for read-only routes. Keys with unlimited credits aren't affected. ```json Example: header key with a prefix, read permission, no credit spend theme={"system"} { "name": "Key auth for the public API", "enabled": true, "match": [{ "path": { "path": { "prefix": "/v1/" } } }], "keyauth": { "keyspaces": ["ks_1234abcd"], "locations": [ { "header": { "name": "X-API-Key" } }, { "bearer": {} } ], "permissionQuery": "api.read", "credits": 0 } } ``` In the dashboard this is the **Key Auth** type in **Policies > Add policy**. Programmatically, include the object above in the `policies` array of `POST /v2/gateway.setPolicies`, or send it as `keyauth` in `POST /v2/gateway.updatePolicy` to replace an existing policy's rule. The change applies to the next deployment. ## What callers get back Checks run in this order and stop at the first failure, so the order decides which error the caller gets: 1. No key found in any location: `401` [`missing_credentials`](/docs/errors/frontline/client/missing_credentials). 2. The key doesn't exist, is disabled, has expired, or its workspace is disabled: `401` [`invalid_key`](/docs/errors/frontline/client/invalid_key). The caller can't tell which. 3. The key is from a keyspace not in `keyspaces`: `401` `invalid_key`. 4. The key lacks the permissions: `403` [`insufficient_permissions`](/docs/errors/frontline/client/insufficient_permissions). 5. A rate limit is used up: `429` [`rate_limited`](/docs/errors/frontline/client/rate_limited). 6. The key has no credits left: `429` [`usage_exceeded`](/docs/errors/frontline/client/usage_exceeded). On success, the request reaches your app with `X-Unkey-Principal` set. If several API key policies match a request, only the first one that succeeds counts, and the rest are skipped. Every check, passed or failed, shows up in API Management analytics next to your own `keys.verifyKey` calls. ## Rate limit headers When a rate limit applies to the key, the response includes `X-RateLimit-Limit`, `X-RateLimit-Remaining`, and `X-RateLimit-Reset` (Unix seconds), whether the request passed or not. If several limits apply, the headers show the strictest one. A `429` also has `Retry-After` in whole seconds, at least 1. A [rate limit policy](/docs/compute/gateway/rate-limiting) uses the same headers and only replaces them if its result is stricter. ## Keys stay out of your logs When a [logging policy](/docs/compute/gateway/logging) saves headers or query data, we redact the `Authorization` header and every header or query parameter listed in any API key policy's `locations`, even turned-off ones. Keys never reach the request log. ## Next steps What your app receives after a key is verified. Every verification outcome and the error it becomes. # Gateway errors Source: https://www.unkey.com/docs/compute/gateway/errors What the gateway returns when it rejects or cannot serve a request. When a policy rejects a request, or the [gateway](/docs/compute/gateway/overview) can't reach your app, the gateway responds with an error code and a request ID. Use this page to find out what a code means and who needs to fix it. Most codes look like `err:frontline:{category}:{specific}`, and each one has its own page. ## Response format If the request's `Accept` header includes `application/json`, `application/*`, or `*/*` without `text/html`, the body is JSON: ```json theme={"system"} { "meta": { "requestId": "req_2gJbXhAr4" }, "error": { "code": "err:frontline:client:rate_limited", "message": "Rate limit exceeded. Please try again later." } } ``` Otherwise it returns an HTML page with the status, message, code, request ID, and a link to the code's page. So browsers see a page and API clients see JSON. This format is different from the one the Unkey API uses. `meta.requestId` matches the `X-Unkey-Request-Id` response header. Quote it when you contact the API owner or Unkey support. ## The codes Codes are grouped by who needs to act. ### Client: fix the request or the key | Code | Status | Cause | | - | - | - | | [`err:frontline:client:missing_credentials`](/docs/errors/frontline/client/missing_credentials) | 401 | An [API key authentication policy](/docs/compute/gateway/api-key-auth) found no key in any configured location. | | [`err:frontline:client:invalid_key`](/docs/errors/frontline/client/invalid_key) | 401 | The key does not exist, is disabled or expired, belongs to a keyspace the policy does not list, or its workspace is disabled. | | [`err:frontline:client:insufficient_permissions`](/docs/errors/frontline/client/insufficient_permissions) | 403 | The key failed the policy's `permissionQuery`. | | [`err:frontline:client:firewall_denied`](/docs/errors/frontline/client/firewall_denied) | 403 | A [firewall policy](/docs/compute/gateway/firewall) matched the request. | | [`err:frontline:client:rate_limited`](/docs/errors/frontline/client/rate_limited) | 429 | A per-key limit or a [rate limit policy](/docs/compute/gateway/rate-limiting) was exceeded, or the rate limit policy couldn't identify the caller. | | [`err:frontline:client:usage_exceeded`](/docs/errors/frontline/client/usage_exceeded) | 429 | The key has no credits left. | | [`err:frontline:client:openapi_validation_failed`](/docs/errors/frontline/client/openapi_validation_failed) | 400 | The request didn't match the deployment's OpenAPI spec under an [OpenAPI validation policy](/docs/compute/gateway/openapi-validation). | ### Configuration and capacity: you own the API | Code | Status | Cause | | - | - | - | | [`err:frontline:config:invalid_configuration`](/docs/errors/frontline/config/invalid_configuration) | 422 | A policy on the deployment is invalid: a `permissionQuery` can't be parsed, a `credits` value is negative, or the OpenAPI spec is invalid. Fix it and redeploy. | | [`err:frontline:capacity:no_running_instances`](/docs/errors/frontline/capacity/no_running_instances) | 503 | No instance in any region is running to take the request. | | [`err:frontline:capacity:deployment_offline`](/docs/errors/frontline/capacity/deployment_offline) | 503 | The deployment was stopped, or its project was cancelled. | | [`err:frontline:capacity:spend_limit_reached`](/docs/errors/frontline/capacity/spend_limit_reached) | 402 | Compute for the workspace is paused because it reached its [spend budget](/docs/compute/configure/spend-budget). Raise or remove the budget to resume. | | [`err:frontline:routing:config_not_found`](/docs/errors/frontline/routing/config_not_found) | 404 | No deployment is routed for this hostname. | | [`err:frontline:routing:deployment_not_found`](/docs/errors/frontline/routing/deployment_not_found) | 404 | The deployment requested by ID doesn't exist. | ### Upstream: your app or its connection | Code | Status | Cause | | - | - | - | | [`err:frontline:upstream:bad_gateway`](/docs/errors/frontline/upstream/bad_gateway) | 502 | The connection to your app failed, for example it was reset. | | [`err:frontline:upstream:proxy_forward_failed`](/docs/errors/frontline/upstream/proxy_forward_failed) | 502 | Forwarding to your app failed for another reason. | | [`err:frontline:upstream:service_unavailable`](/docs/errors/frontline/upstream/service_unavailable) | 503 | The gateway couldn't connect to your app: it refused the connection or couldn't be reached. | | [`err:frontline:upstream:gateway_timeout`](/docs/errors/frontline/upstream/gateway_timeout) | 504 | Your app took too long to accept the connection or to respond. | ### Platform: Unkey acts | Code | Status | Cause | | - | - | - | | [`err:frontline:platform:config_load_failed`](/docs/errors/frontline/platform/config_load_failed) | 500 | The gateway couldn't load the routing for this request. | | [`err:frontline:platform:deployment_selection_failed`](/docs/errors/frontline/platform/deployment_selection_failed) | 500 | The gateway couldn't pick an instance. | | [`err:frontline:platform:internal_server_error`](/docs/errors/frontline/platform/internal_server_error) | 500 | Something unexpected failed, including rate limiting or key checks. | ### Codes that are not `err:frontline:*` A few `err:user:*` codes can also come from the gateway. Their pages describe how the Unkey API uses them, which can differ. For a Compute app, the status and cause are the ones below. | Code | Status | Cause | | - | - | - | | [`err:user:bad_request:client_closed_request`](/docs/errors/user/bad_request/client_closed_request) | 499 | The client disconnected before the response finished. | | [`err:user:bad_request:request_timeout`](/docs/errors/user/bad_request/request_timeout) | 504 | The request took longer than the gateway's 15 minute limit. (The linked page's `408` and shorter limit apply to the Unkey API, not to Compute.) | | [`err:user:bad_request:request_body_unreadable`](/docs/errors/user/bad_request/request_body_unreadable) | 400 | The gateway couldn't read the request body. | | [`err:user:bad_request:request_body_too_large`](/docs/errors/user/bad_request/request_body_too_large) | 413 | You shouldn't see this in front of a Compute app. The gateway doesn't limit request body size. | Requests that fail before reaching your app don't appear in your request log. ## If you already use keys.verifyKey An [API key authentication policy](/docs/compute/gateway/api-key-auth) checks keys like `keys.verifyKey`. If you already handle that endpoint's `code` values, here's what each one becomes at the gateway: | Verification `code` | Gateway response | | - | - | | `VALID` | Forwarded, with `X-Unkey-Principal` set. | | `NOT_FOUND` | 401 `err:frontline:client:invalid_key` | | `DISABLED` | 401 `err:frontline:client:invalid_key` | | `EXPIRED` | 401 `err:frontline:client:invalid_key` | | `FORBIDDEN` | 401 `err:frontline:client:invalid_key` | | `INSUFFICIENT_PERMISSIONS` | 403 `err:frontline:client:insufficient_permissions` | | `RATE_LIMITED` | 429 `err:frontline:client:rate_limited` | | `USAGE_EXCEEDED` | 429 `err:frontline:client:usage_exceeded` | A key from a keyspace the policy doesn't list returns `invalid_key`, and a request with no key returns `missing_credentials`. The verification checks, their order, and the `code` values are documented with `keys.verifyKey` in API Management. ## Next steps All three error systems and both envelope shapes. Evaluation order determines which error a request gets first. # Firewall policy Source: https://www.unkey.com/docs/compute/gateway/firewall Block matching requests at the gateway with a 403 before they reach your app. A firewall policy tells the [gateway](/docs/compute/gateway/overview) to block every request that matches its match expressions. Use it to close internal paths, block a method on a route, refuse requests with a certain header, or block IP ranges, without changing your app. To add one, open the app, go to **Policies**, click **Add policy**, and set **Type** to **Firewall**. You only need a name, an environment, and match conditions. ## Settings Always `ACTION_DENY`. A request the firewall doesn't match moves on to the next policy. ```json Example: block writes to an admin prefix from outside theme={"system"} { "name": "Block admin writes", "enabled": true, "match": [ { "path": { "path": { "prefix": "/admin/", "ignoreCase": true } } }, { "method": { "methods": ["POST", "PUT", "PATCH", "DELETE"] } }, { "header": { "name": "X-Internal-Call", "present": true } } ], "firewall": { "action": "ACTION_DENY" } } ``` A request must match all the expressions, so the example only blocks requests that match all three. To block two unrelated kinds of request, create two firewall policies. ## Block or allow IP ranges A `remoteIp` match with `in` blocks the listed ranges. With `notIn` it blocks everyone outside them, which turns the policy into an allowlist. ```json Example: allow /admin only from the office theme={"system"} { "name": "Admin only from the office", "enabled": true, "match": [ { "path": { "path": { "prefix": "/admin" } } }, { "remoteIp": { "notIn": ["198.51.100.0/24"] } } ], "firewall": { "action": "ACTION_DENY" } } ``` The gateway uses the IP of the connection, never a header like `X-Forwarded-For`, so a client can't claim a different address. It accepts IPv4 connections today, so IPv6 entries match no request yet. A firewall policy with no match expressions blocks every request to the environment. That's a quick way to take a preview environment offline. In the dashboard this is the **Firewall** type in **Policies > Add policy**. The form only asks for a name, an environment, and match conditions because `ACTION_DENY` is the only action. Programmatically, include the object above in the `policies` array of `POST /v2/gateway.setPolicies`. The change applies to the next deployment. ## What the client sees A blocked request gets `403 Forbidden` with the code [`err:frontline:client:firewall_denied`](/docs/errors/frontline/client/firewall_denied) and the message `Forbidden`. The body is JSON or an HTML page depending on the caller's `Accept` header. See [Gateway errors](/docs/compute/gateway/errors). The caller isn't told which policy matched. No later policies run, and the request doesn't appear in your request log. ## Next steps Every match expression type and how they combine. Slow callers down instead of blocking them. # Local development with the gateway Source: https://www.unkey.com/docs/compute/gateway/local-development Run Frontline locally with Docker Compose to verify real API keys and inspect forwarded request headers. Frontline's `local-dev` mode runs the gateway on your machine with routes and policies from a TOML file. This example puts it in front of `traefik/whoami`, an HTTP echo service that returns request headers. No application code is needed. A valid API key reaches the echo service with an [`X-Unkey-Principal` header](/docs/compute/gateway/principal). Frontline rejects missing or invalid keys before they reach the service. Use this example for local development only. The echo response includes the API key's `Authorization` header and principal metadata. Use development keys and keep `.env` out of version control. ## Prerequisites You need Docker Compose, a keyspace, a root key with permission to verify keys in that keyspace, and an API key from that keyspace. ## Configure the containers Create these three files in the same directory. Replace the root key and keyspace ID in `.env` with your development values. ```yaml compose.yaml theme={"system"} services: frontline: image: ghcr.io/unkeyed/frontline:v1.0.26-rc.1 command: ["--config", "/etc/unkey/gateway.toml"] ports: - "127.0.0.1:3000:3000" environment: UNKEY_ROOT_KEY: ${UNKEY_ROOT_KEY} UNKEY_KEYSPACE_ID: ${UNKEY_KEYSPACE_ID} volumes: - ./gateway.toml:/etc/unkey/gateway.toml:ro depends_on: - api api: image: traefik/whoami:v1.11.0 ``` ```toml gateway.toml theme={"system"} [local-dev] http_port = 3000 root_key = "${UNKEY_ROOT_KEY}" [[local-dev.routes]] hostname = "localhost" upstream = "api:80" [[local-dev.routes.policies]] id = "authenticate" enabled = true keyauth = { key_space_ids = ["${UNKEY_KEYSPACE_ID}"] } ``` ```plaintext .env theme={"system"} UNKEY_ROOT_KEY=replace-with-a-root-key UNKEY_KEYSPACE_ID=replace-with-a-keyspace-id ``` Only Frontline is exposed to your host, on loopback port 3000. The echo service is internal to the Compose network. Frontline uses `https://api.unkey.com` to verify keys by default. The authentication policy deducts one credit from keys with limited usage for each successful request. Keys with unlimited usage are unaffected. ## Start the gateway Run these commands from the directory containing the three files: ```bash theme={"system"} docker compose up -d docker compose logs frontline ``` ## Inspect the forwarded headers Set your API key in the terminal, then send a request through Frontline: ```bash theme={"system"} export UNKEY_API_KEY='replace-with-an-api-key-from-your-keyspace' curl http://localhost:3000/ \ -H "Authorization: Bearer ${UNKEY_API_KEY}" ``` The response includes `X-Unkey-Principal`, `X-Forwarded-Host`, and `X-Forwarded-Proto: http`. Header capitalization can differ. The principal's subject is the key's identity external ID, or its key ID if it has no identity. Send the same request without a key to check rejection: ```bash theme={"system"} curl -i http://localhost:3000/ ``` Expect HTTP 401 instead of an echo response. To use your own application, replace the `api` service and set `upstream` to its Compose service name and listening port. Keep its port internal so requests cannot bypass Frontline. ## Stop the example Remove both containers and their Compose network: ```bash theme={"system"} docker compose down ``` ## Next steps Read every field and when it is omitted. Add authentication, rate limits, and other rules to your app. # Logging policy Source: https://www.unkey.com/docs/compute/gateway/logging Add headers, query data, and bodies to the request log for the requests you choose. The [gateway](/docs/compute/gateway/overview) logs the method, host, path, status, and latency of every request that reaches your app. A logging policy also saves headers, query data, or bodies for the requests it matches. Use it to debug a route. It never rejects a request or changes what your app receives. To add one, open the app, go to **Policies**, click **Add policy**, and set **Type** to **Logging**. ## Settings Each setting turns on one kind of data. In the dashboard all five start on, so check the switches before you save. In the API they all default to `false`, so a policy with none set saves nothing extra. Save request headers, plus the user agent and client IP. Save response headers. Save the request body, up to 1 MiB. Save the response body, up to 1 MiB. Save the query string and parameters. It's separate from headers because URLs often carry secrets like `?api_key=...`. ```json Example: capture everything on one debugging route theme={"system"} { "name": "Debug the webhook receiver", "enabled": true, "match": [{ "path": { "path": { "exact": "/webhooks/stripe" } } }], "logging": { "requestHeaders": true, "responseHeaders": true, "requestBody": true, "responseBody": true, "query": true } } ``` In the dashboard this is the **Logging** type in **Policies > Add policy**, with one switch per flag, all on by default. Programmatically, include the object above in the `policies` array of `POST /v2/gateway.setPolicies`. Omitted flags are `false`. The change applies to the next deployment. ## What gets saved * **Several matching policies combine.** A request matched by a `requestHeaders` policy and a `requestBody` policy gets both saved. A policy with no match expressions applies to every request. * **Bodies are cut off at 1 MiB in each direction** in the log. The request and response themselves are never cut. Streaming requests are covered too. * **Rejected requests aren't logged**, whatever the logging policy says. ## What's always redacted These are replaced with `[REDACTED]` before anything is saved: * The `Authorization` header. * Every header and query parameter listed as a key location in any [API key authentication policy](/docs/compute/gateway/api-key-auth), even turned-off ones. When a query parameter is redacted, the saved query string can be in a different order or encoding than the original. * Body fields marked `x-unkey-redact: true` in your OpenAPI spec, when an [OpenAPI validation policy](/docs/compute/gateway/openapi-validation) is on. ## Where to see the data Saved data shows up in the project's **Requests** view in the dashboard and in the `gateway_requests_v1` analytics table. The header, query, and body columns are empty for requests no logging policy matched. ## Next steps Mark body fields for redaction with `x-unkey-redact`. Match expressions that pick which requests to capture. # OpenAPI validation policy Source: https://www.unkey.com/docs/compute/gateway/openapi-validation Reject requests that do not match your OpenAPI spec before they reach your app. An OpenAPI validation policy tells the [gateway](/docs/compute/gateway/overview) to check each matching request against your app's OpenAPI spec. Requests that don't match get a `400`, so your code only sees requests the spec allows. The path, query parameters, headers, and body are all checked. OpenAPI 3.0 and 3.1 are supported. ## Set it up 1. Serve your OpenAPI document from your app, for example at `/openapi.yaml`. 2. Set that path as `openapiSpecPath` in the environment's [runtime settings](/docs/compute/configure/runtime-settings), under **App settings** > **Advanced** or with `environments.updateSettings`. It must start with `/` and be at most 512 characters. 3. Add the policy: open the app, go to **Policies**, click **Add policy**, and set **Type** to **OpenAPI Validation**. There's nothing else to configure. 4. Deploy. When the deployment succeeds, we fetch the spec from it over HTTPS and save it with that deployment. The policy has no settings. In the API it's an empty object: ```json Example theme={"system"} { "name": "Validate the public API", "enabled": true, "match": [{ "path": { "path": { "prefix": "/v1/" } } }], "openapi": {} } ``` In the dashboard this is the **OpenAPI Validation** type in **Policies > Add policy**. The form has no type-specific fields. Its **Then** row shows "Uses the auto-scraped spec." and a **Configure scrape path** link to **App settings** > **Advanced**. Programmatically, include the object above in the `policies` array of `POST /v2/gateway.setPolicies`, and set `openapiSpecPath` with `POST /v2/environments.updateSettings` so the next deployment is scraped. Both changes apply to the next deployment. Each deployment checks requests against the spec it was deployed with. A spec change applies when you deploy the version that serves it. ## What callers see A request that fails the check gets `400` [`openapi_validation_failed`](/docs/errors/frontline/client/openapi_validation_failed). The message describes the first problem and, when there is one, the field that caused it. No later policies run, and the request isn't logged. An `Authorization` header with the wrong scheme isn't reported here, because the [API key authentication policy](/docs/compute/gateway/api-key-auth) gives a clearer error. A missing `Authorization` header that the spec requires still fails. ## Troubleshooting ### Requests aren't being checked If the deployment has no spec, the policy does nothing and requests pass through. A deployment has no spec when: * `openapiSpecPath` isn't set. * The path returns a `404` or an empty response. * The document is over 10 MiB. * Another policy blocked the request for the spec. We fetch it through the gateway like any other request, so keep the spec path out of your API key and firewall policies' match expressions. A missing spec never fails the deployment. Fix the cause and redeploy. ### Every request fails with 422 If the spec is invalid, for example it has a broken schema, every matching request gets `422` [`invalid_configuration`](/docs/errors/frontline/config/invalid_configuration). Fix the spec and redeploy. ## Hide fields in request logs Add `x-unkey-redact: true` to a property in your spec, and its value is hidden in request and response bodies saved by a [logging policy](/docs/compute/gateway/logging). Only that property is hidden, not others with the same name elsewhere in your schemas. Your app still receives the full request. ```yaml theme={"system"} components: schemas: CreateUser: type: object properties: email: type: string password: type: string x-unkey-redact: true ``` ## Next steps Capture bodies so redaction has something to protect. The shape of the 400 and 422 responses. # Gateway Source: https://www.unkey.com/docs/compute/gateway/overview What the gateway in front of every deployment does, and how policies control it. Every deployment on Unkey Compute sits behind the gateway, and there's no other way to reach it. So the gateway can check API keys, rate limit callers, block requests, validate them against your OpenAPI spec, and log them before they reach your app. You set this up with policies. With no policies, the gateway just passes requests through. Each deployment keeps the policies it was created with. Adding, editing, or turning off a policy only affects the **next** deployment. To apply a change, redeploy (**Redeploy** in the dashboard, or `deployments.createDeployment`). ## What policies can do A policy has a name, an on/off switch, up to ten match expressions that pick which requests it applies to, and one rule. There are five kinds of rule: | Type | What it does | On rejection | | - | - | - | | [API key authentication](/docs/compute/gateway/api-key-auth) | Checks an Unkey API key on the request and tells your app who's calling with the [principal header](/docs/compute/gateway/principal). | `401`, `403`, or `429` | | [Rate limit](/docs/compute/gateway/rate-limiting) | Counts requests per client IP, header, path, authenticated caller, or principal field, and rejects when the limit is used up. | `429` | | [Firewall](/docs/compute/gateway/firewall) | Blocks every request its match expressions select. | `403` | | [OpenAPI validation](/docs/compute/gateway/openapi-validation) | Checks the path, query, headers, and body against the OpenAPI spec your deployment serves. | `400` | | [Logging](/docs/compute/gateway/logging) | Saves headers, query data, and bodies of matched requests in the request log. | Never rejects | See [Gateway policies](/docs/compute/gateway/policies) to add them from the dashboard, API, or CLI. ## What happens to a request 1. The request arrives at one of your domains, and the gateway finds the deployment it belongs to. 2. The deployment's policies run in list order. Turned-off policies, and policies whose match expressions don't fit the request, are skipped. 3. If a policy rejects the request, the caller gets a `4xx` with an `err:frontline:...` code. The request never reaches your app and doesn't appear in your request log. See [Gateway errors](/docs/compute/gateway/errors) for every code. 4. If every policy passes, the gateway forwards the request to your app with the headers below. ## What your app receives The gateway keeps the original `Host` header and adds these: | Header | Value | | - | - | | `X-Unkey-Request-Id` | The request ID. The caller sees the same value on the response and in error bodies. | | `X-Unkey-Region` | The gateway's platform and region as `::`. | | `X-Unkey-Frontline-Id` | The ID of the gateway that handled the request. | | `X-Unkey-Principal` | Who's calling, as JSON. Only present when an API key authentication policy succeeded. | | `X-Forwarded-For` | The client IP. | | `X-Forwarded-Host` | The hostname the client requested. | | `X-Forwarded-Proto` | Always `https`. | Callers can't fake these. We remove any `X-Unkey-*` header a client sends before policies run. To get the deployment ID, read the `UNKEY_DEPLOYMENT_ID` environment variable. The response to the caller includes `X-Unkey-Request-Id`, `X-Unkey-Region`, and `X-Unkey-Frontline-Id`. Rate limit headers are added when a rate limit applied. ## Use keys from API Management The API keys the gateway checks live in API Management. To require a key, create a keyspace and keys in API Management, then add the keyspace to an [API key authentication policy](/docs/compute/gateway/api-key-auth). The same keys also work if your own code calls `keys.verifyKey`. Create the keyspace and keys an authentication policy verifies against, and manage their permissions, rate limits, and credits. ## Next steps Match expressions, evaluation order, limits, and how to set policies. Verify Unkey keys at the edge and forward the identity to your app. The JSON your app reads to know who is calling. Every code the gateway returns, with its status and cause. # Gateway policies Source: https://www.unkey.com/docs/compute/gateway/policies Add gateway policies to an environment, choose which requests they apply to, and put them in order. A policy is one rule the [gateway](/docs/compute/gateway/overview) applies to the requests you pick, such as "require an API key" or "100 requests per minute". Each environment of an app has its own list, so production and preview can have different rules. Each deployment keeps the policies it was created with. Adding, editing, turning off, or deleting a policy only affects the next deployment. Redeploy to apply a change. ## Add a policy in the dashboard 1. Open the app and click **Policies** in the sidebar. 2. Click **Add policy**, enter a **Name**, and pick a **Type**: Key Auth, Rate Limit, Firewall, OpenAPI Validation, or Logging. 3. In the **Rule** card, click **Add condition** for each match condition if the policy should only apply to some requests. With no conditions, the **If** row reads "Every request". Open the **Then** row to set the type's settings. 4. Pick the **Environment**. The default, **All environments**, adds the same policy to both production and preview. 5. Click **Add policy**, then redeploy. Add policy panel with the Name and Type fields, the Rule card with If and Then rows, and the Environment select set to All environments Click a policy's row to edit it in the same panel. The type can't change there. The **Environments** switches in that panel turn the policy on or off in production and preview. **How policies work** on the page header explains the evaluation order and what each type rejects. You can't have two policies with the same type and name in one environment. The dashboard shows a policy with the same type and name in both environments as one row, and suggests opening the existing policy to add it to the other environment instead. The dashboard steps above correspond to `POST /v2/gateway.setPolicies` with the environment's complete policy list, or `POST /v2/gateway.updatePolicy` to change one policy. To scope a policy to one environment, call the endpoint once with that environment's slug. Read the current list first with `POST /v2/gateway.listPolicies`. The ids it returns are the only valid `policyId` values for `updatePolicy`. ## Set policies with the API Each endpoint takes the `project`, `app`, and `environment` by ID or slug. * **`gateway.listPolicies`** returns the list in the order it runs, with IDs. * **`gateway.setPolicies`** replaces the whole list with the `policies` array you send, in that order. If any policy is invalid, nothing is saved. An empty array removes every policy. Every policy gets a new ID. * **`gateway.updatePolicy`** changes one policy by `policyId`. Fields you leave out keep their values, and `match: null` removes all match expressions. Sending one of `keyauth`, `ratelimit`, `firewall`, `openapi`, or `logging` replaces the rule and can change its type. ```bash theme={"system"} curl -X POST https://api.unkey.com/v2/gateway.setPolicies \ -H "Authorization: Bearer $UNKEY_ROOT_KEY" \ -H "Content-Type: application/json" \ -d '{ "project": "my-project", "app": "api", "environment": "production", "policies": [ { "name": "Require a key", "enabled": true, "keyauth": { "keyspaces": ["ks_1234abcd"] } }, { "name": "100 per minute per key", "enabled": true, "ratelimit": { "limit": 100, "windowMs": 60000, "identifiers": [{ "authenticatedSubject": {} }] } } ] }' ``` Your root key needs `environment.*.set_policies` to replace the list, `environment.*.read_policies` to list, and `environment.*.update_policy` to change one, or the same actions on `environment..…`. See [root key permissions](/docs/platform/root-keys/permissions). A keyspace in an authentication policy must be in the same workspace, or the call fails with [`err:unkey:data:key_space_not_found`](/docs/errors/unkey/data/key_space_not_found). Every change, from the dashboard, API, or CLI, is recorded in the audit log with the full policy list. ## Set policies with the CLI `unkey api gateway list-policies`, `set-policies`, and `update-policy` do the same as the three endpoints. Each takes `--project`, `--app`, and `--environment`. `set-policies` takes the full list as `--policies ''`, and `update-policy` takes `--policy-id` and `--policy ''`. ## What's in a policy Shown in the dashboard. 1 to 256 characters. A turned-off policy is kept but skipped. Up to 10 expressions. A request must match all of them for the policy to apply. Leave it out to apply the policy to every request. Set exactly one. It picks the policy type and holds its settings. See [API key authentication](/docs/compute/gateway/api-key-auth), [Rate limit](/docs/compute/gateway/rate-limiting), [Firewall](/docs/compute/gateway/firewall), [OpenAPI validation](/docs/compute/gateway/openapi-validation), and [Logging](/docs/compute/gateway/logging). Saved policies also have an `id`. It changes every time `gateway.setPolicies` replaces the list. ## Pick which requests a policy applies to Each match expression sets exactly one of `path`, `method`, `header`, `queryParam`, or `remoteIp`. A request must match every expression in the policy. To match one thing or another, create two policies. Matches the request path. Set exactly one of `exact`, `prefix`, or `regex` (1 to 1024 characters), and optionally `ignoreCase: true`. Regular expressions use RE2 syntax, and one that doesn't compile is rejected when you save. One or more of `GET`, `POST`, `PUT`, `PATCH`, `DELETE`, `HEAD`, `OPTIONS`. Matches any of them, ignoring case. `name` (1 to 256 characters) plus exactly one of `present: true` or `value` (a `StringMatch` like `path`). With `value`, it matches if any value of that header matches. Same as `header`, for a query parameter. Names are case-sensitive. Matches the remote IP. Set exactly one of `in` (the remote IP is in one of the ranges) or `notIn` (the remote IP is in none of them). Each is a list of 1 to 100 IP addresses or CIDRs, such as `203.0.113.7`, `203.0.113.0/24`, or `2001:db8::/32`. A single address is saved as `/32` or `/128`. Ranges with host bits set, such as `10.1.2.3/8`, are rejected. The gateway accepts IPv4 connections today, so IPv6 entries match no request yet. In the dashboard this is **Client IP**, with **is in** or **is not in**. ```json Example: block POST and DELETE on internal paths theme={"system"} { "name": "Block internal paths", "enabled": true, "match": [ { "path": { "path": { "prefix": "/internal/" } } }, { "method": { "methods": ["POST", "DELETE"] } } ], "firewall": { "action": "ACTION_DENY" } } ``` ## Put policies in the right order The gateway runs every enabled policy that matches, from top to bottom. In the dashboard, drag a row by its handle to reorder. Order matters in four ways: * **IP blocks before authentication.** Put a firewall policy that blocks by `remoteIp` above your API key policies, so blocked clients can't try keys. * **Authentication first.** The first API key policy that succeeds decides who the caller is. Later API key policies are skipped. * **Rate limits by caller go after authentication.** A rate limit that counts by `authenticatedSubject` or `principalField` needs to know the caller. If it runs first, every request counts as `unknown`. * **A rejection stops everything.** After a firewall block, a failed key check, a used-up rate limit, or an OpenAPI error, no later policy runs, including logging. Logging policies never reject. If several match, the request is logged with all of their settings combined. ## Limits * 50 policies per environment. * 10 match expressions per policy. * 100 entries per `remoteIp` list. * 5 keyspaces and 10 key rate limits per API key policy. * 5 identifiers per rate limit policy. * Match strings up to 1024 characters, and names up to 256. ## Next steps Verify Unkey keys and forward the principal. Limit by IP, header, path, subject, or principal field. Deny the requests your match expressions select. Capture headers, query data, and bodies for matched requests. # The principal header Source: https://www.unkey.com/docs/compute/gateway/principal The X-Unkey-Principal header your app receives after the gateway verifies a key. When an [API key authentication policy](/docs/compute/gateway/api-key-auth) verifies a key, the [gateway](/docs/compute/gateway/overview) tells your app who's calling in the `X-Unkey-Principal` request header, as JSON. Your code reads this one header instead of calling Unkey. If the header is missing, no key was verified for that request. ## What's in the header ```json theme={"system"} { "version": "v1", "subject": "user_123", "type": "API_KEY", "identity": { "externalId": "user_123", "meta": { "plan": "pro" } }, "source": { "key": { "keyId": "key_2ZSM3h", "keySpaceId": "ks_1234abcd", "name": "production laptop", "expiresAt": 1767225600000, "credits": 41, "meta": { "region": "eu" }, "roles": ["editor"], "permissions": ["documents.read", "documents.write"] } } } ``` Format version, currently `v1`. It only changes if a field is removed, renamed, or changes type. New optional fields can appear without a version change, so ignore fields you don't know. Who's calling. For a key linked to an identity, it's the identity's `externalId`, so all keys of one identity share it. Otherwise it's the key ID. How the caller authenticated. Always `API_KEY` today. Treat any other value as unknown. The identity the key is linked to. Left out when the key has none. `externalId` is the ID you assigned, and `meta` is the identity's metadata (`{}` when empty). The verified key. * `keyId` and `keySpaceId`: always present. * `name`: left out when the key has no name. * `expiresAt`: Unix milliseconds, left out when the key doesn't expire. Always in the future, because expired keys are rejected. * `credits`: credits left after this request. Left out for unlimited keys, so `0` means this request used the last one. * `meta`: the key's metadata (`{}` when empty). * `roles` and `permissions`: the key's roles and permissions, left out when empty. The policy's permission query has already passed, so use these for finer checks in your app. ## Read it in your app Parse the header as JSON, and expect optional fields to be missing. ```ts Hono theme={"system"} import { Hono } from "hono"; type Principal = { version: string; subject: string; type: "API_KEY" | string; identity?: { externalId: string; meta: Record }; source: { key?: { keyId: string; keySpaceId: string; name?: string; expiresAt?: number; credits?: number; meta: Record; roles?: string[]; permissions?: string[]; }; }; }; const app = new Hono<{ Variables: { principal: Principal } }>(); app.use("*", async (c, next) => { const raw = c.req.header("x-unkey-principal"); if (!raw) { return c.json({ error: "unauthenticated" }, 401); } c.set("principal", JSON.parse(raw) as Principal); await next(); }); app.get("/documents", (c) => { const principal = c.get("principal"); if (!principal.source.key?.permissions?.includes("documents.read")) { return c.json({ error: "forbidden" }, 403); } return c.json({ owner: principal.subject }); }); ``` ```ts Next.js route handler theme={"system"} import { NextRequest, NextResponse } from "next/server"; export async function GET(req: NextRequest) { const raw = req.headers.get("x-unkey-principal"); if (!raw) { return NextResponse.json({ error: "unauthenticated" }, { status: 401 }); } const principal = JSON.parse(raw); return NextResponse.json({ owner: principal.subject, plan: principal.identity?.meta?.plan }); } ``` ```go Go net/http theme={"system"} type Principal struct { Version string `json:"version"` Subject string `json:"subject"` Type string `json:"type"` Identity *struct { ExternalID string `json:"externalId"` Meta map[string]any `json:"meta"` } `json:"identity,omitempty"` Source struct { Key *struct { KeyID string `json:"keyId"` KeySpaceID string `json:"keySpaceId"` Name *string `json:"name,omitempty"` ExpiresAt *int64 `json:"expiresAt,omitempty"` Credits *int64 `json:"credits,omitempty"` Meta map[string]any `json:"meta"` Roles []string `json:"roles,omitempty"` Permissions []string `json:"permissions,omitempty"` } `json:"key,omitempty"` } `json:"source"` } func handler(w http.ResponseWriter, r *http.Request) { raw := r.Header.Get("X-Unkey-Principal") if raw == "" { http.Error(w, "unauthenticated", http.StatusUnauthorized) return } var p Principal if err := json.Unmarshal([]byte(raw), &p); err != nil { http.Error(w, "bad principal", http.StatusBadRequest) return } fmt.Fprintf(w, "hello %s", p.Subject) } ``` ## Why you can trust it The header isn't signed, but callers can't fake it. The gateway removes any `X-Unkey-*` header a client sends, and only then sets `X-Unkey-Principal`. Compute deployments can only be reached through the gateway, so inside your app the header is always real. If you run the same code somewhere else, for example during [local development](/docs/compute/gateway/local-development), anyone who can reach it can send a fake header. If several API key policies match a request, the first one that succeeds sets the principal. You can rate limit by the principal: `authenticatedSubject` counts by `subject`, and `principalField` counts by a field such as `source.key.meta.org_id`. Only string values work. See [Rate limit policy](/docs/compute/gateway/rate-limiting). ## Next steps Send the header yourself while developing. The policy that produces the principal. # Rate limit policy Source: https://www.unkey.com/docs/compute/gateway/rate-limiting Limit requests per window at the gateway by IP, header, path, or caller. A rate limit policy tells the [gateway](/docs/compute/gateway/overview) to count requests over a fixed window, such as 100 per minute, and reject the extra ones before they reach your app. This rate limiting works without API keys. You choose what to count by, such as the remote IP or a header. For limits per API key, use the `ratelimits` setting on the [API key authentication policy](/docs/compute/gateway/api-key-auth) instead. To add one, open the app, go to **Policies**, click **Add policy**, and set **Type** to **Rate Limit**. The form starts at 100 requests per 60 seconds per client IP. ## Settings Maximum requests per window for each caller (or each group you count by). At least 1. Window length in milliseconds. At least 1. 1 to 5 things to count by. Each unique combination of values gets its own counter, with the same `limit` and `windowMs`. Each entry sets exactly one of: * `remoteIp: {}`: the remote IP. * `header: { name }`: the value of a request header. * `path: {}`: the request path. * `authenticatedSubject: {}`: the caller's `subject`, from an earlier API key policy. * `principalField: { path }`: a field in the [principal](/docs/compute/gateway/principal), as a dotted path like `source.key.meta.org_id`, up to 512 characters. Only string values work. The older single `identifier` field still works, but responses always return `identifiers`. Don't set both. ```json Example: 100 requests per minute per subject on each path theme={"system"} { "name": "Per-user per-route limit", "enabled": true, "ratelimit": { "limit": 100, "windowMs": 60000, "identifiers": [{ "authenticatedSubject": {} }, { "path": {} }] } } ``` In the dashboard this is the **Rate Limit** type in **Policies > Add policy**. Programmatically, include the object above in the `policies` array of `POST /v2/gateway.setPolicies`, placed after any `keyauth` policy whose principal it depends on. The change applies to the next deployment. ## When a value is missing If a value can't be found, such as a missing header or no caller on an anonymous request, it counts as `unknown`. All requests missing that value share one counter. For example, with `[authenticatedSubject, path]`, anonymous callers are still limited per path, together. If none of the values can be found, the request is rejected with `429` [`rate_limited`](/docs/errors/frontline/client/rate_limited) and the message `Rate limit configuration error. Unable to identify the request.` So a policy that counts by a header rejects every request on a route where callers never send it. `authenticatedSubject` and `principalField` need an [API key authentication policy](/docs/compute/gateway/api-key-auth) earlier in the list. Put the rate limit after it. Every request counts as 1. Replacing the policy list with `gateway.setPolicies` resets every counter once the next deployment is live. ## What callers see Over the limit, the caller gets `429` [`rate_limited`](/docs/errors/frontline/client/rate_limited) with `Rate limit exceeded. Please try again later.` If rate limiting itself fails, the request is rejected with `500` [`internal_server_error`](/docs/errors/frontline/platform/internal_server_error), not let through. Responses to requests the policy applies to include `X-RateLimit-Limit`, `X-RateLimit-Remaining`, and `X-RateLimit-Reset` (Unix seconds). A `429` also has `Retry-After` in whole seconds, at least 1. If an API key policy already set these headers for a per-key limit, the rate limit policy only replaces them when its result is stricter. ## Next steps Per-key limits and the principal that `authenticatedSubject` reads. Match expressions and evaluation order. # Deploy a container image Source: https://www.unkey.com/docs/compute/get-started/deploy-an-image Deploy an image you built yourself, from the dashboard or the CLI. An app deploys and serves traffic without any API key. This path is for images you build yourself, in CI or on your machine, and push to a registry. Unkey pulls the image and runs it. There's no build step, no repository connection, and no GitHub App. Choose it when you already have a build pipeline, when your source isn't on GitHub, or when you want to decide exactly when a version ships. The image must be pullable without credentials, and your container must listen on the port in the `PORT` environment variable, which Unkey sets to the configured port (8080 unless you change it). Reference the image with a tag (`ghcr.io/acme/api:v1.2.3`) or a digest (`ghcr.io/acme/api@sha256:...`). A reference with neither pulls `latest` at deploy time, so which version a deployment got depends on when you ran it. You need a Compute plan before you can deploy. If you don't have one yet, the dashboard asks you to pick one the first time you try (the dialog is titled **Choose a Compute plan**). Only a workspace admin can do that. See [Compute plans](/docs/compute/get-started/plans) for what each plan includes. Open **Projects** in the dashboard and choose **Create project**. An app always lives in a project. When the project exists, the dashboard opens the new-app flow. To add an app to a project later, open the project and choose **Create app**. On **Deploy a new app**, pick **Container image**. Enter the **Image** reference and choose **Continue**. This creates the app, named after the last part of the image path. Run a container image card with ghcr.io/acme/storefront-worker:v1.4.2 in the Image field and a Continue button Build and scaling settings you save on **Set up your app** apply to both environments. Set up your app card with the app name, and Build, Scaling, and Environment variables blocks, each with an Edit button, above Back and Deploy Choose **Deploy**. The dashboard creates the app's first deployment in the preview environment and opens it, so you can check the image before anything reaches production. Leaving before you deploy asks you to discard the app. An image app has no build step, and its source kind is fixed: you can't later connect a repository to it. Its **App settings** > **Build** page shows the image. To ship a new version or reach production, open the app's **Deployments** page and choose **Create deployment**. Pick the **Environment** and enter the **Image Reference**, or pick an image you deployed before. The `unkey api` commands match the public API one to one. `create-deployment` returns immediately with the deployment ID, and `get-deployment` reads its status. Both take the root key from `--root-key` or `UNKEY_ROOT_KEY` and print JSON with `--output json`. ```bash Create the deployment theme={"system"} unkey api deployments create-deployment \ --project=payments --app=api --environment=production \ --oci='{"image":"ghcr.io/acme/api:v1.2.3"}' ``` ```bash Check its status theme={"system"} unkey api deployments get-deployment --deployment-id=d_... --output json ``` ```bash Run an earlier deployment again theme={"system"} unkey api deployments create-deployment \ --project=payments --app=api --environment=production \ --deployment='{"deploymentId":"d_..."}' ``` Pass at most one of `--oci`, `--git`, or `--deployment`. Omit all three and the app deploys its configured default: its default image for an image app, its default branch for a Git app. A tag such as `:latest` is pinned to the exact image it points to when you deploy, so the deployment doesn't change when the tag moves. The root key needs `environment.*.create_deployment` to create and `unkey:v1::projects/*/apps/*/environments/*/deployments/*#read` to read. See [Root keys](/docs/platform/root-keys/overview). An image deployment runs the same steps as a Git one: `pending`, `starting`, `building`, `deploying`, `network`, and `finalizing` before `ready`. The `building` step doesn't build anything, so it's short. The deployment still waits its turn if another deployment in the workspace is running. A production deployment that reaches `ready` becomes the live one and takes over the production domains, unless the app is rolled back. A preview deployment gets its own domains. If the status is `failed`, the `error.code` says why. [Deployments](/docs/compute/concepts/deployments) lists the codes. `unkey api deployments create-deployment` calls `POST /v3/deployments.createDeployment` with an `oci` source. `POST /v2/deployments.createDeployment` still works and takes `image.dockerImage` instead, but it's deprecated. The `POST /v2/deploy.createDeployment` that `unkey deploy` uses is internal and deprecated, and may change; call the public endpoint directly instead. ```bash Create an image deployment theme={"system"} curl -X POST https://api.unkey.com/v3/deployments.createDeployment \ -H "Authorization: Bearer $UNKEY_ROOT_KEY" \ -H "Content-Type: application/json" \ -d '{"project":"payments","app":"api","environment":"production","oci":{"image":"ghcr.io/acme/api:v1.2.3"}}' ``` The response carries `deploymentId`. Poll `POST /v2/deployments.getDeployment` with `{"deploymentId":"..."}` until `status` is one of `ready`, `failed`, `skipped`, `superseded`, `stopped`, or `cancelled`. ## Next steps Promote a deployment, roll back, and redeploy. Let Unkey build and deploy on every push instead. What the runtime defaults mean. Change them on [Runtime settings](/docs/compute/configure/runtime-settings). Give the image its configuration and secrets. When you want the gateway in front of this app to check API keys before requests reach it, create a keyspace and keys in API Management, then attach a key-auth policy to the environment. Nothing in this guide depends on it. # Deploy from GitHub Source: https://www.unkey.com/docs/compute/get-started/deploy-from-github Connect a repository and get your first deployment live on an unkey.app domain. You can deploy an app straight from a GitHub repository, and Unkey deploys on every push. Connect the repository and Unkey tracks its default branch on GitHub. A push to that branch creates a production deployment. A push to any other branch creates a preview deployment. Open the deployment in the dashboard and wait for its status to be `ready`. Then it's live on a `unkey.app` domain. That's the default, with auto deploy on and no watch paths. If you turn auto deploy off or set watch paths, a push can be recorded as `skipped` instead of built. A pull request from a fork waits in `awaiting_approval` until a project member approves it. An app deploys and serves traffic. By the end of this guide you have a project, an app connected to the repository, and a production deployment. If you'd rather bring an image you built yourself, see [Deploy a container image](/docs/compute/get-started/deploy-an-image). You need a Compute plan before you can deploy. If you don't have one yet, the dashboard asks you to pick one the first time you try (the dialog is titled **Choose a Compute plan**). Only a workspace admin can do that. See [Compute plans](/docs/compute/get-started/plans) for what each plan includes. Open **Projects** in the dashboard and choose **Create project**. The dialog asks for a **Project Name** and a **Slug**. The slug is generated from the name, and you can edit it. The slug is the identifier you'll pass to the API and the CLI, it must be unique in your workspace, and `default` is reserved. Create New Project dialog with a project name and the slug generated from it The dashboard then opens the new-app flow. On **Deploy a new app**, pick **GitHub repository**. To add an app to a project later, open the project and choose **Create app**. Deploy a new app card offering GitHub repository or Container image, next to the steps Select a source, Choose your app, and Configure and deploy If the workspace hasn't installed the Unkey GitHub App, the row reads **Connect GitHub** instead. It sends you to GitHub to install the app on an organization or personal account and choose repositories, then brings you back to the repository list. The installation belongs to the workspace, so you do it once. Click a repository. If it's missing, choose **Can't find it? Add repositories on GitHub**. Import a GitHub repository card with an account filter, a search field, and a list of repositories This creates the app, named after the repository. If the project already uses that name, Unkey adds `-2`, `-3`, and so on. The branch starts as the repository's default branch. The defaults build and run most repositories without changes: * **Build**: with no Dockerfile set, Unkey detects your language and toolchain and builds the image. See [Build settings](/docs/compute/configure/build-settings). * **Scaling**: each app starts with 0.25 vCPU, 256 MiB of memory, and one instance in each region available to the workspace. See [Runtime settings](/docs/compute/configure/runtime-settings). * **Environment variables**: mark secrets as sensitive to make them write-only. See [Environment variables](/docs/compute/configure/environment-variables). Build and scaling settings you save here apply to both environments. Auto deploy starts on for both. Your container must listen on the port in the `PORT` environment variable, 8080 unless you change it under **App settings** > **Runtime**. Going back keeps your saved settings. Picking a different repository connects it to the same app. Leaving before you deploy asks you to discard the app. An app that was never deployed reopens this flow. Choose **Deploy**. The dashboard creates a deployment in the production environment from the head of the selected branch and opens the deployment page. The deployment moves through `pending`, `starting`, `building`, `deploying`, `network` (shown as **Assigning Domains**), and `finalizing` before it reaches `ready`. The build log streams while `building` runs. A first build waits its turn if another deployment in the workspace is running. When the status is `ready` the app is live on its automatic domains. With a project `payments`, an app `api`, and a workspace `acme`, the production deployment answers on `payments-api-acme.unkey.app`, with further domains for the environment, the branch, and the exact commit. If the app slug is `default` the domains omit it and start with `payments-`. The deployment detail page lists every domain. Deployment page for a ready production deployment showing its commit, resources, instances, domains, and network If the deployment ends `failed`, the detail page shows the step that failed and an error code. [Deployments](/docs/compute/concepts/deployments) lists the codes and what to change. From now on GitHub tells Unkey about every push. A push to the app's default branch creates a production deployment, and when it reaches `ready` it becomes the live deployment and the production domains move to it. A push to any other branch creates a preview deployment with its own branch domain, so every branch has a URL you can share. A pull request from a fork is held in `awaiting_approval` until a project member approves it, because it runs code from outside your repository. Auto deploy and watch paths decide whether a push builds at all. A push that isn't built is recorded as `skipped` with the reason. The same flow through the API, with a root key in `Authorization: Bearer`. Each call is an HTTP `POST` to `https://api.unkey.com/v2/...` with a JSON body. Required permissions: `project.*.create_project`, `workspace.*.install_github`, `project.*.create_app`, `app.*.connect_repository` (only when `createApp` carries a `git.repository`), `environment.*.create_deployment`, and `unkey:v1::projects/*/apps/*/environments/*/deployments/*#read`. ```bash Create the project theme={"system"} curl -X POST https://api.unkey.com/v2/projects.createProject \ -H "Authorization: Bearer $UNKEY_ROOT_KEY" \ -H "Content-Type: application/json" \ -d '{"name":"Payments","slug":"payments"}' ``` ```bash Install the GitHub App theme={"system"} # Returns a GitHub URL to open in a browser; installation is workspace wide. curl -X POST https://api.unkey.com/v2/github.installApp \ -H "Authorization: Bearer $UNKEY_ROOT_KEY" \ -H "Content-Type: application/json" \ -d '{}' ``` ```bash Create the app with its repository theme={"system"} curl -X POST https://api.unkey.com/v2/apps.createApp \ -H "Authorization: Bearer $UNKEY_ROOT_KEY" \ -H "Content-Type: application/json" \ -d '{"project":"payments","name":"Payments API","slug":"api","git":{"repository":"acme/payments"}}' ``` ```bash Deploy the default branch to production theme={"system"} curl -X POST https://api.unkey.com/v2/deployments.createDeployment \ -H "Authorization: Bearer $UNKEY_ROOT_KEY" \ -H "Content-Type: application/json" \ -d '{"project":"payments","app":"api","environment":"production","git":{}}' ``` ```bash Poll until ready theme={"system"} curl -X POST https://api.unkey.com/v2/deployments.getDeployment \ -H "Authorization: Bearer $UNKEY_ROOT_KEY" \ -H "Content-Type: application/json" \ -d '{"deploymentId":"d_..."}' ``` `getDeployment` returns `status`, `domains`, `availableActions`, and, when the status is `failed`, an `error` with a `code`. Poll until the status is one of `ready`, `failed`, `skipped`, `superseded`, `stopped`, or `cancelled`. ## Next steps Promote, roll back, and what happens to older deployments. Every status, the build queue, and the failure codes. Change CPU, memory, replicas, and health checks. Fork approval, watch paths, and what Unkey writes back to GitHub. When you want the gateway in front of this app to check API keys before requests reach it, create a keyspace and keys in API Management, then attach a key-auth policy to the environment. Nothing in this guide depends on it. # Compute plans Source: https://www.unkey.com/docs/compute/get-started/plans Which Compute plan you need to deploy and what each plan allows. You need a Compute plan to deploy. API Management is separate: you can be on the free API tier and still buy Compute, and a paid API plan still can't deploy until you add Compute. How billing and cancellation work for both products is on [Plans](/docs/platform/billing/plans). Here we focus on the Compute plan. ## Before you can deploy We check for a plan whenever you create or bring back compute: creating a project, creating a deployment, promoting or rolling back, approving a fork deployment, or handling a GitHub push. ## Plans and their limits There are three plans, cheapest to most capable: Starter, Pro, and Business. The plan sets the limits for your workspace. See [Limits](/docs/platform/billing/limits). These are the same numbers the dashboard shows. [Compute limits](/docs/compute/configure/limits) covers where each limit is enforced. | Limit | Starter | Pro | Business | | - | - | - | - | | CPU per instance | 2 vCPU | 8 vCPU | 16 vCPU | | Memory per instance | 2 GiB | 8 GiB | 32 GiB | | Ephemeral disk per instance | 10 GiB | 10 GiB | 10 GiB | | Workspace CPU across running instances | 30 vCPU | 120 vCPU | 240 vCPU | | Workspace memory across running instances | 60 GiB | 240 GiB | 480 GiB | | Workspace ephemeral disk across running instances | 120 GiB | 480 GiB | 960 GiB | | Replicas per region for one app | 4 | 8 | 16 | | Concurrent builds | 1 | 1 | 1 | | Custom domains | 1 | Unlimited | Unlimited | | Log query range | 3 days | 7 days | 14 days | | Team members | No | Yes | Yes | ## Spend budget A Compute plan meters usage above what the plan includes. Workspace admins can set a monthly spend budget in whole dollars under **Settings > Billing**. Unkey emails the admins when month-to-date Compute spend crosses 50, 75, and 100 percent of the budget. If you also turn on the stop option, hitting 100 percent suspends the workspace's compute: running deployments are stopped, the gateway answers every request with `402` and [`spend_limit_reached`](/docs/errors/frontline/capacity/spend_limit_reached), and starting or waking a deployment is refused. Raise or remove the budget, or turn the stop option off, and compute comes back. You do not need to pick a new plan. Full detail is on [Spend budget](/docs/compute/configure/spend-budget). ## If you cancel Compute Canceling Compute takes effect immediately: running deployments are torn down for good, the plan is cleared, and the subscription won't renew. The current period isn't refunded. Until someone picks a plan again, you can't deploy, promote, or roll back. If the workspace is on the free API Management tier and your Compute plan includes team members (Pro or Business), canceling deactivates every member except the workspace creator. Downgrading from Pro or Business to Starter does the same. ## Next steps Connect a repository and ship the first deployment. Every workspace limit with your current usage. # Compute Source: https://www.unkey.com/docs/compute/index Build, deploy, and run your app on Unkey, with or without API Management. Compute deploys an app from a GitHub repository on every push by default, or from a container image you built yourself. Connect the repository, pick the branch, and each push becomes a deployment you can follow in the dashboard until it's `ready`. You get a production and a preview environment, automatic domains, promote and rollback, and a gateway in front of every deployment. The app is the service inside a project. The steps are on [Deploy from GitHub](/docs/compute/get-started/deploy-from-github). ## Get started Connect a repository and let every push deploy. Ship an image you built yourself, from the dashboard or the CLI. What you need before you can deploy, and what each plan allows. ## Concepts How projects, apps, and environments nest. The thirteen lifecycle statuses, the build queue, and why a deployment fails. Promote, roll back, stop, wake, and what happens on a timer. Replicas, resource limits, health probes, and the autoscaler. How regions are chosen, capped, and used for readiness and failover. ## Build Automatic builds or your Dockerfile, the build queue, caching, failures. Build context, the runtime contract, and the errors Unkey explains. Use environment variables during the build without baking them in. Install, connect, branch mapping, fork approval, and what Unkey writes back. ## Configure Dockerfile, root directory, build command, watch paths, auto deploy. Port, CPU, memory, disk, command, protocol, regions, replicas. The probe that gates traffic and restarts unhealthy instances. Kinds, limits, import, and the variables Unkey injects. Per-instance and workspace ceilings, per plan. Alerts and an optional stop on monthly usage spend. The official SDKs wrap the whole Unkey API, so projects, apps, environments, deployments, domains, gateway policies, and the GitHub install flow are reachable from the same client you use for keys. # Automatic domains Source: https://www.unkey.com/docs/compute/networking/automatic-domains The unkey.app hostnames every deployment gets and which ones follow new deployments. Every deployment gets a set of `unkey.app` hostnames, so you can reach it as soon as it's ready, with no DNS setup. Some hostnames always point at that one deployment. Others move to newer deployments. You can't create, rename, or delete them. To use your own name, add a [custom domain](/docs/compute/networking/custom-domains). ## How hostnames are named Each hostname starts with a prefix made from your project and app, and ends with your workspace slug: | App slug | Prefix | Example | | - | - | - | | `default` | `` | `payments` | | anything else | `-` | `payments-api` | For a pull request from a fork, the fork owner's name is added, so its deployments don't clash with yours: `payments-fork-contributor-...`. ## The five hostnames For a deployment in workspace `acme`, project `payments`, app `default`, environment `production`, built from commit `1a2b3c4d5e6f` on branch `feature/new_checkout` with deployment ID `d_3f9kq2x7`, the deployment gets these hostnames: | Kind | Pattern | Example | Follows | | - | - | - | - | | Commit | `-git--.unkey.app` | `payments-git-1a2b3c4-acme.unkey.app` | Nothing. Pinned to this deployment. | | Branch | `-git--.unkey.app` | `payments-git-feature-new-checkout-acme.unkey.app` | The newest deployment of that branch. | | Environment | `--.unkey.app` | `payments-production-acme.unkey.app` | The environment's current deployment. Rollback moves it back. | | Live | `-.unkey.app` | `payments-acme.unkey.app` | The active production deployment. Production only. | | Deployment | `--.unkey.app` | `payments-d-3f9kq2x7-acme.unkey.app` | Nothing. Pinned to this deployment. | The commit hostname uses the first seven characters of the SHA. Commit and branch hostnames only exist when the deployment has a commit and branch, which GitHub deployments always do. A preview deployment gets `payments-preview-acme.unkey.app` as its environment hostname, and no live hostname. Use the two pinned hostnames in bug reports and pull request comments, because they always show that exact build. Use the three moving hostnames in configuration. See [Production and preview](/docs/compute/concepts/production-and-preview) for how promote and rollback move them. ## CLI deployments get a longer commit hostname You can run `unkey deploy` several times on the same commit, so for CLI deployments we add the last four characters of the deployment ID to the commit hostname. For deployment `d_3f9kq2x7` it becomes `payments-git-1a2b3c4-q2x7-acme.unkey.app`. Redeploying the same deployment keeps the same hostnames. ## How branch names appear in hostnames Branch names and deployment IDs are lowercased, every character that isn't a letter or digit becomes a hyphen, repeated hyphens become one, and the result is cut at 80 characters. So `feature/new_checkout` becomes `feature-new-checkout`, and `d_3f9kq2x7` becomes `d-3f9kq2x7`. Two branches that differ only in punctuation or case share one branch hostname. ## Custom domains follow their environment A verified custom domain follows the current deployment of the environment it's attached to. So a custom domain on a preview environment follows preview, not production. See [Custom domains](/docs/compute/networking/custom-domains). # Custom domains Source: https://www.unkey.com/docs/compute/networking/custom-domains Serve an environment from your own domain, from DNS records to certificate. A custom domain lets you serve an environment of your app from a hostname you own, such as `api.acme.com`. You add the domain, publish two DNS records to prove you own it, and we route it and get a certificate for it. It always serves the environment's current deployment, like the [automatic hostnames](/docs/compute/networking/automatic-domains). ## Before you start * **Your plan must allow it.** Starter includes one custom domain, and Pro and Business have no practical limit. Going over returns [`custom_domain_limit_exceeded`](/docs/errors/unkey/limits/custom_domain_limit_exceeded). See [Compute plans](/docs/compute/get-started/plans). * **Each name can be used once in your workspace**, so you can't attach it to two environments. * **It must be under a domain someone can register.** `api.acme.co.uk` is fine, but `co.uk` on its own isn't. Wildcards and IP addresses aren't allowed. Unicode names work, and we store them in lowercase Punycode. * **Port 80 must reach us as well as 443**, so we can get a certificate. ## Attach a domain Add custom domain dialog on the app's Domains page, with a Domain field and an Environment select In the dashboard, open the app, go to **Domains**, and click **Add domain**. Enter the domain and pick the **Environment** it should serve. Or use the CLI: ```bash theme={"system"} unkey api domains create-domain --project=payments --app=api --environment=production --domain=api.acme.com ``` The API equivalent is `POST /v2/domains.createDomain` with `project`, `app`, `environment`, and `domain`. The response is the domain object, including `dnsRecords` and, when available, `domainConnect`. We show the records to create at your DNS provider. On the **Domains** page, click a domain's row to see them: | Type | Name | Value | When | | - | - | - | - | | `CNAME` | `api.acme.com` | `<16 characters>.` | Subdomains | | `ALIAS` | `acme.com` | the same target | Apex domains | | `TXT` | `_unkey.api.acme.com` | `unkey-domain-verify=` | Always | Copy the values exactly, with a TTL of 60 seconds. `ALIAS` means whatever your provider calls an alias at the apex: ALIAS, ANAME, or a flattened CNAME. Some providers want names without the zone. In zone `acme.com`, enter `api` instead of `api.acme.com`, and `_unkey.api` instead of `_unkey.api.acme.com`. The zone itself is usually `@`. If your DNS is hosted at Cloudflare or Vercel, you can skip the manual step. We give you a Domain Connect link (`domainConnect` in the API, **Connect** under "Automatic setup available" in the domain's row). Open it, approve the pre-filled records at your provider, and you're sent back to the app's settings. We check DNS once a minute for up to 24 hours after you added the domain. The status goes from `pending` to `verifying` to `verified`. Each record also shows whether we've found it yet. Once it's verified, we get a Let's Encrypt certificate and start serving HTTPS on the domain. ## Verification failed or is stuck After 24 hours without the right records, the status becomes `failed` and `verificationError` says why. Fix the records, then retry with **Retry verification** on the domain's row, or with the CLI. Retrying starts a new 24 hour window: ```bash theme={"system"} unkey api domains verify-domain --domain=api.acme.com ``` Things to check: * **Publish both records**, even for a subdomain. A subdomain passes when its CNAME points at our target. But an apex domain, a flattened CNAME, or a proxied record that hides the CNAME can only pass with the TXT record. * **An apex domain must also resolve to an IP address** (an A or AAAA record) through the alias. * **Values must match exactly.** We compare them character for character. ## Certificates Certificates come from Let's Encrypt and renew automatically when they have less than 30 days left. The name must be reachable on port 80, because Let's Encrypt checks it over plain HTTP. If Let's Encrypt rate limits us, we wait and retry up to three times, then mark the certificate as failed. HTTPS on custom domains requires TLS 1.2 or later. See [Request lifecycle and headers](/docs/compute/networking/request-lifecycle). ## Move a domain from another workspace If another workspace has already verified the name, publish the TXT record. When your verification passes, the domain moves to your workspace and is removed from the other one. ## While the app is rolled back A custom domain normally moves to each new live deployment. While the app is rolled back, new production deployments don't go live, so your domain keeps serving the deployment you rolled back to until you promote. See [Production and preview](/docs/compute/concepts/production-and-preview). ## Remove a domain Delete the domain, and requests to it stop reaching your app right away. Deleting the app or project also deletes its domains. Remove the DNS records yourself. # Private networking Source: https://www.unkey.com/docs/compute/networking/private-networking What your instances can and cannot reach, and how to call one app from another today. We don't offer private networking yet. To call one app from another, call its public hostname. The request goes out over the internet and back in through the [gateway](/docs/compute/gateway/overview), like a call from anywhere else. Your instances can't open connections to each other. ## Call one app from another Use the other app's hostname, and treat the call as an external one: ```ts theme={"system"} const res = await fetch("https://billing-acme.unkey.app/invoices", { headers: { Authorization: `Bearer ${process.env.BILLING_API_KEY}` }, }); ``` 1. **Put the hostname and credential in [environment variables](/docs/compute/configure/environment-variables)** of the calling app, not in code. Then a preview deployment can point at a preview of the other app. See [Automatic domains](/docs/compute/networking/automatic-domains) for hostname patterns. 2. **Protect the other app with an [API key policy](/docs/compute/gateway/api-key-auth)**, because the call is public. Give the caller a key. Bad requests are rejected at the gateway, and the caller's identity shows up in the [request log](/docs/compute/observe/requests). 3. **Set a timeout.** Each call is a full HTTP round trip, so avoid chains of internal calls on a latency-sensitive path. ## Share state between instances Instances can't talk to each other, so anything two of them need belongs in a store they can both reach, such as a managed Redis or Postgres. Don't rely on peer-to-peer cache invalidation, leader election, or gossip between replicas. For long-lived connections, see [WebSockets](/docs/compute/networking/websockets). ## What your instances can and can't reach * **Outbound: anything.** Your app can call any host it can resolve: a database, a third-party API, an object store, or another Unkey app. * **Inbound: only the gateway**, on your app's port. Nothing else can reach an instance: not other instances of the same deployment, not other apps in the project, not older deployments of the same app. A blocked connection isn't refused or reset. It's silently dropped, so the caller just times out. If you see that, this is the likely cause. Instances also run in a sandbox and get no credentials to our infrastructure. # Request lifecycle and headers Source: https://www.unkey.com/docs/compute/networking/request-lifecycle What callers and your app see on every request: HTTPS, which deployment answers, timeouts, headers, and errors. Every request to your app goes through Unkey's gateway. It handles HTTPS, finds the right deployment, runs your [gateway policies](/docs/compute/gateway/policies), and forwards the request. Here's what that means for your callers and your code. ## HTTPS and redirects Every [automatic hostname](/docs/compute/networking/automatic-domains) and every verified [custom domain](/docs/compute/networking/custom-domains) gets a valid certificate with no configuration. The minimum TLS version is 1.2. A plain HTTP request gets a `308 Permanent Redirect` to the `https://` URL. A 308 keeps the method and body, so a client that follows it resends a `POST` as a `POST`. Your app never sees plain HTTP traffic. ## Which deployment answers The hostname decides. Each hostname points at one deployment. Promote and rollback move the hostnames that follow the live deployment, and the change reaches every region within a few seconds. So a few requests can still reach the old deployment right after you promote. [Automatic domains](/docs/compute/networking/automatic-domains) lists which hostnames move. A hostname that doesn't point at any deployment gets `404` and [`config_not_found`](/docs/errors/frontline/routing/config_not_found). A request is served in the region that received it, if the deployment has running instances there. Otherwise it goes to the nearest region that does. If no region has one, the caller gets `503` and [`no_running_instances`](/docs/errors/frontline/capacity/no_running_instances). See [Regions](/docs/compute/concepts/regions). A request only moves to another instance if the first couldn't be reached at all. Your handler never runs twice for one request. ## Policies run first Before the request is forwarded, the deployment's policies run. A request a policy rejects never reaches your app and doesn't appear in the [request log](/docs/compute/observe/requests). See [Gateway policies](/docs/compute/gateway/policies). ## Protocols, timeouts, and sizes The gateway talks to your app over HTTP/1.1 or h2c, set by the upstream protocol in [runtime settings](/docs/compute/configure/runtime-settings). Responses stream to the caller as your app writes them, so server-sent events work without buffering. A request has 15 minutes to finish. After that, the caller gets `504` with [`err:user:bad_request:request_timeout`](/docs/errors/user/bad_request/request_timeout). (That code's own page describes the Unkey API's shorter limit and `408`, which don't apply here.) If the gateway times out waiting for your app, the caller gets `504` [`gateway_timeout`](/docs/errors/frontline/upstream/gateway_timeout) instead. Neither limit applies to [WebSockets](/docs/compute/networking/websockets). There's no limit on request or response body size. A [logging policy](/docs/compute/gateway/logging) saves at most 1 MiB of each body, but that only affects the log. Your app's responses pass through unchanged, including your own `4xx` and `5xx` responses. ## Headers your app receives | Header | Value | | - | - | | `Host` | The hostname the client asked for, unchanged. | | `X-Forwarded-For` | The client's IP address, as a single value. Any `X-Forwarded-For` the client sent is replaced, not appended to. If you need a chain from a proxy in front of Unkey, send it in your own header. | | `X-Forwarded-Host` | The same hostname as `Host`. | | `X-Forwarded-Proto` | Always `https`. | | `X-Unkey-Request-Id` | The request ID. The same value is returned to the client and stored in the request log. | | `X-Unkey-Region` | The gateway's platform and region as `::`, for example `aws::us-east-1`. | | `X-Unkey-Frontline-Id` | The ID of the gateway that handled the request. | | `X-Unkey-Principal` | Who's calling, as JSON. Only present when an API key authentication policy verified the request. See [the principal header](/docs/compute/gateway/principal). | Callers can't fake the `X-Unkey-*` headers, including `X-Unkey-Principal`: we remove any they send. Every other header arrives unchanged. To know which deployment you're in, read the `UNKEY_DEPLOYMENT_ID` environment variable. ## Headers the client receives | Header | Value | | - | - | | `X-Unkey-Request-Id` | The request ID. Quote it when you contact support. | | `X-Unkey-Region` | The gateway's `::`. | | `X-Unkey-Frontline-Id` | The gateway ID. | | `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` | Present when a per-key rate limit or a rate limit policy applied. `Reset` is Unix seconds. If several limits apply, the strictest one is shown. | | `Retry-After` | Only when rate limited. Whole seconds until the limit resets, at least 1. | Headers your app sets are passed to the client unchanged. ## Errors callers can get When the gateway rejects a request or can't reach your app, it responds with JSON if the client accepts it, or an HTML page if the client prefers `text/html`: ```json theme={"system"} { "meta": { "requestId": "req_..." }, "error": { "code": "err:frontline:upstream:service_unavailable", "message": "The service is temporarily unavailable. Please try again later." } } ``` Each code has a fixed message that never reveals your app's address, and its own page under [`/errors/frontline`](/docs/errors/frontline/upstream/service_unavailable). When the gateway can't reach your app, the caller gets one of these: | What happened | Response | | - | - | | Your instance refused the connection or couldn't be reached | `503` [`service_unavailable`](/docs/errors/frontline/upstream/service_unavailable) | | The connection to your instance was reset | `502` [`bad_gateway`](/docs/errors/frontline/upstream/bad_gateway) | | Your instance didn't answer in time | `504` [`gateway_timeout`](/docs/errors/frontline/upstream/gateway_timeout) | A client that disconnects before the response starts is logged as [`client_closed_request`](/docs/errors/user/bad_request/client_closed_request). [Gateway errors](/docs/compute/gateway/errors) lists every code. # WebSockets Source: https://www.unkey.com/docs/compute/networking/websockets Run WebSocket servers on Unkey and know how upgrades are routed and checked. WebSockets work on Unkey with no setup. Listen on your app's port and deploy. Keep the upstream protocol in [runtime settings](/docs/compute/configure/runtime-settings) at `http1`. WebSockets don't work with `h2c`. ```ts theme={"system"} import { WebSocketServer } from "ws"; const wss = new WebSocketServer({ port: Number(process.env.PORT) }); wss.on("connection", (socket) => { socket.on("message", (data) => socket.send(`echo: ${data}`)); }); ``` ## Connect with wss Connect to any hostname of the deployment, automatic or custom, with `wss://`. Plain `ws://` fails: it gets a `308` redirect to HTTPS, and WebSocket clients don't follow redirects. ```ts theme={"system"} const ws = new WebSocket("wss://payments-acme.unkey.app/ws"); ``` ## Policies check the handshake The opening handshake is a normal HTTP request. Your [gateway policies](/docs/compute/gateway/policies) run on it, so a handshake that fails API key authentication, a rate limit, or a firewall rule is rejected before the connection opens. Your server gets the same headers as any other request, including `X-Unkey-Principal` when a key was verified. See [Request lifecycle and headers](/docs/compute/networking/request-lifecycle). ## How long connections last Once your server accepts the upgrade, the connection stays open as long as both ends keep it. The 15 minute request timeout doesn't apply. We don't limit connection length, frame size, or the number of connections. Your app sets those limits. The handshake shows up in the [request log](/docs/compute/observe/requests) when the connection closes, with status `101` and a latency covering the whole connection. A [logging policy](/docs/compute/gateway/logging) doesn't save the messages. ## Connections and instances Each connection goes to a random instance, so two connections from the same client can land on different instances. Keep state on the connection or in a shared store, not in one instance's memory. If no instance can take the connection, the client gets a `503` right away. When a deployment is replaced or scaled down, the instance gets the shutdown signal and its connections close when the process exits. Make your clients reconnect when a connection closes. The new connection goes to a running instance of the current deployment. # Query gateway requests and runtime logs Source: https://www.unkey.com/docs/compute/observe/analytics-api Query gateway requests and runtime logs with SQL through the analytics API. The SQL you can use, query quotas, and query errors are documented in API Management. Here we cover the two Compute tables. You can query your app's [request log](/docs/compute/observe/requests) and [runtime logs](/docs/compute/observe/runtime-logs) with SQL, to build your own dashboards, alerts, or exports. Each table has its own endpoint and permission. | Table | Endpoint | Root key permission | | - | - | - | | `gateway_requests_v1` | `POST /v2/analytics.getGatewayRequests` | `project.*.read_gateway_requests` | | `runtime_logs_v1` | `POST /v2/analytics.getRuntimeLogs` | `project.*.read_runtime_logs` | You need a root key with the wildcard (`project.*`) permission for the table. A permission for a single project doesn't work here. Create the key under **Settings > Root Keys** and send it as `Authorization: Bearer `. See [Root keys](/docs/platform/root-keys/overview). ## Query rules * Only `SELECT` runs. CTEs, subqueries, `UNION`, and `EXCEPT` are fine. * You only ever see your own workspace's data. To narrow further, filter on `project_id`, `app_id`, or `environment_id`. * The time range can't reach back further than your plan's log query range (3 days on Starter, 7 on Pro, 14 on Business), or the query fails with [`query_range_exceeds_retention`](/docs/errors/user/bad_request/query_range_exceeds_retention). See [Compute limits](/docs/compute/configure/limits#log-query-range). * A result larger than 4 MiB fails with [`query_memory_limit_exceeded`](/docs/errors/user/unprocessable_entity/query_memory_limit_exceeded). The response is `{"meta":{"requestId":...},"data":[...]}` with one object per row. ## Examples ```bash curl theme={"system"} curl -X POST https://api.unkey.com/v2/analytics.getGatewayRequests \ -H "Authorization: Bearer $UNKEY_ROOT_KEY" \ -H "Content-Type: application/json" \ -d '{"query":"SELECT path, count() AS total FROM gateway_requests_v1 WHERE response_status >= 500 AND time >= toUnixTimestamp64Milli(now64(3) - INTERVAL 24 HOUR) GROUP BY path ORDER BY total DESC LIMIT 10"}' ``` ```bash CLI theme={"system"} unkey api analytics get-gateway-requests \ --query="SELECT path, count() AS total FROM gateway_requests_v1 WHERE response_status >= 500 AND time >= toUnixTimestamp64Milli(now64(3) - INTERVAL 24 HOUR) GROUP BY path ORDER BY total DESC LIMIT 10" ``` ```bash curl theme={"system"} curl -X POST https://api.unkey.com/v2/analytics.getRuntimeLogs \ -H "Authorization: Bearer $UNKEY_ROOT_KEY" \ -H "Content-Type: application/json" \ -d '{"query":"SELECT time, severity, message FROM runtime_logs_v1 WHERE lower(message) LIKE '"'"'%timeout%'"'"' ORDER BY time DESC LIMIT 100"}' ``` ```bash CLI theme={"system"} unkey api analytics get-runtime-logs \ --query="SELECT time, severity, message FROM runtime_logs_v1 WHERE lower(message) LIKE '%timeout%' ORDER BY time DESC LIMIT 100" ``` `time` is a Unix timestamp in milliseconds in both tables, so the examples compare it with `toUnixTimestamp64Milli(...)`. ## gateway\_requests\_v1 One row per request that reached your app. Requests a policy rejected aren't here. See [Request logs](/docs/compute/observe/requests). The request ID, also sent as `X-Unkey-Request-Id`. Unix timestamp in milliseconds when the gateway received the request. Your workspace, the one the root key belongs to. The project. The app. The environment. The deployment that served the request. The instance that served the request. The region of the gateway that served the request. Upper-case HTTP method. The requested hostname. The request path without the query string. The raw query string. Empty unless a logging policy captured query data. Parsed query parameters. Empty unless a logging policy captured query data. `Key: Value` strings. Empty unless a logging policy captured request headers. API keys are redacted. Up to 1 MiB. Empty unless a logging policy captured the request body. The status the client received. `Key: Value` strings. Empty unless a logging policy captured response headers. Up to 1 MiB. Empty unless a logging policy captured the response body. Empty unless a logging policy captured request headers. The client IP. Empty unless a logging policy captured request headers. Milliseconds from receipt to the end of the response. Milliseconds spent by your instance. `total_latency` minus `instance_latency`. ## runtime\_logs\_v1 One row per log line from your instances. Structured attributes are in `attributes_text`, as a JSON string. Stable identifier of the log line. Unix timestamp in milliseconds when the line was written. Unix timestamp in milliseconds when the line was stored. Filter on it as well as on `time` to make queries over a wide range faster. Lower-case severity parsed from the line, `info` when none was found. The message, with color codes removed. Your workspace. The project. The environment. The app. The deployment whose instance wrote the line. The region the instance ran in. The parsed attributes as a JSON string. Search it with `lower(attributes_text) LIKE '%...%'`, which the table indexes. # Build logs Source: https://www.unkey.com/docs/compute/observe/build-logs Read the steps and output Unkey records while building a deployment. Build logs show the output of building a deployment as one list, in the order the build printed it. Open the app, click **Deployments**, and open the deployment. The output appears as the build runs and stays afterwards. A deployment from a prebuilt image has no build, so it has no build logs. See [Deploy a container image](/docs/compute/get-started/deploy-an-image). ## What the list shows Each line has the time and the text the build printed. A header line shows the step name each time the output changes to a different step. Steps that run in parallel can alternate, so one step can have more than one header. Unkey adds one line when a step ends: * `CACHED`: the step did not run, because nothing it depends on changed since an earlier build. See [Builds](/docs/compute/build/overview) for what the cache covers. * `DONE 5.3s`: the step ran for this time. A step that took less than 0.05 seconds gets no line. * `ERROR: `: the step failed. Text the build printed to `stderr` is amber. Many tools write progress and warnings to `stderr`, so amber text does not mean that the step failed. `ERROR:` lines are red. When a finished step printed more than 40 entries in a row, the list shows the first 5 and the last 5. Click **Show N more entries** to see the others. The dashboard keeps the last 5,000 log entries of a build. Click **Jump to latest** to go to the newest output. While you stay at the end of the list, it follows new output. ## Find why a build failed Click **Jump to error**, or **View full error** in the build status. The list moves to the last `ERROR:` line. The lines before it usually name the cause, such as a missing dependency, a compile error, or a Dockerfile instruction that failed. When one step fails, BuildKit stops the steps that run in parallel, and these can also show an `ERROR:` line. See [Deployments](/docs/compute/concepts/deployments). ## Read build logs through the API `deployments.listBuildLogs` returns the output of a build as a list of log entries, in the order the build printed them. An entry is one chunk of output and can contain several lines or part of a line. Each entry has the step ID, the step name, and `output`, which is `stdout` or `stderr`. Many tools write progress and warnings to `stderr`, so `stderr` does not mean that the step failed. To follow a running build, poll the endpoint with a cursor: 1. Send a request without `cursor` to get the first entries. 2. Send the `pagination.cursor` of the last response as `cursor` in the next request. The response contains only the entries after it. 3. When `hasMore` is true, request again now. When it is false, wait, then request again with the same cursor. 4. Check the deployment status with `deployments.getDeployment`. After the status is no longer `building`, request until you get an empty response at least 5 seconds later. A response can contain fewer than `limit` entries and still have `hasMore: true` when the entries are large. To get the entries of one step, send its `stepId`. New entries become visible up to a few seconds after the build prints them. When a build attempt is retried, an entry from the earlier attempt can arrive after the cursor has moved past it. A request without a cursor returns it. The root key needs `read` on the deployment's build logs, for example `unkey:v1::projects//apps//environments//deployments//buildLogs#read`. Use `*` for an ID to cover all deployments of an environment, app, or project. ## Retention Build logs are kept for 3 months. # Metrics Source: https://www.unkey.com/docs/compute/observe/metrics The traffic, latency, and resource charts on a deployment and what they measure. Metrics show how much traffic a deployment gets, how fast it responds, and how much CPU and memory it uses. You don't need to add anything to your code. Open the app, click **Deployments**, and open a deployment. Its overview has the charts, and its **Network** view shows where requests are going. ## Traffic Traffic charts cover the last 6 hours in 15 minute buckets. * **Requests per second:** the average over the last 6 hours. * **Latency:** the total time of each request, as in the [request log](/docs/compute/observe/requests). Pick p50, p75, p90, p95, or p99. Like the request log, these don't count requests a policy rejected. ## Resources Resource charts add up all the deployment's instances. You can pick a time window from 15 minutes to a year. | Chart | What a point means | | - | - | | CPU | Millicores used on average during each point's time span | | Memory | Peak memory in use per instance, added up | | Disk | Peak disk in use per instance, added up | | Instances | Number of instances running | | Network ingress, network egress | Bytes per second in each direction, public and private traffic combined | Compare CPU and memory with the limits in [runtime settings](/docs/compute/configure/runtime-settings) to decide whether to raise them or add instances. See [Instances and autoscaling](/docs/compute/concepts/instances-and-autoscaling). Longer windows show coarser points: 15 seconds up to an hour, 1 minute up to a day, 1 hour up to 30 days, and 1 day beyond that. Fine-grained points are deleted first. See [retention](/docs/compute/observe/overview#retention). ## Why an instance restarted The deployment lists instance events: an instance becoming `running`, `waiting`, or `terminated`, with its restart count, exit code, and a reason. Two reasons to know: * **`OOMKilled`** (often with exit code `137`): the instance ran out of memory. Raise the memory limit. * **`CrashLoopBackOff`:** the process keeps exiting before the [health check](/docs/compute/configure/health-checks) passes. The [runtime logs](/docs/compute/observe/runtime-logs) for that instance say why. Events are kept for 90 days. ## Network view The **Network** view shows each region and running instance with its request rate over the last 15 minutes. Use it to check that every region gets traffic and that load is spread across instances. See [Regions](/docs/compute/concepts/regions). Deployment page Network view showing two regions and their running instances # Observability Source: https://www.unkey.com/docs/compute/observe/overview The signals Unkey records for a running app and where to find each one. We record four kinds of data about your app, with nothing to install in your code: * Every request the gateway passes to your app. * Everything your instances print to stdout and stderr. * CPU, memory, disk, and network for each instance. * Each build step and its output. One row per proxied request: method, path, status, and the split between gateway and instance latency. Headers and bodies on request. stdout and stderr from every instance, parsed into severity, message, and searchable attributes. Requests per second, latency percentiles, CPU, memory, disk, network, instance count, and instance events. Each step of a build, whether it was cached, and its output. ## Where to look in the dashboard * **Request and runtime logs:** open a project and click **Requests** or **Logs** in the sidebar. Filter by app, environment, deployment, or region. * **Metrics and build steps:** open the app, click **Deployments**, and open a deployment. Its overview has the traffic and resource charts and the build steps. Its **Network** view shows regions and instances with live request rates. ## Rejected requests aren't in the request log The request log only has requests that reached your app. Requests rejected by a policy, or that the gateway couldn't route (an unknown hostname, or no running instance), aren't there. So your request log can show less traffic than your callers sent. See [Request logs](/docs/compute/observe/requests). ## Retention Each kind of data is kept for a fixed time, shown below. The dashboard can show all of it. The [analytics API](/docs/compute/observe/analytics-api) has a second limit: your plan's log query range, 3 days on Starter, 7 on Pro, and 14 on Business. You can read back whichever is shorter. A query that reaches further fails with [`err:user:bad_request:query_range_exceeds_retention`](/docs/errors/user/bad_request/query_range_exceeds_retention) instead of returning partial results. | Data | Stored for | Queryable | | - | - | - | | Request log rows | 7 days | Dashboard: 7 days. Analytics API: the shorter of 7 days and your plan's range | | Per-minute request counts and latency percentiles | 14 days | Charts, no plan range | | 5 and 15 minute request aggregates | 30 days | Charts, no plan range | | Hourly request aggregates | 90 days | Charts, no plan range | | Daily request aggregates | 365 days | Charts, no plan range | | Runtime logs | 90 days from ingestion | Dashboard: 90 days. Analytics API: the shorter of 90 days and your plan's range | | Build steps and build logs | 3 months | Dashboard only | | 15 second resource metrics | 7 days | Charts, no plan range | | Per-minute resource metrics | 30 days | Charts, no plan range | | Hourly resource metrics | 90 days | Charts, no plan range | | Daily resource metrics | 365 days | Charts, no plan range | | Monthly resource metrics | 1825 days | Charts, no plan range | | Raw resource samples | 95 days | Charts, no plan range | | Instance events | 90 days | Charts, no plan range | For example, on Starter the analytics API reaches back 3 days into runtime logs that are kept for 90. On Business it reaches 7 days of request logs, not 14, because that's how long they're kept. See [Compute limits](/docs/compute/configure/limits). ## Query the data yourself You can query request and runtime logs with SQL through the analytics API, using the `gateway_requests_v1` and `runtime_logs_v1` tables, to build your own dashboards or alerts. See [Query gateway requests and runtime logs](/docs/compute/observe/analytics-api). Read build logs through the API with `deployments.listBuildLogs`. See [Build logs](/docs/compute/observe/build-logs). Resource metrics are only in the dashboard. # Request logs Source: https://www.unkey.com/docs/compute/observe/requests Inspect every request the gateway proxied and what a logging policy adds to it. The request log shows every request that reached your app: method, host, path, status, how long it took, and which environment, deployment, instance, and region served it. Headers, bodies, and query data are only saved when a logging policy asks for them, because they often contain secrets. Open a project and click **Requests** to see it. Requests page listing requests with time, region, status, method, and host, with one request open showing deployment information and request and response details ## Why a request is missing Only requests that reached your app are logged. These aren't: * Requests rejected by a [gateway policy](/docs/compute/gateway/policies). The caller got a `401`, `403`, `429`, or `400`, and your app never saw it. * Requests the gateway couldn't route, such as an unknown hostname or a deployment with no running instances. Errors after the request was forwarded, such as a timeout, are logged with the status the caller got. A request served in another region is logged there. ## What each row has | Field | Meaning | | - | - | | `request_id` | Also sent to the client and to your app as `X-Unkey-Request-Id` | | `time` | When the gateway received the request | | `method`, `host`, `path` | The request line | | `response_status` | The status the client received | | `total_latency` | Milliseconds from receipt to the end of the response | | `instance_latency` | Milliseconds your app took, from when the gateway forwarded the request to the end of your response | | `gateway_latency` | `total_latency` minus `instance_latency`: time spent in the gateway, including policies | | `region` | The region of the gateway that served it | | `project_id`, `app_id`, `environment_id`, `deployment_id`, `instance_id` | Where it went | ## What a logging policy adds A [logging policy](/docs/compute/gateway/logging) can also save request headers (with the user agent and client IP), response headers, request and response bodies (up to 1 MiB each), and the query string. API keys are always redacted, so a saved request never contains a usable key. ## Filter the log Filters narrow the list and the charts above it. | Filter | Operators | | - | - | | Status | is | | Method | is | | Path | is, starts with, contains (at least 3 characters for the last two) | | Host | is | | Request ID | is | | App, environment, deployment, region | is | | Time | since, or start and end | Click a row to see all of it, including any saved headers, bodies, and query data. ## Retention and access Rows are kept for 7 days, and the dashboard shows all of them. The [analytics API](/docs/compute/observe/analytics-api) can reach back 3 days on Starter and 7 on Pro and Business, because Business's 14-day query range is longer than the rows are kept. A query that reaches further fails with [`err:user:bad_request:query_range_exceeds_retention`](/docs/errors/user/bad_request/query_range_exceeds_retention). See [Compute limits](/docs/compute/configure/limits). Request counts and latency charts on [Metrics](/docs/compute/observe/metrics) go back further. To query rows with SQL, use the `gateway_requests_v1` table. See [Query gateway requests and runtime logs](/docs/compute/observe/analytics-api). # Runtime logs Source: https://www.unkey.com/docs/compute/observe/runtime-logs Read and filter what your instances write to stdout and stderr. Everything your instances write to stdout or stderr shows up as runtime logs, with nothing to install or configure. Print a line and it appears under **Logs** in the project a few seconds later, tagged with the app, environment, deployment, region, and instance that wrote it. ## Write logs you can filter Print one JSON object per line, with `level` and `message` (or `msg`), and put the values you'll search for in their own fields: ```json theme={"system"} {"level":"error","message":"charge failed","order_id":"ord_1234","customer":{"id":"cus_98","plan":"pro"},"attempt":3} ``` That entry has severity `error`, message `charge failed`, and attributes `order_id`, `customer.id`, `customer.plan`, and `attempt`, which you can all filter on. ## How lines are read Color codes are removed first. * **JSON object:** the message is `msg` or `message` (or the whole line), the severity is `level` or `severity` (default `info`), and every other key becomes an attribute, including nested ones. * **`key=value` (logfmt):** handled like JSON, but only if the line has `level`, `severity`, `msg`, or `message`. * **Anything else:** plain text. The whole line is the message, and we guess the severity. Panics, tracebacks, exception names, and words like `error`, `fatal`, `failed to`, `crashed`, or `oom` mean error. `warn` or `warning` mean warning, and `debug` or `trace` mean debug. Everything else is info. If severity matters, use JSON. ## Filter the logs Logs page filtered to one app, listing log lines with time, severity, region, instance, and message | Filter | Operators | Notes | | - | - | - | | Severity | is | `ERROR`, `WARN`, `INFO`, `DEBUG` | | Message | contains | Case-insensitive substring, at least 3 characters | | Attributes | is, contains | Written as `path=value`, for example `customer.plan=pro`. The value must be at least 3 characters. | | App, environment, deployment, region, instance | is | Narrow from the project to one source | | Time | since, or start and end | Relative or absolute range | Each entry shows its severity, time, message, attributes, and the instance and region that wrote it. ## Retention and access Runtime logs are kept for 90 days, and the dashboard shows all of them. The [analytics API](/docs/compute/observe/analytics-api) can only reach back as far as your plan's log query range: 3 days on Starter, 7 on Pro, and 14 on Business. A query that reaches further fails with [`err:user:bad_request:query_range_exceeds_retention`](/docs/errors/user/bad_request/query_range_exceeds_retention). See [Compute limits](/docs/compute/configure/limits). To query logs with SQL, use the `runtime_logs_v1` table. See [Query gateway requests and runtime logs](/docs/compute/observe/analytics-api). # deployment_offline Source: https://www.unkey.com/docs/errors/frontline/capacity/deployment_offline The deployment behind this hostname was stopped or its project canceled. `err:frontline:capacity:deployment_offline` The hostname points at a Compute deployment that was stopped, or whose project was canceled. It stays offline until someone starts it. You get `503 Service Unavailable`. ```json Example response theme={"system"} { "meta": { "requestId": "req_3k2j1h0g9f8e7d6c" }, "error": { "code": "err:frontline:capacity:deployment_offline", "message": "This deployment is offline." } } ``` Browsers get the same error as an HTML page. ## If you're calling an API hosted on Unkey Retrying won't help. The owner has taken this deployment offline. If you expected it to be up, contact the API owner with the hostname and the request ID. ## If you own this API A hostname still points at a deployment that isn't running. It starts the moment you stop the deployment, even while instances are still shutting down. Common causes: * You stopped the deployment. * The project was canceled. ### How to fix 1. Start the deployment from the dashboard or with `deployments.startDeployment`. 2. Or promote a different deployment so the hostname points at one that's running. You can't start a deployment while the workspace is paused at its Compute spend limit. If callers also see [`spend_limit_reached`](/docs/errors/frontline/capacity/spend_limit_reached), fix that first. Starting, stopping, promoting, and the deployment lifecycle are Compute features. ## Related errors * [`err:frontline:capacity:no_running_instances`](/docs/errors/frontline/capacity/no_running_instances): the deployment should be running but has no instance yet. * [`err:frontline:capacity:spend_limit_reached`](/docs/errors/frontline/capacity/spend_limit_reached): compute is paused by a spend budget. * [`err:unkey:application:deployment_not_stopped`](/docs/errors/unkey/application/deployment_not_stopped): the API refused to start a deployment that isn't stopped. # no_running_instances Source: https://www.unkey.com/docs/errors/frontline/capacity/no_running_instances The deployment has no running instance in any region right now. `err:frontline:capacity:no_running_instances` The deployment should be running, but right now no instance of it is up in any region. It usually lasts a moment while a deployment starts, scales, or replaces failed instances. You get `503 Service Unavailable`. ```json Example response theme={"system"} { "meta": { "requestId": "req_3k2j1h0g9f8e7d6c" }, "error": { "code": "err:frontline:capacity:no_running_instances", "message": "No running instances are available to handle this request." } } ``` Browsers get the same error as an HTML page. ## If you're calling an API hosted on Unkey Retry after a short delay, with backoff. If it lasts more than a few minutes, only the API owner can fix it. Give them the request ID. ## If you own this API The gateway sends each request to a running instance in its region, or in the nearest region that has one. There was none. Common causes: * The deployment is still starting and has no ready instances yet. * Every instance failed, usually because the process exits at startup or never passes its health check. * The environment has no current deployment, or its regions and replica counts aren't what you expect. ### How to fix 1. Open the deployment in the dashboard. During a rollout, wait for instances to become ready. 2. If instances are `failed`, read the runtime logs for a crash at startup or a failing health check. 3. Check that the environment has a current deployment, and that its regions and replica counts are set as you expect. A deployment you stopped on purpose returns [`deployment_offline`](/docs/errors/frontline/capacity/deployment_offline) instead. Deployments, instances, regions, and runtime logs are Compute features. ## Related errors * [`err:frontline:capacity:deployment_offline`](/docs/errors/frontline/capacity/deployment_offline): the deployment was stopped intentionally. * [`err:frontline:upstream:service_unavailable`](/docs/errors/frontline/upstream/service_unavailable): an instance existed but refused the connection. * [`err:frontline:capacity:spend_limit_reached`](/docs/errors/frontline/capacity/spend_limit_reached): compute is paused by a spend budget. # spend_limit_reached Source: https://www.unkey.com/docs/errors/frontline/capacity/spend_limit_reached Compute is paused because the workspace reached its monthly spend budget. `err:frontline:capacity:spend_limit_reached` The workspace that owns this API reached its monthly Compute spend budget, so its compute is paused. Nothing is broken and no data is lost. You get `402 Payment Required`. ```json Example response theme={"system"} { "meta": { "requestId": "req_3k2j1h0g9f8e7d6c" }, "error": { "code": "err:frontline:capacity:spend_limit_reached", "message": "Compute for this workspace is paused because it reached its spend limit. Raise or remove the spend budget to resume." } } ``` Browsers get the same error as an HTML page. ## If you're calling an API hosted on Unkey You can't fix this, and retrying returns the same 402. The API owner has to raise or remove their spend budget, or wait for the next billing month. Tell them you're seeing this error, in case they missed our email. ## If you own this API You set a monthly Compute budget with **Stop workloads at the budget** turned on, and this month's spend reached it. We email workspace admins at 50, 75, and 100 percent of the budget. Spend is measured before credits are subtracted. While compute is paused, your deployments are torn down and every request gets this 402. You also can't start, promote, roll back, or wake a deployment, and pushes to GitHub don't deploy. ### How to fix 1. Open **Settings > Billing** in the dashboard. 2. Raise the **Monthly budget** above your current spend, remove the budget, or turn off **Stop workloads at the budget**. 3. Allow a few minutes. Removing the budget or turning off the stop resumes compute on the next check. Raising the budget resumes it once two checks in a row see spend under the new budget. A new billing month resets month-to-date spend, and a paused workspace resumes the same way. Spend budgets, alerts, and Compute billing are documented in Compute. ## Related errors * [`err:frontline:capacity:deployment_offline`](/docs/errors/frontline/capacity/deployment_offline): a deployment stopped by you rather than by the spend cap. * [`err:frontline:capacity:no_running_instances`](/docs/errors/frontline/capacity/no_running_instances): a short gap while instances start after a resume. # firewall_denied Source: https://www.unkey.com/docs/errors/frontline/client/firewall_denied A gateway firewall policy matched the request and denied it. `err:frontline:client:firewall_denied` The API you called runs on Unkey, and its owner set up a firewall rule that blocks requests like yours. The request never reached the app. You get `403 Forbidden`. ```json Example response theme={"system"} { "meta": { "requestId": "req_3k2j1h0g9f8e7d6c" }, "error": { "code": "err:frontline:client:firewall_denied", "message": "Forbidden" } } ``` Browsers get the same error as an HTML page. ## If you're calling an API hosted on Unkey The owner blocks requests by path, method, header, query parameter, or a mix of those. The response doesn't say which rule matched. If you think you should have access, contact the API owner with the request ID and the exact request you sent. ## If you own this API A firewall policy blocks a request when all of its match expressions match. A policy with no match expressions blocks every request. Policies run in order. Common causes: * A match expression is broader than you meant and catches real traffic. * An earlier policy is too broad and blocks traffic before a later one runs. * A firewall policy has no match expressions, so it blocks everything. ### How to fix 1. Review the policy's match expressions in the gateway settings and narrow the one that catches real traffic. 2. Reorder your policies if an earlier one is too broad. 3. Create a new deployment. The running deployment keeps the policies it started with. Gateway policies, deployments, and domains are Compute features. The Compute docs explain how to configure them. ## Related errors * [`err:frontline:client:insufficient_permissions`](/docs/errors/frontline/client/insufficient_permissions): a 403 from a key-auth permission query rather than a firewall rule. * [`err:frontline:client:openapi_validation_failed`](/docs/errors/frontline/client/openapi_validation_failed): the request was rejected for not matching the OpenAPI spec. # frontline/client/insufficient_permissions Source: https://www.unkey.com/docs/errors/frontline/client/insufficient_permissions The API key verified but does not satisfy the gateway policy's permission query. `err:frontline:client:insufficient_permissions` The API you called runs on Unkey, and it checks API keys. Your key is valid, but it doesn't have the permissions or roles this route needs. You get `403 Forbidden`. This isn't [`err:unkey:authorization:insufficient_permissions`](/docs/errors/unkey/authorization/insufficient_permissions), which is about a root key calling `api.unkey.com`. ```json Example response theme={"system"} { "meta": { "requestId": "req_3k2j1h0g9f8e7d6c" }, "error": { "code": "err:frontline:client:insufficient_permissions", "message": "Access denied. The API key does not have the required permissions." } } ``` Browsers get the same error as an HTML page. ## If you're calling an API hosted on Unkey Changing the request won't help. Ask the API owner to grant your key the permission or role this route needs, and give them the request ID. ## If you own this API The key's permissions, whether granted directly or through its roles, don't satisfy your key-auth policy's `permissionQuery`. A key passes the gateway exactly when it would pass `keys.verifyKey` with the same query. A rejected request doesn't spend credits. Common causes: * The key is missing the permission or role. * The `permissionQuery` is stricter than you meant. ### How to fix 1. Grant the missing permission or role to the key, or to its identity's roles, in API Management. This applies on the next request. 2. Or loosen the policy's `permissionQuery`, then create a new deployment. Policy changes only apply to new deployments. If the query doesn't parse at all, callers get [`invalid_configuration`](/docs/errors/frontline/config/invalid_configuration) instead. Gateway policies, deployments, and domains are Compute features. The Compute docs explain how to configure them. Permissions, roles, and the permission query grammar are documented in API Management. ## Related errors * [`err:frontline:client:invalid_key`](/docs/errors/frontline/client/invalid_key): the key didn't verify at all. * [`err:frontline:config:invalid_configuration`](/docs/errors/frontline/config/invalid_configuration): the permission query couldn't be parsed. * [`err:unkey:authorization:insufficient_permissions`](/docs/errors/unkey/authorization/insufficient_permissions): a root key lacking permission on the Unkey API. # invalid_key Source: https://www.unkey.com/docs/errors/frontline/client/invalid_key The API key in the request did not verify. `err:frontline:client:invalid_key` The API you called runs on Unkey, and it checks API keys. You sent a key, but it isn't valid for this API. You get `401 Unauthorized`, and the message is the same whatever the reason. ```json Example response theme={"system"} { "meta": { "requestId": "req_3k2j1h0g9f8e7d6c" }, "error": { "code": "err:frontline:client:invalid_key", "message": "Authentication failed. The provided API key is invalid." } } ``` Browsers get the same error as an HTML page. ## If you're calling an API hosted on Unkey Check that you're sending the whole key, with no extra whitespace or quotes, and that it's the key for this API rather than another service. If the key used to work, the API owner may have disabled, expired, rolled, or deleted it. Only they can tell you which, so contact them with the request ID. ## If you own this API The gateway returns this code for any key that isn't valid for this API, unless another code fits better (missing permissions, rate limited, or out of credits). Common causes: * The key doesn't exist or was deleted. * The key is disabled or expired. * The key is from a keyspace your key-auth policy doesn't list. * The workspace that owns the key is disabled. The gateway doesn't enforce a keyspace's IP allow list. That only applies to your own `keys.verifyKey` calls. ### How to fix 1. Look the key up in the dashboard or with `keys.getKey`. Check that it's enabled, not expired, and in a keyspace the policy accepts. 2. If callers hold keys from a keyspace you added recently, add that keyspace to the policy and create a new deployment. Policy changes only apply to deployments created after the change. 3. If the key looks fine, check whether the workspace that owns it is disabled. Gateway policies, deployments, and domains are Compute features. The Compute docs explain how to configure them. Key state, expiry, and keyspace membership are managed in API Management. ## Related errors * [`err:frontline:client:missing_credentials`](/docs/errors/frontline/client/missing_credentials): no key was found at all. * [`err:frontline:client:insufficient_permissions`](/docs/errors/frontline/client/insufficient_permissions): the key is valid but lacks permissions. * [`err:unkey:data:key_not_found`](/docs/errors/unkey/data/key_not_found): the equivalent when you look a key up through the Unkey API rather than the gateway. # missing_credentials Source: https://www.unkey.com/docs/errors/frontline/client/missing_credentials The gateway found no API key in the request. `err:frontline:client:missing_credentials` The API you called runs on Unkey, and it needs an API key. The gateway didn't find one where it looks, so the request never reached the app. You get `401 Unauthorized`. ```json Example response theme={"system"} { "meta": { "requestId": "req_3k2j1h0g9f8e7d6c" }, "error": { "code": "err:frontline:client:missing_credentials", "message": "Authentication required. Please provide a valid API key." } } ``` Browsers get the same error as an HTML page. ## If you're calling an API hosted on Unkey Send your API key where the API expects it. Unless the owner told you otherwise, that's the `Authorization` header as a bearer token: ```bash theme={"system"} curl https://api.example.com/v1/items \ -H "Authorization: Bearer " ``` Common causes: * The `Bearer` prefix is missing. It isn't case-sensitive, but without it the header is ignored. * The header is there but empty. That counts as missing. * The owner uses a custom header or query parameter. Use exactly the name they gave you. * You don't have a key. Only the API owner can issue one. ## If you own this API Your key-auth policy lists where to look for the key: the `Authorization: Bearer` header, a named header (optionally with a prefix to strip), or a query parameter. It uses the first non-empty value, in order. With no locations set, it only reads `Authorization: Bearer`. Common causes: * Callers send the key somewhere the policy doesn't look. * A `stripPrefix` doesn't match what callers send. For example, `stripPrefix: "Key "` finds nothing in `Authorization: Bearer ...`. ### How to fix 1. Compare where your callers send the key with the policy's key locations. 2. Add a second location if you need to accept both styles, and remove or loosen a `stripPrefix` that callers don't send. 3. Create a new deployment. The running deployment keeps the policies it started with. Gateway policies, deployments, and domains are Compute features. The Compute docs explain how to configure them. Keys are issued and managed in API Management. Callers need a key from one of the keyspaces the policy accepts. ## Related errors * [`err:frontline:client:invalid_key`](/docs/errors/frontline/client/invalid_key): a key was found but didn't verify. * [`err:frontline:client:insufficient_permissions`](/docs/errors/frontline/client/insufficient_permissions): the key verified but lacks the required permissions. # openapi_validation_failed Source: https://www.unkey.com/docs/errors/frontline/client/openapi_validation_failed The request does not match the OpenAPI spec the gateway validates against. `err:frontline:client:openapi_validation_failed` The API you called runs on Unkey, and it checks every request against the owner's OpenAPI spec. Your request didn't match, so it wasn't forwarded. You get `400 Bad Request`, and the message names the first thing that failed. ```json Example response theme={"system"} { "meta": { "requestId": "req_3k2j1h0g9f8e7d6c" }, "error": { "code": "err:frontline:client:openapi_validation_failed", "message": "..." } } ``` Browsers get the same error as an HTML page. ## If you're calling an API hosted on Unkey Read `error.message`. It names the first problem, such as an unknown path, a missing required field, or a value of the wrong type. Fix it and retry. If there are more problems, the next attempt reports the next one. Check these against the owner's published spec: * The path and method. * The `Content-Type` header. * Required query parameters. * The request body. ## If you own this API The gateway checks requests against the OpenAPI spec from your deployment, or against a spec attached to the policy if the deployment has none. With no spec at all, every request goes through. If only malformed requests are rejected, the policy is doing its job. If real requests are rejected, the spec is stricter than your app. Common causes: * A field your app treats as optional is required in the spec. * A path or method is missing from the spec. * The spec doesn't allow a content type your app accepts. ### How to fix 1. Update the spec: mark optional fields as optional, add missing paths or methods, and allow the content types you accept. 2. Create a new deployment so the gateway uses the updated spec. If the spec itself is broken, every matched request gets [`invalid_configuration`](/docs/errors/frontline/config/invalid_configuration) instead. Gateway policies, deployments, and domains are Compute features. The Compute docs explain how to configure them. ## Related errors * [`err:frontline:config:invalid_configuration`](/docs/errors/frontline/config/invalid_configuration): the spec couldn't be compiled. * [`err:frontline:client:firewall_denied`](/docs/errors/frontline/client/firewall_denied): a request blocked by a firewall rule rather than by schema validation. # rate_limited Source: https://www.unkey.com/docs/errors/frontline/client/rate_limited The request exceeded a per-key limit or a gateway rate limit policy. `err:frontline:client:rate_limited` The API you called runs on Unkey, and this request went over one of its rate limits. The limit is either on your API key or on a rule that counts requests by something like your IP address. You get `429 Too Many Requests`. ```json Example response theme={"system"} { "meta": { "requestId": "req_3k2j1h0g9f8e7d6c" }, "error": { "code": "err:frontline:client:rate_limited", "message": "Rate limit exceeded. Please try again later." } } ``` Browsers get the same error as an HTML page. ## If you're calling an API hosted on Unkey Wait, then retry with backoff. Don't retry in a tight loop. The response headers tell you how long to wait: * `Retry-After`: seconds to wait before you retry, never lower than 1. * `X-RateLimit-Limit`: how many requests the window allows. * `X-RateLimit-Remaining`: how many you have left. * `X-RateLimit-Reset`: when the window resets, as a Unix timestamp in seconds. The `X-RateLimit-*` headers are on successful responses too, so you can slow down before you hit the limit. If several limits apply, they describe the most restrictive one. ## If you own this API Two kinds of policy return this code. A key-auth policy with `ratelimits` entries limits each key the same way `keys.verifyKey` does. A rate limit policy has its own `limit` and `windowMs`, and counts requests by one or more identifiers: the client IP, a header, the path, the authenticated subject, or a field of the principal. Common causes: * The limit is too low for your real traffic. * The identifier is too broad, so unrelated callers share one counter. * The policy counts by a header that callers don't send. When none of a policy's identifiers are on a request, the request is rejected, so every request without that header gets this 429. ### How to fix 1. Raise `limit` or shorten `windowMs` on the policy, or count by a more specific identifier. 2. For per-key limits, change the limit on the key or identity, or the `ratelimits` entry in the policy (`name`, and optionally `limit`, `duration`, and `cost`). 3. Make sure the identifier you count by is on every request the policy matches. 4. Create a new deployment. Policy changes only apply to deployments created after the change. Gateway policies, deployments, and domains are Compute features. The Compute docs explain how to configure them. Per-key and per-identity limits, and the standalone ratelimit API, are API Management features. ## Related errors * [`err:frontline:client:usage_exceeded`](/docs/errors/frontline/client/usage_exceeded): the key ran out of credits rather than requests per window. * [`err:frontline:client:invalid_key`](/docs/errors/frontline/client/invalid_key): the key failed verification before limits were checked. # usage_exceeded Source: https://www.unkey.com/docs/errors/frontline/client/usage_exceeded The gateway rejected the request because the key has no credits left. `err:frontline:client:usage_exceeded` The API you called runs on Unkey, and each request spends credits from your API key. Your key has no credits left for this one. You get `429 Too Many Requests`. ```json Example response theme={"system"} { "meta": { "requestId": "req_3k2j1h0g9f8e7d6c" }, "error": { "code": "err:frontline:client:usage_exceeded", "message": "Usage limit exceeded. This API key has no remaining credits." } } ``` Browsers get the same error as an HTML page. ## If you're calling an API hosted on Unkey Retrying won't help until the owner adds credits or a scheduled refill runs. There's no `Retry-After` header for this error. Contact the API owner with the request ID and ask about your quota. ## If you own this API Each request that passes a key-auth policy costs the policy's `credits` value, or 1 if it isn't set. Gateway traffic and your own `keys.verifyKey` calls spend from the same balance on the key. Common causes: * The key used up its credits. * A route that shouldn't be metered is costing credits. ### How to fix 1. Add credits with `keys.updateCredits`, set up a refill so the balance tops up on a schedule, or remove the credit limit from the key. 2. For a route that shouldn't cost credits, set `credits: 0` on its policy and create a new deployment. A negative `credits` value makes every matched request fail with [`invalid_configuration`](/docs/errors/frontline/config/invalid_configuration). Gateway policies, deployments, and domains are Compute features. The Compute docs explain how to configure them. Credits, refills, and keys.updateCredits are API Management features. ## Related errors * [`err:frontline:client:rate_limited`](/docs/errors/frontline/client/rate_limited): a per-window request limit rather than a credit balance. * [`err:frontline:config:invalid_configuration`](/docs/errors/frontline/config/invalid_configuration): the policy's credit cost is negative. # invalid_configuration Source: https://www.unkey.com/docs/errors/frontline/config/invalid_configuration The deployment's gateway policy configuration could not be parsed or has an invalid value. `err:frontline:config:invalid_configuration` The API you called runs on Unkey, and the gateway can't use the policies its owner deployed. The problem is in the owner's configuration, not in your request. You get `422 Unprocessable Entity`. ```json Example response theme={"system"} { "meta": { "requestId": "req_3k2j1h0g9f8e7d6c" }, "error": { "code": "err:frontline:config:invalid_configuration", "message": "The deployment configuration is invalid. Please check your config or contact support at support@unkey.com." } } ``` Browsers get the same error as an HTML page. ## If you're calling an API hosted on Unkey Nothing you change will fix this. Requests keep failing until the owner fixes their configuration. Contact the API owner with the request ID. ## If you own this API Common causes: * A key-auth policy has a negative `credits` value. * A key-auth policy's `permissionQuery` doesn't parse. * An OpenAPI policy's spec is invalid. * The policy configuration can't be read at all. For the first three, only routes that the broken policy matches fail. For the last one, every request to the deployment fails. ### How to fix 1. Open the gateway policies for the app. 2. Fix the broken value: make `credits` zero or positive, fix the permission query syntax, or repair the OpenAPI document. 3. Create a new deployment. The running deployment keeps the broken configuration until you deploy again. Gateway policies, deployments, and domains are Compute features. The Compute docs explain how to configure them. ## Related errors * [`err:frontline:platform:config_load_failed`](/docs/errors/frontline/platform/config_load_failed): the configuration couldn't be loaded at all, which is a failure on our side. * [`err:frontline:client:openapi_validation_failed`](/docs/errors/frontline/client/openapi_validation_failed): the spec compiled and the request failed validation. # ratelimit_not_configured Source: https://www.unkey.com/docs/errors/frontline/config/ratelimit_not_configured A gateway policy references a named rate limit that is not configured for the key or its identity. `err:frontline:config:ratelimit_not_configured` This error means the gateway can't enforce a named rate limit. The policy references a limit that doesn't exist on the API key or its identity, and the policy doesn't provide both an inline `limit` and `duration`. The gateway rejects the request with `422 Unprocessable Entity`. This is a configuration error, not an exhausted rate limit or an invalid API key. The response message is: > A rate limit required by the gateway policy is not configured for this key or its identity. ## If you're calling an API hosted on Unkey Contact the API owner with the request ID. Retrying the same request won't fix the missing rate-limit configuration. ## How to fix If you own this API, check the rate-limit names in the matching key-auth policy. For each required name, configure a matching rate limit on the key or its identity, or supply both `limit` and `duration` in the policy to define an inline limit. This error code applies to gateway requests. The `verifyKey` API returns `412 Precondition Failed` for a missing named rate limit. ## Related errors * [`rate_limited`](/docs/errors/frontline/client/rate_limited): a configured rate limit is exhausted, so the gateway returns 429. * [`invalid_configuration`](/docs/errors/frontline/config/invalid_configuration): the gateway policy can't be parsed or has an invalid value. # config_load_failed Source: https://www.unkey.com/docs/errors/frontline/platform/config_load_failed Unkey's gateway failed on our side while loading the configuration it needs to route the request. `err:frontline:platform:config_load_failed` The gateway failed on our side while loading what it needs to route the request. It isn't caused by the request or by the owner's configuration. You get `500 Internal Server Error`, usually with `Failed to load route configuration` or `Failed to load instance configuration`. ```json Example response theme={"system"} { "meta": { "requestId": "req_3k2j1h0g9f8e7d6c" }, "error": { "code": "err:frontline:platform:config_load_failed", "message": "Failed to load route configuration" } } ``` Browsers get the same error as an HTML page. ## If you're calling an API hosted on Unkey Retry with backoff. It's usually brief. If it keeps failing, contact the API owner or Unkey support with the request ID. ## If you own this API Nothing in your deployment causes this. A hostname with no route returns [`config_not_found`](/docs/errors/frontline/routing/config_not_found) instead. ### How to fix There's nothing to change on your side. If it lasts more than a few minutes, check the Unkey status page and contact support with the request ID and the hostname. Gateway policies, deployments, and domains are Compute features. The Compute docs explain how to configure them. ## Related errors * [`err:frontline:routing:config_not_found`](/docs/errors/frontline/routing/config_not_found): the hostname has no route. * [`err:frontline:config:invalid_configuration`](/docs/errors/frontline/config/invalid_configuration): the configuration loaded but couldn't be parsed. * [`err:frontline:platform:internal_server_error`](/docs/errors/frontline/platform/internal_server_error): other failures inside the gateway. # deployment_selection_failed Source: https://www.unkey.com/docs/errors/frontline/platform/deployment_selection_failed A reserved gateway code that no request returns. `err:frontline:platform:deployment_selection_failed` This code is reserved, and the gateway doesn't return it. When there's no instance to serve a request, you get [`err:frontline:capacity:no_running_instances`](/docs/errors/frontline/capacity/no_running_instances). Other failures inside the gateway show up as [`err:frontline:platform:internal_server_error`](/docs/errors/frontline/platform/internal_server_error). ## How to fix There's nothing to fix. If a request didn't reach an instance, start from the two errors below. ## Related errors * [`err:frontline:capacity:no_running_instances`](/docs/errors/frontline/capacity/no_running_instances): what the gateway returns when there's no instance to send the request to. * [`err:frontline:platform:internal_server_error`](/docs/errors/frontline/platform/internal_server_error): the general gateway failure. # internal_server_error Source: https://www.unkey.com/docs/errors/frontline/platform/internal_server_error Unkey's gateway failed on our side while handling the request. `err:frontline:platform:internal_server_error` Something failed on our side, inside Unkey's gateway. It isn't caused by the request or by the owner's configuration. You get `500 Internal Server Error`. ```json Example response theme={"system"} { "meta": { "requestId": "req_3k2j1h0g9f8e7d6c" }, "error": { "code": "err:frontline:platform:internal_server_error", "message": "An unexpected error occurred. Please try again later." } } ``` Browsers get the same error as an HTML page. ## If you're calling an API hosted on Unkey Retry with backoff if the request is safe to repeat. If it keeps failing, contact the API owner or Unkey support with the request ID. ## If you own this API Nothing in your deployment causes this. ### How to fix There's nothing to change on your side. If it lasts more than a few minutes, check the Unkey status page and contact support with a few request IDs and the hostname. Gateway policies, deployments, and domains are Compute features. The Compute docs explain how to configure them. ## Related errors * [`err:frontline:platform:config_load_failed`](/docs/errors/frontline/platform/config_load_failed): the gateway couldn't load the app's configuration. * [`err:frontline:upstream:bad_gateway`](/docs/errors/frontline/upstream/bad_gateway): the connection to your app failed, rather than the gateway itself. # config_not_found Source: https://www.unkey.com/docs/errors/frontline/routing/config_not_found No Compute deployment is routed for the requested hostname. `err:frontline:routing:config_not_found` The request reached Unkey, but the hostname doesn't point at any Compute deployment, so no app was contacted. You get `404 Not Found`. ```json Example response theme={"system"} { "meta": { "requestId": "req_3k2j1h0g9f8e7d6c" }, "error": { "code": "err:frontline:routing:config_not_found", "message": "No deployment found for this hostname. Please check your domain configuration or contact support at support@unkey.com." } } ``` Browsers get the same error as an HTML page. ## If you're calling an API hosted on Unkey Check the hostname. Common causes are a typo, a preview domain for a branch that no longer exists, and a custom domain the owner hasn't finished setting up. If the owner gave you this hostname, send them the request ID. ## If you own this API Unkey domains work as soon as a deployment is created. Custom domains only work once we've verified them. Common causes: * A custom domain is still waiting for verification, even if its DNS already points at Unkey. * The hostname isn't one of the deployment's domains, or has a typo. * The domain was removed, or the deployment it pointed at was deleted. * The certificate for a custom domain won't issue. ### How to fix 1. Check that the hostname is one of the domains listed on the deployment or environment. 2. For a custom domain, check its verification status and the DNS records we asked for, then wait for verification to finish. 3. If the hostname used to work, check whether the domain was removed or its deployment deleted. Automatic domains, custom domains, and verification are Compute features. ## Related errors * [`err:frontline:capacity:deployment_offline`](/docs/errors/frontline/capacity/deployment_offline): the hostname is routed but the deployment is stopped. * [`err:frontline:platform:config_load_failed`](/docs/errors/frontline/platform/config_load_failed): the route couldn't be loaded because of a failure on our side. # frontline/routing/deployment_not_found Source: https://www.unkey.com/docs/errors/frontline/routing/deployment_not_found A reserved gateway code that no request returns. `err:frontline:routing:deployment_not_found` This code is reserved, and the gateway doesn't return it. A hostname that doesn't point at a deployment returns [`err:frontline:routing:config_not_found`](/docs/errors/frontline/routing/config_not_found) instead. ## How to fix There's nothing to fix. If a hostname won't route, start from [`err:frontline:routing:config_not_found`](/docs/errors/frontline/routing/config_not_found). ## Related errors * [`err:frontline:routing:config_not_found`](/docs/errors/frontline/routing/config_not_found): the hostname has no route, which is the routing error you actually see. * [`err:unkey:data:deployment_not_found`](/docs/errors/unkey/data/deployment_not_found): a different error, from the Unkey API, for a deployment ID that doesn't exist. # bad_gateway Source: https://www.unkey.com/docs/errors/frontline/upstream/bad_gateway The app behind the gateway reset the connection or returned an unusable response. `err:frontline:upstream:bad_gateway` The gateway sent the request to a running instance of the app, and the connection broke. The cause is almost always the app itself. You get `502 Bad Gateway`. ```json Example response theme={"system"} { "meta": { "requestId": "req_3k2j1h0g9f8e7d6c" }, "error": { "code": "err:frontline:upstream:bad_gateway", "message": "Unable to connect. Please try again in a few moments." } } ``` Browsers get the same error as an HTML page. ## If you're calling an API hosted on Unkey Retry with backoff. A single 502 during a rollout is normal. If every request fails this way, the app is unhealthy and only the API owner can fix it. Send them the request ID and the time. ## If you own this API Your app accepted the connection and then closed it. If the gateway can't connect at all, it tries your other instances first. It never retries a request your app already accepted, so your handlers don't run twice for one request. Common causes: * The app crashed while handling the request. * The instance was killed for running out of memory. * The app closes idle connections too quickly. * The app doesn't speak the upstream protocol set for the deployment. ### How to fix 1. Read the runtime logs around the time of the request. Look for a crash, an out-of-memory kill, or a panic on the failing route. 2. Make sure your app keeps connections open long enough for the gateway to reuse them. 3. Check that your app handles the upstream protocol set for the deployment. Runtime logs, runtime settings, and the request lifecycle are Compute features. ## Related errors * [`err:frontline:upstream:service_unavailable`](/docs/errors/frontline/upstream/service_unavailable): the instance refused the connection or couldn't be reached. * [`err:frontline:upstream:gateway_timeout`](/docs/errors/frontline/upstream/gateway_timeout): the instance didn't answer in time. * [`err:frontline:capacity:no_running_instances`](/docs/errors/frontline/capacity/no_running_instances): there was no running instance to connect to. # gateway_timeout Source: https://www.unkey.com/docs/errors/frontline/upstream/gateway_timeout The app behind the gateway did not respond before the deadline. `err:frontline:upstream:gateway_timeout` The gateway sent the request to the app and didn't get an answer in time. You get `504 Gateway Timeout`. ```json Example response theme={"system"} { "meta": { "requestId": "req_3k2j1h0g9f8e7d6c" }, "error": { "code": "err:frontline:upstream:gateway_timeout", "message": "The request took too long to process. Please try again later." } } ``` Browsers get the same error as an HTML page. ## If you're calling an API hosted on Unkey If the request is safe to repeat, retry with backoff. If it isn't, ask the API owner whether it went through before you retry, because the app may have finished the work after the gateway stopped waiting. Send them the request ID. ## If you own this API Each request gets 15 minutes, and each connection attempt to an instance gets 10 seconds. If an instance is slow to accept a connection, the gateway tries your other instances first. A request your app already accepted isn't retried. Common causes: * A route takes longer than 15 minutes to respond. * Instances are too slow to accept connections under load. ### How to fix 1. Find the slow route in the runtime logs and request log. 2. Move long-running work off the request path, or stream the response so bytes reach the gateway before the deadline. 3. If instances are slow to accept connections under load, add replicas or raise the CPU in the runtime settings. Request lifecycle, timeouts, replicas, and runtime settings are Compute features. ## Related errors * [`err:frontline:upstream:bad_gateway`](/docs/errors/frontline/upstream/bad_gateway): the connection failed rather than timed out. * [`err:frontline:upstream:service_unavailable`](/docs/errors/frontline/upstream/service_unavailable): the instance refused the connection outright. * [`err:user:bad_request:request_timeout`](/docs/errors/user/bad_request/request_timeout): the Unkey API's own request deadline. # proxy_forward_failed Source: https://www.unkey.com/docs/errors/frontline/upstream/proxy_forward_failed A reserved gateway code for failing to forward a request that no code path returns today. `err:frontline:upstream:proxy_forward_failed` This code is reserved, and the gateway doesn't return it today. Failures to forward a request to the app return [`err:frontline:upstream:bad_gateway`](/docs/errors/frontline/upstream/bad_gateway). If this code were returned, it would have the same status and message: `502 Bad Gateway` with `Unable to connect. Please try again in a few moments.` ## How to fix Treat it like [`bad_gateway`](/docs/errors/frontline/upstream/bad_gateway). Callers should retry with backoff. Owners should read the runtime logs around the time of the request for a crash or a reset connection. If you ever see this code, send the request ID to Unkey support. ## Related errors * [`err:frontline:upstream:bad_gateway`](/docs/errors/frontline/upstream/bad_gateway): the error the gateway actually returns when forwarding fails. * [`err:frontline:upstream:service_unavailable`](/docs/errors/frontline/upstream/service_unavailable): the instance refused or was unreachable. # frontline/upstream/service_unavailable Source: https://www.unkey.com/docs/errors/frontline/upstream/service_unavailable The app instance refused the connection or could not be reached. `err:frontline:upstream:service_unavailable` The gateway sent the request to a running instance of the app and couldn't connect. The instance refused the connection or couldn't be reached. You get `503 Service Unavailable`. This isn't [`err:unkey:application:service_unavailable`](/docs/errors/unkey/application/service_unavailable), which the Unkey API returns when it fails on our side. ```json Example response theme={"system"} { "meta": { "requestId": "req_3k2j1h0g9f8e7d6c" }, "error": { "code": "err:frontline:upstream:service_unavailable", "message": "The service is temporarily unavailable. Please try again later." } } ``` Browsers get the same error as an HTML page. ## If you're calling an API hosted on Unkey Retry after a few seconds. A refused connection during a restart clears on its own. If it keeps happening, only the API owner can fix it. Send them the request ID. ## If you own this API The gateway tries your other instances before it gives up, so none of them accepted the connection. Common causes: * The process hasn't finished starting yet. * The process exited. * The process listens on a different port than your runtime settings say. * The process listens only on localhost instead of all interfaces. ### How to fix 1. Check the runtime logs to see whether the process starts and stays up. 2. Make sure the port in the runtime settings matches the port your process listens on, and that it listens on all interfaces. 3. If the process is slow to start, check the health check settings so we can tell when it's ready. If instances keep being unreachable and none of these apply, contact Unkey support with the request ID. Runtime settings, ports, health checks, and runtime logs are Compute features. ## Related errors * [`err:frontline:upstream:bad_gateway`](/docs/errors/frontline/upstream/bad_gateway): the connection opened and then failed. * [`err:frontline:upstream:gateway_timeout`](/docs/errors/frontline/upstream/gateway_timeout): the instance didn't answer in time. * [`err:frontline:capacity:no_running_instances`](/docs/errors/frontline/capacity/no_running_instances): no instance was running at all. * [`err:unkey:application:service_unavailable`](/docs/errors/unkey/application/service_unavailable): the Unkey API failed on our side. # Error codes Source: https://www.unkey.com/docs/errors/overview What every Unkey error code means and how to fix it. Every Unkey error has a code shaped like `err:{system}:{category}:{specific}`, and every code has a page here at `/errors/{system}/{category}/{specific}`. Each page tells you what the error means, shows an example response, and lists the likely causes and how to fix them. ## Reading an error code The first part tells you where the error came from. | System | Meaning | Returned by | | - | - | - | | `user` | Your request can't be processed as sent: a malformed body, a query that breaks a rule, or a rate limit you went over. | The API at `api.unkey.com` | | `unkey` | The API understood the request but couldn't complete it: authentication, permissions, a missing or conflicting resource, a failed precondition, or a failure on our side. | The API at `api.unkey.com` | | `frontline` | The gateway in front of an app on Compute rejected a request or couldn't forward it. Often the person who sees it is calling your API, not you. | The gateway on your app's domains | The second part groups codes by kind, and the third names the exact problem. Always read the whole code, because some names appear in two categories. `err:unkey:authentication:key_not_found` is about the root key you authenticated with. `err:unkey:data:key_not_found` is about a key you referenced in the request. ## What the API error body looks like The API returns errors as JSON: * `meta.requestId` identifies the request. Include it when you contact support. * `error.type` is the error's page in this catalog. * `error.status` repeats the HTTP status. * `error.title` is a short label. Match on `error.type`, never on the title. * `error.detail` explains what went wrong this time, often with the exact value or permission. ```json Not found theme={"system"} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "error": { "detail": "We could not find the requested key.", "status": 404, "title": "Not Found", "type": "/errors/unkey/data/key_not_found" } } ``` Validation failures (`err:unkey:application:invalid_input`) add an `errors` array. Each entry has a `location`, such as `/required`, and a `message`, such as `missing property 'apiId'`. Some entries also have a `fix` that says what to change. ```json Validation failure theme={"system"} { "meta": { "requestId": "req_8f7g6h5j4k3l2m1n" }, "error": { "detail": "POST request body for '/v2/keys.createKey' failed to validate schema", "errors": [ { "location": "/required", "message": "missing property 'apiId'" } ], "status": 400, "title": "Bad Request", "type": "/errors/unkey/application/invalid_input" } } ``` Don't guess the status from the code's name. Every `err:unkey:data:*_not_found` code is 404, but [`err:unkey:authentication:key_not_found`](/docs/errors/unkey/authentication/key_not_found) and [`err:unkey:authentication:portal_session_not_found`](/docs/errors/unkey/authentication/portal_session_not_found) are 401, because what's missing is your credential. A 5xx status means the failure is on our side, not in your request. Retry with backoff if the request is safe to repeat, and contact support with the request ID if it keeps happening. ## What the gateway error body looks like The gateway in front of apps on Compute returns a smaller body: a request ID, a `code`, and a `message`. Each error's page gives its status. ```json Gateway error theme={"system"} { "meta": { "requestId": "req_3k2j1h0g9f8e7d6c" }, "error": { "code": "err:frontline:client:invalid_key", "message": "Authentication failed. The provided API key is invalid." } } ``` You get this JSON when your request asks for it: an `Accept` header of `application/json`, `application/*`, or `*/*` without `text/html`. Plain `curl` sends `*/*`, so it gets JSON. Everything else, including a browser, gets an HTML page with the same code, message, and request ID. ## Categories ### Unkey errors The root key or session token is missing, malformed, or unknown. Platform. You are identified but not allowed: missing permission, disabled key or workspace, expired key. Platform. A resource does not exist, was deleted, or already exists. API Management and Compute. Validation beyond the schema, preconditions including the deployment lifecycle, delete protection, and failures on our side. A plan allowance is exhausted. Today that only covers Compute custom domains. ### User errors Body size, timeouts, disconnects, permission query syntax, and analytics query rules. An analytics query was valid but exceeded its time, memory, or row budget. The analytics query quota. ### Gateway errors The request to your app was rejected by a gateway policy: missing or invalid key, permissions, rate limit, firewall, schema. The deployment is offline, has no running instances, or the workspace reached its spend limit. No deployment or configuration matches the requested host. Your app did not answer, answered badly, or took too long. The gateway configuration for the deployment is invalid. The gateway failed on our side. # assertion_failed Source: https://www.unkey.com/docs/errors/unkey/application/assertion_failed A check that should never fail did, which means a bug on our side rather than a problem with your request. `err:unkey:application:assertion_failed` Something went wrong on our side. It's a bug in Unkey, not a problem with your request. You get HTTP 500 with an empty `detail`. ```json Example response theme={"system"} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "error": { "detail": "", "status": 500, "title": "Internal Server Error", "type": "/errors/unkey/application/assertion_failed" } } ``` ## How to fix 1. Retry once, with backoff, if the request is safe to repeat. 2. If it fails again, contact support with the `requestId`, the endpoint, and the time. We need the `requestId` to find what failed. ## Related errors * [`err:unkey:application:unexpected_error`](/docs/errors/unkey/application/unexpected_error): another failure on our side with no more specific code. * [`err:unkey:application:service_unavailable`](/docs/errors/unkey/application/service_unavailable): the API failed on our side and `detail` names what failed. # deployment_is_current Source: https://www.unkey.com/docs/errors/unkey/application/deployment_is_current The deployment is already the app's current deployment. `err:unkey:application:deployment_is_current` The deployment you're promoting or rolling back to is already the current deployment. You get HTTP 412. ```json Example response theme={"system"} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "error": { "detail": "The deployment is already the current deployment.", "status": 412, "title": "Precondition Failed", "type": "/errors/unkey/application/deployment_is_current" } } ``` ## Likely causes * You rolled back to the deployment that's already current. That's always rejected, because there's nothing to change. * You promoted the deployment that's already current. The one exception: after a rollback, promoting the current deployment confirms the rollback and is allowed. ## How to fix 1. Check which deployment is current with `apps.getApp` or `deployments.listDeployments` before you promote. 2. If you meant to redeploy, use `deployments.createDeployment` instead. ## Related errors * [`err:unkey:application:deployment_no_current`](/docs/errors/unkey/application/deployment_no_current): the app has no current deployment. * [`err:unkey:application:deployment_not_ready`](/docs/errors/unkey/application/deployment_not_ready): the target isn't ready. # deployment_is_production Source: https://www.unkey.com/docs/errors/unkey/application/deployment_is_production Production deployments cannot be stopped or started directly. `err:unkey:application:deployment_is_production` You tried to stop or start a production deployment, and you can't do that directly. You get HTTP 412. ```json Example response theme={"system"} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "error": { "detail": "Production deployments cannot be stopped.", "status": 412, "title": "Precondition Failed", "type": "/errors/unkey/application/deployment_is_production" } } ``` ## Likely causes * You called `deployments.stopDeployment` or `deployments.startDeployment` on a production deployment. Stop and start are only for preview deployments. ## How to fix 1. To take a production deployment out of service, promote or roll back to another production deployment with `deployments.promoteDeployment` or `deployments.rollbackDeployment`. 2. Use stop and start only on preview deployments. See [Production and preview](/docs/compute/concepts/production-and-preview) for what each kind of environment allows. ## Related errors * [`err:unkey:application:deployment_not_production`](/docs/errors/unkey/application/deployment_not_production): the opposite case: promote and rollback need a production deployment. * [`err:unkey:application:deployment_not_running`](/docs/errors/unkey/application/deployment_not_running): stop was called on a deployment that isn't running. # deployment_is_stopping Source: https://www.unkey.com/docs/errors/unkey/application/deployment_is_stopping A stop is already in progress for this deployment. `err:unkey:application:deployment_is_stopping` You asked to stop a deployment that's already stopping. You get HTTP 412. ```json Example response theme={"system"} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "error": { "detail": "The deployment is already stopping.", "status": 412, "title": "Precondition Failed", "type": "/errors/unkey/application/deployment_is_stopping" } } ``` ## Likely causes * You called `deployments.stopDeployment` again before the first stop finished. A stop takes effect right away, but the status only reads stopped once the last instance has shut down. ## How to fix 1. Nothing to do: the first stop is already in progress. Poll `deployments.getDeployment` until the status reads stopped. 2. If you want the deployment back, call `deployments.startDeployment`. You don't have to wait for the stop to finish. ## Related errors * [`err:unkey:application:deployment_not_running`](/docs/errors/unkey/application/deployment_not_running): the deployment isn't running at all. * [`err:unkey:application:deployment_not_stopped`](/docs/errors/unkey/application/deployment_not_stopped): start was called on a deployment that isn't stopped. # deployment_no_current Source: https://www.unkey.com/docs/errors/unkey/application/deployment_no_current The app has no current deployment, so there is nothing to promote or roll back from. `err:unkey:application:deployment_no_current` The app has no current deployment yet, so there's nothing to promote over or roll back from. You get HTTP 412. ```json Example response theme={"system"} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "error": { "detail": "The app has no current deployment.", "status": 412, "title": "Precondition Failed", "type": "/errors/unkey/application/deployment_no_current" } } ``` ## Likely causes * No production deployment for this app has succeeded yet. The first one that succeeds becomes current on its own. ## How to fix 1. Create a production deployment with `deployments.createDeployment` and let it finish. It becomes current on its own. 2. Use promote and rollback later, to move between deployments that already exist. ## Related errors * [`err:unkey:application:deployment_is_current`](/docs/errors/unkey/application/deployment_is_current): the target is already the current deployment. * [`err:unkey:application:deployment_not_ready`](/docs/errors/unkey/application/deployment_not_ready): the target isn't ready. # deployment_not_production Source: https://www.unkey.com/docs/errors/unkey/application/deployment_not_production Only production deployments can be promoted or rolled back to. `err:unkey:application:deployment_not_production` You asked to promote or roll back to a preview deployment. Only a deployment in a production environment can become the one serving your production hostname. You get HTTP 412. ```json Example response theme={"system"} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "error": { "detail": "Only production deployments can be promoted or rolled back.", "status": 412, "title": "Precondition Failed", "type": "/errors/unkey/application/deployment_not_production" } } ``` ## Likely causes * The deployment ID is from a preview environment. Preview deployments have their own URLs and never serve the production hostname. ## How to fix 1. Deploy the same commit or image to the production environment with `deployments.createDeployment`. 2. Promote that deployment. To test a preview deployment, use its own URL. See [Production and preview](/docs/compute/concepts/production-and-preview) for how the two differ. ## Related errors * [`err:unkey:application:deployment_not_ready`](/docs/errors/unkey/application/deployment_not_ready): the target isn't ready. * [`err:unkey:application:deployment_is_production`](/docs/errors/unkey/application/deployment_is_production): the opposite case: stop and start don't apply to production. # deployment_not_ready Source: https://www.unkey.com/docs/errors/unkey/application/deployment_not_ready The deployment to promote or roll back to is not ready to serve traffic. `err:unkey:application:deployment_not_ready` The deployment you're promoting or rolling back to can't take traffic yet. You get HTTP 412. ```json Example response theme={"system"} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "error": { "detail": "The deployment is not ready.", "status": 412, "title": "Precondition Failed", "type": "/errors/unkey/application/deployment_not_ready" } } ``` ## Likely causes * The deployment is still building. The message is `The deployment is not ready.` * The deployment failed to build or never passed its health checks. Same message. * The deployment is shutting down. The message is `The deployment is shutting down and cannot serve traffic.` ## How to fix 1. Read the deployment with `deployments.getDeployment`. If `promote` or `rollback` isn't in its `availableActions`, the call will fail. 2. If it's still building, wait for `status` to reach `ready`. 3. If it failed, fix the build and deploy again. 4. If it's shutting down, promote a different deployment. ## Related errors * [`err:unkey:application:deployment_not_production`](/docs/errors/unkey/application/deployment_not_production): the deployment is in a preview environment. * [`err:unkey:application:deployment_no_current`](/docs/errors/unkey/application/deployment_no_current): the app has no current deployment yet. * [`err:unkey:application:deployment_is_current`](/docs/errors/unkey/application/deployment_is_current): the deployment is already live. * [`err:unkey:data:deployment_not_found`](/docs/errors/unkey/data/deployment_not_found): the deployment id doesn't exist in this workspace. # deployment_not_running Source: https://www.unkey.com/docs/errors/unkey/application/deployment_not_running The deployment is not running, so there is nothing to stop. `err:unkey:application:deployment_not_running` You asked to stop a deployment that isn't running. You can only stop a deployment that's ready and running. You get HTTP 412. ```json Example response theme={"system"} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "error": { "detail": "The deployment is not running.", "status": 412, "title": "Precondition Failed", "type": "/errors/unkey/application/deployment_not_running" } } ``` ## Likely causes * The deployment is already stopped. * The deployment is still building. * The deployment failed. A deployment that's in the middle of stopping returns [`err:unkey:application:deployment_is_stopping`](/docs/errors/unkey/application/deployment_is_stopping) instead. ## How to fix 1. Read the deployment with `deployments.getDeployment`. `stop` is in its `availableActions` only when the call would succeed. 2. If it's already stopped, there's nothing to do. 3. If it's still building, wait for `status` to reach `ready`. ## Related errors * [`err:unkey:application:deployment_is_stopping`](/docs/errors/unkey/application/deployment_is_stopping): a stop is already in progress. * [`err:unkey:application:deployment_is_production`](/docs/errors/unkey/application/deployment_is_production): production deployments can't be stopped. * [`err:unkey:application:deployment_not_stopped`](/docs/errors/unkey/application/deployment_not_stopped): the mirror case on start. # deployment_not_stopped Source: https://www.unkey.com/docs/errors/unkey/application/deployment_not_stopped The deployment is not stopped, so there is nothing to start. `err:unkey:application:deployment_not_stopped` You asked to start a deployment that isn't stopped. You get HTTP 412. ```json Example response theme={"system"} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "error": { "detail": "The deployment is not stopped.", "status": 412, "title": "Precondition Failed", "type": "/errors/unkey/application/deployment_not_stopped" } } ``` ## Likely causes * The deployment is already running. * The deployment is still building. * The deployment failed. A deployment that's still shutting down after a stop counts as stopped, so you can start it right away. ## How to fix 1. Read the deployment with `deployments.getDeployment`. `start` is in its `availableActions` only when the call would succeed. 2. If it's already running, there's nothing to do. 3. If it never became ready, create a new deployment instead. ## Related errors * [`err:unkey:application:deployment_is_production`](/docs/errors/unkey/application/deployment_is_production): production deployments can't be started directly. * [`err:unkey:application:deployment_not_running`](/docs/errors/unkey/application/deployment_not_running): the mirror case on stop. * [`err:unkey:application:precondition_failed`](/docs/errors/unkey/application/precondition_failed): the workspace is paused at its spend limit, which also blocks start. # invalid_environment_settings Source: https://www.unkey.com/docs/errors/unkey/application/invalid_environment_settings The environment's settings would fail the deploy pipeline, so the deployment was refused. `err:unkey:application:invalid_environment_settings` The environment you're deploying to has settings a deployment can't run with, so `deployments.createDeployment` didn't start one. You get HTTP 400, and `detail` lists every problem, separated by semicolons, with your current value in parentheses. ```json Example response theme={"system"} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "error": { "detail": "This environment cannot be deployed: CPU millicores must be at least 250 (is 100). Update the environment's settings before deploying.", "status": 400, "title": "Bad Request", "type": "/errors/unkey/application/invalid_environment_settings", "errors": [] } } ``` ## Likely causes * CPU is below 250 millicores. * Memory is below 256 MiB. * The environment has no region it can run in. * The port isn't between 1 and 65535. * You're deploying from git to an environment created for prebuilt images, which has no build settings. The message names the environment slug. ## How to fix 1. Update the environment in the dashboard or with `environments.updateSettings` so every problem in `detail` is fixed. 2. Create the deployment again. These values live on the environment, not in your request, so retrying with different arguments won't help. Nothing was created, so there's no deployment to clean up. ## Related errors * [`err:unkey:data:environment_not_found`](/docs/errors/unkey/data/environment_not_found): the project, app, or environment doesn't exist. * [`err:unkey:application:precondition_failed`](/docs/errors/unkey/application/precondition_failed): another precondition such as a missing repository connection blocked the deployment. * [`err:unkey:application:invalid_input`](/docs/errors/unkey/application/invalid_input): a field of the request itself is invalid. # invalid_input Source: https://www.unkey.com/docs/errors/unkey/application/invalid_input The request failed validation, either against the endpoint's schema or against a rule the endpoint checks in code. `err:unkey:application:invalid_input` A value in your request isn't valid. You get HTTP 400, and the response says what's wrong. Any endpoint can return it. ```json Example response theme={"system"} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "error": { "detail": "Metadata is too large: it must be at most 1.0 MB, but is 1.4 MB.", "status": 400, "title": "Bad Request", "type": "/errors/unkey/application/invalid_input", "errors": [] } } ``` ## Likely causes * A required field is missing, or a field has the wrong type, an unknown enum value, or is too long. The `errors` array lists each problem, and `detail` reads like `POST request body for '/v2/keys.createKey' failed to validate schema`. * The `Authorization` header is missing. * A value breaks a rule the endpoint checks, such as a size limit on `meta` or environment variable values, a domain that isn't a fully qualified name or is a public suffix, an image reference we can't parse, or rate limit and credit settings that contradict each other. Here `errors` is empty and `detail` explains. Size limits count bytes, while `maxLength` counts characters. A value with multibyte characters can pass `maxLength` and still be too large. ## How to fix 1. If `errors` has entries, fix each one. `location` is the path that failed (such as `/required`) and `message` says why. 2. Otherwise, follow the rule in `detail`. Size errors show both the limit and your value, so trim to the limit. 3. For domains, send a bare host name such as `api.acme.com`, with no scheme, port, or path. 4. For images, send a full reference such as `ghcr.io/acme/app:1.2.3`. If the message doesn't match what you think you sent, check for hidden characters or encoding differences. ## Related errors * [`err:unkey:application:invalid_environment_settings`](/docs/errors/unkey/application/invalid_environment_settings): the environment's settings, not your request, block a deployment. * [`err:user:bad_request:request_body_too_large`](/docs/errors/user/bad_request/request_body_too_large): the whole body exceeded the size cap. * [`err:user:bad_request:permissions_query_syntax_error`](/docs/errors/user/bad_request/permissions_query_syntax_error): a `keys.verifyKey` permissions query couldn't be parsed. # precondition_failed Source: https://www.unkey.com/docs/errors/unkey/application/precondition_failed The request was valid but the current state does not allow the operation yet. `err:unkey:application:precondition_failed` Your request is valid, but something about your setup has to change first. You get HTTP 412, and `detail` says what. ```json Example response theme={"system"} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "error": { "detail": "The requested API is not set up to handle keys.", "status": 412, "title": "Precondition Failed", "type": "/errors/unkey/application/precondition_failed" } } ``` ## Likely causes In API Management: * The API isn't set up for keys (`The requested API is not set up to handle keys.`). * You asked for key recovery (`recoverable: true` or `decrypt: true`) on an API that doesn't store recoverable keys. * A rate limit you named in `keys.verifyKey` isn't set on the key or its identity. In Compute: * The workspace has no active Compute plan, or compute is paused because the workspace reached its spend limit. * You asked for a git deployment, but the app has no connected GitHub repository, or the Unkey GitHub App isn't installed or can't access the repository. * You redeployed a deployment that never produced an image. * The domain you're verifying is already verified. ## How to fix Do what `detail` says, then retry: * Use an API with key support, or check the API ID. * Turn on recoverable keys for the API before you ask for `recoverable: true` or `decrypt: true`. * Set the named rate limit on the key or identity before you check it. * Connect the repository, or install the GitHub App from the link in the message. * Choose a Compute plan under [Plans](/docs/platform/billing/plans), or raise or remove the spend budget under **Settings > Billing**. * For an already verified domain, there's nothing to do. ## Related errors * [`err:unkey:application:deployment_not_ready`](/docs/errors/unkey/application/deployment_not_ready): a deployment lifecycle precondition on promote or rollback. * [`err:unkey:application:invalid_environment_settings`](/docs/errors/unkey/application/invalid_environment_settings): the environment's settings block the deployment. * [`err:unkey:data:analytics_not_configured`](/docs/errors/unkey/data/analytics_not_configured): analytics isn't set up for the workspace. * [`err:unkey:application:protected_resource`](/docs/errors/unkey/application/protected_resource): a delete was blocked by delete protection. # protected_resource Source: https://www.unkey.com/docs/errors/unkey/application/protected_resource The delete was refused because the resource has delete protection enabled. `err:unkey:application:protected_resource` You tried to delete something that has delete protection turned on. Nothing was deleted. You get HTTP 412. ```json Example response theme={"system"} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "error": { "detail": "This API has delete protection enabled. Disable it before attempting to delete.", "status": 412, "title": "Resource is protected", "type": "/errors/unkey/application/protected_resource" } } ``` ## Likely causes * You called `apis.deleteApi`, `apps.deleteApp`, or `projects.deleteProject` on a resource with delete protection on. The message names the resource type. ## How to fix 1. Make sure you really mean to delete it. Deleting an API from the dashboard also deletes its keyspace and every key in it. Deleting a project removes its apps, environments, and deployments. 2. Turn off delete protection in the dashboard, or with the resource's update endpoint where it has the setting. See [Delete protection](/docs/platform/security/delete-protection) for where the switch is. 3. Delete it again. Turning protection on or off is recorded in the audit log. ## Related errors * [`err:unkey:data:api_not_found`](/docs/errors/unkey/data/api_not_found): the API you tried to delete doesn't exist. * [`err:unkey:data:app_not_found`](/docs/errors/unkey/data/app_not_found): the app doesn't exist. * [`err:unkey:data:project_not_found`](/docs/errors/unkey/data/project_not_found): the project doesn't exist. # unkey/application/service_unavailable Source: https://www.unkey.com/docs/errors/unkey/application/service_unavailable The API failed on our side and couldn't complete the request, so retry with backoff. `err:unkey:application:service_unavailable` The API failed on our side and couldn't finish your request. You get HTTP 500, and `detail` names what failed. Unless `detail` says otherwise, the request didn't take effect. ```json Example response theme={"system"} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "error": { "detail": "Failed to create deployment.", "status": 500, "title": "Internal Server Error", "type": "/errors/unkey/application/service_unavailable" } } ``` ## How to fix 1. Retry with exponential backoff, and check the Unkey status page for an incident. 2. If it keeps failing, contact support with the `requestId`. Two cases need something else: * **HTTP 503 on `domains.listDomains`.** Your filters match too many domains. The `detail` is `The domain scan limit was reached. Narrow the project, app, environment, or search filters and retry.` Narrow the filters. * **`Resource limits are not configured for this workspace.`** Your workspace is missing its plan limits. Retrying won't help, so contact support. ## Related errors * [`err:unkey:application:unexpected_error`](/docs/errors/unkey/application/unexpected_error): another failure on our side with no more specific code. * [`err:unkey:data:analytics_connection_failed`](/docs/errors/unkey/data/analytics_connection_failed): analytics queries couldn't run because of a failure on our side, reported as 503. * [`err:user:bad_request:request_timeout`](/docs/errors/user/bad_request/request_timeout): the request ran out of time instead. # unexpected_error Source: https://www.unkey.com/docs/errors/unkey/application/unexpected_error Something failed on our side that has no more specific error code. `err:unkey:application:unexpected_error` Something failed on our side, and it isn't caused by your request. You get HTTP 500. Any endpoint can return it. ```json Example response theme={"system"} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "error": { "detail": "An unexpected error occurred while processing your request.", "status": 500, "title": "Internal Server Error", "type": "/errors/unkey/application/unexpected_error" } } ``` ## How to fix 1. Retry once, with backoff, if the request is safe to repeat. Don't retry in a tight loop. 2. If it fails again, contact support with the `requestId`, the endpoint, and a redacted copy of the request. ## Related errors * [`err:unkey:application:service_unavailable`](/docs/errors/unkey/application/service_unavailable): the API failed on our side and `detail` names what failed. * [`err:unkey:application:assertion_failed`](/docs/errors/unkey/application/assertion_failed): a bug on our side, with an empty `detail`. # unkey/authentication/key_not_found Source: https://www.unkey.com/docs/errors/unkey/authentication/key_not_found The bearer token is not a root key the API recognizes. `err:unkey:authentication:key_not_found` The bearer token you sent isn't a valid root key. You get HTTP 401. ```json Example response theme={"system"} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "error": { "detail": "The provided root key is invalid. We could not find the requested key.", "status": 401, "title": "Unauthorized", "type": "/errors/unkey/authentication/key_not_found" } } ``` ## Likely causes * The root key was deleted or rotated. * The token has a typo or was copied incompletely. * You sent one of your own API keys instead of a root key. Then `detail` is just `The provided root key is invalid.`, without the second sentence. ## How to fix 1. Check the token against the root key you created in the dashboard. Root keys start with `unkey_`. 2. If you rotated or deleted the key, create a new one and update every service that uses it. 3. Don't send your own API keys in the header. They go in the body of `keys.verifyKey`. This is about the key you authenticate with. A key you manage that doesn't exist is [`err:unkey:data:key_not_found`](/docs/errors/unkey/data/key_not_found). A `keys.verifyKey` result of `valid: false` with code `NOT_FOUND` isn't an error: the call worked and is telling you about one of your keys. ## Related errors * [`err:unkey:authorization:key_disabled`](/docs/errors/unkey/authorization/key_disabled): the root key exists but is disabled. * [`err:unkey:authorization:workspace_disabled`](/docs/errors/unkey/authorization/workspace_disabled): the key's workspace is disabled. * [`err:unkey:data:key_not_found`](/docs/errors/unkey/data/key_not_found): a key referenced in a request body or path doesn't exist. # malformed Source: https://www.unkey.com/docs/errors/unkey/authentication/malformed The Authorization header is not in the form Bearer . `err:unkey:authentication:malformed` The API found an `Authorization` header but couldn't read a credential from it. The header must be `Bearer`, one space, and the token. You get HTTP 400. ```json Example response theme={"system"} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "error": { "detail": "You must provide a valid root key in the Authorization header in the format 'Bearer ROOT_KEY'. Your authorization header is missing the 'Bearer ' prefix.", "status": 400, "title": "Bad Request", "type": "/errors/unkey/authentication/malformed", "errors": [] } } ``` ## Likely causes * The `Bearer ` prefix is missing, or you used a different scheme such as `Basic`. * The token after the prefix is empty. * Whitespace, a newline, or quotes got in when the key was copied, or a client library added its own scheme. * A dashboard session token has expired or doesn't validate. The message is `Invalid bearer token.` ## How to fix 1. Send the header as `Authorization: Bearer unkey_xxx`, with one space and no quotes. 2. Check for stray whitespace or a newline, and for a client library that adds its own scheme. 3. If you're using a dashboard session token, sign in again. ## Related errors * [`err:unkey:application:invalid_input`](/docs/errors/unkey/application/invalid_input): the header is missing entirely. * [`err:unkey:authentication:missing`](/docs/errors/unkey/authentication/missing): the endpoint doesn't accept the kind of credential you sent. * [`err:unkey:authentication:key_not_found`](/docs/errors/unkey/authentication/key_not_found): the token is readable but isn't a valid root key. # missing Source: https://www.unkey.com/docs/errors/unkey/authentication/missing The endpoint didn't accept the kind of credential you sent, so it couldn't identify you. `err:unkey:authentication:missing` The endpoint didn't find a credential it accepts, so it couldn't tell who you are. You get HTTP 400. ```json Example response theme={"system"} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "error": { "detail": "You must provide a bearer credential in the Authorization header.", "status": 400, "title": "Bad Request", "type": "/errors/unkey/authentication/missing", "errors": [] } } ``` ## Likely causes * You called a portal endpoint (`portal.listKeys`, `portal.rerollKey`, `portal.getVerifications`) with an `Authorization` header. Those endpoints only accept the `portal_session` cookie, and ignore it when an `Authorization` header is present. The message is `You must provide a bearer credential in the Authorization header.` * You put the root key in the body or a custom header instead of `Authorization`. * Rarely, an endpoint that needs a signed-in caller got a request without one. The message is `You must authenticate before accessing this resource.` A request with no `Authorization` header at all returns [`err:unkey:application:invalid_input`](/docs/errors/unkey/application/invalid_input) instead. A header that isn't in the form `Bearer ` returns [`err:unkey:authentication:malformed`](/docs/errors/unkey/authentication/malformed). ## How to fix 1. Send your root key as a bearer token on every request: ```bash theme={"system"} curl -X POST https://api.unkey.com/v2/keys.verifyKey \ -H "Authorization: Bearer unkey_xxx" \ -H "Content-Type: application/json" \ -d '{"key": "sk_live_xxx"}' ``` The key you're verifying goes in the body. The root key goes in the header. 2. On a portal endpoint, drop the `Authorization` header and send the `portal_session` cookie instead. Create or rotate root keys in the dashboard under Settings. See [Root keys](/docs/platform/root-keys/overview). ## Related errors * [`err:unkey:authentication:malformed`](/docs/errors/unkey/authentication/malformed): the header is present but not `Bearer `. * [`err:unkey:application:invalid_input`](/docs/errors/unkey/application/invalid_input): the `Authorization` header is missing. * [`err:unkey:authentication:key_not_found`](/docs/errors/unkey/authentication/key_not_found): the token is well formed but isn't a valid root key. # portal_session_not_found Source: https://www.unkey.com/docs/errors/unkey/authentication/portal_session_not_found The portal session token or exchange code is unknown, expired, or already used. `err:unkey:authentication:portal_session_not_found` The portal session token or exchange code you sent doesn't match an active session. You get HTTP 401. ```json Example response theme={"system"} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "error": { "detail": "The portal session is invalid or has expired.", "status": 401, "title": "Unauthorized", "type": "/errors/unkey/authentication/portal_session_not_found" } } ``` ## Likely causes * The session in the `portal_session` cookie expired or was revoked. The message is the same for an unknown token, so it doesn't say which. * On `portal.exchangeCode`, the code was already used, has expired, or was never issued. The message is `Session is invalid, expired, or has already been used.` A code works once, so a second exchange fails even if the first succeeded. ## How to fix 1. Start a new session from your backend with `portal.createSession` and complete the exchange promptly. 2. If your frontend retries `portal.exchangeCode` on a slow network, keep the access token from the first success instead of exchanging again. 3. If an active session starts returning this error, send the user back through your login flow. ## Related errors * [`err:unkey:authentication:portal_token_missing`](/docs/errors/unkey/authentication/portal_token_missing): no token was sent at all. * [`err:unkey:data:portal_not_found`](/docs/errors/unkey/data/portal_not_found): the portal itself doesn't exist or isn't visible to your root key. * [`err:unkey:authorization:forbidden`](/docs/errors/unkey/authorization/forbidden): the portal is disabled or can't serve sessions right now. # portal_token_missing Source: https://www.unkey.com/docs/errors/unkey/authentication/portal_token_missing A portal endpoint was called without a portal session token. `err:unkey:authentication:portal_token_missing` You called a portal endpoint with an empty `portal_session` cookie. You get HTTP 400. ```json Example response theme={"system"} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "error": { "detail": "A valid portal session token is required.", "status": 400, "title": "Bad Request", "type": "/errors/unkey/authentication/portal_token_missing", "errors": [] } } ``` ## Likely causes * The session flow didn't finish, so there's no token to send. * The cookie was lost, for example across a redirect. Portal endpoints such as `portal.listKeys`, `portal.rerollKey`, and `portal.getVerifications` use the access token from `portal.exchangeCode` in the `portal_session` cookie, not a root key. A request with no cookie at all returns [`err:unkey:application:invalid_input`](/docs/errors/unkey/application/invalid_input). A token that's unknown, expired, or revoked returns [`err:unkey:authentication:portal_session_not_found`](/docs/errors/unkey/authentication/portal_session_not_found). ## How to fix 1. Finish the session flow: your backend calls `portal.createSession` with a root key to get an exchange code, and the user's browser calls `portal.exchangeCode` with it to get the access token. 2. Send that token in the `portal_session` cookie on every portal request. If the browser calls the portal API directly, make sure the cookie is set after the exchange and survives redirects. 3. Don't send the token as `Authorization: Bearer`. Portal endpoints ignore the cookie when an `Authorization` header is present, and you get [`err:unkey:authentication:missing`](/docs/errors/unkey/authentication/missing). ## Related errors * [`err:unkey:authentication:portal_session_not_found`](/docs/errors/unkey/authentication/portal_session_not_found): the token is present but not an active session. * [`err:unkey:authentication:missing`](/docs/errors/unkey/authentication/missing): a portal endpoint was called with an `Authorization` header instead of the cookie. # forbidden Source: https://www.unkey.com/docs/errors/unkey/authorization/forbidden The request is authenticated but not allowed, for a reason other than a missing permission. `err:unkey:authorization:forbidden` The API knows who you are but won't do this, for a reason other than a missing permission. You get HTTP 403, and `detail` gives the reason. ```json Example response theme={"system"} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "error": { "detail": "Portal is disabled.", "status": 403, "title": "Forbidden", "type": "/errors/unkey/authorization/forbidden" } } ``` ## Likely causes * Your root key has expired. The message starts with `The provided root key is invalid.` and then gives the expiry. * On `portal.createSession`, the portal is disabled: `Portal is disabled.` * On `portal.createSession`, the portal can't serve sessions because the app has no active deployment, the app's gateway policies don't verify keys, or the portal's API was deleted. The message starts with `Portal is not available`. * You called `portal.createSession` with a dashboard session or a portal token: `Portal sessions can only be created with a root key.` * You called a portal-only endpoint without a portal session: `This endpoint is only accessible with a portal session.` * Your dashboard organization has no usable workspace: `Your organization does not have an active workspace.` ## How to fix 1. For an expired root key, create a new one. Use one without an expiry for server-to-server calls. 2. For portal messages, turn the portal on, deploy the app, or add a key verification policy to the app's gateway, then retry. 3. For a portal-only endpoint, send the portal session instead of a root key. For `portal.createSession`, use a root key. 4. For a dashboard session without a workspace, finish setting up the workspace in the dashboard. ## Related errors * [`err:unkey:authorization:insufficient_permissions`](/docs/errors/unkey/authorization/insufficient_permissions): a specific permission is missing. * [`err:unkey:authorization:key_disabled`](/docs/errors/unkey/authorization/key_disabled): the root key is disabled. * [`err:unkey:authorization:workspace_disabled`](/docs/errors/unkey/authorization/workspace_disabled): the workspace is disabled. # unkey/authorization/insufficient_permissions Source: https://www.unkey.com/docs/errors/unkey/authorization/insufficient_permissions The root key is valid but lacks a permission this operation needs. `err:unkey:authorization:insufficient_permissions` Your root key is valid, but it doesn't have a permission this endpoint needs. You get HTTP 403. On most endpoints, `detail` shows the permission that was missing. ```json Example response theme={"system"} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "error": { "detail": "Insufficient permissions to access this resource.", "status": 403, "title": "Insufficient Permissions", "type": "/errors/unkey/authorization/insufficient_permissions" } } ``` ## Likely causes * The root key was never given this permission. * The permission names the wrong resource ID, for example an API ID where the URN needs a keyspace ID. * The request asks for a broader scope than the key's permission, such as listing deployments without environment filters. * Someone saved the key in **Edit root key** and left the permission unchecked. Saving replaces the whole set. A missing permission doesn't always return 403. Most endpoints that read or change a resource you named return a 404 for it instead, so a 404 doesn't prove the resource is gone. Check the root key's permissions before you decide the ID is wrong. ## How to fix 1. Identify the required resource and action. In `unkey:v1:{workspace_id}:{resource_path}#{action}`, the path selects the resource and the part after `#` selects the action. 2. Check the workspace and resource IDs, including every parent in the path. Narrow the request's filters if it asks for more resources than the task needs. 3. Assign the missing permission through the [dashboard or API](/docs/platform/root-keys/configure). For example, `unkey:v1:ws_demo:projects/proj_shop/keyspaces/ks_customers/keys/*#write` permits creating and updating keys in one keyspace. Replace the example IDs with your own. Give each service its own root key with only what it needs, so a leaked key can't do more. See [Root key permissions](/docs/platform/root-keys/permissions). `keys.verifyKey` distinguishes the root key's access from your customer's permissions. If the root key lacks `verify` access, the response has `valid: false` and code `NOT_FOUND`. If the customer's key lacks a permission required by your verification query, the code is `INSUFFICIENT_PERMISSIONS`. ## Related errors * [`err:unkey:authorization:forbidden`](/docs/errors/unkey/authorization/forbidden): the operation isn't allowed for a reason other than a missing permission. * [`err:unkey:authentication:key_not_found`](/docs/errors/unkey/authentication/key_not_found): the bearer token isn't a root key at all. * [`err:user:bad_request:permissions_query_syntax_error`](/docs/errors/user/bad_request/permissions_query_syntax_error): a permissions query you wrote for `keys.verifyKey` couldn't be parsed. # key_disabled Source: https://www.unkey.com/docs/errors/unkey/authorization/key_disabled The root key used to authenticate has been disabled. `err:unkey:authorization:key_disabled` The root key you're using still exists, but it's disabled, so every request with it fails. You get HTTP 403. ```json Example response theme={"system"} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "error": { "detail": "The provided root key is invalid. The key is disabled.", "status": 403, "title": "Key is disabled", "type": "/errors/unkey/authorization/key_disabled" } } ``` Keys you check with `keys.verifyKey` never return this error. A disabled key there comes back as `valid: false` with code `DISABLED`. ## How to fix 1. Create a new root key under **Settings > Root Keys**, with only the permissions the service needs. 2. Switch the service to the new key and delete the disabled one. 3. If you don't know why the key was disabled, contact support with the `requestId`. ## Related errors * [`err:unkey:authentication:key_not_found`](/docs/errors/unkey/authentication/key_not_found): the key doesn't exist or was deleted. * [`err:unkey:authorization:workspace_disabled`](/docs/errors/unkey/authorization/workspace_disabled): the whole workspace is disabled. * [`err:unkey:authorization:forbidden`](/docs/errors/unkey/authorization/forbidden): the key is expired. # workspace_disabled Source: https://www.unkey.com/docs/errors/unkey/authorization/workspace_disabled The workspace that owns the root key is disabled. `err:unkey:authorization:workspace_disabled` Your workspace is disabled, so every request made with its root keys fails, whatever the state of the key. You get HTTP 403. A dashboard session for a disabled workspace gets the same error with `Your organization does not have an active workspace.` ```json Example response theme={"system"} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "error": { "detail": "The provided root key is invalid. The workspace is disabled.", "status": 403, "title": "Workspace is disabled", "type": "/errors/unkey/authorization/workspace_disabled" } } ``` ## How to fix You can't turn a workspace back on yourself. Contact support with the `requestId` and your workspace ID, and ask why it was disabled. Until it's back on, nothing works for the workspace, including key verification. ## Related errors * [`err:unkey:authorization:key_disabled`](/docs/errors/unkey/authorization/key_disabled): only the key is disabled. * [`err:unkey:data:workspace_not_found`](/docs/errors/unkey/data/workspace_not_found): the key points at a workspace that no longer exists. # analytics_connection_failed Source: https://www.unkey.com/docs/errors/unkey/data/analytics_connection_failed Analytics queries couldn't run because of a failure on our side, so retry with backoff. `err:unkey:data:analytics_connection_failed` Your analytics query couldn't run because of a failure on our side. Nothing about the query caused it. You get HTTP 503. ```json Example response theme={"system"} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "error": { "detail": "Failed to connect to ClickHouse analytics database", "status": 503, "title": "Service Unavailable", "type": "/errors/unkey/data/analytics_connection_failed" } } ``` ## How to fix 1. Retry with exponential backoff. It usually clears quickly. 2. Check the Unkey status page for an incident. 3. If it keeps failing, contact support with the `requestId`. ## Related errors * [`err:unkey:data:analytics_not_configured`](/docs/errors/unkey/data/analytics_not_configured): analytics isn't enabled for your workspace. * [`err:unkey:application:service_unavailable`](/docs/errors/unkey/application/service_unavailable): other failures on our side, reported as 500. * [`err:user:unprocessable_entity:query_execution_timeout`](/docs/errors/user/unprocessable_entity/query_execution_timeout): the query ran but timed out. # analytics_not_configured Source: https://www.unkey.com/docs/errors/unkey/data/analytics_not_configured Analytics isn't enabled for your workspace, so the analytics endpoints can't run. `err:unkey:data:analytics_not_configured` Analytics isn't turned on for your workspace, so every analytics query fails, whatever it says. You get HTTP 412. ```json Example response theme={"system"} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "error": { "detail": "ClickHouse analytics is not configured for this workspace", "status": 412, "title": "Precondition Failed", "type": "/errors/unkey/data/analytics_not_configured" } } ``` ## How to fix There's no setting for this in the dashboard. Contact support with your workspace ID and ask us to turn on analytics. ## Related errors * [`err:unkey:data:analytics_connection_failed`](/docs/errors/unkey/data/analytics_connection_failed): analytics is on, but the query couldn't run because of a failure on our side. * [`err:user:too_many_requests:query_quota_exceeded`](/docs/errors/user/too_many_requests/query_quota_exceeded): analytics is on and your workspace used up its query quota. # api_not_found Source: https://www.unkey.com/docs/errors/unkey/data/api_not_found The apiId does not match a live API in your workspace. `err:unkey:data:api_not_found` The `apiId` you sent doesn't match an API in your workspace. You get HTTP 404. ```json Example response theme={"system"} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "error": { "detail": "The specified API was not found.", "status": 404, "title": "Not Found", "type": "/errors/unkey/data/api_not_found" } } ``` ## Likely causes * The ID has a typo, or isn't an API ID. API IDs start with `api_`. * The API was deleted. * The API is in another workspace. * Your root key doesn't have permission on the API. `apis.getApi`, `apis.listKeys`, and `keys.createKey` return this 404 instead of a 403, so a 404 doesn't prove the API is gone. ## How to fix 1. Copy the ID from the dashboard or from the `apis.createApi` response. 2. If the ID is right, check that the root key has the permission the endpoint needs on that API. 3. If the API was deleted, create a new one. Deleting an API from the dashboard also deletes its keyspace and all its keys. Deleting it with `apis.deleteApi` removes only the API, and its keys can't be reached through that `apiId` any more. ## Related errors * [`err:unkey:data:key_not_found`](/docs/errors/unkey/data/key_not_found): a key within the API doesn't exist. * [`err:unkey:application:precondition_failed`](/docs/errors/unkey/application/precondition_failed): the API exists but isn't set up for keys or for key recovery. * [`err:unkey:application:protected_resource`](/docs/errors/unkey/application/protected_resource): the API can't be deleted while delete protection is on. # app_already_exists Source: https://www.unkey.com/docs/errors/unkey/data/app_already_exists Another app in the project already uses that slug. `err:unkey:data:app_already_exists` Another app in the same project already uses that slug. Nothing was created or changed. You get HTTP 409. ```json Example response theme={"system"} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "error": { "detail": "An app with slug 'api' already exists in this project.", "status": 409, "title": "Conflicting Resource", "type": "/errors/unkey/data/app_already_exists" } } ``` ## Likely causes * You called `apps.createApp`, or changed an app's slug with `apps.updateApp`, and the slug is taken in this project. * Two requests created the same slug at the same time, and this one lost. ## How to fix 1. Choose a different slug, or use the existing app. Slugs only need to be unique within a project, so you can use the same slug in another project. ## Related errors * [`err:unkey:data:app_not_found`](/docs/errors/unkey/data/app_not_found): the app doesn't exist. * [`err:unkey:data:project_already_exists`](/docs/errors/unkey/data/project_already_exists): a project slug collides. # app_not_found Source: https://www.unkey.com/docs/errors/unkey/data/app_not_found The app does not exist in the project. `err:unkey:data:app_not_found` The app you referenced doesn't exist in the project. You get HTTP 404. ```json Example response theme={"system"} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "error": { "detail": "The requested app does not exist.", "status": 404, "title": "Not Found", "type": "/errors/unkey/data/app_not_found" } } ``` ## Likely causes * The app ID or slug has a typo. * You passed the wrong project, so the app isn't in it. * Your root key doesn't have read permission on the app. `apps.getApp` returns this 404 instead of a 403. ## How to fix 1. List the project's apps with `apps.listApps` and use the returned ID or slug. 2. Make sure the project you pass is the one that owns the app. 3. Check that the root key has read (or the relevant write) permission on the app. ## Related errors * [`err:unkey:data:project_not_found`](/docs/errors/unkey/data/project_not_found): the project doesn't exist. * [`err:unkey:data:environment_not_found`](/docs/errors/unkey/data/environment_not_found): an environment of the app doesn't exist. * [`err:unkey:application:protected_resource`](/docs/errors/unkey/application/protected_resource): the app can't be deleted while delete protection is on. # audit_log_not_found Source: https://www.unkey.com/docs/errors/unkey/data/audit_log_not_found A reserved code that no endpoint returns. `err:unkey:data:audit_log_not_found` This code is reserved, and the API doesn't return it. No endpoint looks up a single audit log entry. ## How to fix There's nothing to fix. You read audit logs in the dashboard. Entries are dropped once they're older than your plan's retention. See [Limits](/docs/platform/billing/limits). ## Related errors * [`err:unkey:data:workspace_not_found`](/docs/errors/unkey/data/workspace_not_found): the workspace doesn't exist. # unkey/data/deployment_not_found Source: https://www.unkey.com/docs/errors/unkey/data/deployment_not_found The deployment does not exist in your workspace. `err:unkey:data:deployment_not_found` The deployment you referenced doesn't exist in your workspace. You get HTTP 404. ```json Example response theme={"system"} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "error": { "detail": "The requested deployment does not exist.", "status": 404, "title": "Not Found", "type": "/errors/unkey/data/deployment_not_found" } } ``` ## Likely causes * The deployment ID has a typo. Deployment IDs start with `d_`. * Your root key doesn't have the permission the call needs. The get, promote, rollback, stop, and start endpoints return this 404 instead of a 403, so it's as likely a permission problem as a wrong ID. * For a redeploy with `deployments.createDeployment`, the source deployment is from a different app or environment. * The deployment is in another workspace. ## How to fix 1. List deployments with `deployments.listDeployments` and use the returned ID. 2. Check that the root key has the right permission: `read` on the deployment for `deployments.getDeployment`, `read` on the deployment's `buildLogs` for `deployments.listBuildLogs`, or `promote_deployment`, `rollback_deployment`, `stop_deployment`, or `start_deployment` for the matching call. 3. For a redeploy, pick a deployment from the same app and environment you're deploying to. A deployment that exists but can't take traffic returns a precondition error instead. ## Related errors * [`err:unkey:application:deployment_not_ready`](/docs/errors/unkey/application/deployment_not_ready): the deployment exists but isn't ready. * [`err:unkey:data:environment_not_found`](/docs/errors/unkey/data/environment_not_found): the environment doesn't exist. * [`err:unkey:data:app_not_found`](/docs/errors/unkey/data/app_not_found): the app doesn't exist. # domain_already_exists Source: https://www.unkey.com/docs/errors/unkey/data/domain_already_exists The custom domain is already attached to your workspace. `err:unkey:data:domain_already_exists` The domain you tried to add is already attached to your workspace, maybe on a different environment. You get HTTP 409. It says nothing about whether another workspace uses the domain. ```json Example response theme={"system"} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "error": { "detail": "The domain 'api.acme.com' is already attached to this workspace.", "status": 409, "title": "Conflicting Resource", "type": "/errors/unkey/data/domain_already_exists" } } ``` ## Likely causes * You already added this domain, on this environment or another one. * You added the domain earlier and it isn't verified yet. ## How to fix 1. Look the domain up with `domains.getDomain` or `domains.listDomains`. 2. If it's on the wrong environment, delete it there and create it on the right one. 3. If it's there but not verified, call `domains.verifyDomain` instead of creating it again. ## Related errors * [`err:unkey:data:domain_not_found`](/docs/errors/unkey/data/domain_not_found): the domain isn't attached to this workspace. * [`err:unkey:limits:custom_domain_limit_exceeded`](/docs/errors/unkey/limits/custom_domain_limit_exceeded): the workspace is at its domain limit. * [`err:unkey:application:precondition_failed`](/docs/errors/unkey/application/precondition_failed): the domain is already verified. # domain_not_found Source: https://www.unkey.com/docs/errors/unkey/data/domain_not_found The custom domain is not attached to your workspace. `err:unkey:data:domain_not_found` The custom domain you referenced isn't attached to your workspace. You get HTTP 404. ```json Example response theme={"system"} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "error": { "detail": "The requested domain does not exist.", "status": 404, "title": "Not Found", "type": "/errors/unkey/data/domain_not_found" } } ``` ## Likely causes * The domain ID or name is wrong, or the domain was never added to this workspace. * Your root key doesn't have permission. `domains.getDomain`, `domains.deleteDomain`, and `domains.verifyDomain` return this 404 instead of a 403, so it's as likely a permission problem as a missing domain. Names are compared in lowercase and with Unicode converted to Punycode, so `MÜNCHEN.de` and `xn--mnchen-3ya.de` are the same domain. ## How to fix 1. List domains with `domains.listDomains` and use the returned ID. 2. If the ID is right, check the root key's permissions before you decide the domain is gone. ## Related errors * [`err:unkey:data:domain_already_exists`](/docs/errors/unkey/data/domain_already_exists): the domain is already attached. * [`err:unkey:data:environment_not_found`](/docs/errors/unkey/data/environment_not_found): the environment the domain should belong to doesn't exist. # environment_not_found Source: https://www.unkey.com/docs/errors/unkey/data/environment_not_found The project, app, or environment in the request did not resolve. `err:unkey:data:environment_not_found` The environment you referenced couldn't be found. An environment is addressed by project, app, and environment together, and one of them didn't match. You get HTTP 404. ```json Example response theme={"system"} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "error": { "detail": "The requested environment does not exist.", "status": 404, "title": "Not Found", "type": "/errors/unkey/data/environment_not_found" } } ``` ## Likely causes * The environment slug is wrong. * The project or app slug is wrong. That's also reported as a missing environment. On `deployments.createDeployment` the message says so: `The requested project, app, or environment does not exist.` * Your root key doesn't have permission on the environment. `environments.getEnvironment` and `domains.createDomain` return this 404 instead of a 403. ## How to fix 1. List the app's environments with `environments.listEnvironments` and use the returned slug. 2. Check all three parts: project, app, and environment. 3. Check that the root key has the permission the endpoint needs on the environment. ## Related errors * [`err:unkey:data:app_not_found`](/docs/errors/unkey/data/app_not_found): the app doesn't exist. * [`err:unkey:application:invalid_environment_settings`](/docs/errors/unkey/application/invalid_environment_settings): the environment exists but its settings block deployments. * [`err:unkey:data:policy_not_found`](/docs/errors/unkey/data/policy_not_found): a gateway policy within the environment doesn't exist. # flag_not_found Source: https://www.unkey.com/docs/errors/unkey/data/flag_not_found NotFound indicates the requested flag definition was not found. `err:unkey:data:flag_not_found` # identity_already_exists Source: https://www.unkey.com/docs/errors/unkey/data/identity_already_exists An identity with that externalId already exists in the workspace. `err:unkey:data:identity_already_exists` An identity with the same `externalId` already exists in your workspace, so `identities.createIdentity` didn't create one. You get HTTP 409. ```json Example response theme={"system"} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "error": { "detail": "Identity with externalId 'user_123' already exists in this workspace.", "status": 409, "title": "Conflicting Resource", "type": "/errors/unkey/data/identity_already_exists" } } ``` ## Likely causes * You already created an identity for this user or tenant. `externalId` is unique per workspace. ## How to fix 1. Use the existing identity: fetch it with `identities.getIdentity`, or change its `meta` or rate limits with `identities.updateIdentity`. 2. To attach keys to it, pass its `externalId` on `keys.createKey`. In provisioning code, treat a 409 as success. ## Related errors * [`err:unkey:data:identity_not_found`](/docs/errors/unkey/data/identity_not_found): the identity doesn't exist. # identity_not_found Source: https://www.unkey.com/docs/errors/unkey/data/identity_not_found The identity does not exist in your workspace. `err:unkey:data:identity_not_found` The identity you referenced doesn't exist in your workspace. You get HTTP 404. ```json Example response theme={"system"} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "error": { "detail": "This identity does not exist.", "status": 404, "title": "Not Found", "type": "/errors/unkey/data/identity_not_found" } } ``` ## Likely causes * You attached a key to an `externalId` before creating the identity. * The ID or `externalId` has a typo or a case difference. `externalId` is matched exactly. * On `keys.createKey`, `keys.updateKey`, or `keys.migrateKeys`, the identity is in a different project from the keyspace. * Your root key doesn't have permission. `identities.updateIdentity` and `identities.deleteIdentity` return this 404 instead of a 403. ## How to fix 1. Create the identity with `identities.createIdentity` before you attach keys to it. 2. Check the `externalId` for typos and case. `identities.listIdentities` shows what exists. 3. On `identities.updateIdentity` or `identities.deleteIdentity`, check the root key's permissions before you decide the identity is gone. ## Related errors * [`err:unkey:data:identity_already_exists`](/docs/errors/unkey/data/identity_already_exists): an identity with that `externalId` already exists. * [`err:unkey:data:key_not_found`](/docs/errors/unkey/data/key_not_found): the key doesn't exist. # key_auth_not_found Source: https://www.unkey.com/docs/errors/unkey/data/key_auth_not_found The keyspaceId passed to the deprecated deploy.createDeployment does not exist. `err:unkey:data:key_auth_not_found` The `keyspaceId` you sent doesn't match a keyspace in your workspace. You get HTTP 404. Only the deprecated `deploy.createDeployment` endpoint returns this, and the `unkey deploy` CLI command still calls it. ```json Example response theme={"system"} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "error": { "detail": "The specified keyspace was not found.", "status": 404, "title": "Not Found", "type": "/errors/unkey/data/key_auth_not_found" } } ``` ## Likely causes * The keyspace ID has a typo, or isn't a keyspace ID. Keyspace IDs start with `ks_`. * The keyspace is in another workspace. ## How to fix 1. Pass the keyspace ID of an API in the same workspace, or leave out `keyspaceId`. 2. For new integrations, use `deployments.createDeployment` and set key auth with `gateway.setPolicies`. A missing keyspace there returns [`err:unkey:data:key_space_not_found`](/docs/errors/unkey/data/key_space_not_found). ## Related errors * [`err:unkey:data:key_space_not_found`](/docs/errors/unkey/data/key_space_not_found): the same problem on the gateway policy endpoints. * [`err:unkey:data:api_not_found`](/docs/errors/unkey/data/api_not_found): the API doesn't exist. * [`err:unkey:data:project_not_found`](/docs/errors/unkey/data/project_not_found): the project doesn't exist. # unkey/data/key_not_found Source: https://www.unkey.com/docs/errors/unkey/data/key_not_found The keyId in the request does not match a live key in your workspace. `err:unkey:data:key_not_found` The `keyId` you sent doesn't match a key in your workspace. You get HTTP 404. This is about a key you manage. If your root key is the problem, you get [`err:unkey:authentication:key_not_found`](/docs/errors/unkey/authentication/key_not_found) instead. ```json Example response theme={"system"} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "error": { "detail": "We could not find the requested key.", "status": 404, "title": "Not Found", "type": "/errors/unkey/data/key_not_found" } } ``` ## Likely causes * You sent the secret key string where a `keyId` is expected. * The key was deleted. * The key ID is from another workspace, such as a staging workspace used with a production root key. * On `keys.whoami`, which takes the key string, the string doesn't match any key. ## How to fix 1. Use the `keyId` returned when the key was created. It starts with `key_`. 2. If you lost it, list keys with `apis.listKeys` to find it. 3. If the key was deleted, create a new one. Deleted keys can't be restored through the API. ## Related errors * [`err:unkey:authentication:key_not_found`](/docs/errors/unkey/authentication/key_not_found): the root key in the `Authorization` header isn't valid. * [`err:unkey:data:api_not_found`](/docs/errors/unkey/data/api_not_found): the API that would own the key doesn't exist. * [`err:unkey:data:key_auth_not_found`](/docs/errors/unkey/data/key_auth_not_found): the keyspace id doesn't exist. # key_space_not_found Source: https://www.unkey.com/docs/errors/unkey/data/key_space_not_found A gateway key auth policy references a keyspace that does not exist in your workspace. `err:unkey:data:key_space_not_found` A gateway key auth policy you're saving points at a keyspace that doesn't exist in your workspace. You get HTTP 404, and `detail` names the first missing keyspace. ```json Example response theme={"system"} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "error": { "detail": "Keyspace \"ks_123\" does not exist.", "status": 404, "title": "Not Found", "type": "/errors/unkey/data/key_space_not_found" } } ``` ## Likely causes * You passed the API ID (`api_...`) instead of the keyspace ID (`ks_...`). They're different values on the same API. * The API behind the keyspace was deleted. Deleting an API from the dashboard also deletes its keyspace, so a policy that used to work can start failing. * The keyspace is in another workspace. ## How to fix 1. Use the keyspace ID shown on the API in the dashboard, or returned by `apis.getApi`. 2. Fix every keyspace ID in the policy. `gateway.setPolicies` and `gateway.updatePolicy` stop at the first missing one, so several bad IDs show up one at a time. ## Related errors * [`err:unkey:data:key_auth_not_found`](/docs/errors/unkey/data/key_auth_not_found): the deprecated deploy endpoint couldn't find a keyspace. * [`err:unkey:data:policy_not_found`](/docs/errors/unkey/data/policy_not_found): the policy id doesn't exist. * [`err:unkey:data:api_not_found`](/docs/errors/unkey/data/api_not_found): the API itself doesn't exist. # migration_not_found Source: https://www.unkey.com/docs/errors/unkey/data/migration_not_found The migrationId passed to keys.migrateKeys does not exist in your workspace. `err:unkey:data:migration_not_found` The `migrationId` you sent to `keys.migrateKeys` doesn't exist in your workspace. You get HTTP 404. ```json Example response theme={"system"} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "error": { "detail": "The requested Migration does not exist or has been deleted.", "status": 404, "title": "Not Found", "type": "/errors/unkey/data/migration_not_found" } } ``` ## Likely causes * The migration ID is for another workspace, such as a staging ID used with a production root key. * You don't have a migration ID yet. There's no endpoint to create one: our support team sets up the migration and gives you the ID. ## How to fix 1. Use the `migrationId` we gave you, with a root key from the same workspace. Use the same ID for every batch of the import. 2. If you don't have one, contact support before you import. ## Related errors * [`err:unkey:data:api_not_found`](/docs/errors/unkey/data/api_not_found): the target API doesn't exist. * [`err:unkey:data:identity_not_found`](/docs/errors/unkey/data/identity_not_found): an identity named in the batch doesn't exist. * [`err:unkey:data:permission_not_found`](/docs/errors/unkey/data/permission_not_found): a permission named in the batch doesn't exist. # permission_already_exists Source: https://www.unkey.com/docs/errors/unkey/data/permission_already_exists A permission with that slug already exists in the workspace. `err:unkey:data:permission_already_exists` A permission with the same slug already exists in your workspace, so `permissions.createPermission` didn't create one. You get HTTP 409. ```json Example response theme={"system"} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "error": { "detail": "A permission with slug 'documents.read' already exists in this workspace", "status": 409, "title": "Conflicting Resource", "type": "/errors/unkey/data/permission_already_exists" } } ``` ## Likely causes * You already created this permission. Slugs are unique per workspace. ## How to fix 1. Reuse the existing permission (find it with `permissions.listPermissions`), or pick a different slug. In provisioning code, treat a 409 as success and continue. ## Related errors * [`err:unkey:data:permission_not_found`](/docs/errors/unkey/data/permission_not_found): a referenced permission doesn't exist. * [`err:unkey:data:role_already_exists`](/docs/errors/unkey/data/role_already_exists): a role name collides. # permission_not_found Source: https://www.unkey.com/docs/errors/unkey/data/permission_not_found A permission named in the request does not exist in the workspace. `err:unkey:data:permission_not_found` A permission you referenced doesn't exist in your workspace. You get HTTP 404, and `detail` names the slug. ```json Example response theme={"system"} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "error": { "detail": "Permission 'documents.read' was not found.", "status": 404, "title": "Not Found", "type": "/errors/unkey/data/permission_not_found" } } ``` ## Likely causes * You attached a permission to a key or role before creating it. The whole request fails if any one slug is missing, and `detail` names the first. * The slug has a typo. Slugs aren't case-sensitive. * Your root key doesn't have permission. `permissions.getPermission` and `permissions.deletePermission` return this 404 instead of a 403. ## How to fix 1. Create every permission you need with `permissions.createPermission` before you reference it. 2. Check the slug. `permissions.listPermissions` shows what exists. 3. On `permissions.getPermission` or `permissions.deletePermission`, check the root key's permissions before you decide the permission is missing. ## Related errors * [`err:unkey:data:permission_already_exists`](/docs/errors/unkey/data/permission_already_exists): a slug collides on create. * [`err:unkey:data:role_not_found`](/docs/errors/unkey/data/role_not_found): a role doesn't exist. * [`err:unkey:data:key_not_found`](/docs/errors/unkey/data/key_not_found): the key doesn't exist. # policy_not_found Source: https://www.unkey.com/docs/errors/unkey/data/policy_not_found The gateway policy id is not in the environment's current policy list. `err:unkey:data:policy_not_found` The policy ID you sent to `gateway.updatePolicy` isn't in the environment's current policy list. You get HTTP 404. ```json Example response theme={"system"} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "error": { "detail": "The requested policy does not exist. Note that policy ids change when the policy list is replaced.", "status": 404, "title": "Not Found", "type": "/errors/unkey/data/policy_not_found" } } ``` ## Likely causes * Someone called `gateway.setPolicies` since you saved the ID. It replaces the whole list and gives every policy a new ID. * The environment has never had policies set, so no ID matches. ## How to fix 1. Get the current list with `gateway.listPolicies` and use an ID from it. 2. In automation, replace the whole list with `gateway.setPolicies` instead of updating by ID, or fetch fresh IDs before each update. ## Related errors * [`err:unkey:data:environment_not_found`](/docs/errors/unkey/data/environment_not_found): the environment doesn't exist. * [`err:unkey:data:key_space_not_found`](/docs/errors/unkey/data/key_space_not_found): a keyspace referenced by a key auth policy doesn't exist in the project. # portal_already_exists Source: https://www.unkey.com/docs/errors/unkey/data/portal_already_exists The portal slug is taken, or the app or keyspace already has a portal. `err:unkey:data:portal_already_exists` `portal.createPortal` or `portal.updatePortal` clashed with an existing portal. Nothing was created or changed. You get HTTP 409. ```json Example response theme={"system"} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "error": { "detail": "A portal already exists for that slug, app, or keyspace.", "status": 409, "title": "Conflicting Resource", "type": "/errors/unkey/data/portal_already_exists" } } ``` ## Likely causes * The slug is already used in your workspace: `That slug is already in use.` * The app or keyspace already has a portal: `That app or keyspace already has a portal.` Each one can back only one portal across all workspaces, so the existing portal may be one you can't see. * Two requests clashed at the same moment. You get the generic message shown above. ## How to fix 1. For a slug clash, pick another slug or update the existing portal. 2. For an app or keyspace clash, find the existing portal with `portal.getPortal` by app or keyspace. If you can't see one in this workspace, it's in another workspace. ## Related errors * [`err:unkey:data:portal_not_found`](/docs/errors/unkey/data/portal_not_found): the portal or the mapped resource isn't visible to you. * [`err:unkey:authorization:insufficient_permissions`](/docs/errors/unkey/authorization/insufficient_permissions): your root key may not point a portal at that app or keyspace. # portal_changed Source: https://www.unkey.com/docs/errors/unkey/data/portal_changed The portal was pointed at a different app or keyspace while the session was being created. `err:unkey:data:portal_changed` `portal.createSession` was working out the session's access from the portal's app or keyspace when the portal was pointed somewhere else. The session was not created, because it would have carried access to the old app or keyspace. You get HTTP 409. ```json Example response theme={"system"} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "error": { "detail": "The portal changed while the session was being created. Try again.", "status": 409, "title": "Conflicting Resource", "type": "/errors/unkey/data/portal_changed" } } ``` ## Likely causes * Someone called `portal.updatePortal` with a new `keyspaceId` or `appId` at the same moment you called `portal.createSession`. ## How to fix 1. Retry `portal.createSession`. The new session uses the portal's current app or keyspace. # portal_not_found Source: https://www.unkey.com/docs/errors/unkey/data/portal_not_found No portal in your workspace matches the id, slug, app, or keyspace. `err:unkey:data:portal_not_found` No portal in your workspace matches the ID, slug, app, or keyspace you sent. You get HTTP 404. ```json Example response theme={"system"} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "error": { "detail": "Portal not found.", "status": 404, "title": "Not Found", "type": "/errors/unkey/data/portal_not_found" } } ``` ## Likely causes * The slug or ID is wrong. * You addressed the portal by an app or keyspace from another workspace. * Your root key doesn't have permission to read the portal. On `portal.createSession`, it needs `create_portal_session`. The message is the same either way. ## How to fix 1. Check the slug or ID in the dashboard's portal settings. 2. If you address the portal by app or keyspace, use one from this workspace. 3. For `portal.createSession`, make sure the root key has the `create_portal_session` permission on the portal. ## Related errors * [`err:unkey:data:portal_already_exists`](/docs/errors/unkey/data/portal_already_exists): a portal collides on create or update. * [`err:unkey:authentication:portal_session_not_found`](/docs/errors/unkey/authentication/portal_session_not_found): a session token, not the portal, is invalid. * [`err:unkey:authorization:forbidden`](/docs/errors/unkey/authorization/forbidden): the portal exists but is disabled or can't serve sessions. # project_already_exists Source: https://www.unkey.com/docs/errors/unkey/data/project_already_exists Another project in the workspace already uses that slug. `err:unkey:data:project_already_exists` Another project in your workspace already uses that slug. Nothing was created or changed. You get HTTP 409. ```json Example response theme={"system"} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "error": { "detail": "A project with slug 'billing' already exists in this workspace.", "status": 409, "title": "Conflicting Resource", "type": "/errors/unkey/data/project_already_exists" } } ``` ## Likely causes * You called `projects.createProject`, or changed a slug with `projects.updateProject`, and the slug is taken. * Two requests created the same slug at the same time, and this one lost. ## How to fix 1. Choose a different slug, or use the existing project. In automation, treat a 409 as "already there" and read the project with `projects.getProject`. ## Related errors * [`err:unkey:data:project_not_found`](/docs/errors/unkey/data/project_not_found): the project doesn't exist. * [`err:unkey:data:app_already_exists`](/docs/errors/unkey/data/app_already_exists): an app slug collides within a project. # project_not_found Source: https://www.unkey.com/docs/errors/unkey/data/project_not_found The project does not exist in your workspace. `err:unkey:data:project_not_found` The project you referenced doesn't exist in your workspace. You get HTTP 404. Some Compute calls, including deployment calls, use the shorter message `Project not found.` ```json Example response theme={"system"} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "error": { "detail": "The requested project does not exist.", "status": 404, "title": "Not Found", "type": "/errors/unkey/data/project_not_found" } } ``` ## Likely causes * The ID or slug is wrong, or the project was renamed and you're using the old slug. * The project is in another workspace. * Your root key doesn't have permission on the project. Most of these endpoints return this 404 instead of a 403. * You just tried to create the project and the create failed. New projects are available right away. ## How to fix 1. List projects with `projects.listProjects` and use the returned ID or slug. 2. Check that the root key has the permission the endpoint needs on the project. ## Related errors * [`err:unkey:data:app_not_found`](/docs/errors/unkey/data/app_not_found): the app within the project doesn't exist. * [`err:unkey:data:project_already_exists`](/docs/errors/unkey/data/project_already_exists): a project with that slug already exists. * [`err:unkey:application:protected_resource`](/docs/errors/unkey/application/protected_resource): the project can't be deleted while delete protection is on. # ratelimit_namespace_already_exists Source: https://www.unkey.com/docs/errors/unkey/data/ratelimit_namespace_already_exists A rate limit namespace with that name already exists or was deleted in the workspace. `err:unkey:data:ratelimit_namespace_already_exists` `ratelimit.createNamespace` named a namespace that already exists or was deleted in your workspace, so it didn't create one. You get HTTP 409. ```json Example response theme={"system"} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "error": { "detail": "A namespace named 'email.outbound' already exists or was deleted in this workspace. Choose a different name.", "status": 409, "title": "Conflicting Resource", "type": "/errors/unkey/data/ratelimit_namespace_already_exists" } } ``` ## Likely causes * You already created this namespace, or `ratelimit.limit` created it the first time a root key that can create namespaces used the name. * A namespace with this name was deleted. A deleted name can't be reused. ## How to fix 1. Reuse the existing namespace. Find it with `ratelimit.listNamespaces`. 2. Or choose another name. Names are case-sensitive, so `Email` and `email` are different namespaces. ## Related errors * [`err:unkey:data:ratelimit_namespace_gone`](/docs/errors/unkey/data/ratelimit_namespace_gone): the namespace was deleted. * [`err:unkey:data:ratelimit_namespace_not_found`](/docs/errors/unkey/data/ratelimit_namespace_not_found): the namespace doesn't exist. # ratelimit_namespace_gone Source: https://www.unkey.com/docs/errors/unkey/data/ratelimit_namespace_gone The rate limit namespace was deleted, so ratelimit.limit refuses to use it. `err:unkey:data:ratelimit_namespace_gone` `ratelimit.limit` or `ratelimit.multiLimit` named a rate limit namespace that was deleted. You get HTTP 410. ```json Example response theme={"system"} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "error": { "detail": "This namespace has been deleted. Contact support to restore.", "status": 410, "title": "Resource Gone", "type": "/errors/unkey/data/ratelimit_namespace_gone" } } ``` ## Likely causes * The namespace was deleted. A deleted name can't be reused. Normally `ratelimit.limit` creates a namespace the first time you use a name, but for a deleted name it returns this error instead, so an accidental delete can't turn into unlimited traffic. ## How to fix 1. Use a new namespace name. 2. Or, if you need the old overrides and history, ask support to restore it. Don't delete a namespace to reset its counters. Only delete one you're retiring. ## Related errors * [`err:unkey:data:ratelimit_namespace_not_found`](/docs/errors/unkey/data/ratelimit_namespace_not_found): the namespace never existed. * [`err:unkey:data:ratelimit_override_not_found`](/docs/errors/unkey/data/ratelimit_override_not_found): an override doesn't exist. # ratelimit_namespace_not_found Source: https://www.unkey.com/docs/errors/unkey/data/ratelimit_namespace_not_found The rate limit namespace does not exist in your workspace. `err:unkey:data:ratelimit_namespace_not_found` The rate limit namespace you referenced doesn't exist in your workspace. You get HTTP 404. ```json Example response theme={"system"} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "error": { "detail": "This namespace does not exist.", "status": 404, "title": "Not Found", "type": "/errors/unkey/data/ratelimit_namespace_not_found" } } ``` ## Likely causes * You set an override before the namespace existed. Namespaces are created the first time you call `ratelimit.limit` or `ratelimit.multiLimit` with a new name. The override endpoints (`ratelimit.setOverride`, `ratelimit.getOverride`, `ratelimit.listOverrides`, `ratelimit.deleteOverride`) don't create them. * The name has a typo. Names are matched exactly. * The namespace was deleted. `ratelimit.setOverride` returns this 404 for it, while `ratelimit.limit` returns [`err:unkey:data:ratelimit_namespace_gone`](/docs/errors/unkey/data/ratelimit_namespace_gone). ## How to fix 1. Call `ratelimit.limit` with the namespace once to create it, then set overrides. 2. Check the spelling. If you pass an ID, use the `rlns_` ID from the dashboard. ## Related errors * [`err:unkey:data:ratelimit_namespace_gone`](/docs/errors/unkey/data/ratelimit_namespace_gone): the namespace existed and was deleted. * [`err:unkey:data:ratelimit_override_not_found`](/docs/errors/unkey/data/ratelimit_override_not_found): the namespace exists but the override doesn't. # ratelimit_override_not_found Source: https://www.unkey.com/docs/errors/unkey/data/ratelimit_override_not_found No rate limit override matches the identifier in that namespace. `err:unkey:data:ratelimit_override_not_found` No override in that namespace matches the identifier you sent. You get HTTP 404. ```json Example response theme={"system"} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "error": { "detail": "This override does not exist.", "status": 404, "title": "Not Found", "type": "/errors/unkey/data/ratelimit_override_not_found" } } ``` ## Likely causes * The identifier doesn't exactly match an override, including any wildcard. * Your root key doesn't have permission on the namespace's overrides. `ratelimit.getOverride` and `ratelimit.deleteOverride` return this 404 instead of a 403. ## How to fix 1. List overrides with `ratelimit.listOverrides` and use an identifier from the list, with any wildcard exactly as it was set. 2. Check that the root key has the override permission for the namespace. ## Related errors * [`err:unkey:data:ratelimit_namespace_not_found`](/docs/errors/unkey/data/ratelimit_namespace_not_found): the namespace doesn't exist. * [`err:unkey:authorization:insufficient_permissions`](/docs/errors/unkey/authorization/insufficient_permissions): how permission failures look when they aren't hidden behind a 404. # role_already_exists Source: https://www.unkey.com/docs/errors/unkey/data/role_already_exists A role with that name already exists in the workspace. `err:unkey:data:role_already_exists` A role with the same name already exists in your workspace, so `permissions.createRole` didn't create one. You get HTTP 409. ```json Example response theme={"system"} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "error": { "detail": "A role with name 'admin' already exists in this workspace", "status": 409, "title": "Conflicting Resource", "type": "/errors/unkey/data/role_already_exists" } } ``` ## Likely causes * You already created this role. Role names are unique per workspace, and separately per project. * The name differs only in case. `Admin` clashes with an existing `admin`. ## How to fix 1. Reuse the existing role (find it with `permissions.listRoles`), or choose another name. 2. To change what a role grants, use `permissions.setRolePermissions` on the existing role instead of creating a second one. ## Related errors * [`err:unkey:data:role_not_found`](/docs/errors/unkey/data/role_not_found): a referenced role doesn't exist. * [`err:unkey:data:permission_already_exists`](/docs/errors/unkey/data/permission_already_exists): a permission slug collides. # role_not_found Source: https://www.unkey.com/docs/errors/unkey/data/role_not_found A role named in the request does not exist in the workspace. `err:unkey:data:role_not_found` A role you referenced doesn't exist in your workspace. You get HTTP 404, and `detail` names the role. ```json Example response theme={"system"} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "error": { "detail": "Role 'admin' was not found.", "status": 404, "title": "Not Found", "type": "/errors/unkey/data/role_not_found" } } ``` ## Likely causes * You attached a role to a key before creating it. The request stops at the first missing role. * The name has a typo. Names aren't case-sensitive, so `Admin` finds `admin`. * Your root key doesn't have permission. `permissions.getRole` and `permissions.deleteRole` return this 404 instead of a 403. ## How to fix 1. Create the role with `permissions.createRole` before you reference it. 2. Check the name. `permissions.listRoles` shows what exists. 3. On `permissions.getRole` or `permissions.deleteRole`, check the root key's permissions before you decide the role is missing. ## Related errors * [`err:unkey:data:role_already_exists`](/docs/errors/unkey/data/role_already_exists): a name collides on create. * [`err:unkey:data:permission_not_found`](/docs/errors/unkey/data/permission_not_found): a permission doesn't exist. * [`err:unkey:data:key_not_found`](/docs/errors/unkey/data/key_not_found): the key doesn't exist. # workspace_not_found Source: https://www.unkey.com/docs/errors/unkey/data/workspace_not_found The workspace the root key belongs to no longer exists. `err:unkey:data:workspace_not_found` The root key you're using belongs to a workspace that no longer exists. You get HTTP 404. It's rare, and means the key outlived its workspace. ```json Example response theme={"system"} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "error": { "detail": "The provided root key is invalid. The requested workspace does not exist.", "status": 404, "title": "Not Found", "type": "/errors/unkey/data/workspace_not_found" } } ``` ## How to fix 1. Sign in to the dashboard, open the workspace you want to use, and create a new root key there. 2. Replace the old key in your services. 3. If you think the workspace should still exist, contact support with the `requestId`. ## Related errors * [`err:unkey:authorization:workspace_disabled`](/docs/errors/unkey/authorization/workspace_disabled): the workspace exists but is disabled. * [`err:unkey:authentication:key_not_found`](/docs/errors/unkey/authentication/key_not_found): the root key itself doesn't exist. # custom_domain_limit_exceeded Source: https://www.unkey.com/docs/errors/unkey/limits/custom_domain_limit_exceeded The workspace already has as many custom domains as its plan allows. `err:unkey:limits:custom_domain_limit_exceeded` Your workspace already has as many custom domains as your plan allows, so `domains.createDomain` didn't add another one. You get HTTP 403. Waiting won't change it. ```json Example response theme={"system"} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "error": { "detail": "Your plan does not allow another custom domain. Upgrade your plan, or remove a domain you no longer need, then retry.", "status": 403, "title": "Forbidden", "type": "/errors/unkey/limits/custom_domain_limit_exceeded" } } ``` ## How to fix 1. Remove a domain you no longer use with `domains.deleteDomain`, or move to a plan with a higher limit. See [Limits](/docs/platform/billing/limits) and [Plans](/docs/platform/billing/plans). To see your domain count and limit from code, call `workspace.getLimits` with a root key that has `unkey:v1:{workspace_id}:limits#read`. 2. Create the domain again. ## Related errors * [`err:unkey:data:domain_already_exists`](/docs/errors/unkey/data/domain_already_exists): the domain is already attached to this workspace. * [`err:unkey:application:invalid_input`](/docs/errors/unkey/application/invalid_input): the domain name itself isn't valid. * [`err:unkey:application:service_unavailable`](/docs/errors/unkey/application/service_unavailable): your workspace's plan limits aren't set up (`Resource limits are not configured for this workspace.`), which support has to fix. # client_closed_request Source: https://www.unkey.com/docs/errors/user/bad_request/client_closed_request The client disconnected before the API finished handling the request. `err:user:bad_request:client_closed_request` Your client closed the connection before the API finished, so the API stopped working on the request. It's recorded as HTTP 499. You'll usually see it in your Unkey request logs rather than in a response, because no client was left to receive one. ```json Example response theme={"system"} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "error": { "detail": "Client closed request", "status": 499, "title": "Client Closed Request", "type": "/errors/user/bad_request/client_closed_request", "errors": [] } } ``` ## Likely causes * Your HTTP client's timeout is shorter than the request takes. This is common on `keys.verifyKey` when the verification timeout is tighter than the network allows. * Your code aborted the request, for example in a retry loop that cancels requests still in flight. * The client closed the connection or the process exited. ## How to fix 1. Set your client timeout to at least the API's one-minute deadline, or measure real latency and leave headroom. 2. Don't cancel in-flight requests when you retry. 3. A canceled write may or may not have gone through. Read the resource back before you retry a create. ## Related errors * [`err:user:bad_request:request_timeout`](/docs/errors/user/bad_request/request_timeout): the server-side deadline expired instead. * [`err:user:bad_request:request_body_unreadable`](/docs/errors/user/bad_request/request_body_unreadable): the connection dropped while the body was still being uploaded. # invalid_analytics_function Source: https://www.unkey.com/docs/errors/user/bad_request/invalid_analytics_function The analytics query calls a ClickHouse function that is not on the allowlist. `err:user:bad_request:invalid_analytics_function` Your analytics query uses a function we don't allow. You get HTTP 400, and `detail` names the function. ```json Example response theme={"system"} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "error": { "detail": "Function 'toUnixTimestamp' is not allowed", "status": 400, "title": "Bad Request", "type": "/errors/user/bad_request/invalid_analytics_function", "errors": [] } } ``` ## Likely causes * You used `toUnixTimestamp`. The tables store milliseconds, so use `toUnixTimestamp64Milli`. * You used a `JSONExtract*` variant other than `JSONExtractString`. * You used a window function. They aren't allowed. * You used a table function such as `url`, `file`, or `remote`. These get their own message. These functions are allowed (names aren't case-sensitive): * **Aggregates:** `count`, `sum`, `avg`, `min`, `max`, `any`, `groupArray`, `groupUniqArray`, `uniq`, `uniqExact`, `quantile`. * **Date and time:** `now`, `now64`, `today`, `toDate`, `toDateTime`, `toDateTime64`, the `toStartOf*` family from minute to year, `date_trunc`, `formatDateTime`, `fromUnixTimestamp64Milli`, `toUnixTimestamp64Milli`, and the `toInterval*` family. * **Strings:** `lower`, `upper`, `substring`, `concat`, `length`, `trim`, `startsWith`, `endsWith`. * **JSON:** `JSONExtractString`. * **Math:** `round`, `floor`, `ceil`, `abs`. * **Conditionals:** `if`, `sumIf`, `countIf`, `case`, `coalesce`. * **Conversions:** `toString`, `toInt32`, `toInt64`, `toFloat64`. * **Arrays:** `has`, `hasAny`, `hasAll`, `arrayJoin`, `arrayFilter`. ## How to fix 1. Rewrite the expression with an allowed function: `toUnixTimestamp64Milli` for `toUnixTimestamp`, `JSONExtractString` for other `JSONExtract*` variants, and a `GROUP BY` with an aggregate instead of a window function. 2. If you need a function that's missing, ask in Discord or open an issue. ## Related errors * [`err:user:bad_request:invalid_analytics_query`](/docs/errors/user/bad_request/invalid_analytics_query): the query failed for another structural reason. * [`err:user:bad_request:invalid_analytics_table`](/docs/errors/user/bad_request/invalid_analytics_table): the query names a table that isn't exposed. # invalid_analytics_query Source: https://www.unkey.com/docs/errors/user/bad_request/invalid_analytics_query The analytics query breaks one of the query rules, or failed when it ran. `err:user:bad_request:invalid_analytics_query` We can't run the query you sent to an analytics endpoint (`analytics.getVerifications`, `analytics.getRatelimits`, `analytics.getGatewayRequests`, or `analytics.getRuntimeLogs`). You get HTTP 400, and `detail` says what's wrong. ```json Example response theme={"system"} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "error": { "detail": "Analytics queries must contain exactly one statement", "status": 400, "title": "Bad Request", "type": "/errors/user/bad_request/invalid_analytics_query", "errors": [] } } ``` ## Likely causes * More than one statement, separated by `;`, or no statement at all. * `SELECT *`, or a column that isn't exposed. The message says the column isn't available. * A `SETTINGS` clause, often pasted from a console session, such as `SETTINGS max_threads = 4`. * A syntax error, or no `FROM` clause. * An `IN` that reads from a table, such as `IN (SELECT ... FROM table)`. * The query is too big: more than 64 selected columns, more than 16 KiB of text, or more than 2000 syntax tree nodes (`Analytics query is too complex`). * The query failed while running, for example on an unknown column (`Unknown identifier`), a type mismatch, or a division by zero. A disallowed table, a disallowed function, and a statement that isn't a `SELECT` each have their own error. ## How to fix 1. Send exactly one `SELECT` with a `FROM`, using only the documented columns of the analytics tables. 2. Remove `SETTINGS`. 3. Replace an `IN (SELECT ... FROM table)` with a literal list or a CTE. 4. For `Unknown identifier`, check the spelling and any CTE aliases. 5. Keep the query under 16 KiB and 64 output columns. ## Related errors * [`err:user:bad_request:invalid_analytics_table`](/docs/errors/user/bad_request/invalid_analytics_table): the query names a table that isn't exposed. * [`err:user:bad_request:invalid_analytics_function`](/docs/errors/user/bad_request/invalid_analytics_function): the query calls a function outside the allowlist. * [`err:user:bad_request:invalid_analytics_query_type`](/docs/errors/user/bad_request/invalid_analytics_query_type): the statement isn't a `SELECT`. * [`err:user:bad_request:query_range_exceeds_retention`](/docs/errors/user/bad_request/query_range_exceeds_retention): the time filter reaches past the workspace's retention window. # invalid_analytics_query_type Source: https://www.unkey.com/docs/errors/user/bad_request/invalid_analytics_query_type The analytics query is not a SELECT statement. `err:user:bad_request:invalid_analytics_query_type` The statement you sent to an analytics endpoint isn't a `SELECT`. Analytics is read-only. You get HTTP 400. ```json Example response theme={"system"} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "error": { "detail": "Only SELECT queries are allowed", "status": 400, "title": "Bad Request", "type": "/errors/user/bad_request/invalid_analytics_query_type", "errors": [] } } ``` ## Likely causes * You sent `INSERT`, `ALTER`, `CREATE`, `DROP`, or another statement that isn't a `SELECT`. * You used `DESCRIBE` to look at a table's columns. ## How to fix 1. Send a `SELECT`. 2. To see a table's columns, use the table reference on this site. 3. To remove or correct data, contact support with the `requestId`. The API can't write to the analytics tables. ## Related errors * [`err:user:bad_request:invalid_analytics_query`](/docs/errors/user/bad_request/invalid_analytics_query): the `SELECT` itself was malformed, or the statement couldn't be recognized at all. # invalid_analytics_table Source: https://www.unkey.com/docs/errors/user/bad_request/invalid_analytics_table The analytics query reads a table the endpoint does not expose. `err:user:bad_request:invalid_analytics_table` Your analytics query reads from a table this endpoint doesn't expose. You get HTTP 400, and `detail` names the table. ```json Example response theme={"system"} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "error": { "detail": "Access to table 'system.tables' is not allowed", "status": 400, "title": "Bad Request", "type": "/errors/user/bad_request/invalid_analytics_table", "errors": [] } } ``` ## Likely causes * The table belongs to another endpoint. Each endpoint reads only its own tables: verification tables on `analytics.getVerifications`, rate limit tables on `analytics.getRatelimits`, and so on. * You copied a query that uses an internal table name, such as `default.key_verifications_raw_v1`. * You queried a ClickHouse system table. * A CTE has the same name as one of the endpoint's tables. ## How to fix 1. Use the table names documented for the endpoint you're calling. For another product area's data, call that endpoint. 2. Replace internal table names with the documented name. 3. Rename any CTE that shares a name with a documented table. ## Related errors * [`err:user:bad_request:invalid_analytics_query`](/docs/errors/user/bad_request/invalid_analytics_query): the query failed for another structural reason. * [`err:user:bad_request:invalid_analytics_function`](/docs/errors/user/bad_request/invalid_analytics_function): the query calls a function outside the allowlist. # missing_required_header Source: https://www.unkey.com/docs/errors/user/bad_request/missing_required_header A reserved code that no endpoint returns. `err:user:bad_request:missing_required_header` This code is reserved, and the API doesn't return it. ## How to fix There's nothing to fix. Send `Authorization: Bearer ` and `Content-Type: application/json` on every request. If one is missing, you get one of the errors below. ## Related errors * [`err:unkey:authentication:missing`](/docs/errors/unkey/authentication/missing): the endpoint doesn't accept the kind of credential you sent. * [`err:unkey:application:invalid_input`](/docs/errors/unkey/application/invalid_input): the request failed validation, including a missing `Authorization` header or a wrong or missing `Content-Type`. # per_key_breakout_too_large Source: https://www.unkey.com/docs/errors/user/bad_request/per_key_breakout_too_large A portal analytics window covers more keys than the per-key breakout returns. `err:user:bad_request:per_key_breakout_too_large` `portal.getVerifications` returns one series per key that had traffic in the window, up to 150 keys. More than 150 of the user's keys had traffic in this window, so you get HTTP 400 instead of a partial result that leaves keys out. ```json Example response theme={"system"} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "error": { "detail": "This window covers more than 150 keys. Request a narrower window or a single `keyId`.", "status": 400, "title": "Bad Request", "type": "/errors/user/bad_request/per_key_breakout_too_large", "errors": [] } } ``` ## Likely causes * The window is too wide. The count is of keys with at least one verification in the window, not keys the user owns, so a wider window makes this more likely. * The window covers a bulk migration or a lot of short-lived keys. ## How to fix 1. Narrow the window. Halving it roughly halves the number of keys with traffic, and gives you finer buckets. 2. Or pass `keyId` to get just that key's series. That isn't subject to the cap, and is the right call when the user is looking at one key. 3. For usage over a wider window, query from your own backend with [analytics](/docs/api-management/analytics/overview). The portal response has no combined total, so the cap applies even if you only want account-wide numbers. ## Related errors * [`err:user:bad_request:query_range_exceeds_retention`](/docs/errors/user/bad_request/query_range_exceeds_retention): the window reaches back past what your workspace keeps. # permissions_query_syntax_error Source: https://www.unkey.com/docs/errors/user/bad_request/permissions_query_syntax_error The permissions query passed to keys.verifyKey could not be parsed. `err:user:bad_request:permissions_query_syntax_error` The `permissions` string you sent to `keys.verifyKey` isn't a valid permission query. You get HTTP 400 before the key is checked, and `detail` gives the position of the problem and what was expected there. ```json Example response theme={"system"} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "error": { "detail": "Missing closing parenthesis ')' at position 24. Every opening parenthesis must have a matching closing parenthesis.", "status": 400, "title": "Bad Request", "type": "/errors/user/bad_request/permissions_query_syntax_error", "errors": [] } } ``` ## Likely causes * You wrote `&&` or `||` instead of `AND` or `OR`. * You put quotes around slugs. * A `(` has no matching `)`, or a group is empty: `()`. * A slug has a character outside the allowed set: letters, numbers, dots, underscores, hyphens, slashes, colons, asterisks, and parentheses. * A token is where an operator or slug was expected, or the query is blank. If you pass an array of slugs instead of a string, you get [`err:unkey:application:invalid_input`](/docs/errors/unkey/application/invalid_input) instead. ## How to fix 1. Fix the query at the position named in `detail`. 2. Write it as slugs joined with `AND` and `OR`, grouped with parentheses. For example: `documents.read AND (billing.read OR billing.write)`. 3. If you build the query from user input, validate it first. A syntax error fails the whole request instead of returning `valid: false`. ## Related errors * [`err:unkey:authorization:insufficient_permissions`](/docs/errors/unkey/authorization/insufficient_permissions): the query parsed but your root key lacks a permission the endpoint needs. * [`err:unkey:application:invalid_input`](/docs/errors/unkey/application/invalid_input): another field of the request failed validation. # query_range_exceeds_retention Source: https://www.unkey.com/docs/errors/user/bad_request/query_range_exceeds_retention The analytics query asks for data older than the workspace's retention window. `err:user:bad_request:query_range_exceeds_retention` The time filter in your analytics query asks for data older than your workspace keeps. You get HTTP 400, and `detail` gives the earliest date you can query and the date you asked for. ```json Example response theme={"system"} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "error": { "detail": "Cannot query data older than 30 days. Your query attempts to access data from 2026-07-01, but only data from 2026-08-23 onwards is available.", "status": 400, "title": "Bad Request", "type": "/errors/user/bad_request/query_range_exceeds_retention", "errors": [] } } ``` ## Likely causes * The lower bound on `time` (with `>`, `>=`, `=`, or `BETWEEN`) goes back further than your plan's retention. We check bounds written as a literal timestamp or date, `today()`, or `now()` with `INTERVAL N UNIT`, such as `now() - INTERVAL 90 DAY`. A bound written another way, such as `toIntervalDay(90)`, or no lower bound at all, isn't rejected. The query runs, and rows older than your retention are left out without an error. ## How to fix 1. Move the lower bound to the date in `detail` or later. 2. If you need older data, move to a plan with longer retention. See [Limits](/docs/platform/billing/limits). 3. To keep data long-term, schedule exports that run inside the retention window. ## Related errors * [`err:user:bad_request:invalid_analytics_query`](/docs/errors/user/bad_request/invalid_analytics_query): the query failed for another structural reason. * [`err:user:too_many_requests:query_quota_exceeded`](/docs/errors/user/too_many_requests/query_quota_exceeded): the workspace used up its analytics query quota. # request_body_too_large Source: https://www.unkey.com/docs/errors/user/bad_request/request_body_too_large The request body is larger than the 10 MiB the API accepts. `err:user:bad_request:request_body_too_large` Your request body is larger than the 10 MiB (10485760 bytes) the API accepts. You get HTTP 413 on any endpoint. ```json Example response theme={"system"} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "error": { "detail": "The request body exceeds the maximum allowed size of 10485760 bytes.", "status": 413, "title": "Request Entity Too Large", "type": "/errors/user/bad_request/request_body_too_large", "errors": [] } } ``` ## Likely causes * Large `meta` objects on keys or identities. * A `keys.migrateKeys` batch that's too big. ## How to fix 1. Send less in one request. Keep bulky data on your side and store a reference in `meta`. 2. Split `keys.migrateKeys` batches into smaller requests. Each endpoint also has much smaller per-field limits, so a trimmed body can still fail field validation. ## Related errors * [`err:user:bad_request:request_body_unreadable`](/docs/errors/user/bad_request/request_body_unreadable): the body couldn't be read for a reason other than size. * [`err:unkey:application:invalid_input`](/docs/errors/unkey/application/invalid_input): a field exceeds its own limit. # request_body_unreadable Source: https://www.unkey.com/docs/errors/user/bad_request/request_body_unreadable The API could not read the request body to the end. `err:user:bad_request:request_body_unreadable` The API couldn't read your request body to the end, for a reason other than size. You get HTTP 400 on any endpoint. ```json Example response theme={"system"} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "error": { "detail": "The request body could not be read.", "status": 400, "title": "Bad Request", "type": "/errors/user/bad_request/request_body_unreadable", "errors": [] } } ``` ## Likely causes * Your client disconnected mid-upload. * The `Content-Length` header doesn't match the bytes sent. * A proxy between you and `api.unkey.com` cut the body short. ## How to fix 1. Retry the request. 2. If it keeps failing, make sure your HTTP client sends the full body with a correct `Content-Length` and doesn't close the connection early on a slow network. 3. Check that any proxy or gateway in between forwards the body unchanged. The official SDKs avoid most of these problems. ## Related errors * [`err:user:bad_request:request_body_too_large`](/docs/errors/user/bad_request/request_body_too_large): the body exceeded the size cap. * [`err:user:bad_request:client_closed_request`](/docs/errors/user/bad_request/client_closed_request): the client disconnected while the request was being processed. # request_timeout Source: https://www.unkey.com/docs/errors/user/bad_request/request_timeout The request did not finish within the API's one minute deadline. `err:user:bad_request:request_timeout` Your request didn't finish within the API's one-minute limit, so the API canceled it. You get HTTP 408 on any endpoint. ```json Example response theme={"system"} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "error": { "detail": "Request timeout", "status": 408, "title": "Request Timeout", "type": "/errors/user/bad_request/request_timeout", "errors": [] } } ``` ## Likely causes * A brief slowdown. Most timeouts don't repeat. * The request asks for too much at once, such as a large list without paging. If your client gives up first, the request is recorded as [`client_closed_request`](/docs/errors/user/bad_request/client_closed_request) instead. Analytics queries have their own, shorter limit, reported as [`query_execution_timeout`](/docs/errors/user/unprocessable_entity/query_execution_timeout), so a 408 on an analytics endpoint isn't about the query itself. ## How to fix 1. Retry the request. 2. If one endpoint keeps timing out, ask for less: page through large lists with `cursor` and `limit`. 3. If it still happens, contact support with the `requestId`. ## Related errors * [`err:user:bad_request:client_closed_request`](/docs/errors/user/bad_request/client_closed_request): your client closed the connection first. * [`err:user:unprocessable_entity:query_execution_timeout`](/docs/errors/user/unprocessable_entity/query_execution_timeout): an analytics query hit its own time limit. * [`err:unkey:application:service_unavailable`](/docs/errors/unkey/application/service_unavailable): a dependency failed without timing out. # query_quota_exceeded Source: https://www.unkey.com/docs/errors/user/too_many_requests/query_quota_exceeded Your workspace used up its analytics query quota for the current window. `err:user:too_many_requests:query_quota_exceeded` Your workspace used up its analytics query quota for the current hour, so this query was refused. You get HTTP 429. ```json Example response theme={"system"} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "error": { "detail": "Query quota exceeded for the current time window. Please try again later.", "status": 429, "title": "Too Many Requests", "type": "/errors/user/too_many_requests/query_quota_exceeded" } } ``` ## Likely causes * Too many queries. The quota is 1000 queries per rolling hour, unless we've raised it for you. * Too much query time. The same hour allows 1800 seconds of combined execution time, so slow queries can run out well before 1000. * A dashboard or job polls with many small queries. ## How to fix 1. Wait for the window to roll over, then retry with backoff. 2. Cache results on your side for dashboards that poll, and combine small queries into one aggregation. 3. Make queries faster, for example by narrowing time ranges, so they use less of the time budget. 4. If you need more, contact [support@unkey.com](mailto:support@unkey.com). See [Analytics restrictions and quotas](/docs/api-management/analytics/restrictions-and-quotas). ## Related errors * [`err:user:unprocessable_entity:query_execution_timeout`](/docs/errors/user/unprocessable_entity/query_execution_timeout): a single query ran too long. # workspace_rate_limited Source: https://www.unkey.com/docs/errors/user/too_many_requests/workspace_rate_limited The workspace is sending too many requests to the Unkey API. `err:user:too_many_requests:workspace_rate_limited` Your workspace is sending too many requests to the Unkey API, so this request was refused. Nothing ran, so it's safe to retry. You get HTTP 429. ```json Example response theme={"system"} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "error": { "detail": "This workspace has exceeded its API rate limit of 600/1m0s. Please try again later.", "status": 429, "title": "Too Many Requests", "type": "/errors/user/too_many_requests/workspace_rate_limited" } } ``` ## How to fix 1. Wait a moment, then retry with exponential backoff. 2. If it keeps happening, contact [support@unkey.com](mailto:support@unkey.com). # query_execution_timeout Source: https://www.unkey.com/docs/errors/user/unprocessable_entity/query_execution_timeout The analytics query ran longer than its 10 second budget and was canceled. `err:user:unprocessable_entity:query_execution_timeout` Your analytics query was valid, but it ran longer than its 10 seconds and was canceled. You get HTTP 422. ```json Example response theme={"system"} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "error": { "detail": "Query execution time limit exceeded. Try simplifying your query or reducing the time range.", "status": 422, "title": "Unprocessable Entity", "type": "/errors/user/unprocessable_entity/query_execution_timeout" } } ``` ## Likely causes * The time range is too wide. * The query reads a raw table when a rollup would do. * The query filters late, or pulls raw rows instead of aggregating. A request that hits the API's one-minute limit returns [`request_timeout`](/docs/errors/user/bad_request/request_timeout) instead. ## How to fix Make the query cheaper: * **Narrow the time range first.** A shorter range reads fewer rows. * **Query a rollup table** (per-minute, per-hour, per-day) instead of the raw table when you don't need individual events. * **Filter early** on identifiers such as key ID, identity ID, or namespace before grouping. * **Aggregate in the query** instead of pulling raw rows and aggregating on your side. * **Split long ranges** into several queries by day or week and combine the results. ## Related errors * [`err:user:unprocessable_entity:query_rows_limit_exceeded`](/docs/errors/user/unprocessable_entity/query_rows_limit_exceeded): the query read or returned too many rows. * [`err:user:unprocessable_entity:query_memory_limit_exceeded`](/docs/errors/user/unprocessable_entity/query_memory_limit_exceeded): the query used too much memory or the result was too large. * [`err:user:bad_request:request_timeout`](/docs/errors/user/bad_request/request_timeout): the whole request hit the API deadline. # query_memory_limit_exceeded Source: https://www.unkey.com/docs/errors/user/unprocessable_entity/query_memory_limit_exceeded The analytics query used too much memory or its result exceeded 4 MiB. `err:user:unprocessable_entity:query_memory_limit_exceeded` Your analytics query returned a result that's too large, or needed too much memory. You get HTTP 422, and `detail` says which. ```json Example response theme={"system"} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "error": { "detail": "Query result exceeds the maximum response size.", "status": 422, "title": "Unprocessable Entity", "type": "/errors/user/unprocessable_entity/query_memory_limit_exceeded" } } ``` ## Likely causes * The result is over 4 MiB as JSON. This is the common one, and `detail` is `Query result exceeds the maximum response size.` * The query needed more than 1 GB of memory while running, unless we've raised that for you. `detail` is a memory message. ## How to fix 1. For a result that's too large, return fewer rows or columns: add a `LIMIT`, drop wide columns you don't need, and page through long ranges by time. Aggregations that return a few rows never hit this. 2. For memory, filter earlier, group on fewer or lower-cardinality columns, and avoid `groupArray` over large groups. ## Related errors * [`err:user:unprocessable_entity:query_rows_limit_exceeded`](/docs/errors/user/unprocessable_entity/query_rows_limit_exceeded): the query read or returned too many rows. * [`err:user:unprocessable_entity:query_execution_timeout`](/docs/errors/user/unprocessable_entity/query_execution_timeout): the query ran too long. # query_rows_limit_exceeded Source: https://www.unkey.com/docs/errors/user/unprocessable_entity/query_rows_limit_exceeded The analytics query read or returned more rows than the workspace allows. `err:user:unprocessable_entity:query_rows_limit_exceeded` Your analytics query read or tried to return more rows than your workspace allows. You get HTTP 422, and `detail` says which. ```json Example response theme={"system"} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "error": { "detail": "Query attempted to read too many rows. Try adding more filters or reducing the time range.", "status": 422, "title": "Unprocessable Entity", "type": "/errors/user/unprocessable_entity/query_rows_limit_exceeded" } } ``` ## Likely causes * The query scans too much: a wide time range, few filters, or raw rows instead of an aggregate. The cap is 10,000,000 rows unless we've changed it for you. Every `SELECT`, including subqueries, gets a `LIMIT` at the cap, and a larger `LIMIT` you write is lowered to it. You'll usually hit the 4 MiB result size first, which returns [`query_memory_limit_exceeded`](/docs/errors/user/unprocessable_entity/query_memory_limit_exceeded). ## How to fix 1. Narrow the time range. 2. Filter on key ID, identity ID, namespace, or another indexed column. 3. Aggregate instead of selecting raw rows. 4. If you need every row in a large range, split it into smaller windows and run one query per window. See [Analytics restrictions and quotas](/docs/api-management/analytics/restrictions-and-quotas). ## Related errors * [`err:user:unprocessable_entity:query_memory_limit_exceeded`](/docs/errors/user/unprocessable_entity/query_memory_limit_exceeded): the query used too much memory or the result was too large. * [`err:user:unprocessable_entity:query_execution_timeout`](/docs/errors/user/unprocessable_entity/query_execution_timeout): the query ran too long. * [`err:user:bad_request:query_range_exceeds_retention`](/docs/errors/user/bad_request/query_range_exceeds_retention): the time filter reaches past the retention window. # Unkey documentation Source: https://www.unkey.com/docs/index Deploy apps with Compute or add API keys to any app with API Management. Unkey issues and verifies API keys, enforces rate limits and usage quotas, and deploys apps from a GitHub repository or a container image. It's for teams that expose a public API, or that want each push to a repository to become a running deployment. API Management does the keys, rate limits, and quotas, and your app can stay on the host you already use. Compute builds and runs the app. You can use either product on its own. # Use Unkey with Claude Code Source: https://www.unkey.com/docs/platform/ai-coding/claude-code Add the Unkey MCP servers and docs to Claude Code with one command each. You need a root key for your assistant. See [Create a root key](/docs/platform/ai-coding/mcp-servers#create-a-root-key). ## Add the Unkey server Export the key in the shell you start Claude Code from, or add it to your shell profile. ```bash theme={"system"} export UNKEY_ROOT_KEY="unkey_..." ``` ```bash theme={"system"} claude mcp add --transport http unkey "https://mcp.unkey.com/mcp/v2" \ --header 'Mcp-Unkey-V2-Bearer:${UNKEY_ROOT_KEY}' ``` Keep the single quotes. Claude Code saves `${UNKEY_ROOT_KEY}` as written and reads the variable each time it connects, so the key isn't stored in its config. To add the Compute server instead, use its URL: ```bash theme={"system"} claude mcp add --transport http unkey-compute "https://mcp.unkey.com/mcp/compute" \ --header 'Mcp-Unkey-V2-Bearer:${UNKEY_ROOT_KEY}' ``` ```bash theme={"system"} claude mcp list ``` The server shows **Connected**. Inside a session, run `/mcp` to see its status and tools. The server is added for you in the current project. Add `--scope user` to use it in every project, or `--scope project` to save it to `.mcp.json` so your team gets it too. Each person then sets `UNKEY_ROOT_KEY` to their own key. ## Add the docs ```bash theme={"system"} claude mcp add --transport http unkey-docs "https://www.unkey.com/docs/mcp" ``` Claude Code can then search these docs when you ask about Unkey. ## Next steps Example requests for keys, analytics, and deployments. What to check when the server doesn't connect. # Use Unkey with Cursor Source: https://www.unkey.com/docs/platform/ai-coding/cursor Add the Unkey MCP servers and docs to Cursor. You need a root key for your assistant. See [Create a root key](/docs/platform/ai-coding/mcp-servers#create-a-root-key). ## Add the Unkey server Set `UNKEY_ROOT_KEY` in your shell profile or system environment, then restart Cursor so it picks up the variable. Open `~/.cursor/mcp.json` to use Unkey in every project, or `.cursor/mcp.json` in a project to use it there only. Add the server: ```json theme={"system"} { "mcpServers": { "unkey": { "url": "https://mcp.unkey.com/mcp/v2", "headers": { "Mcp-Unkey-V2-Bearer": "${env:UNKEY_ROOT_KEY}" } } } } ``` For the Compute server, use `https://mcp.unkey.com/mcp/compute` as the URL. Open **Customize** in the sidebar and check that **unkey** is listed and turned on. Then ask Cursor's agent something like "List my Unkey keyspaces." If the server doesn't connect, open the Output panel, choose **MCP Logs**, and look for an authentication or connection error. ## Add the docs On any docs page, open the menu next to **Copy page** and choose **Connect to Cursor**. Cursor opens and asks you to install the docs MCP server. ## Next steps Example requests for keys, analytics, and deployments. What to check when the server doesn't connect. # Unkey MCP servers Source: https://www.unkey.com/docs/platform/ai-coding/mcp-servers Let an AI assistant manage your keys, APIs, and deployments through Unkey's hosted MCP servers. With an Unkey MCP server added, your assistant can create keys, check analytics, or roll back a deployment when you ask it to in chat. It calls the Unkey API for you, using a root key you provide. ## Choose a server | Server | URL | What it can do | | - | - | - | | Unkey | `https://mcp.unkey.com/mcp/v2` | Every v2 API endpoint: keys, keyspaces, identities, permissions, rate limits, analytics, portals, and Compute | | Compute | `https://mcp.unkey.com/mcp/compute` | Projects, apps, environments and their variables, deployments, domains, gateway policies, and the GitHub app | Add the Unkey server if you're not sure. If you only use Compute, the Compute server gives your assistant a shorter list of tools to choose from. ## Create a root key The server acts with the permissions of the root key you give it, so create a key just for your assistant. 1. Under **Settings > Root Keys**, click **New root key** and name it after the assistant, for example "Claude Code". 2. Select only the permissions for what you want the assistant to do. See [Root key permissions](/docs/platform/root-keys/permissions). 3. Copy the key and store it as an environment variable, for example `UNKEY_ROOT_KEY`. See [Root keys](/docs/platform/root-keys/overview) for how to rotate or delete the key later. ## Connect your client Every client needs the server URL and one header with your root key: ```text theme={"system"} Mcp-Unkey-V2-Bearer: ``` Both servers use the same header. Most clients take a JSON entry like this one: ```json theme={"system"} { "mcpServers": { "unkey": { "url": "https://mcp.unkey.com/mcp/v2", "headers": { "Mcp-Unkey-V2-Bearer": "" } } } } ``` For steps in a specific client, see [Claude Code](/docs/platform/ai-coding/claude-code), [Cursor](/docs/platform/ai-coding/cursor), or [Other clients](/docs/platform/ai-coding/other-clients). The [Unkey server's install page](https://mcp.unkey.com/mcp/v2/install) and the [Compute server's install page](https://mcp.unkey.com/mcp/compute/install) also list every tool and have snippets for each client. ## Try it Ask in plain language. The assistant picks the tools and fills in the IDs. ```text theme={"system"} Create a key in my production keyspace for user_123 that allows 100 requests per minute. ``` ```text theme={"system"} How many verifications did my production keyspace get yesterday, broken down by outcome? ``` ```text theme={"system"} Show the latest deployment for my api app's production environment, then roll back to the one before it. ``` ```text theme={"system"} Add the domain api.example.com to production and tell me which DNS records to create. ``` Most clients ask you to approve each tool call. Read what the assistant is about to do before you approve a change like deleting a key or promoting a deployment. ## Fix connection problems * **The provided root key is invalid**: the key in the header is wrong, deleted, or rotated. See [key\_not\_found](/docs/errors/unkey/authentication/key_not_found). * **Missing permissions**: the root key can't do what the assistant tried. Add the permission to the key or ask for something else. See [insufficient\_permissions](/docs/errors/unkey/authorization/insufficient_permissions). * **The server doesn't connect**: check that the URL matches the table exactly, with no trailing slash, and that the header is named `Mcp-Unkey-V2-Bearer`. Restart your client after you edit its config. # Connect other MCP clients Source: https://www.unkey.com/docs/platform/ai-coding/other-clients Set up the Unkey MCP servers in VS Code, Claude Desktop, Codex CLI, or any other MCP client. Each example adds the Unkey server. For the Compute server, swap in `https://mcp.unkey.com/mcp/compute`. You need a root key for your assistant. See [Create a root key](/docs/platform/ai-coding/mcp-servers#create-a-root-key). ## VS Code Add the server to `.vscode/mcp.json` in your project: ```json theme={"system"} { "inputs": [ { "type": "promptString", "id": "unkey-root-key", "description": "Unkey root key", "password": true } ], "servers": { "unkey": { "type": "http", "url": "https://mcp.unkey.com/mcp/v2", "headers": { "Mcp-Unkey-V2-Bearer": "${input:unkey-root-key}" } } } } ``` VS Code asks for your root key the first time the server starts and stores it for you. To add the docs, open the menu next to **Copy page** on any docs page and choose **Connect to VS Code**. ## Claude Desktop Claude Desktop's custom connectors can't send headers, so it connects through `mcp-remote`, which needs Node.js. 1. Go to **Settings > Developer > Local MCP Servers > Edit config** to open `claude_desktop_config.json`. 2. Add the server and replace the placeholder with your root key: ```json theme={"system"} { "mcpServers": { "unkey": { "command": "npx", "args": [ "mcp-remote@0.1.25", "https://mcp.unkey.com/mcp/v2", "--header", "Mcp-Unkey-V2-Bearer:${UNKEY_ROOT_KEY}" ], "env": { "UNKEY_ROOT_KEY": "" } } } } ``` 3. Save the file and restart Claude Desktop. ## Codex CLI ```bash theme={"system"} codex mcp add unkey --url "https://mcp.unkey.com/mcp/v2" ``` Codex can't set headers from the command line. Open `~/.codex/config.toml` and add this line to the `[mcp_servers.unkey]` entry the command created, so Codex reads the key from `UNKEY_ROOT_KEY`: ```toml theme={"system"} env_http_headers = { "Mcp-Unkey-V2-Bearer" = "UNKEY_ROOT_KEY" } ``` Restart Codex to load the server. ## Any other client Any client that connects to remote MCP servers over HTTP and can send a custom header works. Give it the server URL and the `Mcp-Unkey-V2-Bearer` header with your root key. The [install page](https://mcp.unkey.com/mcp/v2/install) also has snippets for opencode and Antigravity. # AI coding Source: https://www.unkey.com/docs/platform/ai-coding/overview Connect your AI coding assistant to Unkey so it can manage your workspace and read the docs. You can connect an AI coding assistant like Claude Code, Cursor, or VS Code to Unkey in two ways. It can act on your workspace through Unkey's MCP servers, and it can read these docs while it writes code for you. ## Let your assistant act on Unkey Unkey hosts two MCP servers. Add one to your assistant with a root key, and it can call the Unkey API for you. * **Unkey**: every v2 API endpoint, so keys, keyspaces, identities, permissions, rate limits, analytics, portals, and Compute. * **Compute**: only the Compute endpoints, for projects, apps, environments, deployments, domains, and gateway policies. See [Unkey MCP servers](/docs/platform/ai-coding/mcp-servers) for the URLs, the root key to use, and example prompts. ## Give your assistant the docs * **Docs MCP server**: lets your assistant search these docs while it works. Its URL is below. You can also add it from the menu next to **Copy page** at the top of any page, which has **Connect to Cursor**, **Connect to VS Code**, **Copy MCP Server**, and **Copy MCP install command**. * **Markdown**: **Copy page** copies the page as Markdown you can paste into a chat. Add `.md` to the end of any docs URL to get the same thing as plain text. * **llms.txt**: an index of every page, built for language models. `llms-full.txt` has the full text of the docs in one file. ```text theme={"system"} https://www.unkey.com/docs/mcp https://www.unkey.com/docs/llms.txt https://www.unkey.com/docs/llms-full.txt ``` ## Use the CLI Assistants that run terminal commands can also use the `unkey` CLI. Pass `--output=json` so they get the full API response. See [Install the CLI](/docs/platform/cli/install). ## Next steps Pick a server, create a root key for it, and try some prompts. Add Unkey to Claude Code with one command. Add Unkey to Cursor's `mcp.json`. VS Code, Claude Desktop, Codex CLI, and any other MCP client. # Query gateway request data Source: https://www.unkey.com/docs/platform/api-reference/analytics/query-gateway-request-data https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v2/analytics.getGatewayRequests A query can use only the public alias `gateway_requests_v1`. CTEs, subqueries, UNION, and EXCEPT are permitted. The root key must have the `project.*.read_gateway_requests` permission. Unkey limits each query to the workspace of the root key. To get the data for one project, app, or environment, add a filter on `project_id`, `app_id`, or `environment_id`. The workspace retention period and the workspace query limits apply. For the columns and more query examples, see the gateway request analytics documentation. # Query key verification data Source: https://www.unkey.com/docs/platform/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/platform/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. # Query runtime log data Source: https://www.unkey.com/docs/platform/api-reference/analytics/query-runtime-log-data https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v2/analytics.getRuntimeLogs A query can use only the public alias `runtime_logs_v1`. CTEs, subqueries, UNION, and EXCEPT are permitted. The root key must have the `project.*.read_runtime_logs` permission. Unkey limits each query to the workspace of the root key. To get the logs of one project, app, environment, or deployment, add a filter on `project_id`, `app_id`, `environment_id`, or `deployment_id`. The workspace retention period and the workspace query limits apply. For the table, the columns, and more query examples, see [Query runtime logs](/platform/analytics/get-runtime-logs). # Create API namespace Source: https://www.unkey.com/docs/platform/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/platform/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/platform/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/platform/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 app Source: https://www.unkey.com/docs/platform/api-reference/apps/create-app https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v2/apps.createApp Create an app within a project. The app is created with default `production` and `preview` environments. Set exactly one source. Use `git` to create a GitHub-sourced app and connect its repository. Use `oci` to create an app from a prebuilt OCI image. The slug you provide is the stable, caller-defined handle used to reference this app. It must be unique within the project. **Important**: The slug cannot collide with an existing app in the same project. A duplicate slug returns a 409 conflict. **Required Permissions** Your root key must have one of the following permissions: - `project.*.create_app` (to create apps in any project) - `project..create_app` (to create apps in a specific project) # Delete app Source: https://www.unkey.com/docs/platform/api-reference/apps/delete-app https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v2/apps.deleteApp Delete an existing app, identified by its id. Deletion is asynchronous and eventually consistent. The app and all of its associated resources (environments, deployments, custom domains) are torn down by a background workflow. A successful response indicates the deletion was enqueued, not that every resource has already been removed. Apps with delete protection enabled cannot be deleted until protection is disabled. **Required Permissions** Your root key must have one of the following permissions: - `app.*.delete_app` (to delete any app) - `app..delete_app` (to delete a specific app) # Get app Source: https://www.unkey.com/docs/platform/api-reference/apps/get-app https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v2/apps.getApp Retrieve a single app by its id or slug within a project. Use this to fetch app details after creation or to verify an app exists before performing operations. **Required Permissions** Your root key must have one of the following permissions: - `app.*.read_app` (to read any app) - `app..read_app` (to read a specific app) # List apps Source: https://www.unkey.com/docs/platform/api-reference/apps/list-apps https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v2/apps.listApps Retrieve a paginated list of apps within a project. Use this to enumerate every app in a project. Results are ordered by app id and paginated; when `hasMore` is true, pass the returned `cursor` to fetch the next page. **Required Permissions** Your root key must have the following permission: - `app.*.read_app` (to read apps in any project) # Update app Source: https://www.unkey.com/docs/platform/api-reference/apps/update-app https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v2/apps.updateApp Update an existing app, identified by its id. The app name, slug, default branch, and delete protection setting can be changed. Omitted fields are left unchanged. Changing the slug affects the deployment domains generated for this app. **Important**: The slug cannot collide with an existing app in the same project. A duplicate slug returns a 409 conflict. **Required Permissions** Your root key must have one of the following permissions: - `app.*.update_app` (to update any app) - `app..update_app` (to update a specific app) # Create deployment Source: https://www.unkey.com/docs/platform/api-reference/deployments/create-deployment https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v3/deployments.createDeployment Create a deployment for an app in a project. Omit the source to use the app's configured default. A Git app builds its default branch. An OCI app deploys its default image. Optionally provide one source override: - `oci`: deploy a prebuilt OCI image without a build. Mutable tags are resolved to immutable digests before rollout. - `git`: build and deploy from the app's connected GitHub repository, a branch, a specific commit, or a fork commit. Requires the app to have a repository connected. - `deployment`: re-run an existing deployment by its id. Git deployments rebuild from the recorded commit; OCI deployments reuse the recorded resolved image. Returns immediately with a `deploymentId`. The build and rollout run asynchronously. Poll `deployments.getDeployment` to watch status until it is ready. **Authentication**: requires a root key with permission to create deployments. # Get deployment Source: https://www.unkey.com/docs/platform/api-reference/deployments/get-deployment https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v2/deployments.getDeployment Retrieve a single deployment by its id. Use this to check a deployment's status after creating it, or to inspect the runtime configuration of an existing deployment. **Required Permissions** Your root key must have this permission: - `unkey:v1::projects//apps//environments//deployments/#read` (use `*` for any level) # List deployments Source: https://www.unkey.com/docs/platform/api-reference/deployments/list-deployments https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v2/deployments.listDeployments Retrieve a paginated list of deployments within a workspace, newest first. Filter by project, app, environment, lifecycle status, git branch, and creation time. All filters are optional; with none set, every deployment in the workspace is returned. Filters nest: `app` requires `project`, and `environment` and `branch` require both `project` and `app`. Results are paginated; when `hasMore` is true, pass the returned `cursor` to fetch the next page. **Required Permissions** Your root key must have this permission: - `unkey:v1::projects//apps//environments//deployments/*#read` (use `*` for every level you do not filter by) The permission must cover every deployment the filters select: a grant on one environment is not sufficient for a request that spans more than that environment. # Promote deployment Source: https://www.unkey.com/docs/platform/api-reference/deployments/promote-deployment https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v2/deployments.promoteDeployment Promote a deployment to become the current deployment for its environment. All sticky domains are reassigned from the current deployment to the promoted one, and the previous deployment is scheduled for standby. The deployment must be ready, not already shutting down, belong to the production environment, and its app must already have a current deployment. Promoting the deployment that is already current fails, unless the app is in a rolled-back state, in which case promoting the current deployment confirms the rollback and re-enables automatic promotion of future deployments. Promotion runs as a durable workflow: this endpoint returns once the promotion is accepted. Poll `getDeployment` or `listDeployments` to observe the result. **Required Permissions** Your root key must have one of the following permissions: - `environment.*.promote_deployment` (to promote deployments in any environment) - `environment..promote_deployment` (to promote deployments in a specific environment) # Rollback deployment Source: https://www.unkey.com/docs/platform/api-reference/deployments/rollback-deployment https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v2/deployments.rollbackDeployment Roll live traffic back to a previous deployment. `deploymentId` is the deployment to roll back TO; the app's current deployment is used as the rollback source automatically. The target deployment must be ready, not already shutting down, belong to the production environment, and must not itself be the current deployment. After a rollback the app is marked as rolled back, which prevents new deployments from automatically taking over live traffic. Promote the rolled-back deployment (or a newer one) to clear this state. Rollback runs as a durable workflow: this endpoint returns once the rollback is accepted. Poll `getDeployment` or `listDeployments` to observe the result. **Required Permissions** Your root key must have one of the following permissions: - `environment.*.rollback_deployment` (to roll back deployments in any environment) - `environment..rollback_deployment` (to roll back deployments in a specific environment) # Start deployment Source: https://www.unkey.com/docs/platform/api-reference/deployments/start-deployment https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v2/deployments.startDeployment Start a deployment that was previously stopped with `stopDeployment`, so it serves traffic again. The deployment keeps the configuration it had when it was stopped; nothing is rebuilt or redeployed. The deployment must currently be stopped, and must belong to a non-production environment. Production deployments are never stopped, so they cannot be started. Starting is asynchronous: this endpoint only enqueues the start and returns immediately. Poll `getDeployment` until the status reaches `ready`. **Required Permissions** Your root key must have one of the following permissions: - `environment.*.start_deployment` (to start deployments in any environment) - `environment..start_deployment` (to start deployments in a specific environment) # Stop deployment Source: https://www.unkey.com/docs/platform/api-reference/deployments/stop-deployment https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v2/deployments.stopDeployment Stop a running preview deployment. Stopped deployments keep their configuration and can be resumed later with `startDeployment`. The deployment must be ready and running, and must belong to a non-production environment; production deployments cannot be stopped. A deployment that is already draining from a previous stop is rejected with a precondition error. Stopping is asynchronous: this endpoint only enqueues the stop and returns immediately. Poll `getDeployment` until the status reaches `stopped`. **Required Permissions** Your root key must have one of the following permissions: - `environment.*.stop_deployment` (to stop deployments in any environment) - `environment..stop_deployment` (to stop deployments in a specific environment) # Create domain Source: https://www.unkey.com/docs/platform/api-reference/domains/create-domain https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v2/domains.createDomain Attach a custom domain to an environment and start verifying it. The domain is created in the `pending` state and does not serve traffic until verification succeeds. Verification runs in the background and polls DNS, so it is eventually consistent. The response returns `dnsRecords`: every record needed to finish setup, already resolved for whether this domain is an apex or a subdomain. Create every entry exactly as given. One record establishes routing and one proves ownership, and both are needed: whether ownership can be inferred from the routing record depends on how your provider publishes it, and a name another workspace has already verified can only be claimed through the ownership record. Neither is knowable before the records exist. When your DNS provider supports Domain Connect, the response also carries a `domainConnect` object; opening its `url` applies the same records at the provider in one step. The object is absent when the shortcut is unavailable. Domains are unique per workspace, so the same name cannot be attached to two environments. Attaching a domain that already exists in your workspace returns a 409 conflict. How many domains you may attach is set by your plan. Attaching one beyond that allowance returns a 403; upgrade the plan or remove a domain you no longer need. **Important**: verification stops after 24 hours without the required DNS records, and the domain moves to `failed`. **Required Permissions** Your root key must have one of the following permissions: - `environment.*.create_domain` (to attach domains to any environment) - `environment..create_domain` (to attach domains to a specific environment) # Delete domain Source: https://www.unkey.com/docs/platform/api-reference/domains/delete-domain https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v2/domains.deleteDomain Delete a custom domain from your workspace. Address the domain by its id or by its name. Names are unique per workspace, so `api.acme.com` is enough. Unkey stops serving the domain. Later requests fail with a certificate error. The DNS records at your provider stay in place. **Required Permissions** Your root key must have one of the following permissions: - `environment.*.delete_domain` (to delete domains in any environment) - `environment..delete_domain` (to delete domains in a specific environment) # Get domain Source: https://www.unkey.com/docs/platform/api-reference/domains/get-domain https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v2/domains.getDomain Retrieve a custom domain and its verification status. Address the domain by its id or by its name. Names are unique per workspace, so `api.acme.com` is sufficient. You do not need to supply a project, app, or environment. Use this endpoint to poll after `domains.createDomain`. Verification runs in the background and checks DNS approximately each minute. `status: verified` means the domain is verified. Unkey has configured routing and requested a certificate. Each entry in `dnsRecords` has a `verified` flag. The flag shows which records Unkey has read back, so you can see which records are still missing. Some providers hide a record from DNS lookups, for example a proxied or flattened routing record. Such a record stays `false` while it serves traffic. `verificationError` gives the reason for the last failed attempt. `dnsRecords` contains the same values that `domains.createDomain` returned. Use it to recover the values without creating the domain again. **Important**: verification stops 24 hours after the domain was created, and the status becomes `failed`. The window starts at `createdAt`, not at the last attempt. **Required Permissions** Your root key must have one of the following permissions: - `environment.*.read_domain` (to read domains in any environment) - `environment..read_domain` (to read domains in a specific environment) # List domains Source: https://www.unkey.com/docs/platform/api-reference/domains/list-domains https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v2/domains.listDomains List your custom domains with their verification status and DNS records. Filter by project, app, or environment using IDs or slugs, or send `{}` to list domains across your workspace. Use any filter on its own or combine filters to narrow the results. Results match all supplied filters. Omitting `environment` includes all matching environments. Results include only domains you have permission to read, sorted by ID. When `hasMore` is true, send the returned `cursor` to get the next page. `status: verified` means the domain is verified. Unkey has configured routing and requested a certificate. Each domain includes its full `dnsRecords`. Each record has a `verified` flag. The flag shows which records Unkey has read back, so you can see which records are still missing without a second call. Some providers hide a record from DNS lookups, for example a proxied or flattened routing record. Such a record stays `false` while it serves traffic. **Required Permissions** Use a root key with the `environment.*.read_domain` permission. A successful request returns an empty list if no matching domains are readable by your key. # Verify domain Source: https://www.unkey.com/docs/platform/api-reference/domains/verify-domain https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v2/domains.verifyDomain Restart verification for a custom domain. Address the domain by its ID or by its name. Names are unique per workspace, so `api.acme.com` is enough. Call this after you correct the DNS records of a domain that shows `failed`, or to give a `pending` domain a new 24-hour verification period. The domain goes back to `pending` and the 24-hour period starts again. The endpoint returns when Unkey accepts the retry. Poll `domains.getDomain` for the result. A domain that is already `verified` returns a 412. **Required Permissions** Your root key must have one of the following permissions: - `environment.*.verify_domain` (to verify domains in any environment) - `environment..verify_domain` (to verify domains in a specific environment) # Get environment Source: https://www.unkey.com/docs/platform/api-reference/environments/get-environment https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v2/environments.getEnvironment Retrieve a single environment by its id. Use this to fetch environment details after creation or to verify an environment exists before performing operations. **Required Permissions** Your root key must have one of the following permissions: - `environment.*.read_environment` (to read any environment) - `environment..read_environment` (to read a specific environment) # List environment variables Source: https://www.unkey.com/docs/platform/api-reference/environments/list-environment-variables https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v2/environments.listEnvironmentVariables Retrieve the environment variables for an environment. Identify the environment by its project, app, and environment identifiers. Results are ordered by variable id and paginated; when `hasMore` is true, pass the returned `cursor` to fetch the next page. `recoverable` variables are returned with their decrypted plaintext value. `writeonly` variables never expose a value: only the key, kind, and description are returned. **Required Permissions** Your root key must have one of the following permissions: - `environment.*.read_environment_variables` (for any environment) - `environment..read_environment_variables` (for a specific environment) # List environments Source: https://www.unkey.com/docs/platform/api-reference/environments/list-environments https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v2/environments.listEnvironments Retrieve the environments within an app. Use this to enumerate every environment in an app. Identify the app by its project slug and app slug. Results are ordered by environment id. An app has only a handful of environments, so all of them are returned in a single response. **Required Permissions** Your root key must have the following permission: - `environment.*.read_environment` (to read environments in any app) # Remove environment variables Source: https://www.unkey.com/docs/platform/api-reference/environments/remove-environment-variables https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v2/environments.removeEnvironmentVariables Remove environment variables from an environment in a single atomic request. This operation only deletes keys by name. Keys in the payload that exist are removed; keys that are not present are ignored, since their absence already matches the requested state. To replace or update values, use `setEnvironmentVariables` instead. The whole operation is atomic: if any part fails the environment is left unchanged. Duplicate keys in the payload collapse to a single removal. Values are never read or returned. **Required Permissions** Your root key must have one of the following permissions: - `environment.*.remove_environment_variables` (for any environment) - `environment..remove_environment_variables` (for a specific environment) # Set environment variables Source: https://www.unkey.com/docs/platform/api-reference/environments/set-environment-variables https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v2/environments.setEnvironmentVariables Create or update environment variables for an environment in a single atomic request. By default this is an upsert: each variable in the payload is created if new or fully overwritten if the key already exists, and any variable not in the payload is left untouched. This lets you change one variable without re-sending the others, which matters for write-only secrets you can no longer read back. Set `prune: true` to make it a full replace instead: after upserting, every variable not in the payload is deleted. Sending `prune: true` with an empty `variables` list resets the environment by deleting every variable. Each variable is written exactly as sent, never merged, so omitted optional fields fall back to their defaults rather than the previous value. Values are always encrypted at rest. Set `kind: recoverable` to allow a value to be read back; it defaults to `writeonly`, which can never be read back through the API. **Required Permissions** Your root key must have one of the following permissions: - `environment.*.set_environment_variables` (for any environment) - `environment..set_environment_variables` (for a specific environment) # Update environment settings Source: https://www.unkey.com/docs/platform/api-reference/environments/update-environment-settings https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v2/environments.updateSettings Update the build, runtime, and regional settings for an environment. All settings fields are optional. Omit a field to leave it unchanged. For nullable fields (`dockerfile`, `healthcheck`, `openapiSpecPath`), send null to clear the value. When `regions` is present it replaces the full set of regions for the environment. **Required Permissions** Your root key must have one of the following permissions: - `environment.*.update_environment` (to update any environment) - `environment..update_environment` (to update a specific environment) # List policies Source: https://www.unkey.com/docs/platform/api-reference/gateway/list-policies https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v2/gateway.listPolicies Retrieve an environment's gateway policies in evaluation order: the gateway evaluates them top to bottom and the first rejection short-circuits the request. The full policy list is returned in a single response. **Required Permissions** Your root key must have one of the following permissions: - `environment.*.read_policies` (for any environment) - `environment..read_policies` (for a specific environment) # Set policies Source: https://www.unkey.com/docs/platform/api-reference/gateway/set-policies https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v2/gateway.setPolicies Replace an environment's gateway policies in a single atomic request. Policies run at the edge before requests reach your app: verify API keys, rate limit, block requests outright, or validate them against your OpenAPI spec. Policies are an ordered list: the gateway evaluates them top to bottom and the first rejection short-circuits the request. Each policy sets exactly one of `keyauth`, `ratelimit`, `firewall` or `openapi`, plus optional `match` expressions restricting which requests it applies to. Every call is a full replace: the environment's policies become exactly the request list in the given order, and the server generates a fresh id for each one. An empty list removes all policies. The operation is atomic: if any policy is invalid, nothing is written. **Required Permissions** Your root key must have one of the following permissions: - `environment.*.set_policies` (for any environment) - `environment..set_policies` (for a specific environment) # Update policy Source: https://www.unkey.com/docs/platform/api-reference/gateway/update-policy https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v2/gateway.updatePolicy Update a single policy in place without resending the environment's full policy list. The policy keeps its id and its position in the evaluation order, and all other policies are untouched. Omitted fields keep their stored values; at least one updatable field must be provided. Setting `match` to null removes all match expressions so the policy applies to every request. Providing one of `keyauth`, `ratelimit`, `firewall` or `openapi` replaces the policy's rule entirely, including switching its type; at most one may be set. Policy ids are regenerated whenever `gateway.setPolicies` replaces the list, so fetch current ids via `gateway.listPolicies` first. **Required Permissions** Your root key must have one of the following permissions: - `environment.*.update_policy` (for any environment) - `environment..update_policy` (for a specific environment) # Install GitHub App Source: https://www.unkey.com/docs/platform/api-reference/github/install-github-app https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v2/github.installApp Start installing the Unkey GitHub App for your workspace. Returns a GitHub App install URL: open it in a browser to install the app and grant repository access. After installation GitHub returns to Unkey, which binds the installation to your workspace and lands you in the workspace settings. Installation is workspace-wide and takes no parameters. Once installed, link repositories to individual apps with the `git` field on `apps.createApp` and `apps.updateApp`. **Required Permissions** Your root key must have the following permission: - `workspace.*.install_github` # Create Identity Source: https://www.unkey.com/docs/platform/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/platform/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/platform/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/platform/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/platform/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/platform/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/platform/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/platform/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/platform/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/platform/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/platform/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/platform/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/platform/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/platform/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/platform/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/platform/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/platform/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/platform/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/platform/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/platform/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/platform/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/platform/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/platform/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/platform/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/platform/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/platform/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/platform/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/platform/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/platform/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/platform/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/platform/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/platform/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/platform/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/platform/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/platform/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/platform/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. # Create project Source: https://www.unkey.com/docs/platform/api-reference/projects/create-project https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v2/projects.createProject Create a project to group deployments and applications under a workspace-scoped slug. The slug you provide is the stable, caller-defined handle used to reference this project in subsequent operations (get, update, delete). It must be unique within your workspace. **Important**: The slug cannot collide with an existing project in your workspace. A duplicate slug returns a 409 conflict. **Required Permissions** Your root key must have the following permission: - `project.*.create_project` (to create projects in your workspace) # Delete project Source: https://www.unkey.com/docs/platform/api-reference/projects/delete-project https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v2/projects.deleteProject Delete an existing project in your workspace, identified by its id. Deletion is asynchronous and eventually consistent. The project and all of its associated resources (apps, environments, deployments, custom domains) are torn down by a background workflow. A successful response indicates the deletion was enqueued, not that every resource has already been removed. Projects with delete protection enabled cannot be deleted until protection is disabled. **Required Permissions** Your root key must have one of the following permissions: - `project.*.delete_project` (to delete any project) - `project..delete_project` (to delete a specific project) # Get project Source: https://www.unkey.com/docs/platform/api-reference/projects/get-project https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v2/projects.getProject Retrieve a single project in your workspace by its id. Use this to fetch project details after creation, verify a project exists before performing operations, or resolve a project's metadata from its id. **Required Permissions** Your root key must have one of the following permissions: - `project.*.read_project` (to read any project) - `project..read_project` (to read a specific project) # List projects Source: https://www.unkey.com/docs/platform/api-reference/projects/list-projects https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v2/projects.listProjects Retrieve a paginated list of projects in your workspace. Use this to build project management dashboards or to enumerate projects for administrative purposes. Results are ordered by project id and returned in pages. When `hasMore` is true, pass the returned `cursor` to fetch the next page. **Required Permissions** Your root key must have the following permission: - `project.*.read_project` (to read projects in your workspace) # Update project Source: https://www.unkey.com/docs/platform/api-reference/projects/update-project https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v2/projects.updateProject Update an existing project in your workspace, identified by its id. The project name, slug, and delete protection setting can be changed. Omitted fields are left unchanged. Changing the slug affects the deployment domains generated for this project. **Required Permissions** Your root key must have one of the following permissions: - `project.*.update_project` (to update any project) - `project..update_project` (to update a specific project) # Apply multiple rate limit checks Source: https://www.unkey.com/docs/platform/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/platform/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/platform/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/platform/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/platform/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/platform/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` # API authentication Source: https://www.unkey.com/docs/platform/api/authentication Send your root key as a bearer token and read the errors a bad key returns. Every call to `https://api.unkey.com` needs a root key as a bearer token. The only exception is the customer portal's end-user endpoints, which take a portal session instead. ## Send a root key Put the key in the `Authorization` header after `Bearer `: ```bash theme={"system"} curl -X POST https://api.unkey.com/v2/apis.listKeys \ -H "Authorization: Bearer unkey_1234abcd..." \ -H "Content-Type: application/json" \ -d '{"apiId": "api_1234abcd"}' ``` A root key belongs to one workspace, and every request stays inside it. You don't pass a workspace ID, and a key can't reach another workspace. See [Root keys](/docs/platform/root-keys/overview) to create one. ## Errors a bad key returns | Status | Code | What it means | | - | - | - | | 400 | `err:unkey:authentication:missing` | There's no `Authorization` header. | | 400 | `err:unkey:authentication:malformed` | The header is missing the `Bearer ` prefix, or has nothing after it. | | 401 | `err:unkey:authentication:key_not_found` | The key doesn't exist or was deleted. | | 403 | `err:unkey:authorization:key_disabled` | The key is disabled. | | 403 | `err:unkey:authorization:workspace_disabled` | The key's workspace is disabled. | | 403 | `err:unkey:authorization:forbidden` | The key has expired, for example because a rotation's grace period ended. | | 403 | `err:unkey:authorization:insufficient_permissions` | The key doesn't have the permission the endpoint needs. The `detail` names it. Some endpoints return `404` instead, the same as if the resource didn't exist. See [Root key permissions](/docs/platform/root-keys/permissions). | All of these use the standard [error envelope](/docs/platform/api/errors), so one error handler covers them. ## Keep root keys safe Treat a root key like a database password: * Create one per service with only the permissions that service needs. A leaked key then does less damage and is easy to replace. * Never put a root key in client-side code, a mobile app, or a public repository. * If one leaks, rotate or delete it under **Settings > Root Keys**. See [Root keys](/docs/platform/root-keys/overview). Root keys are for managing Unkey. They aren't the API keys you issue to your own users, which you check with `keys.verifyKey`. See [Verifying keys](/docs/api-management/keys/verifying-keys). # API error envelope Source: https://www.unkey.com/docs/platform/api/errors Read the error object a failed request returns and find the page for its code. When a request fails, the response has an `error` object instead of `data`. Every Unkey error has the same shape (Problem Details for HTTP APIs, RFC 7807), so one handler covers them all. The `type` field links to the page for that error. ```json theme={"system"} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "error": { "title": "Insufficient Permissions", "detail": "Missing one of these permissions: api.*.delete_api or api.api_1234abcd.delete_api", "status": 403, "type": "https://unkey.com/docs/errors/unkey/authorization/insufficient_permissions" } } ``` ## Fields A short summary, for example `Not Found` or `Insufficient Permissions`. It doesn't change for a given problem, but match on `type` in code. A plain explanation of what went wrong, written to help a developer fix it. It can name the field, resource, or permission involved. Don't parse it, because the wording can change. The HTTP status code, repeated in the body. A URL that identifies the error and links to its page. The last three path segments are the error code, `err:{system}:{category}:{specific}`. Match on this field to handle a specific error. Error pages on this site shorten it to the path, but the API always sends the full URL. Sent with 400, 408, 413, and 499 responses. When the request fails validation, it has one entry per problem, so you can fix them all at once. Otherwise it's an empty array. A JSON path to the part of the request with the problem, such as `body.permissions[0].name` or `body.limit`. Which validation rule failed. A suggestion for satisfying the rule. Not every validation error carries one. ## Status codes What each status means on this API, and the codes you'll see most often with it: | Status | Meaning | Typical codes | | - | - | - | | 400 | The request is malformed or fails validation. `errors` lists the problems. | `err:unkey:application:invalid_input`, `err:unkey:authentication:missing`, `err:unkey:authentication:malformed`, and, under `err:user:bad_request:`, `request_body_unreadable`, `permissions_query_syntax_error`, `invalid_analytics_query`, `invalid_analytics_table`, `invalid_analytics_function`, `invalid_analytics_query_type`, `query_range_exceeds_retention`, `per_key_breakout_too_large` | | 401 | The bearer token does not identify a key or a portal session. | `err:unkey:authentication:key_not_found`, `err:unkey:authentication:portal_session_not_found` | | 403 | The key is known but may not do this. | `err:unkey:authorization:insufficient_permissions`, `err:unkey:authorization:key_disabled`, `err:unkey:authorization:workspace_disabled`, `err:unkey:authorization:forbidden`, `err:unkey:limits:custom_domain_limit_exceeded` | | 404 | The named resource does not exist in your workspace, or you may not see it. | `err:unkey:data:*_not_found` for each resource type | | 408 | The request took too long to read. | `err:user:bad_request:request_timeout` | | 409 | Creating a resource that already exists. | `err:unkey:data:*_already_exists` for identities, roles, permissions, projects, apps, domains, portals | | 410 | The resource existed and has been removed. | `err:unkey:data:ratelimit_namespace_gone` | | 412 | A precondition on the resource is not met. | `err:unkey:application:protected_resource`, `err:unkey:application:precondition_failed` | | 413 | The body exceeds the size limit. | `err:user:bad_request:request_body_too_large` | | 422 | The request was valid but could not be executed as asked. | Under `err:user:unprocessable_entity:`, `query_execution_timeout`, `query_memory_limit_exceeded`, `query_rows_limit_exceeded` | | 429 | A quota is exceeded. | `err:user:too_many_requests:query_quota_exceeded` | | 499 | You closed the connection before the response was written. | `err:user:bad_request:client_closed_request` | | 500 | Something failed on our side. Quote `meta.requestId` to support. | `err:unkey:application:unexpected_error`, `err:unkey:application:service_unavailable`, `err:unkey:application:assertion_failed` | | 503 | The analytics backend is unreachable. Retry with backoff. | `err:unkey:data:analytics_connection_failed` | An invalid key isn't an HTTP error. When you verify a key that isn't valid, `keys.verifyKey` returns HTTP 200 with `data.valid: false` and a `data.code`, because your request worked. The HTTP errors above are for problems with your request or your root key. See [Verifying keys](/docs/api-management/keys/verifying-keys). ## Handling errors 1. Check `status`. A 4xx means fix the request. A 500 or 503 means retry. 2. For a 4xx, branch on `type` if you need to tell causes apart. Show `detail` to developers, never to end users. 3. For a 400, show each entry in `errors` with its `location` and `message`. 4. Always log `meta.requestId` with the error, so support can find the exact request. Every code has its own page with a "How to fix" section, under Errors in the Platform navigation. # HTTP API basics Source: https://www.unkey.com/docs/platform/api/overview Call the Unkey API: one base URL, POST endpoints, and a shared response envelope. Compute and API Management share one HTTP API. Here's how to make a request, read the response, and page through lists. ## Make a request Send a `POST` with a JSON body to `https://api.unkey.com/v2/{service}.{procedure}`. There are no path or query parameters. The only `GET` routes are `/v2/liveness`, the health check, and `/openapi.yaml` and `/reference`, which serve the OpenAPI document and a browsable reference. See [RPC naming convention](/docs/platform/api/rpc-convention) for how endpoints are named. ```bash theme={"system"} curl -X POST https://api.unkey.com/v2/keys.verifyKey \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{"key": "sk_live_..."}' ``` Put a root key in the `Authorization: Bearer` header. See [API authentication](/docs/platform/api/authentication). ## Read the response Every response is a JSON object with `meta`. A success adds `data`, and a failure adds `error` instead. Present on every response. A unique ID for this request, prefixed `req_`. It's the only field in `meta`, and it isn't sent as a response header. Log it with every call and include it when you contact support, so we can find the exact request. The endpoint's result. Its shape is documented per endpoint in each product's API reference. List endpoints return an array here. Present instead of `data` when the request failed. Its fields are on [API error envelope](/docs/platform/api/errors). Present only on list endpoints. See [Page through lists](#page-through-lists). ```json A successful response theme={"system"} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "data": { "keyId": "key_1234abcd" } } ``` ## Page through lists List endpoints use a cursor instead of page numbers. Each request takes an optional `limit` and `cursor`, and each response has a `pagination` object next to `data`. The example uses `apis.listKeys`, but every list endpoint works the same way. Maximum items to return. For `apis.listKeys` it's 1 to 100, default 100. Most list endpoints default to 100, but `ratelimit.listOverrides` defaults to 50. Send `limit` if the page size matters to you. The `pagination.cursor` value from the previous response, sent back unchanged. Leave it out for the first page. Cursors can expire, so don't store them long term or build them yourself. `true` when there's another page. When `false`, you have everything and `cursor` may be missing or `null`. The value to send as `cursor` on the next request. Only use it while `hasMore` is `true`. ```bash theme={"system"} curl -X POST https://api.unkey.com/v2/apis.listKeys \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{"apiId": "api_1234abcd", "limit": 50}' ``` ```json theme={"system"} { "meta": { "requestId": "req_5678efgh" }, "data": [ { "keyId": "key_1111aaaa" }, { "keyId": "key_2222bbbb" } ], "pagination": { "cursor": "key_3333cccc", "hasMore": true } } ``` ```bash theme={"system"} curl -X POST https://api.unkey.com/v2/apis.listKeys \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{"apiId": "api_1234abcd", "limit": 50, "cursor": "key_3333cccc"}' ``` Keep the other request fields the same on every page. If you change a filter partway through, the cursor no longer works. # Too many requests Source: https://www.unkey.com/docs/platform/api/rate-limits What a 429 from api.unkey.com means and what to do about it. A 429 with `err:user:too_many_requests:workspace_rate_limited` means your workspace is sending too many requests to the Unkey API. Nothing ran, so it's safe to retry. Wait a moment, then retry with exponential backoff. If it keeps happening, contact [support@unkey.com](mailto:support@unkey.com). A 429 with `err:user:too_many_requests:query_quota_exceeded` is the analytics query quota, covered on [Analytics restrictions and quotas](/docs/api-management/analytics/restrictions-and-quotas). # RPC naming convention Source: https://www.unkey.com/docs/platform/api/rpc-convention Work out any endpoint's name from the POST /v2/{service}.{procedure} pattern. Every Unkey API endpoint is a `POST` to a path that names a service and a procedure, like `/v2/keys.verifyKey` or `/v2/deployments.promoteDeployment`. It isn't REST: the path doesn't name a resource, and the HTTP method doesn't say what the call does. All parameters go in the JSON body. ## The shape ```text theme={"system"} POST https://api.unkey.com/v2/{service}.{procedure} Content-Type: application/json Authorization: Bearer ``` The `service` is a camelCase noun for a group of resources, and the `procedure` is a camelCase verb phrase that usually ends in the resource name. IDs go in the body, not the path. For example, to fetch a key you send `{"keyId": "key_..."}` to `keys.getKey`. There are no query strings. There are three `GET` exceptions: `/v2/liveness`, a health check that needs no root key, and `/openapi.yaml` and `/reference`, which serve the OpenAPI document and a browsable reference. ## Services Almost every procedure is under `/v2`. A procedure gets a new version prefix when its request or response changes in a breaking way. So far that's happened once, for `/v3/deployments.createDeployment`. Each service belongs to one product, and its endpoints are documented there. | Service | Product | What its procedures manage | | - | - | - | | `apis` | API Management | Keyspaces (called APIs in the endpoint names) and listing their keys. | | `keys` | API Management | Creating, verifying, updating, rerolling, and deleting API keys, and their permissions and roles. | | `identities` | API Management | Identities shared by several keys. | | `permissions` | API Management | Permissions and roles you define for your keys. | | `ratelimit` | API Management | The standalone rate limiting service and its overrides. Singular, not `ratelimits`. | | `portal` | API Management | Developer portals and the end-user sessions they mint. | | `analytics` | Both | SQL queries over verifications, rate limits, gateway requests, and runtime logs. | | `projects` | Compute | Projects. | | `apps` | Compute | Apps within a project, including the git connection. | | `environments` | Compute | Environment settings and variables. | | `deployments` | Compute | Creating, starting, stopping, promoting, and rolling back deployments. | | `domains` | Compute | Custom domains and their verification. | | `gateway` | Compute | Gateway policies on an environment. | | `github` | Compute | Installing the Unkey GitHub App for the workspace. | ## Verbs Procedures use a small set of verbs, so you can often guess a name: * `create`, `get`, `list`, `update`, and `delete` for the basics. * Action verbs where a resource does something, such as `verifyKey`, `rerollKey`, `migrateKeys`, `limit`, `multiLimit`, `setOverride`, `promoteDeployment`, `rollbackDeployment`, `setPolicies`, and `installApp`. `set` procedures (`setPermissions`, `setRoles`, `setPolicies`, `setEnvironmentVariables`) replace the whole list. `add` and `remove` procedures change one part of it. ## Deprecated procedures When a procedure is replaced, the old path keeps working for a while and shows a deprecation badge in the reference. Use the newest path in new code. Three paths are deprecated today: * `/v2/deploy.createDeployment` and `/v2/deploy.getDeployment` are replaced by the `deployments` service. * `/v2/deployments.createDeployment` is replaced by `/v3/deployments.createDeployment`, which takes an `oci` source instead of `image` and lets you leave out the source to use the app's default. # Limits Source: https://www.unkey.com/docs/platform/billing/limits See the limits your plan puts on your workspace. The two products are bought separately, and your workspace holds one set of limits between them. An API Management tier sets your request quota, key allowance and retention. A Compute plan sets the Compute values, and also sets retention and team members when you have no paid API tier. Read the API Management table by the tier you pay for, and the Compute tables by your Compute plan: Starter, Pro, or Business. The two products' plan names are unrelated, so a Compute Pro plan says nothing about your API tier. ## API management API Management is sold as a free tier, seven fixed Pro tiers, and Enterprise. The tier you're on sets your monthly request quota, your key allowance, and how long logs are kept. | Plan | Price | Valid requests per month | API keys | Log retention | Audit log retention | | - | - | - | - | - | - | | Free | \$0 | 150,000 | 1,000 | 1 day | 3 days | | Pro | \$25 | 250,000 | 1,000,000 | 7 days | 14 days | | Pro | \$50 | 500,000 | 1,000,000 | 7 days | 14 days | | Pro | \$75 | 1,000,000 | 1,000,000 | 7 days | 14 days | | Pro | \$100 | 2,000,000 | 1,000,000 | 7 days | 14 days | | Pro | \$250 | 10,000,000 | 1,000,000 | 7 days | 14 days | | Pro | \$500 | 50,000,000 | 1,000,000 | 7 days | 14 days | | Pro | \$1,000 | 100,000,000 | 1,000,000 | 7 days | 14 days | | Enterprise | Custom | Custom | Custom | Custom | Custom | Only valid requests count against the quota. A verification rejected for a bad key, an exhausted rate limit, or any other reason isn't billed, so a key under attack doesn't consume the quota you bought. Passing the quota means moving to a higher tier, and you will receive an email when you do so. The API keys column is the allowance your plan includes. Unlimited keyspaces are included on every tier. [Enterprise](mailto:support@unkey.com?subject=Unkey%20Enterprise%20Quote) covers custom quotas, IP allow listing, a dedicated Slack channel, annual contracts and SLAs. ## Logs A paid API tier owns both retention values, from the table above, whatever Compute plan you hold. Only when you have no paid API tier does your Compute plan set them instead: | Limit | No Compute plan | Starter | Pro | Business | | - | - | - | - | - | | Log retention (request and runtime logs) | 1 day | 3 days | 7 days | 14 days | | Audit log retention | 3 days | 7 days | 14 days | 30 days | | Log drains | 0 | 0 | 0 | 0 | Log drains aren't included on any plan today. Ask [support@unkey.com](mailto:support@unkey.com) if you need one. ## Compute These rows apply once you have a Compute plan. Workspace rows measure what your running deployments have allocated. Per-instance rows cap what a single instance can request. | Limit | Starter | Pro | Business | | - | - | - | - | | Workspace CPU | 30 vCPU | 120 vCPU | 240 vCPU | | CPU per instance | 2 vCPU | 8 vCPU | 16 vCPU | | Workspace memory | 60 GiB | 240 GiB | 480 GiB | | Memory per instance | 2 GiB | 8 GiB | 32 GiB | | Workspace ephemeral disk | 120 GiB | 480 GiB | 960 GiB | | Ephemeral disk per instance | 10 GiB | 10 GiB | 10 GiB | | Concurrent builds | 1 | 1 | 1 | | Replicas per region | 4 | 8 | 16 | | Custom domains | 1 | Unlimited | Unlimited | At a Compute limit, scale down or remove a deployment, or move to a plan with a higher ceiling. For a higher ceiling on the plan you already hold, ask [support@unkey.com](mailto:support@unkey.com). "Unlimited" custom domains is a very high cap that exists to stop abuse, not a priced feature. # Unkey plans and free tier Source: https://www.unkey.com/docs/platform/billing/plans Free API Management has 150,000 valid requests a month, and Compute Starter is $5 a month after a free first month. The free API Management plan is \$0 a month with 150,000 valid requests a month, 1,000 API keys, 1 day of logs, and 3 days of audit logs. Compute plans deploy and run your APIs: * Starter (\$0 for the first month, then \$5 a month) * Pro (\$20 for the first month, then \$25 a month) * Business (\$45 for the first month, then \$50 a month) Every Compute plan includes monthly usage credits for compute, storage, egress, and active keys, and you get \$5 off your first month through December 31, 2026. ## What each product includes Paid API tiers, and the CPU, memory, disk, replica, and domain limits on each Compute plan, are on [Limits](/docs/platform/billing/limits). Prices are on [unkey.com/pricing](https://unkey.com/pricing). | | API Management | Compute | | - | - | - | | Starting point | Free tier. Every workspace starts here | Starter, Pro, or Business. Required before you can deploy | | Included from the start | 150,000 valid requests a month, 1,000 API keys, 1 day of logs, 3 days of audit logs | Starter is \$0 for the first month, then \$5 a month, with monthly usage credits for compute, storage, egress, and active keys, and \$5 off your first month through December 31, 2026. See [Usage](/docs/platform/billing/usage) | | What you get by paying more | A higher monthly quota, longer retention, and team members | Resource limits differ by plan and are listed on [Limits](/docs/platform/billing/limits). Pro and Business also include team members | Your workspace pays for the two products separately. API Management has a tier and Compute has a plan, and you can have either one without the other. Both use one payment method and one set of invoices. Manage both under **Settings > Billing**. Only [admins](/docs/platform/workspace/team) can change plans or payment details. Everyone else sees the controls disabled with "Admin access required to manage billing". Billing settings page showing the Compute and API management plans with Change and Upgrade buttons, and the Compute cost control budget with its email alerts ## API Management tier Every workspace starts on the **Free** tier: 150,000 billable operations per month, 1,000 API keys, 1 day of request log retention, and 3 days of audit log retention. Billable operations are key verifications and rate limit operations. See [Usage](/docs/platform/billing/usage) for what counts. A paid tier raises the monthly quota and both retention periods, and includes [team members](/docs/platform/workspace/team). Each tier's numbers are on [Limits](/docs/platform/billing/limits) and [unkey.com/pricing](https://unkey.com/pricing), and the billing page shows them when you choose. ## Compute plan You need a Compute plan before you can deploy an app. There are three: * **Starter** for hobby projects and testing ideas. * **Pro** for growing apps in production. * **Business** for teams scaling up. Every plan includes git push to deploy, a preview environment per pull request, instant rollback, autoscaling, and a monthly usage credit. The plans differ in their resource limits (CPU, memory, disk, replicas, and custom domains), listed on [Limits](/docs/platform/billing/limits). Pro and Business also include team members, so they give a team to a workspace on the free API tier. Pick a plan under **Settings > Billing** in the **Choose a Compute plan** dialog. If you try to create a project or deploy without one, the dashboard opens the same dialog. Change Compute plan dialog listing Starter, Pro, and Business with their monthly prices and what every plan includes Usage above the included credit is billed on average actual vCPU and memory (not your configured ceilings), allocated ephemeral disk, egress, and active keys verified through Compute Key Auth. Details are on [Usage](/docs/platform/billing/usage) and [unkey.com/pricing](https://unkey.com/pricing). To cap it, set a monthly [spend budget](/docs/compute/configure/spend-budget). It emails you at 50, 75, and 100 percent and can stop your usage at 100 percent. ## Add a payment method You need a card on file before you can upgrade. Under **Settings > Billing**, click **Add payment method** and enter the card in Stripe checkout. Both products charge that card. To see invoices or change the card later, click **Open Portal** on the **Billing Portal** card. ## Change or cancel the API tier Choose a tier under **Settings > Billing**. * **Upgrade.** Applies immediately. * **Downgrade or cancel.** Takes effect at the end of the current period. The page shows the date, and you can resume your current tier until then. While a cancellation is pending you can't pick another tier, and you'll see "Resume your API plan before changing it." When a cancellation takes effect, the workspace moves to the free tier, and the free tier's quota and retention apply. ## Change or cancel Compute Choose a new plan in the **Change Compute plan** dialog under **Settings > Billing**. The timing is different from the API tier: * **Upgrade.** You're charged the prorated fee difference right away and get more usage credit for the rest of the period. * **Downgrade.** Your [limits](/docs/platform/billing/limits) drop right away. The fee difference isn't refunded, and you keep the usage credit the old fee gave you. The lower fee and credit start at the next renewal. * **Cancel.** Click **Cancel Compute**. It takes effect immediately, not at the end of the period. We tear down your running deployments right away and stop billing further usage. Usage up to that point is billed as normal, and the fee isn't refunded. If you pick a Compute plan again before the canceled period ends, we resume that subscription instead of charging a new fee. Picking a higher plan than the one you canceled charges the prorated difference. Your deployments don't come back on their own, so deploy again once the plan is active. Canceling one product doesn't affect the other. You can drop Compute and keep a paid API tier, or the reverse. ## What happens to your team when you downgrade Team members come with any paid API tier, or with Compute Pro or Business. If the workspace loses all of these, we deactivate every member except the one who created the workspace. That happens when: * A paid API tier ends at the end of its period, and you don't have Compute Pro or Business. * You're on the free API tier and downgrade Compute to Starter or cancel it. # Usage Source: https://www.unkey.com/docs/platform/billing/usage Check what your workspace has used this month before the invoice arrives. Usage covers one calendar month (UTC), with one total per product: API for every workspace, Compute once you have a Compute plan. ## API management Usage has two parts, **Key verifications** and **standalone rate limit operations**, metered against your monthly operations quota. Only successful calls count: invalid, expired, or over-limit requests aren't billed. These two are the billable operations that [Limits](/docs/platform/billing/limits) caps and that a paid API tier prices. Usage page showing Compute cost per app and, below it, API management requests this period split into key verifications and rate limit operations ## Compute Compute usage totals cost for the period, broken down by project, then app, then environment. Each level shows the metered quantities and a cost. Usage page with a daily Compute cost chart and a project expanded to show spend by resource and cost per app ### What we meter | Quantity | How it's billed | | - | - | | CPU (vCPU-hrs) | Average active virtual CPU per running instance. Billed on actual usage, not the vCPU ceiling you configured. Idle time waiting on I/O does not count as active CPU. | | Memory (GiB-hrs) | Average memory used per running instance. Billed on actual usage, not the memory ceiling you configured. | | Storage (GiB-hrs) | Ephemeral disk volume mounted per instance. Billed on the allocated size for as long as the instance runs, up to 10 GiB per instance. | | Egress (GiB) | Total outbound network data transfer for the month. | | Verified keys | Distinct keys with at least one verification during the month through a Key Auth policy on Compute. These bill to the project, not a keyspace. | Runtime settings are ceilings and headroom. Most workloads never pin 100% of CPU or memory, so configuring 1 vCPU while averaging 0.5 vCPU bills the 0.5, not the 1. Preview deployments use the same rates as production. Unit rates and the plan fee with included usage credit are on [unkey.com/pricing](https://unkey.com/pricing). Use the calculator at the bottom of that page to estimate a month. The plan fee and included credit are also summarized on [Plans](/docs/platform/billing/plans). Usage is what you consumed. [Limits](/docs/platform/billing/limits) are the ceilings. Costs here are the metered part of the bill. # unkey auth login Source: https://www.unkey.com/docs/platform/cli/auth/login Store a root key locally so unkey api commands stop asking for it. Store a root key locally so the `unkey api` commands can authenticate without `--root-key` on every call. ## Usage ```bash theme={"system"} unkey auth login ``` The command takes no flags or arguments. It prompts `Enter your root key:` and doesn't show what you type. An empty answer fails with `root key cannot be empty`. On success it saves the key to `~/.unkey/config.toml` and prints `Authentication successful. Key stored in `. ## What gets written ```toml ~/.unkey/config.toml theme={"system"} root_key = "unkey_xxx" ``` Only your user can read the file (mode `0600`, in a `0700` directory). Running the command again replaces the file, which is how you change the stored key. The command doesn't check the key with the API, so it saves whatever you type. A wrong key only shows up on your next `unkey api` call. Only `unkey api` commands read this file, and only when neither `--root-key` nor `UNKEY_ROOT_KEY` is set. `unkey deploy` ignores it. See [CLI authentication](/docs/platform/cli/authentication) for which key wins. ## Examples Log in, then run commands without a key flag: ```bash theme={"system"} unkey auth login unkey api apis list-keys --api-id=api_1234abcd ``` Replace the stored key with a new one: ```bash theme={"system"} unkey auth login ``` Bypass the stored key for a single command: ```bash theme={"system"} unkey api apis list-keys --api-id=api_1234abcd --root-key=unkey_other ``` # CLI authentication Source: https://www.unkey.com/docs/platform/cli/authentication Give the CLI a root key by flag, environment variable, or stored config. Every CLI command that calls the API needs a root key, the same one you'd send in `Authorization: Bearer`. The easiest way is to store it once with `unkey auth login`. You can also pass it with a flag or an environment variable. ## Store a key with `unkey auth login` ```bash theme={"system"} unkey auth login ``` It asks for your root key (`Enter your root key:`) and doesn't show what you type, so the key stays out of your screen and shell history. It saves the key to `~/.unkey/config.toml`, which only your user can read. Run it again to replace the stored key. See [unkey auth login](/docs/platform/cli/auth/login) for the full reference. The file holds the key in plain text, so treat it like the key. Keep it out of version control and off shared machines. It's best to create a root key just for the CLI, with only the permissions your scripts need. See [Root key permissions](/docs/platform/root-keys/permissions). ## Override the stored key For one command, pass `--root-key`: ```bash theme={"system"} unkey api keys get-key --key-id=key_1234abcd --root-key=unkey_xxx ``` For a whole shell session, set `UNKEY_ROOT_KEY`: ```bash theme={"system"} export UNKEY_ROOT_KEY=unkey_xxx unkey api keys get-key --key-id=key_1234abcd ``` ## Which key `unkey api` uses `unkey api` commands use the first key they find: 1. The `--root-key` flag. 2. The `UNKEY_ROOT_KEY` environment variable. 3. The `root_key` value in the config file. That's `~/.unkey/config.toml` unless you set `--config` or `UNKEY_CONFIG`. If there's no key anywhere, the command stops before calling the API with `no root key provided`. If the API rejects the key, you get `Authentication failed: ...` and a hint to run `unkey auth login`. ## `unkey deploy` doesn't read the config file `unkey deploy` only takes a key from `--root-key` or `UNKEY_ROOT_KEY`. It never reads `~/.unkey/config.toml`, so `unkey auth login` doesn't help it. In CI, set `UNKEY_ROOT_KEY` as a secret. Locally, export it in the shell you deploy from. See [unkey deploy](/docs/compute/cli/deploy). ## Point at a different API host `--api-url` (or `UNKEY_API_BASE_URL`) sets the base URL for `unkey api` commands, and `unkey deploy` has `--api-base-url`. The default is `https://api.unkey.com`. You don't normally need to change it. # Install the CLI Source: https://www.unkey.com/docs/platform/cli/install Install the unkey CLI from npm or a GitHub release and check its version. The CLI is a single static binary named `unkey`. ```bash theme={"system"} npm install -g unkey ``` Without Node, download the archive for your platform from the [releases page](https://github.com/unkeyed/unkey/releases) and put the `unkey` binary on your `PATH` instead. Both paths ship the same binary, currently . ## Supported platforms The CLI runs on Linux and macOS, on both x86\_64 and arm64. There's no Windows build. On the [releases page](https://github.com/unkeyed/unkey/releases), CLI releases are tagged `cli/vX.Y.Z` and each archive is named `unkey__.tar.gz`, for example `unkey_Darwin_arm64.tar.gz` for an Apple silicon Mac or `unkey_Linux_x86_64.tar.gz`. On npm, stable releases are tagged `latest` and pre-releases `next`. ## Check the installation ```bash theme={"system"} unkey --version ``` This prints the installed version. `unkey --help` lists top-level commands. See [The unkey CLI](/docs/platform/cli/overview) for how they're organized. ## Next steps Store a root key with `unkey auth login` or pass one per command. JSON output for scripts and the flags every `unkey api` command accepts. # CLI output and shared flags Source: https://www.unkey.com/docs/platform/cli/output-and-flags Use the flags every unkey api command shares to pipe JSON or send a raw body. Every `unkey api` command accepts these five flags on top of its own. Four of them can also be set with an environment variable, which is the usual way in CI. Root key used for the request. Falls back to `UNKEY_ROOT_KEY`, then to the config file. See [CLI authentication](/docs/platform/cli/authentication). Base URL of the API. Falls back to `UNKEY_API_BASE_URL`. You don't normally need to set it. Path of the TOML config file that `unkey api` commands read the root key from. Falls back to `UNKEY_CONFIG`. `unkey auth login` always writes `~/.unkey/config.toml`, so only point this elsewhere if you write that other file yourself. Output format. Falls back to `UNKEY_OUTPUT`. Set it to `json` for the full response. Any other value gives the default layout. See [Output formats](#output-formats). A JSON request body to send instead of building one from the command's flags. See [Send a raw body](#send-a-raw-body). ## Output formats By default a successful command prints the request ID, a blank line, and then the response's `data` as indented JSON: ```text theme={"system"} req_1234abcd { "keyId": "key_1234abcd", "key": "prod_abc..." } ``` With `--output=json`, the CLI prints the whole response, `meta` and `data`, exactly as the API returned it. Use this when you pipe into `jq` or another tool, because the default layout mixes a plain-text line with JSON: ```bash theme={"system"} unkey api keys get-key --key-id=key_1234abcd --output=json | jq '.data.enabled' ``` Set `UNKEY_OUTPUT=json` once in a script to do the same for every command. ## Send a raw body Normally a command builds its request from its flags, one per request field, and checks them before sending. If you already have the request as JSON, for example from a file or another tool, pass it with `--body` instead: ```bash theme={"system"} unkey api keys create-key --body='{"apiId":"api_1234abcd","name":"Payment service","enabled":true}' ``` Rules for `--body`: * It must be one JSON object that matches the endpoint's request. * Unknown fields fail before anything is sent, so a typo in a field name doesn't get silently ignored. * You can't combine it with the command's request flags. Required flags aren't needed either, so `keys create-key --body='...'` doesn't also need `--api-id`. * The five shared flags above still work. ## Errors and exit codes A failed command prints one message and exits with status 1. API errors are shortened: | Status | Message | | - | - | | 400 | The detail, then one line per invalid field with its location. | | 401 | `Authentication failed: ...` and a hint to run `unkey auth login`. | | 403 | `Permission denied: ...` | | 404 | `Not found: ...` | Other failures print the error text as is. Help and version requests (`--help`, `-h`, `help `, `--version`) exit 0 without calling the API. ## Flag syntax `--api-id=api_123` and `--api-id api_123` are the same. A boolean flag on its own (`--enabled`) means true, and you can write `--enabled=false`. Flags can go before or after positional arguments. An unknown flag fails right away, and the message lists the flags the command accepts. # The unkey CLI Source: https://www.unkey.com/docs/platform/cli/overview Run Unkey from your terminal with the unkey CLI and find the page for each command. The `unkey` CLI lets you do from your terminal what you'd do with the API. It covers both products. Nearly every API endpoint has a matching `unkey api `. The exception is the customer portal's end-user endpoints, which use a portal session instead of a root key. There are also three top-level commands: `unkey deploy` deploys a prebuilt container image to Compute, `unkey auth login` stores a root key so you don't pass it on every call, and `unkey healthcheck` checks a URL from a script. The CLI is early and best effort. Commands, flags, and output can change without notice. The HTTP API is versioned and stable, so use the API for scripts that must not break. ## Command shape Commands look like `unkey [subcommand] [flags] [arguments]`. `unkey --help`, `unkey --help`, and `unkey help ` print usage, and `unkey --version` prints the installed version. Every `unkey api` command shares five flags for auth, output, and a raw request body. They're on [CLI output and shared flags](/docs/platform/cli/output-and-flags), along with flag syntax, so each command page only lists its own flags. ## Command groups `unkey api` has 14 groups, one per API service. Each command has its own page in the CLI section of the product it belongs to. ### Compute Apps, deployments, domains, environments, gateway policies, and the GitHub App installation are Compute resources. | Group | What it manages | | - | - | | [`unkey api projects`](/docs/compute/cli/projects/create-project) | Create, read, update, and delete workspace projects | | [`unkey api apps`](/docs/compute/cli/apps/create-app) | Create, read, update, and delete apps within projects | | [`unkey api environments`](/docs/compute/cli/environments/list-environments) | Environment variables and settings | | [`unkey api deployments`](/docs/compute/cli/deployments/create-deployment) | Create and control deployments | | [`unkey api domains`](/docs/compute/cli/domains/create-domain) | Create, read, verify, and delete custom domains | | [`unkey api gateway`](/docs/compute/cli/gateway/list-policies) | List, replace, and update gateway policies | | [`unkey api github`](/docs/compute/cli/github/install-app) | GitHub App installations | | `unkey api analytics` | [`get-gateway-requests`](/docs/compute/cli/analytics/get-gateway-requests) and [`get-runtime-logs`](/docs/compute/cli/analytics/get-runtime-logs) query gateway and runtime data | ### API Management Keyspaces, keys, identities, permissions and roles, rate limiting, and the customer portal are API Management resources. | Group | What it manages | | - | - | | [`unkey api apis`](/docs/api-management/cli/apis/create-api) | Keyspaces (the API namespaces keys belong to) | | [`unkey api keys`](/docs/api-management/cli/keys/create-key) | Create, verify, update, and delete keys and their permissions and roles | | [`unkey api identities`](/docs/api-management/cli/identities/create-identity) | Identities that group keys | | [`unkey api permissions`](/docs/api-management/cli/permissions/create-permission) | Permissions and roles | | [`unkey api ratelimit`](/docs/api-management/cli/ratelimit/limit) | Rate limit checks and overrides | | [`unkey api portal`](/docs/api-management/cli/portal/create-session) | Customer portals and their sessions | | `unkey api analytics` | [`get-verifications`](/docs/api-management/cli/analytics/get-verifications) and [`get-ratelimits`](/docs/api-management/cli/analytics/get-ratelimits) query key and rate limit data | `analytics` appears in both tables because two of its commands read Compute data and two read API Management data. ## Top-level commands | Command | Product | What it does | | - | - | - | | `unkey deploy ` | Compute | Deploys a prebuilt container image to a project and waits until the deployment is active. Needs `--project` (or `UNKEY_PROJECT`), and a root key from `--root-key` or `UNKEY_ROOT_KEY`. It doesn't read the stored key. Reference: [unkey deploy](/docs/compute/cli/deploy). | | `unkey auth login` | Platform | Prompts for a root key and stores it in `~/.unkey/config.toml`. Reference: [unkey auth login](/docs/platform/cli/auth/login). | | `unkey healthcheck ` | Compute | Sends one HTTP GET and exits 0 on a 200 response, or 1 otherwise. Useful in CI and monitoring scripts. Reference: [unkey healthcheck](/docs/compute/cli/healthcheck). | ## Next steps npm package or GitHub release. Where the root key comes from and in which order. `--output json`, `--body`, `--api-url`, `--config`. # Glossary Source: https://www.unkey.com/docs/platform/glossary Look up the terms Unkey uses and which product each one belongs to. Look up a term here. Some terms mean different things in different products, so each entry starts with the product that owns it. ## Terms with more than one meaning ### App **Compute.** A service you deploy, inside a project. It has a GitHub repository or an image as its source, two environments, build and runtime settings, and a history of deployments. On this site, "app" always means this. When we mean the software you run anywhere, including outside Unkey, we say "your application". ### Environment **Compute.** Every app has two environments, production and preview, and each deployment belongs to one. Environment variables, runtime settings, and gateway policies are set per environment. **API Management** uses the same word for an optional label on a key, the key's `environment` field, so you can tag keys as `live` or `test`. The label doesn't change how the key behaves, and it has nothing to do with Compute environments. ### Rate limiting Three different features share this name: * **API Management**: the standalone ratelimit API. You call `ratelimit.limit` with your own identifier and namespace and get an allow or deny. * **API Management**: key and identity rate limits. They're attached to a key or an identity and checked when the key is verified. * **Compute**: the gateway rate limit policy. The gateway applies it to requests before they reach your app. ### Verify **API Management.** Key verification is `keys.verifyKey`: you send a key, and we tell you whether it's valid and, if not, why. **Compute** uses the word twice more: custom domain verification, where you prove you control a domain before the gateway serves it, and the gateway key-auth policy, which verifies keys on incoming requests so your app only sees verified callers. ## Platform ### Workspace **Platform.** The top-level container for everything else. It has a slug used in dashboard URLs, a `ws_` ID used in the API, and its own team, billing, and limits. Workspaces are fully separate from each other. See [Workspaces](/docs/platform/workspace/overview). ### Root key **Platform.** The credential for calling the Unkey API and using the CLI. A root key belongs to one workspace, has a list of permissions, and is stored as a SHA-256 hash like every other key. See [Root keys](/docs/platform/root-keys/overview). ### Role **Platform.** Admin or developer, given to each dashboard user per workspace. Developers can work with every resource in the workspace. Admins can also manage members, rename the workspace, manage root keys, and change billing. These aren't the roles you attach to API keys in API Management, which group permissions for your own users. See [Team](/docs/platform/workspace/team). ### Tier and plan **Platform.** The billing page says "tier" for API Management (Free by default) and "plan" for Compute (Starter, Pro, or Business). They're billed separately to the same payment method. See [Plans](/docs/platform/billing/plans). ### Limits **Platform.** Your workspace's ceilings, shown under **Settings > Limits**: monthly API operations, log and audit log retention, log drains, team members, and Compute resources. Most come from your API tier and Compute plan. See [Limits](/docs/platform/billing/limits). ### Delete protection **Platform.** A setting on keyspaces, projects, and apps that blocks deletion while it's on. A delete attempt returns `err:unkey:application:protected_resource`. See [Delete protection](/docs/platform/security/delete-protection). ## API Management ### Keyspace **API Management.** A container for API keys, called an API in endpoint names (`apis.createApi`). Each key belongs to one keyspace. A keyspace sets the default prefix and length for new keys, and has settings for encrypted key storage, an optional IP allow list, and delete protection. ### Key **API Management.** A credential you give to a user of your application and verify on each request. A key has a prefix and can have a name, metadata, an expiry, credits, rate limits, permissions and roles, and an identity. We store only its hash, unless it's a recoverable key. ### Identity **API Management.** One of your users or tenants, identified by your own `externalId`, that several keys can belong to. Rate limits on an identity are shared by all of its keys. ### Credits **API Management.** The number of uses a key has left. Each verification uses one, and credits can refill on a schedule. At zero, verification reports the key as out of usage. ### Permission and role (keys) **API Management.** Permissions are strings you define and attach to keys, directly or through roles. Verification can check that a key has a permission or matches a permission query. These are separate from dashboard roles. ### Recoverable key **API Management.** A key created with `recoverable: true` in a keyspace with encrypted storage turned on. We keep an encrypted copy, so you can read the key again later. See [Key storage](/docs/platform/security/key-storage). ## Compute ### Project **Compute.** A group of apps, usually one per codebase or product. Compute usage is billed and reported per project, and a project can have delete protection. ### Deployment **Compute.** One built version of an app, running in one environment. It's created from a git commit, an image, or an existing deployment. You can promote it or roll back to it, and it keeps the gateway policies that were in place when it was created. ### Gateway **Compute.** What sits in front of every deployment and receives its traffic. It handles HTTPS, routes requests by domain, and applies the environment's policies: key authentication, rate limiting, firewall rules, OpenAPI validation, and logging. ### Gateway policy **Compute.** A rule the gateway applies to requests for an environment. The key-auth policy uses a keyspace from API Management, which is where the two products connect. ### Replica **Compute.** One running instance of a deployment in a region. Autoscaling can add replicas up to your plan's replicas-per-region limit. See [Limits](/docs/platform/billing/limits). ### Spend budget **Compute.** A monthly dollar cap on Compute usage, set by an admin. It emails you at 50, 75, and 100 percent and can optionally stop your workloads at 100 percent. See [Spend budget](/docs/compute/configure/spend-budget). # The Unkey platform Source: https://www.unkey.com/docs/platform/index Unkey hosts API management and Compute, and you can self-host the source. Hosted Unkey is the dashboard at `app.unkey.com` and the API at `api.unkey.com`. API management (keys and rate limits) and Compute (deploying and running APIs) share the workspace, billing, root keys, and CLI on this page, and both run on Unkey's infrastructure. You can fork the public source and run it yourself: code outside `packages` and `internal/icons` is AGPLv3, these docs have no self-hosting guide, and security fixes aren't backported, so run a current build of `main` or the latest release. Set up what Compute and API Management share: your workspace and team, billing, security, root keys, the CLI, and the HTTP API. ## Workspace What a workspace isolates, its slug and ID, creating one. Name, workspace ID, and the GitHub App connection. Admin and developer roles, and how invitations work. Enroll a second factor on your own account. ## Billing API tier and Compute plan, billed separately. Every ceiling, per plan. What you consumed this month. ## Security How we protect your keys and what you can turn on. Why a key is shown once, and recoverable keys. What happens when a key leaks to a public repository. Block deletion of keyspaces, apps, and projects. ## CLI What the command groups are and which product owns each. npm package or GitHub release. Flag, environment variable, or `unkey auth login`, in that order. `--output json`, `--body`, `--api-url`, `--config`. ## API Base URL, envelope, request IDs, pagination. Bearer root keys and the errors they can produce. POST /v2/., services, verbs. RFC 7807 fields, validation errors, status codes. ## Root keys Create, edit, rotate, and delete the credential for the API. Choose least-privilege permissions for deployments, agents, and API management. Resource paths, supported actions, and operation requirements. ## Self-host Unkey These docs cover hosted Unkey: the dashboard at `app.unkey.com` and the API at `api.unkey.com`. Compute and API Management both run on Unkey's infrastructure, and there's no self-hosting guide in these docs. The [Unkey source code](https://github.com/unkeyed/unkey) is public. Code outside the `packages` and `internal/icons` directories is licensed under AGPLv3, so you can fork it and run it yourself under those terms. If you self-host, run a current build of `main` or the latest release. Unkey applies security fixes there and doesn't backport them to older tags. Hosted Unkey and a fork you run are different setups. | | Hosted Unkey | A fork you run | | - | - | - | | Where it runs | Unkey's infrastructure, at `app.unkey.com` and `api.unkey.com` | A host you operate. These docs have no self-hosting guide | | Source you run | Unkey operates the service | The public repo. AGPLv3 outside `packages` and `internal/icons` | | Which build | The hosted service | A current build of `main` or the latest release. Security fixes aren't backported to older tags | ## Reference Every term, tagged with the product that owns it. Build, deploy, and run your app. Keys, rate limits, identities, analytics. # Configure root keys Source: https://www.unkey.com/docs/platform/root-keys/configure Assign scoped permissions in the dashboard or create and update root keys through the API. You can create a root key and assign its permissions in the dashboard or through the API. Both methods control the same access. If you're still deciding what your service or agent needs, start with [Root key permissions](/docs/platform/root-keys/permissions). This guide creates a key for a debugging agent. The agent can read deployment status and runtime logs in the billing app's staging environment. It cannot deploy changes or access other environments. The dashboard steps and API request below configure that same permission set. ## Create a key in the dashboard You need the workspace [admin role](/docs/platform/workspace/team) to manage root keys in the dashboard. Open **Root Keys** in your workspace and click **New Root Key**. Depending on your workspace's navigation, **Root Keys** is either an entry in the dashboard sidebar or a tab under **Settings**. If you don't see it in either place, you don't have the admin role. Root key list with the New Root Key button in the top right Root key list with the New Root Key button in the top right Enter a name such as `billing-staging-debugger` that describes the key's job. New root key form with billing-staging-debugger entered as the name New root key form with billing-staging-debugger entered as the name Under **Permissions**, select **Start new**. The policy editor opens with no actions selected. Custom policy editor with Workspace scope and no permissions selected Custom policy editor with Workspace scope and no permissions selected Set **Scope** to **Environments**, then select the billing app's staging environment instead of all environments. Policy scoped to the billing app's staging environment with no actions selected Policy scoped to the billing app's staging environment with no actions selected Select **Read** on **Deployments** and **Runtime logs**. Leave the other actions and resources unselected. Only deployment and runtime log read actions selected for staging Only deployment and runtime log read actions selected for staging Close the policy editor with its × button to review the summary. Check every policy on the key, then click **Create key**. New root key panel with a read-only staging policy ready to create New root key panel with a read-only staging policy ready to create Use the copy button beside the key and save the secret in your secret manager. Unkey shows it only once. Click **Done** after saving it. Root Key created dialog with a hidden secret, copy button, and one-time storage warning Root Key created dialog with a hidden secret, copy button, and one-time storage warning A policy groups a scope and its selected actions. Selecting the environment sets the boundary for the deployment and log permissions; it doesn't automatically grant access to environment settings or variables. Use **Add policy** when the key needs another scope. Policies add access together, so a workspace-wide policy can make a narrower environment policy ineffective as a restriction. Templates are starting points, not least-privilege recommendations. For example, **All read permissions** includes more data than deployment status and logs, and **Verify keys** starts with every keyspace. Review both the resources and actions before saving. Changing **Scope** resets that policy's selections. Use the API below if you need an expiry or a permission on one exact deployment rather than all deployments in the selected environment. ## Edit permissions in the dashboard Open the key's row menu and select **Edit root key**. Change its policies, review the complete selection, then click **Save**. Permission editor with only deployment and runtime log read access in staging Permission editor with only deployment and runtime log read access in staging Saving replaces the key's permission set. To narrow access, remove broader policies as well as adding narrower ones. Clearing a search filter before reviewing helps you see selections that the filter hides. For rotation and deletion, see [Root keys](/docs/platform/root-keys/overview). ## Create a key through the API Authenticate with a root key that has `unkey:v1:ws_xxx:rootKeys/*#write` and covers every permission you're assigning. If you don't have a management key yet, create one in the dashboard first. Keep that credential in trusted automation, not in the agent that receives the new key. This request creates a debugging key that expires in one hour. IDs ending in `_xxx` are placeholders. Replace each one with the full resource ID, not a name or slug. Use the billing app's ID for `app_xxx` and its staging environment's ID for `env_xxx`. Set `UNKEY_ROOT_KEY` through your secret manager or environment, rather than putting the secret in the script. ```bash wrap theme={"system"} EXPIRES_AT_MS=$(( ($(date +%s) + 3600) * 1000 )) curl --fail-with-body https://api.unkey.com/v2/rootKeys.createKey \ -H "Authorization: Bearer $UNKEY_ROOT_KEY" \ -H "Content-Type: application/json" \ --data @- < Root key list with separate keys for CI, debugging, and API key verification Root key list with separate keys for CI, debugging, and API key verification ## What a root key looks like A root key starts with `unkey_`, followed by random characters. It's shown to you only once (see [Key storage](/docs/platform/security/key-storage)). The bearer key identifies the workspace for API requests. Permission URNs also include that workspace's ID. A root key has no credit limit or rate limit of its own. You can set an expiry when creating it through the API. Use short-lived keys for temporary jobs and agents. ## Create a root key Give each service, job, or agent its own root key. Choose the resource scope and actions in [Root key permissions](/docs/platform/root-keys/permissions), then follow [Configure root keys](/docs/platform/root-keys/configure) to create it in the dashboard or API. Store the secret in your secret manager. You can't retrieve it again. The audit log records who created the key and which permissions it received. ## Edit a root key From the key's row menu, choose **Edit root key** to change its name or permissions. Saving replaces the complete permission set, so review all policies before saving. See [Edit permissions](/docs/platform/root-keys/configure#edit-permissions-in-the-dashboard) for dashboard and API instructions. ## Rotate a root key Rotating gives you a new root key with the same name and permissions, and stops the old one after a grace period. 1. Choose **Rotate root key** from the row menu. 2. Pick a grace period: revoke immediately, 15 minutes, 1 hour, 6 hours, or 24 hours. 3. Check the confirmation, click **Rotate root key**, then confirm the rotation. 4. Copy the new key. It's shown once, like a new key. 5. Update your services before the grace period ends. Root key rotation dialog with a one-hour grace period Root key rotation dialog with a one-hour grace period When the grace period ends, requests with the old key fail with `err:unkey:authorization:forbidden`. If the old key was already set to expire sooner, it stops at that earlier time. Rotate on a schedule that fits your security policy, and right away if a key has been exposed. With "revoke immediately", the old key stops working as soon as the new one exists. ## Delete a root key Choose **Delete root key** from the row menu and confirm. You can't undo this. The key can no longer authenticate after the deletion propagates. The audit log records the deletion. Delete keys for services you've retired instead of leaving them active. Root key deletion dialog with its confirmation unchecked Root key deletion dialog with its confirmation unchecked ## If a root key leaks Rotate it with "revoke immediately" or delete it, then remove the secret from wherever it was exposed. The key keeps working until you do. If a root key is pushed to a public GitHub repository, [GitHub secret scanning](/docs/platform/security/github-secret-scanning) reports it and every member of the workspace gets an email saying where it was found. Unkey stores root keys hashed, so it can't recover the secret, only identify which key ID a request used. That's another reason to create one key per service. ## Permissions reference Choose least-privilege permissions for deployments, agents, and API management. Resource paths, supported actions, and operation requirements. # Permission reference Source: https://www.unkey.com/docs/platform/root-keys/permission-reference Look up resource paths, supported actions, and permission requirements for common operations. A root key permission has the form `unkey:v1:{workspace_id}:{resource_path}#{action}`. Use this page to choose a path and action. See [Root key permissions](/docs/platform/root-keys/permissions) for least-privilege examples and [Configure root keys](/docs/platform/root-keys/configure) to assign them. ## Build a permission string Replace placeholders such as `{project_id}` with resource IDs, not names or slugs. A resource ID can be `*` to select all resources at that position, including future resources. After the first wildcard ID, every later ID must also be `*`. The workspace ID must always be exact. Paths and actions are case-sensitive. Keep path segments such as `rootKeys` exactly as shown. IDs contain letters, digits, and underscores. Partial ID matches such as `proj_*` aren't supported. For example, to read every app in one project: ```plaintext theme={"system"} unkey:v1:ws_xxx:projects/proj_xxx/apps/*#read ``` IDs ending in `_xxx` are placeholders. Replace `ws_xxx` and `proj_xxx` with the full workspace and project IDs. The resource tables list supported path/action combinations. They don't mean every combination has a corresponding API endpoint. Use the operation requirements below when an endpoint needs a collection permission or more than one permission. ## Workspace resources Append one of these paths to `unkey:v1:{workspace_id}:`, then add `#` and an action. | Resource | Path | Actions | | - | - | - | | Projects | `projects/{project_id}` | `read`, `write`, `delete` | | Root keys | `rootKeys/{root_key_id}` | `read`, `write`, `delete` | | GitHub apps | `github/apps/{github_app_id}` | `read`, `write`, `delete` | | Limits | `limits` | `read` | | Usage | `usage` | `read` | ## Deploy resources All paths in this table follow the prefix `unkey:v1:{workspace_id}:projects/{project_id}/`. | Resource | Path after the project prefix | Actions | | - | - | - | | Apps | `apps/{app_id}` | `read`, `write`, `delete` | | Environments | `apps/{app_id}/environments/{environment_id}` | `read`, `write`, `delete` | The resources below follow the environment prefix: ```plaintext wrap theme={"system"} unkey:v1:{workspace_id}:projects/{project_id}/apps/{app_id}/environments/{environment_id} ``` Append the suffix, then `#` and the action. In these suffixes, `{id}` is the ID of the deployment, domain, variable, or policy. For example, append `/deployments/*#read` to read deployments in that environment. | Resource | Suffix | Actions | | - | - | - | | Deployments | `/deployments/{id}` | `read`, `write`, `delete` | | Runtime logs | `/deployments/{id}/logs` | `read` | | Domains | `/domains/{id}` | `read`, `write`, `delete` | | Environment variables | `/variables/{id}` | `read`, `write`, `delete` | | HTTP request logs | `/gateway/logs` | `read` | | Gateway policies | `/gateway/policies/{id}` | `read`, `write`, `delete` | ### Deployment operations These requirements use the environment prefix above. Here, `{id}` is the deployment ID. Each row identifies the resource and action the operation checks. | Operation | Required suffix and action | | - | - | | Create a deployment | `/deployments/*#write` | | Get a deployment | `/deployments/{id}#read` | | List deployments in an environment | `/deployments/*#read` | | Start or stop a deployment | `/deployments/{id}#write` | | Read a deployment's runtime logs | `/deployments/{id}/logs#read` | | Explicitly promote or roll back a deployment | `#write` on the environment itself | When listing deployments with an environment-scoped key, include the `project`, `app`, and `environment` request filters. A broader list request needs a broader permission. For logs, the API restricts results to the deployments covered by the key's log permissions. Environment `write` also authorizes environment settings changes. Deployment `write` authorizes both creation and updates within its scope. Neither action is a create-only or promote-only permission. Permissions don't bypass operation restrictions, such as which deployments can be stopped. ### Environment variable operations Variable endpoints check the collection path `/variables/*`, not a single variable ID. | Operation | Required suffix and action | | - | - | | List variables | `/variables/*#read` | | Set variables | `/variables/*#write` | | Remove variables | `/variables/*#delete` | | Set variables with `prune: true` | Both `/variables/*#write` and `/variables/*#delete` | Read access can reveal recoverable variable values. It doesn't reveal write-only values. There is no separate `decrypt` permission for environment variables. Don't grant variable read access to an agent that only needs deployment logs. ## API management resources All paths in this table follow `unkey:v1:{workspace_id}:projects/{project_id}/`. | Resource | Path after the project prefix | Actions | | - | - | - | | Keyspaces | `keyspaces/{keyspace_id}` | `read`, `write`, `delete` | | Verification logs | `keyspaces/{keyspace_id}/logs` | `read` | | API keys | `keyspaces/{keyspace_id}/keys/{key_id}` | `read`, `write`, `delete`, `verify`, `decrypt` | | Identities | `identities/{identity_id}` | `read`, `write`, `delete` | | Rate limit namespaces | `ratelimits/namespaces/{namespace_id}` | `read`, `write`, `delete`, `limit` | | Rate limit logs | `ratelimits/namespaces/{namespace_id}/logs` | `read` | | Rate limit overrides | `ratelimits/namespaces/{namespace_id}/overrides/{override_id}` | `read`, `write`, `delete` | | Roles | `rbac/roles/{role_id}` | `read`, `write`, `delete` | | Permission definitions | `rbac/permissions/{permission_id}` | `read`, `write`, `delete` | | Portals | `portals/{portal_id}` | `read`, `write`, `delete` | | Portal sessions | `portals/{portal_id}/sessions/{session_id}` | `write` | Use the keyspace ID in permission paths, not the `apiId` accepted by some key-management requests. Roles and permission definitions in this table are resources used to authorize your customers' keys. They aren't the root key permissions themselves. ### Key operations For these operations, append each suffix to `unkey:v1:{workspace_id}:projects/{project_id}/keyspaces/{keyspace_id}`. | Operation | Required suffix and action | | - | - | | Create a key, including a recoverable key | `/keys/*#write` | | Get a key | `/keys/{key_id}#read` | | Update a key, its credits, roles, or direct permissions | `/keys/{key_id}#write` | | Delete a key | `/keys/{key_id}#delete` | | Verify a key | `/keys/{key_id}#verify` | | Get a recoverable key's plaintext | Both `/keys/{key_id}#read` and `/keys/{key_id}#decrypt` | | List keys | Both `/keys/*#read` and `#read` on the keyspace itself | | List keys with plaintext | Both list permissions above, plus `/keys/*#decrypt` | When `keys.addPermissions` or `keys.setPermissions` must create a missing permission definition, also grant `write` on `projects/{project_id}/rbac/permissions/*`. Attaching an existing definition doesn't need that extra creation permission. ## Root key operations These suffixes follow `unkey:v1:{workspace_id}:`. A caller that assigns permissions must already cover each assigned permission in the same workspace. | Operation | Required path and action | | - | - | | Create a root key | `rootKeys/*#write` | | List root keys | `rootKeys/{root_key_id}#read` for each returned key, or `rootKeys/*#read` for all | | Update a root key | `rootKeys/{root_key_id}#write` | | Delete a root key | `rootKeys/{root_key_id}#delete` | | Rotate a root key | `rootKeys/{root_key_id}#write`; also `#delete` on that key when setting an expiry on the old key | Creating or updating a key cannot assign permissions beyond the caller's permissions. Rotation checks that the caller covers the permissions being copied to the replacement key. The management permission alone doesn't grant permission to copy broader access. See [Configure root keys](/docs/platform/root-keys/configure#create-a-key-through-the-api) for creation, permission replacement, and expiry rules. # Root key permissions Source: https://www.unkey.com/docs/platform/root-keys/permissions Choose permissions for your root key based on the task it needs to perform. A root key authenticates your requests to Unkey, but it has no permissions by default. You assign permissions to tell Unkey what the key is allowed to do. Permissions let you make that access specific. A key can deploy one app without being able to deploy every app in your workspace. Another key can read deployment status and logs without being able to change anything. You don't need to give either key full access to make it useful. Start with its job, choose where it can work, and decide what it can do there. This is least privilege: enough access to complete the task, without access to unrelated resources or actions. ## Start with the key's job Describe what the service or agent needs to accomplish before choosing permissions. "Investigate a failed deployment" might mean reading its status and runtime logs. It doesn't necessarily mean restarting it, deploying a fix, or reading environment variables. Break the job into the capabilities it needs. For example: * Deploy an app and check the result: create deployments and read their status * Investigate a runtime problem: read deployment status and runtime logs * Authenticate requests to your API: verify customer API keys Use a separate root key for each service, job, or agent. That lets you change or revoke one workload's access without affecting the others. It also keeps a debugging agent from inheriting a deployment job's ability to change your app. ## Choose which resources it can access A resource is something you manage in Unkey, such as an app, deployment, or API key. The resource scope of a permission determines which of those resources the key can access. For deployments, the scope follows the structure of your workspace: * A [project](/docs/compute/concepts/projects-apps-environments#projects) groups apps in your workspace. * An [app](/docs/compute/concepts/projects-apps-environments#apps) is a service you deploy. * An [environment](/docs/compute/concepts/projects-apps-environments#environments) separates an app's settings and deployments, for example production from preview. * A [deployment](/docs/compute/concepts/deployments) is one built version of an app in one environment. For API management, a [keyspace](/docs/api-management/keyspaces/overview) groups related customer API keys. You can restrict a root key to the keys in one keyspace instead of giving it access to every customer's keys in the workspace. Choose the smallest scope that covers the job. If an agent is investigating one deployment, it only needs that deployment and its logs. If it monitors an app's environment continuously, it may need all deployments in that environment, including future ones. Neither task needs access to another app. The hierarchy helps you identify a resource; it doesn't make access automatic. Permission to read an environment doesn't include its deployments. Permission to read a deployment doesn't include its logs. Include each resource type the task needs. ## Choose what it can do with those resources An action determines what the key can do within its scope. Most resources support `read` to inspect them, `write` to create or update them, and `delete` to remove them. Some have task-specific actions, such as `verify` for checking customer API keys. Choose each action separately. A deployment job needs `write` to create a deployment and `read` to check its status. A debugging agent that only inspects deployments and logs needs `read`, not `write`. Having one action doesn't include the others. Read-only access still needs a limited scope. Logs can contain sensitive application data, so an agent that can't change anything may still be able to see more than its task requires. Check all permissions on the key before using it. Permissions add access together: a permission for one environment doesn't restrict another permission that covers every environment. To reduce access, remove or replace the broader permission. There are no deny permissions that cancel other permissions. ## Read a permission Once you've chosen the resources and actions, you can express each permission as a string. Unkey uses a resource name called a URN to identify the resource, followed by `#` and the action: ```plaintext theme={"system"} unkey:v1:{workspace_id}:{resource_path}#{action} ``` For example, a permission to read one deployment looks like this: ```plaintext wrap theme={"system"} unkey:v1:ws_xxx:projects/proj_xxx/apps/app_xxx/environments/env_xxx/deployments/d_xxx#read ``` `ws_xxx` identifies the workspace. The path identifies the project, app, environment, and deployment. `#read` permits reading that deployment's details. It doesn't permit changes or access to its logs. IDs ending in `_xxx` are placeholders. Replace each one with the full ID Unkey assigned to that resource. For example, replace `app_xxx` with the app's ID, not its name or slug. This applies even if an API request accepts a name or slug for the same resource. The dashboard builds these strings from your selections. You only need to write them yourself when using the API. See [Configure root keys](/docs/platform/root-keys/configure) for both methods, or continue below for scope rules and examples. The [permission reference](/docs/platform/root-keys/permission-reference) lists the supported resources, actions, and operation requirements. ## Use wildcards deliberately Use `*` in place of one resource ID when the task needs every resource at that position. This permission reads every deployment in one environment: ```plaintext wrap theme={"system"} unkey:v1:ws_xxx:projects/proj_xxx/apps/app_xxx/environments/env_xxx/deployments/*#read ``` It covers existing and future deployments in `env_xxx`. It doesn't cover another environment or deployment logs. To read logs too, add: ```plaintext wrap theme={"system"} unkey:v1:ws_xxx:projects/proj_xxx/apps/app_xxx/environments/env_xxx/deployments/*/logs#read ``` Keep IDs fixed as far down the path as the task permits. If a task needs two environments, use one permission for each instead of granting access to every environment. After a wildcard ID, all later IDs must also be wildcards. For example, `projects/proj_xxx/apps/*/environments/env_xxx` is invalid. To select an environment, specify its app and project IDs too. Partial wildcards such as `app_*` are also invalid, and the workspace ID must be exact. Creation needs a wildcard at the resource being created because its ID doesn't exist yet. A permission on `deployments/d_xxx` can't create the next deployment. A permission on `deployments/*` can create deployments and also covers updates to existing deployments in that scope. ## Examples These examples turn common deployment and API management tasks into scoped permissions. Each explains what the key needs to do and what remains outside its access. In the deployment examples, `app_xxx` stands for the billing app's ID and `env_xxx` stands for its staging environment's ID. ### Deploy one app from CI A CI job needs to deploy the billing app to staging and check whether the deployment succeeded. Its project, app, and environment already exist, so the job doesn't need permission to create or configure them. Scope the key to deployments in that one environment. Add `write` for deployment creation and `read` for status checks. Use a deployment wildcard because each run creates a deployment with a different ID: ```json theme={"system"} [ "unkey:v1:ws_xxx:projects/proj_xxx/apps/app_xxx/environments/env_xxx/deployments/*#write", "unkey:v1:ws_xxx:projects/proj_xxx/apps/app_xxx/environments/env_xxx/deployments/*#read" ] ``` `write` authorizes `deployments.createDeployment`. `read` lets the job inspect the result with `deployments.getDeployment`. When using `deployments.listDeployments`, supply the project, app, and environment filters to match the key's scope. This key cannot deploy to production, change environment settings, or read environment variables. It also cannot read runtime logs unless you add the separate logs permission. Deployment `write` isn't a create-only permission. It also authorizes start and stop operations on deployments in this environment, subject to those operations' restrictions. Explicit promotion and rollback require `write` on the environment itself. Don't add that permission unless the job needs those operations. See [deployment operation requirements](/docs/platform/root-keys/permission-reference#deployment-operations). These permissions cover the deployment API calls above. If a tool also lists projects or apps, it needs read access for those calls. Prefer supplying known IDs over adding discovery permissions the job doesn't need. ### Investigate deployments without changing them A debugging agent needs to investigate runtime problems in the billing app's staging environment. It needs to see deployment status and application logs, but it doesn't need to deploy a fix or change settings. Keep the scope to that environment and assign `read` on two resource types: deployments and runtime logs. The logs permission is separate because reading a deployment doesn't include reading its logs: ```json theme={"system"} [ "unkey:v1:ws_xxx:projects/proj_xxx/apps/app_xxx/environments/env_xxx/deployments/*#read", "unkey:v1:ws_xxx:projects/proj_xxx/apps/app_xxx/environments/env_xxx/deployments/*/logs#read" ] ``` The agent can inspect deployments and query their runtime logs with `analytics.getRuntimeLogs`. It cannot create, start, stop, promote, or roll back deployments. It cannot read environment variables or another environment's logs. If the investigation concerns only one deployment, replace the deployment `*` in both permissions with that deployment's ID. The agent can then fetch that deployment by ID and read its logs, but it cannot list every deployment in the environment. Read-only doesn't mean the data is public. Logs can contain sensitive application data. Give the agent only the environment or deployment it needs, even when all its actions are `read`. ### Verify customer API keys A backend needs to authenticate incoming requests by checking its customers' API keys. Those keys belong to one keyspace. The backend doesn't need to issue keys or change their settings. Scope the root key to keys in that keyspace and choose `verify`. Use a wildcard so the backend can check every customer's key, including keys created later: ```json theme={"system"} [ "unkey:v1:ws_xxx:projects/proj_xxx/keyspaces/ks_xxx/keys/*#verify" ] ``` `ks_xxx` stands for the ID of the keyspace that holds your customers' keys. This permission authorizes `keys.verifyKey` for keys in that keyspace, including keys created later. The keyspace ID in the URN is not the `apiId` used by some API requests. The backend cannot list, update, delete, or decrypt keys. Verification returns its own result data and can consume credits or rate limits. `verify` isn't a general-purpose read permission. Root key permissions differ from the permissions on your customers' API keys. Customer permissions, such as `documents.read`, describe access to your API and are checked during verification. They don't grant access to Unkey resources. See [Authorization](/docs/api-management/authorization/overview). ### Issue and update customer keys A provisioning service needs to issue keys when customers sign up and update their settings when their plans change. Unlike the backend that verifies requests, this service needs to change keys. Give it `write` on keys in the customer keyspace. It doesn't need access to other keyspaces, permission to delete keys, or permission to retrieve stored plaintext: ```json theme={"system"} [ "unkey:v1:ws_xxx:projects/proj_xxx/keyspaces/ks_xxx/keys/*#write" ] ``` The wildcard lets the service create keys and update existing keys in that keyspace. It doesn't grant access to other keyspaces. The service receives the plaintext of keys it creates, but cannot decrypt stored keys without a separate permission. Add `read` on the same path only if the service needs to look up existing keys with `keys.getKey`. Listing keys with `apis.listKeys` needs both key and keyspace read access: ```json theme={"system"} [ "unkey:v1:ws_xxx:projects/proj_xxx/keyspaces/ks_xxx/keys/*#read", "unkey:v1:ws_xxx:projects/proj_xxx/keyspaces/ks_xxx#read" ] ``` Without `delete`, the service can't delete a key. It can still disable keys or change their expiry through `write`, so withholding `delete` doesn't make a write-capable key harmless. ### Give an agent a short-lived key A trusted automation service can create a separate root key for a debugging agent. Keep key creation in that service rather than giving its credential to the agent. To create a key that reads staging deployment status and logs, the automation service needs: ```json theme={"system"} [ "unkey:v1:ws_xxx:rootKeys/*#write", "unkey:v1:ws_xxx:projects/proj_xxx/apps/app_xxx/environments/env_xxx/deployments/*#read", "unkey:v1:ws_xxx:projects/proj_xxx/apps/app_xxx/environments/env_xxx/deployments/*/logs#read" ] ``` Give the agent only the two read permissions and set an expiry for its task. Omit `rootKeys/*#write` from the agent's key so it cannot create more root keys. The API checks that each requested permission is covered by the caller's permissions in the same workspace. The service can narrow its deployment wildcard to one deployment, but it cannot grant production access it doesn't have. If the calling root key expires, the new key must expire no later than the caller. `rootKeys/*#write` also authorizes updates to existing root keys, subject to the API's permission and expiry checks. Treat the automation service's key as a management credential, not a credential for ordinary workload tasks. See [Create a key through the API](/docs/platform/root-keys/configure#create-a-key-through-the-api) for the request. ## Check the boundary before using the key Test both what the key must do and what it must not do. For the read-only agent, fetch a staging deployment, then try a read outside its scope. Check the complete permission list for broader permissions that would make the second request succeed. A denied request may return `403` with `err:unkey:authorization:insufficient_permissions`, or `404` to hide whether the resource exists. A missing root-key verify permission returns `200` with `valid: false` and `code: NOT_FOUND` from `keys.verifyKey`. Don't respond to a denial by granting workspace-wide access. Check the resource IDs, action, and request filters first. Use [Configure root keys](/docs/platform/root-keys/configure) to assign the permissions in the dashboard or API. Use the [permission reference](/docs/platform/root-keys/permission-reference) when you need another resource type or operation. # Delete protection Source: https://www.unkey.com/docs/platform/security/delete-protection Stop a keyspace, project, or app from being deleted by accident. Delete protection stops a keyspace, project, or app from being deleted by accident. While it's on, nobody can delete that resource from the dashboard or the API. To delete it, you first turn protection off, which is logged in the audit log, and then delete it. ## What you can protect Protection is off until you turn it on. The one exception is the **Default** project we create for your workspace, which starts protected. | Resource | Product | Where to turn it on | | - | - | - | | Keyspace (API) | API Management | Dashboard: the keyspace's **Settings** page. | | Project | Compute | API or CLI: `deleteProtection` on `projects.updateProject`. | | App | Compute | API or CLI: `deleteProtection` on `apps.updateApp`. | ## Protect a keyspace In the dashboard, open the keyspace and go to its **Settings** page. The **Delete Protection** card shows **Enabled** or **Disabled**. Keyspace settings page with the name, API ID, keyspace ID, key defaults, and a Danger Zone holding the Delete Protection card and the Delete Keyspace button Click **Enable** (or **Disable**), type the keyspace name to confirm, and submit. The audit log records the change as `api.update`. ## Protect an app or project Use the CLI or the API with a root key that has `update_app` or `update_project`. ```bash CLI theme={"system"} unkey api apps update-app --project=payments --app=payments-api --delete-protection ``` ```bash API theme={"system"} curl -X POST https://api.unkey.com/v2/apps.updateApp \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{"project": "payments", "app": "payments-api", "deleteProtection": true}' ``` For a project, use `unkey api projects update-project --delete-protection` or `projects.updateProject`. To turn it off, pass `--delete-protection=false` or `"deleteProtection": false`. If you leave the field out, the current value stays, so an update that only renames the resource doesn't clear protection. `apps.getApp`, `projects.getProject`, and the list endpoints return the current flag. Toggle from a script with a root key that is allowed to update the app. `project` and `app` accept either the prefixed ID or the slug. ```bash theme={"system"} curl -X POST https://api.unkey.com/v2/apps.updateApp \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{"project": "proj_...", "app": "app_...", "deleteProtection": true}' ``` ## When you try to delete a protected resource In the dashboard, the **Delete** action on a protected keyspace refuses with "This API has delete protection enabled. Please disable it before deleting the API." In the API, `apis.deleteApi`, `apps.deleteApp`, and `projects.deleteProject` return HTTP 412 with the code `err:unkey:application:protected_resource`. The detail names the resource type: "This API has delete protection enabled", "This app has delete protection enabled", or "This project has delete protection enabled". ```json theme={"system"} { "meta": { "requestId": "req_..." }, "error": { "title": "Resource is protected", "type": "/errors/unkey/application/protected_resource", "detail": "This API has delete protection enabled. Disable it before attempting to delete.", "status": 412 } } ``` To fix it, turn protection off on that resource, then delete it again. See the [error page](/docs/errors/unkey/application/protected_resource). # GitHub secret scanning Source: https://www.unkey.com/docs/platform/security/github-secret-scanning Learn what happens when a root key leaks into a public GitHub repository. When an Unkey [root key](/docs/platform/root-keys/overview) is pushed to a public GitHub repository, GitHub tells us and we email everyone in the workspace that owns it. We don't revoke or disable the key. That's up to you. Only root keys are covered. They all start with `unkey_`. The API keys you issue to your own users use prefixes you choose, so nobody gets emailed about them. ## What happens GitHub spots a string that looks like an Unkey root key in a public repository and reports it to us with the URL where it was found. If the string matches a root key that exists and hasn't been deleted, GitHub marks the alert as a real leak for the repository owner. Otherwise it's marked as a false positive, and nobody is emailed. Every member of the workspace gets one email naming the source and the URL where the key was found, even if GitHub finds the key in several files. ## What you should do Treat the key as compromised. Anyone could have copied it, so replacing it is the only fix: 1. Under **Settings > Root Keys**, rotate the key with "revoke immediately", or delete it. 2. Update the service that used it. 3. Remove the secret from the repository's history. # Key storage Source: https://www.unkey.com/docs/platform/security/key-storage Know why Unkey shows a key only once and when you can get it back. Keyspaces, `keys.createKey`, and recoverable keys are part of API Management. Here we only cover how keys are stored. ## A key is shown once When you create an API key or a root key, you see it once, in the create response or the dashboard dialog. We keep only a SHA-256 hash of it. A leaked copy of our database doesn't yield working keys, and we can't show you a key again unless it's a recoverable key. Moving keys from another system? [Migrating keys](/docs/api-management/keys/migrating-keys) lists the hash formats we accept. ## Recoverable keys A recoverable key is one you can show again later. To create one: 1. Ask [support@unkey.com](mailto:support@unkey.com) to turn on encrypted storage for the keyspace. It's off by default and set per keyspace. 2. Create the key with `recoverable: true`, using a root key with the encrypt permission for that keyspace. We keep an encrypted copy next to the hash. A root key with the decrypt permission can read the key back later. Without encrypted storage, creating a recoverable key fails with `err:unkey:application:precondition_failed` and "This API does not support key encryption." This weakens the guarantee above: anyone with a root key that has decrypt can read the key, so grant decrypt narrowly. ## What you should do Copy a non-recoverable key when it's returned. That's the only time you'll see it. If you lose a key, create a new one and delete the old one. The same goes for root keys. Use a separate root key per service so a leak affects as little as possible. If a root key lands in a public repo, [GitHub secret scanning](/docs/platform/security/github-secret-scanning) tells you, but it doesn't disable the key for you. # Security Source: https://www.unkey.com/docs/platform/security/overview See how Unkey protects your keys and workspace and what you can turn on. Unkey stores every API key and root key as a SHA-256 hash, so you can issue and verify keys without keeping a homegrown key table. A copy of the database isn't a list of working keys. This page covers that storage and what you can turn on around it. ## How Unkey stores API keys Unkey doesn't store the API key. On create, it hashes the full key string with SHA-256 and stores the hash as standard base64, not hex. Verification hashes the key your user sent and compares the two strings. There's no salt. The plaintext is returned once, in the create response, and isn't kept. See the OWASP Secrets Management Cheat Sheet and the OWASP REST Security Cheat Sheet for hashing and storing secrets you only need to verify. SHA-256 here is a fast hash of a random key, not a password hash such as bcrypt or Argon2. If a keyspace must show a key again, you opt in to an encrypted copy beside the hash. The copy is AES-256-GCM, the keyring is the workspace, and only a root key with decrypt permission can read the key back. Verification still uses the hash. See [Key storage](/docs/platform/security/key-storage) and [Recovering keys](/docs/api-management/security/recovering-keys). ## Should you build your own API key system? You can. What moves to Unkey is generating the random key, storing only the hash, and checking it on each request. `keys.createKey` issues the key. `keys.verifyKey` checks enabled, expiry, credits, rate limits, permissions, and the IP allow list. If you already have hashes and the format matches, [Migrating keys](/docs/api-management/keys/migrating-keys) covers importing them. You still run your API, unless you deploy it on Compute. You still decide the HTTP status you return to your users. You stop maintaining the keys table, the hash comparison, and, if you use Unkey for them, the rate-limit counters and the monthly credit balance. ## How Unkey protects the workspace Two controls apply to the workspace itself, separate from how keys are stored. **Access is scoped.** A root key only works in the workspace that issued it, and every endpoint needs a specific permission. Dashboard users are either an admin or a developer. A developer who tries an admin-only action gets `This action requires admin privileges.` See [Team](/docs/platform/workspace/team). **Changes are audited.** Changes made in the dashboard appear in the audit log with who made them, the event, and a description. For example, renaming a workspace logs `workspace.update`. How long entries are kept depends on your plan. See [Limits](/docs/platform/billing/limits). ## Security features you can use Hashing, verification, and the opt-in encrypted storage for recoverable keys. What happens when a key is pushed to a public repository. A flag that blocks deleting a keyspace, project, or app until you turn it off. Enroll a second factor on your own account. ## Reporting a vulnerability If you find a security issue, email [security@unkey.com](mailto:security@unkey.com) with steps to reproduce. Don't open a public issue or pull request. Reports about `api.unkey.com` and `app.unkey.com` are in scope. The docs site, the marketing site, and misconfigured self-hosted setups aren't. The full policy is in [SECURITY.md](https://github.com/unkeyed/unkey/blob/main/SECURITY.md). # Audit log event types Source: https://www.unkey.com/docs/platform/workspace/audit-logs/event-types Every event the audit log records and what triggers it. Every audit log entry has one of the event names below, in the form `resource.action` (a few have an extra part, such as `portal.session.create`). Filter on them in the dashboard's **Events** menu or in a [log drain](/docs/platform/workspace/log-drains). The **Events** menu and the log drain picker also list six event types that aren't recorded. Filtering on these event types matches nothing: * `environment.create` * `deployment.redeploy` * `secret.update` * `webhook.create` * `webhook.update` * `webhook.delete` Unkey doesn't send webhooks. Use a [log drain](/docs/platform/workspace/log-drains) to forward entries to your own endpoint. ## Workspace | Event | Recorded when | | - | - | | `workspace.create` | A workspace is created. | | `workspace.update` | The workspace is renamed, changes plan, or another workspace-level setting changes. | | `workspace.delete` | A workspace is deleted. | | `workspace.opt_in` | The workspace opts into a feature. | | `workspace.install_github` | The GitHub app is installed for the workspace. | | `auditLogBucket.create` | An audit log bucket is created. | | `logdrain.create`, `logdrain.update`, `logdrain.delete` | A [log drain](/docs/platform/workspace/log-drains) is created, changed, or deleted. | | `secret.create`, `secret.decrypt` | A one-time share link is created, or someone reveals it. | ## Keyspaces and keys | Event | Recorded when | | - | - | | `api.create` | A keyspace is created. | | `api.update` | A keyspace setting changes, including delete protection. | | `api.delete` | A keyspace is deleted. | | `key.create` | A key is created, including keys created through a portal session. | | `key.update` | A key's fields, credits, permissions, or roles change. | | `key.reroll` | A key is rerolled. | | `key.delete` | A key is deleted. | | `key.verify` | A root key calls `keys.verifyKey`. | ## Root keys Root-key lifecycle actions use separate events from API key actions. | Event | Recorded when | | - | - | | `rootKey.create` | A root key is created. | | `rootKey.update` | A root key's name or permissions change. | | `rootKey.reroll` | A root key is rerolled. | | `rootKey.delete` | A root key is deleted. | ## Authorization | Event | Recorded when | | - | - | | `permission.create` | A permission is defined. | | `permission.update` | A permission changes. | | `permission.delete` | A permission is deleted. | | `role.create` | A role is defined. | | `role.update` | A role changes. | | `role.delete` | A role is deleted. | | `authorization.connect_role_and_permission` | A permission is added to a role. | | `authorization.disconnect_role_and_permissions` | Permissions are removed from a role. | | `authorization.connect_role_and_key` | A role is added to a key. | | `authorization.disconnect_role_and_key` | A role is removed from a key. | | `authorization.connect_permission_and_key` | A permission is attached directly to a key. | | `authorization.disconnect_permission_and_key` | A permission is detached from a key. | ## Identities | Event | Recorded when | | - | - | | `identity.create` | An identity is created. | | `identity.update` | An identity's metadata or rate limits change. | | `identity.delete` | An identity is deleted. | ## Rate limiting | Event | Recorded when | | - | - | | `ratelimitNamespace.create` | A namespace is created. | | `ratelimitNamespace.update` | A namespace is renamed or changed. | | `ratelimitNamespace.delete` | A namespace is deleted. | | `ratelimit.create` | A rate limit is created on a key or identity. | | `ratelimit.update` | A rate limit on a key or identity changes. | | `ratelimit.delete` | A rate limit is removed. | | `ratelimit.limit` | A root key calls `ratelimit.limit` or `ratelimit.multiLimit`. | | `ratelimit.set_override` | An override is created or changed. | | `ratelimit.read_override` | An override is read. | | `ratelimit.delete_override` | An override is deleted. | ## Developer portal | Event | Recorded when | | - | - | | `portal.create` | A portal is created. | | `portal.update` | A portal is renamed, re-pointed, enabled, disabled, or rebranded. The metadata records how many sessions were revoked, which is only more than 0 for a re-point or a disable. | | `portal.delete` | A portal is deleted, with the number of sessions revoked in the metadata. | | `portal.session.create` | `portal.createSession` mints a session. | | `portal.session.exchange` | `portal.exchangeCode` turns a code into an access token. | | `portal.session.revoke` | `portal.revokeSession` revokes an end user's live sessions, with their `externalId`, the session ids, and the count in the metadata. Not written when nothing was revoked. | ## Compute Compute actions appear in the same log. | Event | Recorded when | | - | - | | `project.create`, `project.update`, `project.delete` | A project is created, changed, or deleted. | | `app.create`, `app.update`, `app.delete` | An app is created, changed, or deleted. | | `app.connect_repository`, `app.disconnect_repository` | A GitHub repository is connected to or disconnected from an app. | | `environment.update`, `environment.delete` | An environment's settings or variables change, or it is deleted. | | `deployment.create` | A deployment is created from git, an image, or another deployment. | | `deployment.authorize` | A deployment from an unauthorized contributor is approved to run. | | `deployment.rebuild` | A deployment is rebuilt. | | `deployment.cancel` | A build still in progress is cancelled, for example when its environment is deleted. | | `deployment.stop`, `deployment.wake` | A deployment is stopped or started again. | | `deployment.promote`, `deployment.rollback` | The environment's live deployment changes. | | `domain.create`, `domain.delete`, `domain.verify` | A custom domain is added, removed, or has verification restarted. | # Audit logs Source: https://www.unkey.com/docs/platform/workspace/audit-logs/overview Who changed what in your workspace, and how long the record is kept. The audit log records every change made through the API or the dashboard: who did it, what they did, what it touched, and from where. Use it to answer "who deleted that key?" or to show a compliance reviewer. Entries are written automatically, and nobody can edit or delete them. ## What an entry contains Each entry has: * **An event**, such as `key.create` or `portal.session.exchange`. See [Audit log event types](/docs/platform/workspace/audit-logs/event-types). * **An actor**, with a type, ID, and name. The type is `user` (someone in the dashboard), `rootkey` (an API call), `portalEndUser` (one of your customers in a [developer portal](/docs/api-management/portal/overview)), `github` (a push or pull request on a connected repository), or `system` (Unkey itself). * **The resources** it touched, each with a type, ID, name, and optional details such as the fields that changed. * **The caller's IP address and user agent**, and a readable description. * **A correlation ID** that groups entries from one action, such as a key creation and the permissions it attached. Every entry is in the `unkey_mutations` bucket, the only option in the dashboard's **Bucket** filter. ## Which actions are logged Every create, update, and delete is logged, for keyspaces, keys, identities, roles, permissions, rate limit namespaces and overrides, portals, every Compute resource, and workspace and team changes. Reads aren't logged, with two exceptions: `key.verify` when a **root key** calls `keys.verifyKey`, and `ratelimit.limit` when a root key checks a rate limit. Verifications by the gateway or a portal session aren't in the audit log. They're in [analytics](/docs/api-management/analytics/overview). ## Reading the log Open **Audit log** in the dashboard sidebar. Entries are newest first. Expand one to see its resources, description, IP address, and user agent. Filter by **Events**, **Users**, **Root Keys**, **Bucket**, and time range. Press **F** to open the filters and **Q** to reopen saved filters. Every workspace role can read the log, including viewers, but only admins see keys in the **Root Keys** filter. Audit log page listing events with time, actor, action, and event, with one entry open showing its description, actor, and details ## Retention Your plan sets how long entries are kept. Check the `Audit log retention` row on **Settings > Limits**. See [Limits](/docs/platform/billing/limits). | Plan | Audit log retention | | - | - | | Free, no Compute plan | 3 days | | API Pro, any tier | 14 days | | Compute Starter | 7 days | | Compute Pro | 14 days | | Compute Business | 30 days | | Enterprise | Custom | No plan keeps audit logs longer than 90 days. To keep them longer, or in your own systems, stream them out with a [log drain](/docs/platform/workspace/log-drains). # Log drains Source: https://www.unkey.com/docs/platform/workspace/log-drains Forward audit logs, verifications, gateway traffic, runtime logs, or rate limits to HTTP or Axiom. A log drain forwards one stream of workspace events to an HTTPS endpoint or [Axiom](https://axiom.co) dataset you control. When you create a drain, you pick the stream: **Audit logs**, **Key verifications**, **Gateway HTTP requests**, **Runtime logs**, or **Rate limits**. Use drains to keep history past plan retention, feed a SIEM, or alert on events such as `key.delete` or `5xx` gateway responses. Every plan starts with a `Log drains` limit of 0, so **Settings > Log Drains** won't let you create one yet. Ask [support@unkey.com](mailto:support@unkey.com) to raise the limit for your workspace. ## Streams Each drain sends exactly one stream. Create another drain if you need a second stream or destination. | Stream | What arrives | Filters | | - | - | - | | Audit logs | Workspace change events (`key.create`, `portal.session.exchange`, and the rest). See [Audit logs](/docs/platform/workspace/audit-logs/overview) and [event types](/docs/platform/workspace/audit-logs/event-types). | **Event types**: all events, or specific names by category | | Key verifications | Results from verifying API keys | **Keyspaces** and **Outcomes** (for example `VALID`, `RATE_LIMITED`, `EXPIRED`) | | Gateway HTTP requests | HTTP requests your Compute gateway handled | **Sources** (projects, apps, environments) and **HTTP statuses** (`2xx`, `3xx`, `4xx`, `5xx`) | | Runtime logs | Application log lines from Compute | **Sources** (projects, apps, environments) and **Severity** (`error`, `warn`, `info`, `debug`) | | Rate limits | Pass or block decisions from rate-limit checks | **Namespaces** and **Results** (**Passed** or **Blocked**) | Leave a filter empty to send every value for that filter, including ones added later. When you set more than one filter on a stream, an event must match all of them. For audit logs, **Specific event types** opens a category tree (for example `key`). Expanding a category lists full names such as `key.create`. Checking a category selects every listed action under it. For gateway requests and runtime logs, **All sources** includes current and future resources in the workspace. **Specific sources** limits to the projects, apps, and environments you pick. A selection of whole projects or apps also covers environments added later under those resources; mixed environment IDs do not. Filter changes apply from the drain's current position. They do not replay events that an earlier filter skipped. ## How delivery works Unkey sends events in batches after they happen. Your destination must accept the whole batch (HTTP: a `2xx` within 30 seconds) before delivery counts as successful. Failures retry the same batch. Delivery is at least once, so retries can send an event more than once. Deduplicate in your system: use `id` for audit logs, `request_id` for key verifications and gateway requests, and `log_id` for runtime logs. Rate-limit checks in one multi-limit request share `request_id`, so that field alone does not uniquely identify each decision. A drain starts when you create it. Earlier events are not backfilled. After a pause or failure, delivery resumes from the last committed position, as long as the events are still within retention. Verification logs are kept for 90 days, gateway requests for seven days, and audit logs for your plan's [audit log retention](/docs/platform/workspace/audit-logs/overview#retention) (up to 90 days). ## Destinations Unkey posts batches to an HTTPS URL you provide. Don't put a username or password in the URL. Use a header instead. Under **Encoding**, pick **JSON** (an array of events per request), **NDJSON** (one event per line), or **HEC** (Splunk HTTP Event Collector and compatible sinks such as CrowdStrike NG-SIEM). HEC wraps each event as `time`, `source` (`unkey`), `sourcetype` (the stream name), and `event`, and treats the delivery as successful only when the response is `2xx` with HEC `code` `0`. Add up to 32 unique headers, such as an `Authorization` header your endpoint checks. Names can be up to 256 characters and values up to 8192. Header values are stored encrypted and never shown again after you save. Unkey sends events to an Axiom dataset you name, using an Axiom API token you provide. The token is stored encrypted. ## Create a drain Under **Settings > Log Drains**, create a drain and pick **HTTP** or **Axiom**. Give it a name (shown in the list and on the drain page). Under **Stream**, choose **Audit logs**, **Key verifications**, **Gateway HTTP requests**, **Runtime logs**, or **Rate limits**. Optionally set that stream's filters. For HTTP, set the URL, encoding (JSON, NDJSON, or HEC), and optional headers. For Axiom, set the dataset and API token. Then create the drain. Delivery is asynchronous and can take a few minutes. The drain's detail page shows events delivered and failed in the past 24 hours, the success rate, and recent deliveries with your endpoint's response. ## Pause a drain, or fix a failing one A drain shows one of three states: * **Running:** it's delivering. * **Paused** (`paused_by_user`): you clicked **Pause deliveries**. Click **Resume deliveries** to pick up where it stopped, including events that arrived in between (within retention). * **Failing** (`paused_by_failure`): 50 deliveries in a row failed and Unkey stopped trying. Fix the destination, then click **Resume deliveries**. Between failed attempts, the wait grows from one minute to about two hours, then settles at every four hours, so it takes about a week of a dead endpoint to reach 50 failures. Because an event can arrive more than once, make your endpoint ignore duplicates. ## Delete a drain Deleting a drain stops all deliveries and removes its settings. Events already delivered stay where you sent them. Events not yet delivered aren't sent. # Workspaces Source: https://www.unkey.com/docs/platform/workspace/overview Understand what a workspace holds and how to create or switch between them. A workspace is the top-level container. Everything you create sits in exactly one. Whether you deploy an app with Compute or only issue API keys, it's the same workspace with different parts in use. Most teams need one workspace. Make a second when you need hard isolation: separate billing for two business units, or a client whose data must never sit next to yours. ## What sits in a workspace Everything on this site is scoped to a workspace: the [team](/docs/platform/workspace/team) and roles, [billing plans](/docs/platform/billing/plans) and [limits](/docs/platform/billing/limits), root keys, keyspaces and keys, and Compute projects with their apps and environments. A root key from one workspace can't touch another. ## Slug and ID A workspace has two identifiers. They do different jobs. The URL handle you choose when creating the workspace. It appears in every dashboard URL as `app.unkey.com//...`, must be unique across Unkey, and can't be changed later. It's 3 to 64 characters of lowercase letters, digits, and hyphens, and can't start or end with a hyphen. The dashboard derives a slug from the workspace name as you type and lets you edit it before creating. A generated identifier prefixed with `ws_`. Use it when an API call or a support request needs to name the workspace. Copy it from **Settings > General**. See [Workspace settings](/docs/platform/workspace/settings). ## Create a workspace Go to [app.unkey.com/new](https://app.unkey.com/new). Signing in with a new account lands you here automatically. Enter a name between 3 and 50 characters. The name isn't shown to your users, so choose one your team recognizes. The dashboard fills in a slug derived from the name. Edit it if you want a shorter handle. If the slug is already taken, the form tells you and you pick another. Create Company Workspace form with a workspace name entered and the URL handle filled in from it Click **Create workspace**. The workspace starts on the free tier with the [free limits](/docs/platform/billing/limits) and no Compute plan. You can deploy once you pick one under [Plans](/docs/platform/billing/plans). ## Delete a workspace The dashboard and the API don't offer workspace deletion. To close a workspace, email [support@unkey.com](mailto:support@unkey.com) from an address that belongs to a workspace admin. Make sure you cancel any paid plans under **Settings > Billing** so nothing renews while the request is handled. # Workspace settings Source: https://www.unkey.com/docs/platform/workspace/settings Rename your workspace, copy its ID, and connect the GitHub App. **Settings > General** holds the three workspace-wide settings: the name, the workspace ID, and the GitHub App connection. ## General General settings page showing the workspace name, the workspace ID with a copy button, and the GitHub App card marked Connected Between 3 and 50 characters. Internal only: your users never see it. Only an admin can save a new name. Other roles see the field, but **Save** stays disabled with "Admin access required to rename the workspace". Renaming writes a `workspace.update` audit log entry. Read-only. The `ws_` identifier with a copy button. Use it in API calls and support requests. The slug in the URL is a different identifier and can't be changed. See [Workspaces](/docs/platform/workspace/overview). Whether the Unkey GitHub App is installed for this workspace. **Install GitHub App** sends you to GitHub to pick the repos Unkey may read. Once it's installed, the card shows **Connected** and the button becomes **Manage on GitHub**. That connection is what lets Compute deploy an app from a repo and create preview environments per pull request. API Management-only workspaces can leave it disconnected. ## Other settings pages The rest of the settings sidebar lives on its own pages. Roles, invitations, and removing members. The credential for the API. Admins only. API tier, Compute plan, payment method, invoices. What you used this month and the ceilings your plan sets. **Settings > Billing** is where you see your plan and what you've used this month, and the ceilings themselves are tabulated on [Limits](/docs/platform/billing/limits). Log drains also live under Settings and are documented under [Log drains](/docs/platform/workspace/log-drains). You set up [two-factor authentication](/docs/platform/workspace/two-factor-auth) from your account page, not from workspace settings. # Team Source: https://www.unkey.com/docs/platform/workspace/team Invite people to your workspace and choose whether each one is an admin or a developer. A workspace has its own members and roles. A role covers the whole workspace: there are no per-project or per-keyspace permissions for dashboard users. Manage members under **Settings > Team**. ## Roles There are two roles. | Role | What it can do | | - | - | | Admin | Everything a developer can, plus manage members and invitations, rename the workspace, create and edit root keys, and change billing. | | Developer | Read and change every resource in the workspace: keyspaces, keys, identities, projects, apps, deployments, domains, and gateway policies. Cannot manage members, root keys, billing, or the workspace name. | A developer who tries an admin-only action gets `This action requires admin privileges.`, and the **Root Keys** tab doesn't appear in their settings sidebar. Accounts created before these two roles existed carry a legacy `basic_member` role. It keeps full developer access and the dashboard labels it Developer. ## Which plans include team members Free and Compute Starter don't include team members, and the Team page shows **Upgrade Your Plan to Add Team Members** with a link to Billing instead of the member list. Compute Pro and Business include them, and so does any paid API tier. See [Plans](/docs/platform/billing/plans) and [Limits](/docs/platform/billing/limits). If the workspace loses team access, we deactivate every member except the one who created the workspace. That happens when a paid API tier ends and you don't have Compute Pro or Business, or when you're on the free API tier and downgrade Compute to Starter or cancel it. ## Invite, change a role, remove someone Use the **Members** section of **Settings > Team**. From that one table you invite someone by email address, pick Admin or Developer, change someone's role, and remove a member. Team settings page listing workspace members with their roles and last active times, and an Invite user button Edit role dialog for a member with the role dropdown open Invitations expire after a while. If one shows as expired, send a new one. Only an admin can manage members. Anyone else sees **Admin access required** with a note that their role doesn't allow them to manage workspace members, and the table isn't shown. Removing a member ends their dashboard access immediately. It doesn't delete anything they created, and it doesn't touch root keys, which are workspace credentials rather than personal ones. To leave a workspace yourself, ask another admin to remove you. # Two-factor authentication Source: https://www.unkey.com/docs/platform/workspace/two-factor-auth Add a second factor to your Unkey sign-in from Account settings. Two-factor authentication (2FA) adds a second factor to your sign-in. It belongs to your user account but is scoped to your workspace, so enrolling one workspace will not enroll it for any others you may have. Manage it on your **Account settings** page, which is in the user menu in the top navigation. ## Enroll The **Security** section of Account settings holds the enrollment controls, alongside a **Profile** section for your name and email. The methods offered and the enrollment steps come from WorkOS, the identity provider behind Unkey sign-in, so they can change without an Unkey release. Account settings page with the Profile section and, under Security, the Multi-factor authentication row with a Set up authenticator app button You can enroll whether or not your organization requires MFA. ## If you lose your device Remove the old factor and enroll the new one from Account settings while you still have access. If you've already lost the device and can't sign in, email [support@unkey.com](mailto:support@unkey.com) from the address on your account.