This guide covers all key management endpoints including creation, verification, updates, permissions, and roles.

Overview

Key management endpoints are the core of the Unkey API, handling creation, verification, updates, permissions, and roles for API keys.

Key Changes in v2:

  • Response format: Direct responses → {meta, data} envelope
  • Owner ID: ownerId field removed, use externalId only
  • Credits: remaining + refillcredits object
  • Rate limits: ratelimit object → ratelimits array
  • Permission queries: Object syntax → string syntax

Migration Impact:

  • Existing in v1: Full key CRUD operations with permissions, roles, and rate limiting
  • Enhanced in v2: Improved response format, simplified field structures, and string-based queries
  • Maintained in v2: All core key management functionality with backward-compatible request formats

POST /v1/keys.createKey → POST /v2/keys.createKey

Key Changes:
  • Remove ownerId field, use externalId instead
  • Restructure remaining + refillcredits object
  • Change ratelimit object → ratelimits array
  • Response format: Direct response → {meta, data} envelope
Key Creation Request Diff
{
  "apiId": "api_1234567890abcdef",
  "prefix": "prod",
  "name": "Production API Key",
  "ownerId": "user_456", 
  "externalId": "customer_789",
  "permissions": ["documents.read", "documents.write"],
  "roles": ["editor"],
  "expires": 1735689600000,
  "remaining": 10000, 
  "refill": { 
    "interval": "monthly", 
    "amount": 10000
  }, 
  "credits": { 
    "remaining": 10000, 
    "refill": { 
      "interval": "monthly", 
      "amount": 10000, 
      "refillDay": 1
    } 
  }, 
  "ratelimit": { 
    "limit": 1000, 
    "duration": 3600000, 
    "async": true
  }, 
  "ratelimits": [ 
    { 
      "name": "api_requests", 
      "limit": 1000, 
      "duration": 3600000, 
      "autoApply": true
    } 
  ], 
  "enabled": true
}

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 remainingcredits for cost parameters
  • Add support for multiple named rate limits
  • Response format: Direct response → {meta, data} envelope
Major Authentication Change in v2The 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
Key Verification Request Changes
{
  "key": "sk_1234abcdef"
}
v1 cURL
curl -X POST https://api.unkey.dev/v1/keys.verifyKey \
  -H "Content-Type: application/json" \
  -d '{"key": "sk_1234abcdef"}'
v2 cURL
curl -X POST https://api.unkey.com/v2/keys.verifyKey \
  -H "Authorization: Bearer <your-root-key-with-api.*.verify_key-permission>" \
  -H "Content-Type: application/json" \
  -d '{"key": "sk_1234abcdef"}'
Permission Verification
Permission Query Syntax
// v1 Request
{
  "key": "sk_1234abcdef",
  "authorization": { 
    "permissions": { 
      "and": ["documents.read", "documents.write"] 
    } 
  } 
}

// v2 Request
{
  "key": "sk_1234abcdef",
  "permissions": "documents.read AND documents.write"
}
Credits and Rate Limits
Credits & Rate Limits Structure
// v1 Request
{
  "key": "sk_1234abcdef",
  "remaining": { 
    "cost": 5
  } 
}

// v2 Request
{
  "key": "sk_1234abcdef",
  "credits": { 
    "cost": 5
  }, 
  "ratelimits": [ 
    { 
      "name": "heavy_operations", 
      "cost": 3
    } 
  ] 
}

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:
Root Key with Permission to Verify Any API 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.

Migration from v1 apiId Parameter

In v1, you could specify which API’s keys to verify using the apiId parameter:
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:
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:
  • HTTP method: GET → POST
  • Request body format required instead of query parameters
  • Response format: Direct response → {meta, data} envelope
HTTP Method Change
# v1: GET with query parameters
curl -X GET "https://api.unkey.dev/v1/keys.getKey?keyId=key_123" \ # [!code --]
  -H "Authorization: Bearer <your-root-key>"

# v2: POST with request body
curl -X POST https://api.unkey.com/v2/keys.getKey \ # [!code ++]
  -H "Authorization: Bearer <your-root-key>" \ # [!code ++]
  -H "Content-Type: application/json" \ # [!code ++]
  -d '{"keyId": "key_123"}'

POST /v1/keys.deleteKey → POST /v2/keys.deleteKey

Purpose: Permanently delete an API key. Key Changes:
  • Response format: Direct response → {meta, data} envelope
  • Added permanent parameter for hard deletion
  • Added meta.requestId for debugging
Delete Key Request
// v1 Request
{
  "keyId": "key_123"
}

// v2 Request (enhanced)
{
  "keyId": "key_123",
  "permanent": false
}

POST /v1/keys.updateKey → POST /v2/keys.updateKey

Purpose: Update an existing API key’s properties. Key Changes:
  • Same structural changes as createKey (credits, ratelimits, no ownerId)
  • Response format: Direct response → {meta, data} envelope
  • Support for partial updates
Update Key Request Diff
// v1 Request
{
  "keyId": "key_123",
  "name": "Updated Production Key",
  "ownerId": "user_456", 
  "remaining": 5000, 
  "ratelimit": { 
    "limit": 2000, 
    "duration": 3600000
  } 
}

// v2 Request
{
  "keyId": "key_123",
  "name": "Updated Production Key",
  "externalId": "user_456", 
  "credits": { 
    "remaining": 5000
  }, 
  "ratelimits": [ 
    { 
      "name": "api_requests", 
      "limit": 2000, 
      "duration": 3600000
    } 
  ] 
}

POST /v1/keys.updateRemaining → POST /v2/keys.updateCredits

Purpose: Update the credit/usage count for an API key. Key Changes:
  • Endpoint name change: updateRemainingupdateCredits
  • New operation types: set, increment, decrement
  • Response format: Direct response → {meta, data} envelope
