diff --git a/apps/docs/api-reference/v1/migration/keys.mdx b/apps/docs/api-reference/v1/migration/keys.mdx
index dd5c8377b8..589edecaf7 100644
--- a/apps/docs/api-reference/v1/migration/keys.mdx
+++ b/apps/docs/api-reference/v1/migration/keys.mdx
@@ -160,12 +160,25 @@ curl -X POST https://api.unkey.com/v2/keys.createKey \
## POST /v1/keys.verifyKey → POST /v2/keys.verifyKey
**Key Changes:**
+- **🚨 CRITICAL**: v2 requires root key authentication with `api.*.verify_key` permission
+- **🚨 CRITICAL**: `apiId` parameter is no longer accepted in v2
- Remove `authorization` wrapper for permissions
- Use string-based permission queries instead of object syntax
- Change `remaining` → `credits` for cost parameters
- Add support for multiple named rate limits
- Response format: Direct response → `{meta, data}` envelope
-- **Important**: v1 doesn't require root key authentication, v2 does
+
+
+**Major Authentication Change in v2**
+
+The biggest change in v2 is that key verification now requires authentication with a root key that has the `api.*.verify_key` permission. This enables fine-grained access control:
+
+- **Wildcard permission**: `api.*.verify_key` allows verifying keys from any API in your workspace
+- **Specific API permission**: `api.api_123.verify_key` allows verifying only keys from API `api_123`
+- **No apiId parameter**: Unlike v1, you cannot specify which API's keys to verify - this is controlled by the root key's permissions
+
+This change improves security by ensuring only authorized services can verify keys, and provides workspace owners control over which services can verify keys from which APIs.
+
**Simple Key Verification**
@@ -185,7 +198,7 @@ curl -X POST https://api.unkey.dev/v1/keys.verifyKey \
```bash title="v2 cURL" icon="terminal" highlight={2}
curl -X POST https://api.unkey.com/v2/keys.verifyKey \
- -H "Authorization: Bearer " \
+ -H "Authorization: Bearer " \
-H "Content-Type: application/json" \
-d '{"key": "sk_1234abcdef"}'
```
@@ -291,7 +304,7 @@ curl -X POST https://api.unkey.dev/v1/keys.verifyKey \
```bash title="v2 cURL" icon="terminal" highlight={2,6}
curl -X POST https://api.unkey.com/v2/keys.verifyKey \
- -H "Authorization: Bearer " \
+ -H "Authorization: Bearer " \
-H "Content-Type: application/json" \
-d '{
"key": "sk_1234abcdef",
@@ -341,7 +354,7 @@ curl -X POST https://api.unkey.dev/v1/keys.verifyKey \
```bash title="v2 cURL" icon="terminal" highlight={2,6-10}
curl -X POST https://api.unkey.com/v2/keys.verifyKey \
- -H "Authorization: Bearer " \
+ -H "Authorization: Bearer " \
-H "Content-Type: application/json" \
-d '{
"key": "sk_1234abcdef",
@@ -357,6 +370,94 @@ curl -X POST https://api.unkey.com/v2/keys.verifyKey \
---
+## Understanding v2 Root Key Permissions for Key Verification
+
+The v2 `keys.verifyKey` endpoint introduces a powerful permission system that gives you granular control over which services can verify keys from which APIs.
+
+### Setting Up Root Key Permissions
+
+When creating a root key for key verification, you need to grant it the appropriate `api.*.verify_key` permission:
+
+
+
+```json title="Root Key with Permission to Verify Any API Key" icon="key"
+{
+ "name": "Service Authentication Key",
+ "permissions": [
+ {
+ "name": "api.*.verify_key",
+ "description": "Allow verification of keys from any API in the workspace"
+ }
+ ]
+}
+```
+
+This root key can verify keys from any API in your workspace. Use this for services that need to authenticate users across multiple APIs.
+
+
+```json title="Root Key with Permission for Specific API" icon="shield"
+{
+ "name": "Production API Verification Key",
+ "permissions": [
+ {
+ "name": "api.api_1234567890abcdef.verify_key",
+ "description": "Allow verification of keys only from the Production API"
+ }
+ ]
+}
+```
+
+This root key can only verify keys from the specific API `api_1234567890abcdef`. Use this for services that should only authenticate users from a particular API.
+
+
+```json title="Root Key with Permission for Multiple APIs" icon="layers"
+{
+ "name": "Multi-Service Verification Key",
+ "permissions": [
+ {
+ "name": "api.api_prod123.verify_key",
+ "description": "Verify keys from Production API"
+ },
+ {
+ "name": "api.api_staging456.verify_key",
+ "description": "Verify keys from Staging API"
+ }
+ ]
+}
+```
+
+This root key can verify keys from multiple specific APIs. Use this when you need to authenticate users from several APIs but not all APIs in the workspace.
+
+
+
+### Migration from v1 apiId Parameter
+
+In v1, you could specify which API's keys to verify using the `apiId` parameter:
+
+```json title="v1: Explicit API Selection"
+{
+ "key": "sk_1234abcdef",
+ "apiId": "api_1234567890abcdef" // ❌ No longer supported in v2
+}
+```
+
+In v2, this control is moved to the root key's permissions:
+
+```json title="v2: Permission-Based API Selection"
+{
+ "key": "sk_1234abcdef"
+ // API access controlled by root key's api.*.verify_key permissions
+}
+```
+
+**Benefits of the New System:**
+- **Better Security**: Only authorized services can verify keys
+- **Granular Control**: Workspace owners control which services can verify keys from which APIs
+- **Simpler Integration**: No need to manage `apiId` parameters in your application code
+- **Audit Trail**: All key verifications are tied to specific root keys with known permissions
+
+---
+
## GET /v1/keys.getKey → POST /v2/keys.getKey
**Key Changes:**
@@ -1294,10 +1395,13 @@ console.log(keyData.keyId);
- [ ] Add `refillDay` for monthly intervals
### Key Verification
-- [ ] Add root key authentication header to key verification calls
+- [ ] **CRITICAL**: Create root key with `api.*.verify_key` permission for your verification service
+- [ ] Add root key authentication header to all key verification calls
+- [ ] Remove `apiId` parameter from verification requests (controlled by root key permissions now)
- [ ] Convert permission query objects to strings: `"perm1 AND perm2"`
- [ ] Update `remaining` → `credits` for cost parameters
- [ ] Handle new rate limits array structure in responses
+- [ ] Test verification with both wildcard (`api.*.verify_key`) and specific API permissions
### Response Handling
- [ ] Change `response` (direct) to `response.data` in all key operations
diff --git a/apps/docs/errors/unkey/application/assertion_failed.mdx b/apps/docs/errors/unkey/application/assertion_failed.mdx
index a7ec77ff4a..e2f6c7c004 100644
--- a/apps/docs/errors/unkey/application/assertion_failed.mdx
+++ b/apps/docs/errors/unkey/application/assertion_failed.mdx
@@ -1,8 +1,10 @@
---
-title: "err:unkey:application:assertion_failed"
+title: "assertion_failed"
description: "A runtime assertion or invariant check failed"
---
+`err:unkey:application:invalid_input`
+
```json Example
{
@@ -54,4 +56,4 @@ When contacting support, be sure to include:
## Related Errors
- [err:unkey:application:unexpected_error](./unexpected_error) - A more general internal error
-- [err:unkey:application:service_unavailable](./service_unavailable) - When a service is temporarily unavailable
\ No newline at end of file
+- [err:unkey:application:service_unavailable](./service_unavailable) - When a service is temporarily unavailable
diff --git a/apps/docs/errors/unkey/application/invalid_input.mdx b/apps/docs/errors/unkey/application/invalid_input.mdx
index e1eb6afa8d..73118019e8 100644
--- a/apps/docs/errors/unkey/application/invalid_input.mdx
+++ b/apps/docs/errors/unkey/application/invalid_input.mdx
@@ -1,8 +1,10 @@
---
-title: "err:unkey:application:invalid_input"
+title: "invalid_input"
description: "Client provided input that failed validation"
---
+`err:unkey:application:invalid_input`
+
```json Example
{
diff --git a/apps/docs/errors/unkey/application/protected_resource.mdx b/apps/docs/errors/unkey/application/protected_resource.mdx
index f5fa2c5728..937e6118ed 100644
--- a/apps/docs/errors/unkey/application/protected_resource.mdx
+++ b/apps/docs/errors/unkey/application/protected_resource.mdx
@@ -1,8 +1,10 @@
---
-title: "err:unkey:application:protected_resource"
+title: "protected_resource"
description: "Attempt to modify a protected resource"
---
+`err:unkey:application:protected_resource`
+
```json Example
{
diff --git a/apps/docs/errors/unkey/application/service_unavailable.mdx b/apps/docs/errors/unkey/application/service_unavailable.mdx
index 40b9983fbd..7da28c4ec5 100644
--- a/apps/docs/errors/unkey/application/service_unavailable.mdx
+++ b/apps/docs/errors/unkey/application/service_unavailable.mdx
@@ -1,8 +1,10 @@
---
-title: "err:unkey:application:service_unavailable"
+title: "service_unavailable"
description: "A service is temporarily unavailable"
---
+`err:unkey:application:service_unavailable`
+
```json Example
{
diff --git a/apps/docs/errors/unkey/application/unexpected_error.mdx b/apps/docs/errors/unkey/application/unexpected_error.mdx
index 4b0580ecf2..7a400436c1 100644
--- a/apps/docs/errors/unkey/application/unexpected_error.mdx
+++ b/apps/docs/errors/unkey/application/unexpected_error.mdx
@@ -1,8 +1,10 @@
---
-title: "err:unkey:application:unexpected_error"
+title: "unexpected_error"
description: "An unhandled or unexpected error occurred"
---
+`err:unkey:application:unexpected_error`
+
```json Example
{
diff --git a/apps/docs/errors/unkey/authentication/key_not_found.mdx b/apps/docs/errors/unkey/authentication/key_not_found.mdx
index 3ac216988c..144d005533 100644
--- a/apps/docs/errors/unkey/authentication/key_not_found.mdx
+++ b/apps/docs/errors/unkey/authentication/key_not_found.mdx
@@ -1,8 +1,10 @@
---
-title: "err:unkey:authentication:key_not_found"
+title: "key_not_found"
description: "The authentication key was not found"
---
+`err:unkey:authentication:key_not_found`
+
```json Example
{
diff --git a/apps/docs/errors/unkey/authentication/malformed.mdx b/apps/docs/errors/unkey/authentication/malformed.mdx
index 789d6dc0d7..f030ea637c 100644
--- a/apps/docs/errors/unkey/authentication/malformed.mdx
+++ b/apps/docs/errors/unkey/authentication/malformed.mdx
@@ -1,8 +1,10 @@
---
-title: "err:unkey:authentication:malformed"
+title: "malformed"
description: "Authentication credentials were incorrectly formatted"
---
+`err:unkey:authentication:malformed`
+
```json Example
{
diff --git a/apps/docs/errors/unkey/authentication/missing.mdx b/apps/docs/errors/unkey/authentication/missing.mdx
index a0e6fdebac..4913ce12b7 100644
--- a/apps/docs/errors/unkey/authentication/missing.mdx
+++ b/apps/docs/errors/unkey/authentication/missing.mdx
@@ -1,8 +1,10 @@
---
-title: "err:unkey:authentication:missing"
+title: "missing"
description: "Authentication credentials were not provided in the request"
---
+`err:unkey:authentication:missing`
+
```json Example
{
diff --git a/apps/docs/errors/unkey/authorization/forbidden.mdx b/apps/docs/errors/unkey/authorization/forbidden.mdx
index 9450f725eb..1f20f712bf 100644
--- a/apps/docs/errors/unkey/authorization/forbidden.mdx
+++ b/apps/docs/errors/unkey/authorization/forbidden.mdx
@@ -1,8 +1,10 @@
---
-title: "err:unkey:authorization:forbidden"
+title: "forbidden"
description: "The operation is not allowed"
---
+`err:unkey:authorization:forbidden`
+
```json Example
{
diff --git a/apps/docs/errors/unkey/authorization/insufficient_permissions.mdx b/apps/docs/errors/unkey/authorization/insufficient_permissions.mdx
index ba52dc8544..45301622f8 100644
--- a/apps/docs/errors/unkey/authorization/insufficient_permissions.mdx
+++ b/apps/docs/errors/unkey/authorization/insufficient_permissions.mdx
@@ -1,8 +1,10 @@
---
-title: "err:unkey:authorization:insufficient_permissions"
+title: "insufficient_permissions"
description: "The authenticated entity lacks sufficient permissions for the requested operation"
---
+`err:unkey:authorization:insufficient_permissions`
+
```json Example
{
diff --git a/apps/docs/errors/unkey/authorization/key_disabled.mdx b/apps/docs/errors/unkey/authorization/key_disabled.mdx
index a811ae756f..e059c8d114 100644
--- a/apps/docs/errors/unkey/authorization/key_disabled.mdx
+++ b/apps/docs/errors/unkey/authorization/key_disabled.mdx
@@ -1,8 +1,10 @@
---
-title: "err:unkey:authorization:key_disabled"
+title: "key_disabled"
description: "The authentication key is disabled"
---
+`err:unkey:authorization:key_disabled`
+
```json Example
{
diff --git a/apps/docs/errors/unkey/authorization/workspace_disabled.mdx b/apps/docs/errors/unkey/authorization/workspace_disabled.mdx
index 6cb4b0d74c..8a759bb020 100644
--- a/apps/docs/errors/unkey/authorization/workspace_disabled.mdx
+++ b/apps/docs/errors/unkey/authorization/workspace_disabled.mdx
@@ -1,8 +1,10 @@
---
-title: "err:unkey:authorization:workspace_disabled"
+title: "workspace_disabled"
description: "The associated workspace is disabled"
---
+`err:unkey:authorization:workspace_disabled`
+
```json Example
{
diff --git a/apps/docs/errors/unkey/data/api_not_found.mdx b/apps/docs/errors/unkey/data/api_not_found.mdx
index 4cc819f6e7..dec0e45d06 100644
--- a/apps/docs/errors/unkey/data/api_not_found.mdx
+++ b/apps/docs/errors/unkey/data/api_not_found.mdx
@@ -1,8 +1,12 @@
---
-title: "err:unkey:data:api_not_found"
+title: "api_not_found"
description: "The requested API was not found"
---
+
+err:unkey:data:api_not_found
+
+
```json Example
{
diff --git a/apps/docs/errors/unkey/data/audit_log_not_found.mdx b/apps/docs/errors/unkey/data/audit_log_not_found.mdx
index c411379b6e..8af3665dbb 100644
--- a/apps/docs/errors/unkey/data/audit_log_not_found.mdx
+++ b/apps/docs/errors/unkey/data/audit_log_not_found.mdx
@@ -1,8 +1,12 @@
---
-title: "err:unkey:data:audit_log_not_found"
+title: "audit_log_not_found"
description: "The requested audit log was not found"
---
+
+err:unkey:data:audit_log_not_found
+
+
```json Example
{
diff --git a/apps/docs/errors/unkey/data/identity_already_exists.mdx b/apps/docs/errors/unkey/data/identity_already_exists.mdx
index 1a3c83723f..b3efae1211 100644
--- a/apps/docs/errors/unkey/data/identity_already_exists.mdx
+++ b/apps/docs/errors/unkey/data/identity_already_exists.mdx
@@ -1,8 +1,12 @@
---
-title: "err:unkey:data:identity_already_exists"
+title: "identity_already_exists"
description: "The requested identity already exists"
---
+
+err:unkey:data:identity_already_exists
+
+
```json Example
{
diff --git a/apps/docs/errors/unkey/data/identity_not_found.mdx b/apps/docs/errors/unkey/data/identity_not_found.mdx
index ad22b07c7e..674a5dde9a 100644
--- a/apps/docs/errors/unkey/data/identity_not_found.mdx
+++ b/apps/docs/errors/unkey/data/identity_not_found.mdx
@@ -1,8 +1,12 @@
---
-title: "err:unkey:data:identity_not_found"
+title: "identity_not_found"
description: "The requested identity was not found"
---
+
+err:unkey:data:identity_not_found
+
+
```json Example
{
diff --git a/apps/docs/errors/unkey/data/key_auth_not_found.mdx b/apps/docs/errors/unkey/data/key_auth_not_found.mdx
index b919e0b893..ba05799817 100644
--- a/apps/docs/errors/unkey/data/key_auth_not_found.mdx
+++ b/apps/docs/errors/unkey/data/key_auth_not_found.mdx
@@ -1,8 +1,12 @@
---
-title: "err:unkey:data:key_auth_not_found"
+title: "key_auth_not_found"
description: "The requested key authentication was not found"
---
+
+err:unkey:data:key_auth_not_found
+
+
```json Example
{
diff --git a/apps/docs/errors/unkey/data/key_not_found.mdx b/apps/docs/errors/unkey/data/key_not_found.mdx
index 91a2676fad..ee68df322d 100644
--- a/apps/docs/errors/unkey/data/key_not_found.mdx
+++ b/apps/docs/errors/unkey/data/key_not_found.mdx
@@ -1,8 +1,12 @@
---
-title: "err:unkey:data:key_not_found"
+title: "key_not_found"
description: "The requested key was not found"
---
+
+err:unkey:data:key_not_found
+
+
```json Example
{
diff --git a/apps/docs/errors/unkey/data/permission_already_exists.mdx b/apps/docs/errors/unkey/data/permission_already_exists.mdx
index da47abe0c9..95bd2cc4dd 100644
--- a/apps/docs/errors/unkey/data/permission_already_exists.mdx
+++ b/apps/docs/errors/unkey/data/permission_already_exists.mdx
@@ -1,8 +1,12 @@
---
-title: "err:unkey:data:permission:duplicate"
+title: "permission_already_exists"
description: "A permission with this slug already exists"
---
+
+err:unkey:data:permission_already_exists
+
+
```json Example
{
"meta": {
diff --git a/apps/docs/errors/unkey/data/permission_not_found.mdx b/apps/docs/errors/unkey/data/permission_not_found.mdx
index 1bf8c2b9dc..445ddd2821 100644
--- a/apps/docs/errors/unkey/data/permission_not_found.mdx
+++ b/apps/docs/errors/unkey/data/permission_not_found.mdx
@@ -1,8 +1,12 @@
---
-title: "err:unkey:data:permission_not_found"
+title: "permission_not_found"
description: "The requested permission was not found"
---
+
+err:unkey:data:permission_not_found
+
+
```json Example
{
diff --git a/apps/docs/errors/unkey/data/ratelimit_namespace_not_found.mdx b/apps/docs/errors/unkey/data/ratelimit_namespace_not_found.mdx
index 119771da08..c94f12618e 100644
--- a/apps/docs/errors/unkey/data/ratelimit_namespace_not_found.mdx
+++ b/apps/docs/errors/unkey/data/ratelimit_namespace_not_found.mdx
@@ -1,8 +1,12 @@
---
-title: "err:unkey:data:ratelimit_namespace_not_found"
+title: "ratelimit_namespace_not_found"
description: "The requested rate limit namespace was not found"
---
+
+err:unkey:data:ratelimit_namespace_not_found
+
+
```json Example
{
diff --git a/apps/docs/errors/unkey/data/ratelimit_override_not_found.mdx b/apps/docs/errors/unkey/data/ratelimit_override_not_found.mdx
index cfa4ac1638..b6ddebacd9 100644
--- a/apps/docs/errors/unkey/data/ratelimit_override_not_found.mdx
+++ b/apps/docs/errors/unkey/data/ratelimit_override_not_found.mdx
@@ -1,8 +1,12 @@
---
-title: "err:unkey:data:ratelimit_override_not_found"
+title: "ratelimit_override_not_found"
description: "The requested rate limit override was not found"
---
+
+err:unkey:data:ratelimit_override_not_found
+
+
```json Example
{
diff --git a/apps/docs/errors/unkey/data/role_already_exists.mdx b/apps/docs/errors/unkey/data/role_already_exists.mdx
index 4c3777e0de..55dec3e109 100644
--- a/apps/docs/errors/unkey/data/role_already_exists.mdx
+++ b/apps/docs/errors/unkey/data/role_already_exists.mdx
@@ -1,8 +1,12 @@
---
-title: "err:unkey:data:role:duplicate"
+title: "role_already_exists"
description: "A role with this name already exists"
---
+
+err:unkey:data:role_already_exists
+
+
```json Example
{
"meta": {
diff --git a/apps/docs/errors/unkey/data/role_not_found.mdx b/apps/docs/errors/unkey/data/role_not_found.mdx
index 3363288191..0764864215 100644
--- a/apps/docs/errors/unkey/data/role_not_found.mdx
+++ b/apps/docs/errors/unkey/data/role_not_found.mdx
@@ -1,8 +1,12 @@
---
-title: "err:unkey:data:role_not_found"
+title: "role_not_found"
description: "The requested role was not found"
---
+
+err:unkey:data:role_not_found
+
+
```json Example
{
diff --git a/apps/docs/errors/unkey/data/workspace_not_found.mdx b/apps/docs/errors/unkey/data/workspace_not_found.mdx
index b0fc6f05cb..6d94c79944 100644
--- a/apps/docs/errors/unkey/data/workspace_not_found.mdx
+++ b/apps/docs/errors/unkey/data/workspace_not_found.mdx
@@ -1,8 +1,12 @@
---
-title: "err:unkey:data:workspace_not_found"
+title: "workspace_not_found"
description: "The requested workspace was not found"
---
+
+err:unkey:data:workspace_not_found
+
+
```json Example
{
diff --git a/apps/docs/errors/user/bad_request/permissions_query_syntax_error.mdx b/apps/docs/errors/user/bad_request/permissions_query_syntax_error.mdx
index 108b629fa6..68fe898862 100644
--- a/apps/docs/errors/user/bad_request/permissions_query_syntax_error.mdx
+++ b/apps/docs/errors/user/bad_request/permissions_query_syntax_error.mdx
@@ -1,8 +1,10 @@
---
-title: "err:user:bad_request:permissions_query_syntax_error"
+title: "permissions_query_syntax_error"
description: "Invalid syntax or characters in verifyKey permissions query"
---
+`err:user:bad_request:permissions_query_syntax_error`
+
```json Example
{
"meta": {