Update Credits Request Diff
// v1 Request
{
  "keyId": "key_123",
  "value": 1000
}

// v2 Request
{
  "keyId": "key_123",
  "operation": "set", 
  "value": 1000
}

POST /v1/keys.whoami → POST /v2/keys.whoami

Purpose: Get information about the current API key being used. Key Changes:
  • Response format: Direct response → {meta, data} envelope
  • Enhanced response with additional metadata
  • Added meta.requestId for debugging
Whoami Request
// v1 & v2 Request (unchanged)
{}

Permission Management Endpoints

POST /v1/keys.addPermissions → POST /v2/keys.addPermissions

Purpose: Add permissions to an existing API key. Key Changes:
  • Response format: Direct response → {meta, data} envelope
  • Auto-creation of permissions if they don’t exist
  • Added meta.requestId for debugging
Add Permissions Request
// v1 & v2 Request (unchanged)
{
  "keyId": "key_123",
  "permissions": [
    {
      "name": "documents.read",
      "description": "Read access to documents"
    },
    {
      "name": "documents.write",
      "description": "Write access to documents"
    }
  ]
}

POST /v1/keys.removePermissions → POST /v2/keys.removePermissions

Purpose: Remove permissions from an existing API key. Key Changes:
  • Response format: Direct response → {meta, data} envelope
  • Added meta.requestId for debugging
Remove Permissions Request
// v1 & v2 Request (unchanged)
{
  "keyId": "key_123",
  "permissions": ["documents.write", "documents.delete"]
}

POST /v1/keys.setPermissions → POST /v2/keys.setPermissions

Purpose: Atomically replace all permissions on an API key. Key Changes:
  • Response format: Direct response → {meta, data} envelope
  • Atomic replacement of all permissions
  • Added meta.requestId for debugging
Set Permissions Request
// v1 & v2 Request (unchanged)
{
  "keyId": "key_123",
  "permissions": [
    {
      "name": "documents.read",
      "description": "Read access to documents"
    },
    {
      "name": "comments.moderate",
      "description": "Moderate comments"
    }
  ]
}

Role Management Endpoints

POST /v1/keys.addRoles → POST /v2/keys.addRoles

Purpose: Add roles to an existing API key. Key Changes:
  • Response format: Direct response → {meta, data} envelope
  • Auto-creation of roles if they don’t exist
  • Added meta.requestId for debugging
Add Roles Request
// v1 & v2 Request (unchanged)
{
  "keyId": "key_123",
  "roles": ["editor", "moderator"]
}

POST /v1/keys.removeRoles → POST /v2/keys.removeRoles

Purpose: Remove roles from an existing API key. Key Changes:
  • Response format: Direct response → {meta, data} envelope
  • Added meta.requestId for debugging
Remove Roles Request
// v1 & v2 Request (unchanged)
{
  "keyId": "key_123",
  "roles": ["moderator"]
}

POST /v1/keys.setRoles → POST /v2/keys.setRoles

Purpose: Atomically replace all roles on an API key. Key Changes:
  • Response format: Direct response → {meta, data} envelope
  • Atomic replacement of all roles
  • Added meta.requestId for debugging
Set Roles Request
// v1 & v2 Request (unchanged)
{
  "keyId": "key_123",
  "roles": ["editor", "admin"]
}

Migration Patterns

Response Format Migration

v1 vs v2: Response Handling
// v1: Access data directly
const key = await fetch('/v1/keys.getKey', { 
  method: 'POST',
  headers: {
    'Authorization': 'Bearer <root-key>',
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({ keyId: 'key_123' })
});

const data = await key.json(); 
const keyData = data; // v1 direct format
console.log(keyData.keyId);

// v2: Access data through data field
const key = await fetch('/v2/keys.getKey', { 
  method: 'POST',
  headers: {
    'Authorization': 'Bearer <root-key>',
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({ keyId: 'key_123' })
});

const response = await key.json(); 
const keyData = response.data; // v2 format
const requestId = response.meta.requestId; // for debugging
console.log(keyData.keyId);

Key Structure Migration

v1 vs v2: Key Structure
// v1 Key Structure
{
  "apiId": "api_123",
  "ownerId": "user_456", 
  "remaining": 1000, 
  "refill": { 
    "interval": "monthly", 
    "amount": 1000
  }, 
  "ratelimit": { 
    "limit": 100, 
    "duration": 60000, 
    "async": true
  } 
}

// v2 Key Structure
{
  "apiId": "api_123",
  "externalId": "user_456", 
  "credits": { 
    "remaining": 1000, 
    "refill": { 
      "interval": "monthly", 
      "amount": 1000, 
      "refillDay": 1
    } 
  }, 
  "ratelimits": [ 
    { 
      "name": "api_requests", 
      "limit": 100, 
      "duration": 60000, 
      "autoApply": true
    } 
  ] 
}

Migration Checklist

Key Creation & Updates

  • Replace ownerId with externalId
  • Update remaining + refillcredits structure
  • Convert ratelimitratelimits array
  • Add name field to rate limits
  • Change async parameter to autoApply
  • Add refillDay for monthly intervals

Key Verification

  • 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 remainingcredits 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
  • Extract and log meta.requestId from responses for debugging
  • Remove references to ownerId in response parsing
  • Update error handling for new response structure

Endpoint Updates

  • Update keys.updateRemainingkeys.updateCredits
  • Add operation parameter for credit updates (set/increment/decrement)
  • Add permanent parameter for key deletion if needed

Testing

  • Test key creation with new structure
  • Test key verification with string-based permission queries
  • Test permission and role management operations
  • Verify key updates work with new credit structure
  • Confirm all responses follow new envelope format