# Using Cursor with Unkey Source: https://unkey.com/docs/ai-code-gen/cursor Configure Cursor with Unkey documentation context to generate accurate key issuance, rate limiting, and verification code in your projects. Cursor is an AI-powered code editor that can help you build applications faster. When combined with Unkey's APIs, you can quickly generate secure, scalable applications with API keys and rate limiting. ## Prerequisites ## Getting Started ### 1. Set Up Your Unkey Workspace First, create your Unkey workspace and get your API keys: Navigate to the [Unkey Dashboard](https://app.unkey.com/apis) and create a new keyspace for your project. Go to [Settings > Root Keys](https://app.unkey.com/settings/root-keys) and create a new root key with the necessary permissions. Copy your API ID from the dashboard - you'll need this for generating API keys. ### 2. Set Up Unkey MCP Server (Optional) Cursor supports the Model Context Protocol (MCP) which allows you to connect directly to Unkey's APIs. This gives Cursor access to your Unkey workspace for more intelligent suggestions. #### Install Unkey MCP Server 1. **Configure the MCP Server** Create or update your Cursor configuration file with the Unkey MCP server: ```json theme={"theme":"kanagawa-wave"} { "mcpServers": { "Unkey": { "command": "npx", "args": [ "mcp-remote", "https://mcp.unkey.com/mcp/v2", "--header", "MCP-UNKEY-BEARER-AUTH:${UNKEY_ROOT_KEY}" ] } } } ``` For ratelimiting specific operations, you can also add: ```json theme={"theme":"kanagawa-wave"} { "mcpServers": { "UnkeyRateLimiting": { "command": "npx", "args": [ "mcp-remote", "https://mcp.unkey.com/mcp/ratelimits/", "--header", "MCP-UNKEY-V2-ROOT-KEY:${UNKEY_ROOT_KEY}" ] } } } ``` 2. **Set Environment Variable** ```bash theme={"theme":"kanagawa-wave"} export UNKEY_ROOT_KEY="your_root_key_here" ``` 3. **Restart Cursor** Restart Cursor to load the MCP server configuration. ## Tips and Tricks for Cursor with Unkey ### 1. Keep Your Requests Small When working with Cursor, break down complex tasks into smaller, focused requests: **Good:** ```text theme={"theme":"kanagawa-wave"} Create a function to verify an API key with Unkey that returns a boolean ``` **Better:** ```text theme={"theme":"kanagawa-wave"} Create a TypeScript function that: - Takes an API key string as input - Uses @unkey/api to verify the key - Returns a boolean indicating if the key is valid - Includes proper error handling ``` ### 2. Update and Reference Your README.md Keep your project's README.md updated with Unkey-specific information. Cursor uses this context to provide better suggestions: ```markdown theme={"theme":"kanagawa-wave"} # My Project This project uses Unkey for API authentication and ratelimiting. ## Environment Variables - `UNKEY_ROOT_KEY`: Your Unkey root key - `UNKEY_API_ID`: Your API ID from the Unkey dashboard ## API Routes - `/api/protected` - Requires valid API key - `/api/keys` - Manage API keys (admin only) ## Rate Limiting - Free tier: 100 requests/hour - Pro tier: 1000 requests/hour ``` ## Add Unkey Documentation Context Adding Unkey docs can let you specifically refer to Unkey features when building your app. From Cursor Settings > Features > Docs add new doc, use the URL "[https://unkey.com/docs](https://unkey.com/docs)" # AI Code Gen with Unkey Source: https://unkey.com/docs/ai-code-gen/overview Set up AI-powered code generation tools like Cursor, Windsurf, and the Unkey MCP server to build applications with Unkey APIs and SDKs faster. Unkey provides powerful integrations with AI code generation tools to help you build applications faster and more efficiently. Whether you're using Cursor, Windsurf, or other AI tools, you can leverage AI to generate code that integrates seamlessly with Unkey's APIs. ## Available AI Tools Use Cursor's AI capabilities with Unkey's API documentation and examples Build Unkey applications with Windsurf's AI-powered development environment Connect your favorite AI code generation tool to Unkey's APIs using Model Context Protocol # Unkey MCP (Model Context Protocol) Source: https://unkey.com/docs/ai-code-gen/unkey-mcp Use the Unkey MCP server to connect AI coding assistants to Unkey APIs. Manage API keys, rate limits, and permissions directly from your IDE. The Unkey Model Context Protocol (MCP) servers provide direct integration between AI tools and Unkey's APIs. This allows you to interact with your Unkey workspace directly, enabling AI-powered key operations, rate limiting configuration, and analytics queries. ## What is MCP? Model Context Protocol (MCP) is an open standard that allows AI applications to securely access external data and services. Unkey's MCP servers give Claude Desktop direct access to your Unkey APIs, enabling intelligent assistance with API keys and rate limiting. ## Available MCP Servers Unkey provides two MCP servers: Full access to Unkey's API management capabilities Specialized server for rate limiting operations ## Prerequisites * [Unkey account](https://app.unkey.com/auth/sign-up) created * Unkey root key with appropriate permissions * Node.js installed (for npx command) ## Installation Below is an example of using Unkey MCP with Claude but it can also be used with other AI applications. ### Unkey API MCP The main Unkey MCP server provides access to the complete Unkey API: 1. **Open Claude Desktop Configuration** Navigate to your Claude Desktop configuration file: * **macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json` * **Windows**: `%APPDATA%\Claude\claude_desktop_config.json` 2. **Add the MCP Server Configuration** Add the following configuration to your `claude_desktop_config.json`: ```json theme={"theme":"kanagawa-wave"} { "mcpServers": { "Unkey": { "command": "npx", "args": [ "mcp-remote", "https://mcp.unkey.com/mcp/v2", "--header", "MCP-UNKEY-BEARER-AUTH:${UNKEY_ROOT_KEY}" ] } } } ``` 3. **Set Environment Variable** Set your Unkey root key as an environment variable: ```bash theme={"theme":"kanagawa-wave"} # macOS/Linux export UNKEY_ROOT_KEY="unkey_xxx" # Windows set UNKEY_ROOT_KEY=unkey_xxx ``` 4. **Restart Claude Desktop** Close and restart Claude Desktop to load the MCP server. ### Unkey Ratelimiting MCP For specialized rate limiting operations, use the dedicated ratelimiting MCP server: 1. **Add Ratelimiting MCP Configuration** Add this configuration to your `claude_desktop_config.json`: ```json theme={"theme":"kanagawa-wave"} { "mcpServers": { "UnkeyRateLimiting": { "command": "npx", "args": [ "mcp-remote", "https://mcp.unkey.com/mcp/ratelimits/", "--header", "MCP-UNKEY-V2-ROOT-KEY:${UNKEY_ROOT_KEY}" ] } } } ``` 2. **Use Both Servers** You can configure both MCP servers simultaneously: ```json theme={"theme":"kanagawa-wave"} { "mcpServers": { "Unkey": { "command": "npx", "args": [ "mcp-remote", "https://mcp.unkey.com/mcp/v2", "--header", "MCP-UNKEY-BEARER-AUTH:${UNKEY_ROOT_KEY}" ] }, "UnkeyRateLimiting": { "command": "npx", "args": [ "mcp-remote", "https://mcp.unkey.com/mcp/ratelimits/", "--header", "MCP-UNKEY-V2-ROOT-KEY:${UNKEY_ROOT_KEY}" ] } } } ``` ## Getting Your Root Key To use the MCP servers, you need an Unkey root key with appropriate permissions: Go to [Settings > Root Keys](https://app.unkey.com/settings/root-keys) in your Unkey dashboard. Click "Create New Root Key" and select the permissions you need: * **API Key Management**: For creating, updating, and deleting API keys * **Rate Limiting**: For configuring rate limits * **Analytics**: For querying usage data Copy your root key and store it securely. You'll use this as your environment variable. ## Using MCP with Claude Desktop Once configured, you can interact with your Unkey workspace directly through Claude Desktop: ### Working with API keys Ask Claude to help with API key operations: ```text theme={"theme":"kanagawa-wave"} Can you create a new API key for my mobile app with the following settings: - Name: "Mobile App Production" - Rate limit: 1000 requests per hour - Expiration: 90 days from now - Metadata: {"app": "mobile", "version": "1.0", "environment": "production"} ``` ### Ratelimiting Configuration Configure rate limits through natural language: ```text theme={"theme":"kanagawa-wave"} I need to set up rate limiting for my API with these tiers: - Free tier: 100 requests per hour - Pro tier: 1000 requests per hour - Enterprise tier: 10000 requests per hour Can you help me configure these limits? ``` ### Analytics and Monitoring Query your API usage data: ```text theme={"theme":"kanagawa-wave"} Show me the API usage statistics for the last 7 days, including: - Total requests - Top 5 API keys by usage - Error rates - Geographic distribution of requests ``` ### Troubleshooting Get help with common issues: ```text theme={"theme":"kanagawa-wave"} I'm seeing 401 errors for API key verification. Can you help me debug this? The API key is: uk_xxx The API ID is: api_xxx ``` ## Available Commands ### API Key Operations * **Create API Key**: Generate new API keys with custom settings * **List API Keys**: View all API keys in your workspace * **Update API Key**: Modify existing API key properties * **Delete API Key**: Remove API keys from your workspace * **Verify API Key**: Check if an API key is valid and active ### Rate Limiting Operations * **Configure Rate Limits**: Set up rate limiting rules * **Check Rate Limit Status**: Monitor current rate limit usage * **Update Rate Limits**: Modify existing rate limit configurations * **Delete Rate Limits**: Remove rate limiting rules ### Analytics and Monitoring * **Usage Analytics**: Query API usage statistics * **Error Analysis**: Investigate API errors and issues * **Performance Metrics**: Monitor API performance data * **Usage Reports**: Generate custom usage reports ## Example Conversations ### Creating an API Key **You**: "Create a new API key for my development environment with a rate limit of 500 requests per hour and set it to expire in 30 days." **Claude**: "I'll create a new API key for your development environment with the specified settings. Let me use the Unkey MCP to create this key with a 500 requests/hour rate limit and 30-day expiration." ### Analyzing API Usage **You**: "What are my top 3 API keys by usage this month?" **Claude**: "Let me query your Unkey analytics to find your top 3 API keys by usage this month. I'll pull the usage data and provide you with detailed statistics." ### Debugging Issues **You**: "I'm getting rate limit errors but I thought my limit was higher. Can you check my current rate limit configuration?" **Claude**: "I'll check your current rate limit configuration using the Unkey MCP. Let me examine your rate limiting settings and current usage to help diagnose the issue." ## Security Best Practices ### Environment Variables * Store your root key in environment variables, never in configuration files * Use different root keys for different environments (development, staging, production) * Regularly rotate your root keys ### Permissions * Grant only the minimum required permissions to your root keys * Use separate root keys for different operations when possible * Monitor root key usage through audit logs ### Access Control * Limit access to your Claude Desktop configuration * Use secure storage for your root keys * Implement proper backup and recovery procedures ## Troubleshooting ### Common Issues 1. **MCP Server Not Loading** * Check that Node.js is installed and accessible * Verify your configuration file syntax * Ensure environment variables are set correctly 2. **Authentication Errors** * Verify your root key is correct and has proper permissions * Check that the environment variable is set * Confirm your root key hasn't expired 3. **Connection Issues** * Ensure you have internet connectivity * Check if your firewall is blocking connections * Verify the MCP server URLs are correct ### Getting Help If you encounter issues: 1. Check the Claude Desktop logs for error messages 2. Verify your configuration matches the examples exactly 3. Test your root key directly with the Unkey API 4. Join the [Unkey Discord](https://go.unkey.com/discord) for community support ## Advanced Configuration ### Custom Environment Variables You can use custom environment variable names: ```json theme={"theme":"kanagawa-wave"} { "mcpServers": { "Unkey": { "command": "npx", "args": [ "mcp-remote", "https://mcp.unkey.com/mcp/v2", "--header", "MCP-UNKEY-BEARER-AUTH:${MY_CUSTOM_UNKEY_KEY}" ] } } } ``` ### Multiple Workspaces Configure multiple Unkey workspaces: ```json theme={"theme":"kanagawa-wave"} { "mcpServers": { "UnkeyProduction": { "command": "npx", "args": [ "mcp-remote", "https://mcp.unkey.com/mcp/v2", "--header", "MCP-UNKEY-BEARER-AUTH:${UNKEY_PROD_KEY}" ] }, "UnkeyStaging": { "command": "npx", "args": [ "mcp-remote", "https://mcp.unkey.com/mcp/v2", "--header", "MCP-UNKEY-BEARER-AUTH:${UNKEY_STAGING_KEY}" ] } } } ``` ## Next Steps * Explore [Cursor with Unkey](/docs/ai-code-gen/cursor) for IDE-based AI assistance * Check out [Windsurf with Unkey](/docs/ai-code-gen/windsurf) for collaborative development ## Resources * [Unkey API Reference](/docs/api-reference/auth) * [Unkey Dashboard](https://app.unkey.com) * [Community Discord](https://go.unkey.com/discord) # Using Windsurf with Unkey Source: https://unkey.com/docs/ai-code-gen/windsurf Set up Windsurf with Unkey documentation context to generate accurate API key verification, rate limiting, and authentication code in your apps. Windsurf is an AI-powered development environment that combines the power of AI assistance with a full-featured IDE. When integrated with Unkey's APIs, you can rapidly build secure, scalable applications with intelligent code generation and real-time collaboration features. ## Getting Started ### 1. Set Up Your Unkey Environment Before working with Windsurf, ensure you have your Unkey credentials ready: Navigate to the [Unkey Dashboard](https://app.unkey.com/apis) and create a new keyspace for your project. Go to [Settings > Root Keys](https://app.unkey.com/settings/root-keys) and create a new root key with appropriate permissions. Note down your API ID and root key - you'll need these for your application. ### 2. Set Up Unkey MCP Server (Optional) Windsurf supports the Model Context Protocol (MCP) which allows you to connect directly to Unkey's APIs. This gives Windsurf access to your Unkey workspace for more intelligent suggestions. #### Install Unkey MCP Server To get started with Windsurf, open "Windsurf Settings > Cascade > Model Context Protocol (MCP) Servers", click on "Add Server", click "Add custom server", and add the following configuration for Unkey. 1. **Configure the MCP Server** ```json theme={"theme":"kanagawa-wave"} { "mcpServers": { "Unkey": { "command": "npx", "args": [ "mcp-remote", "https://mcp.unkey.com/mcp/v2", "--header", "MCP-UNKEY-BEARER-AUTH:${UNKEY_ROOT_KEY}" ] } } } ``` For ratelimiting specific operations, you can also add: ```json theme={"theme":"kanagawa-wave"} { "mcpServers": { "UnkeyRateLimiting": { "command": "npx", "args": [ "mcp-remote", "https://mcp.unkey.com/mcp/ratelimits/", "--header", "MCP-UNKEY-V2-ROOT-KEY:${UNKEY_ROOT_KEY}" ] } } } ``` 2. **Set Environment Variable** ```bash theme={"theme":"kanagawa-wave"} export UNKEY_ROOT_KEY="your_root_key_here" ``` 3. **Restart Windsurf** Restart Windsurf to load the MCP server configuration. # Query key verification data Source: https://unkey.com/docs/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. For complete documentation including available tables, columns, data types, query examples, see the schema reference in the API documentation. # Create API namespace Source: https://unkey.com/docs/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://unkey.com/docs/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://unkey.com/docs/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://unkey.com/docs/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` # Authentication Source: https://unkey.com/docs/api-reference/auth Authenticate your requests to the Unkey API using root keys passed as Bearer tokens in the Authorization header. Generate and manage root keys. Almost all Unkey API endpoints require authentication using a root key. Root keys provide access to your Unkey resources based on their assigned permissions. ## Bearer Authentication Authentication is performed using HTTP Bearer authentication in the `Authorization` header: ```bash theme={"theme":"kanagawa-wave"} Authorization: Bearer unkey_1234567890 ``` Example request: ```bash theme={"theme":"kanagawa-wave"} curl -X POST "https://api.unkey.com/v2/keys.createKey" \ -H "Authorization: Bearer unkey_1234567890" \ -H "Content-Type: application/json" \ -d '{ "apiId": "api_1234" }' ``` ## Security Best Practices Never expose your root key in client-side code or include it in public repositories. For frontend applications, always use a backend server to proxy requests to the Unkey API. ## Root Key Management Root keys can be created and managed through the Unkey dashboard. We recommend: 1. **Using Different Keys for Different Environments**: Maintain separate root keys for development, staging, and production 2. **Rotating Keys Regularly**: Create new keys periodically and phase out old ones 3. **Setting Clear Key Names**: Name your keys according to their use case for better manageability ## Key Permissions System Unkey implements a sophisticated RBAC (Role-Based Access Control) system for root keys. Permissions are defined as tuples of: * **ResourceType**: The category of resource (api, ratelimit, rbac, identity) * **ResourceID**: The specific resource instance * **Action**: The operation to perform on that resource ### Available Resource Types | Resource Type | Description | | ------------- | ------------------------------------------------- | | `api` | API-related resources, such as endpoints and keys | | `ratelimit` | Rate limiting resources and configuration | | `rbac` | Permissions and roles management | | `identity` | User and identity management | ### Permission Examples Specific permission to manage a single API: ```text theme={"theme":"kanagawa-wave"} api.api_1234.read_api api.api_1234.update_api ``` Wildcard permission to manage all rate limit namespaces: ```text theme={"theme":"kanagawa-wave"} ratelimit.*.create_namespace ratelimit.*.read_namespace ``` When creating root keys, you can specify exactly what actions they're allowed to perform. ## Authentication Errors If your authentication fails, you'll receive a 401 Unauthorized or 403 Forbidden response with an error message: ```json theme={"theme":"kanagawa-wave"} { "meta": { "requestId": "req_abc123xyz789" }, "error": { "title": "Unauthorized", "detail": "The provided root key is invalid or has been revoked", "status": 401, "type": "https://unkey.com/docs/errors/unauthorized" } } ``` If your key is valid but lacks sufficient permissions, you'll receive a 403 Forbidden response: ```json theme={"theme":"kanagawa-wave"} { "meta": { "requestId": "req_abc123xyz789" }, "error": { "title": "Forbidden", "detail": "Your key does not have the required 'api.api_1234.update_api' permission", "status": 403, "type": "https://unkey.com/docs/errors/forbidden" } } ``` Common authentication issues include: * Missing the Authorization header * Invalid key format * Revoked or expired root key * Using a key with insufficient permissions # Create deployment Source: https://unkey.com/docs/api-reference/deploy/create-deployment https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v2/deploy.createDeployment **INTERNAL** - This endpoint is internal and may change without notice. Not recommended for production use. Creates a new deployment for a project using either a pre-built Docker image or build context. **Authentication**: Requires a valid root key with appropriate permissions. # Get deployment Source: https://unkey.com/docs/api-reference/deploy/get-deployment https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v2/deploy.getDeployment **INTERNAL** - This endpoint is internal and may change without notice. Not recommended for production use. Retrieves deployment information including status, error messages, and steps. **Authentication**: Requires a valid root key with appropriate permissions. # Error Handling Source: https://unkey.com/docs/api-reference/errors Handle Unkey API errors with structured error codes, HTTP status codes, and actionable messages. Includes retry strategies and examples. Error responses maintain the same top-level structure as successful responses, but with an `error` object instead of `data`: ```json theme={"theme":"kanagawa-wave"} { "meta": { "requestId": "req_abc123xyz789" }, "error": { "title": "Validation Error", "detail": "You must provide a valid API ID.", "status": 400, "type": "https://unkey.com/docs/errors/validation-error", "errors": [ { "location": "body.apiId", "message": "API not found", "fix": "Provide a valid API ID or create a new API" } ] } } ``` ## Error Format Our error format follows RFC7807 Problem Details standard within our consistent envelope structure, providing: * **title**: A short, human-readable summary of the problem * **detail**: A human-readable explanation specific to this occurrence * **status**: The HTTP status code (also returned in the HTTP response) * **type**: A URI reference that identifies the problem type and points to documentation * **errors**: (Optional) An array of specific validation errors when multiple issues occur ## Common Error Types | Status | Error Type | Description | | ------ | --------------------- | ------------------------------------------------ | | 400 | validation-error | The request body failed validation | | 401 | unauthorized | Missing or invalid authorization | | 403 | forbidden | Valid authorization but insufficient permissions | | 404 | not-found | The requested resource was not found | | 409 | conflict | The request conflicts with the current state | | 429 | rate-limited | You've exceeded your rate limit | | 500 | internal-server-error | An unexpected error occurred on our servers | ## Validation Errors For validation errors, we provide detailed information about each failed validation: * **location**: Where in the request the error occurred (e.g., `body.name`, `query.limit`) * **message**: What went wrong with the specific field * **fix**: (When possible) A suggestion for how to fix the issue ## Error Recovery Our error messages are designed to be actionable. Each error includes: 1. A clear explanation of what went wrong 2. Often, a suggestion for how to fix the issue 3. For validation errors, the specific fields that failed validation ## Using the Request ID for Support When reporting issues to our support team, always include the `requestId` from the error response. This unique identifier allows us to quickly locate the specific request in our logs and provide faster, more accurate assistance. ## Error Handling Best Practices 1. **Check for Status Codes**: Always check HTTP status codes first to determine broad error categories 2. **Extract Error Details**: Parse the error object for detailed information 3. **Implement Retries Carefully**: Only retry on 5xx errors or when explicitly advised 4. **Log Complete Errors**: Log the full error response for debugging purposes Example error handling in JavaScript: ```javascript theme={"theme":"kanagawa-wave"} try { const response = await fetch("https://api.unkey.com/v2/keys.createKey", { method: "POST", headers: { Authorization: `Bearer ${rootKey}`, "Content-Type": "application/json", }, body: JSON.stringify(keyData), }); const data = await response.json(); if (!response.ok) { // Extract and handle the error const { meta, error } = data; console.error(`Error ${error.status}: ${error.title}`, { requestId: meta.requestId, detail: error.detail, docs: error.type, }); // Handle validation errors specifically if (error.errors) { error.errors.forEach((err) => { console.error(`- ${err.location}: ${err.message}`); }); } throw new Error(`API Error: ${error.detail}`); } return data.data; // Return just the data portion on success } catch (err) { // Handle network errors or other exceptions console.error("Request failed:", err); throw err; } ``` # Create Identity Source: https://unkey.com/docs/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://unkey.com/docs/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://unkey.com/docs/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://unkey.com/docs/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://unkey.com/docs/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://unkey.com/docs/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://unkey.com/docs/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://unkey.com/docs/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 root key needs one of: - `api.*.create_key` (create keys in any API) - `api..create_key` (create keys in specific API) # Delete API keys Source: https://unkey.com/docs/api-reference/keys/delete-api-keys https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v2/keys.deleteKey Delete API keys permanently from user accounts or for cleanup purposes. Use this for user-requested key deletion, 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 root key 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) # Get API key Source: https://unkey.com/docs/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 root key 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) Additional permission required for decrypt functionality: - `api.*.decrypt_key` or `api..decrypt_key` # Get API key by hash Source: https://unkey.com/docs/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 root key 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) If your rootkey lacks permissions but the key exists, we may return a 404 status here to prevent leaking the existance of a key to unauthorized clients. If you believe that a key should exist, but receive a 404, please double check your root key has the correct permissions. # Migrate API key(s) Source: https://unkey.com/docs/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://unkey.com/docs/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://unkey.com/docs/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://unkey.com/docs/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 root key must have: - `api.*.create_key` or `api..create_key` - `api.*.encrypt_key` or `api..encrypt_key` (only when the original key is recoverable) # Set key permissions Source: https://unkey.com/docs/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://unkey.com/docs/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://unkey.com/docs/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 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** 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://unkey.com/docs/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 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** 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://unkey.com/docs/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 root key needs one of: - `api.*.verify_key` (verify keys in any API) - `api..verify_key` (verify keys in specific API) **Note**: If your root key has no verify permissions at all, you will receive a `403 Forbidden` error. If your root key has verify permissions for a different API than the key you're verifying, you will receive a `200` response with `code: NOT_FOUND` to avoid leaking key existence. # Overview Source: https://unkey.com/docs/api-reference/overview Learn about the Unkey API design philosophy including RPC-style endpoints, consistent error handling, idempotent operations, and JSON responses. ## Core Principles * **Clear Communication**: Structured responses make success and failure equally informative * **Practical Over Purist**: We make pragmatic choices rather than rigidly adhering to any single paradigm * **Predictable Patterns**: Once you learn one endpoint, you'll understand them all ## Response Structure All API responses follow a consistent structure, making them predictable and easy to parse: ```json theme={"theme":"kanagawa-wave"} { "meta": { "requestId": "req_abc123xyz789" }, "data": { // Operation-specific response data } } ``` For paginated responses, we include a pagination object: ```json theme={"theme":"kanagawa-wave"} { "meta": { "requestId": "req_abc123xyz789", "timestamp": "2023-11-08T15:22:30Z" }, "data": [ // Array of items ], "pagination": { "cursor": "cursor_xyz123", "hasMore": true } } ``` ## Working with the API ### Always Use the Request ID Every response includes a unique `requestId` in the metadata. When seeking support or debugging issues, always include this ID as it allows us to trace exactly what happened with your request. You can also search for the `requestId` yourself in the [logs](https://app.unkey.com/logs). ### Handling Pagination For endpoints that return lists of items: 1. Make your initial request 2. Check for `pagination.hasMore` to see if more items exist 3. If `true`, use the `pagination.cursor` value in your next request Example: ```js theme={"theme":"kanagawa-wave"} // First request const response = await fetch("https://api.unkey.com/v2/keys.listKeys", { method: "POST", headers: { Authorization: `Bearer ${rootKey}` }, body: JSON.stringify({ apiId: "api_123" }), }); // Follow-up request with cursor if (response.pagination?.hasMore) { const nextPage = await fetch("https://api.unkey.com/v2/keys.listKeys", { method: "POST", headers: { Authorization: `Bearer ${rootKey}` }, body: JSON.stringify({ apiId: "api_123", cursor: response.pagination.cursor, }), }); } ``` ## Versioning Our API uses a major version in the URL (e.g., `/v2/`) and maintains backwards compatibility within each major version. When we need to make breaking changes, we increment the major version number. We communicate deprecations well in advance, giving you time to update your integration before endpoints are removed. # Create permission Source: https://unkey.com/docs/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 names must be unique within the workspace. 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://unkey.com/docs/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. **Important:** Role names must be unique within the workspace. Once created, roles are immediately available for assignment. **Required Permissions** Your root key must have the following permission: - `rbac.*.create_role` # Delete permission Source: https://unkey.com/docs/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://unkey.com/docs/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://unkey.com/docs/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://unkey.com/docs/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://unkey.com/docs/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://unkey.com/docs/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` # Create portal session Source: https://unkey.com/docs/api-reference/portal/create-portal-session https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v2/portal.createSession Create a short-lived session token for an end user to access the Customer Portal. The returned session ID is valid for 15 minutes and can be exchanged exactly once for a 24-hour browser session via `portal.exchangeSession`. Redirect the end user to the returned URL to start the portal experience. **Required Permissions** Your root key must be associated with a workspace that has an enabled portal configuration. # Exchange session token Source: https://unkey.com/docs/api-reference/portal/exchange-session-token https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v2/portal.exchangeSession Exchange a short-lived session token for a long-lived browser session. This endpoint is unauthenticated. The session token itself serves as proof of authorization. Each token can only be exchanged once; subsequent attempts return 401. The returned browser session token is valid for 24 hours and should be stored as an httpOnly cookie or used in the Authorization header for subsequent API calls. # Apply multiple rate limit checks Source: https://unkey.com/docs/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://unkey.com/docs/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://unkey.com/docs/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://unkey.com/docs/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://unkey.com/docs/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://unkey.com/docs/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` # RPC-Style API Source: https://unkey.com/docs/api-reference/rpc Understand Unkey's RPC-style API design that uses action-oriented endpoints like verifyKey and createKey instead of REST resources. We use an RPC (Remote Procedure Call) style API that focuses on *actions* rather than resources. This means endpoints represent specific operations: ```text theme={"theme":"kanagawa-wave"} https://api.unkey.com/v2/{service}.{procedure} ``` For example: * `POST /v2/keys.createKey` - Create a new API key * `POST /v2/ratelimit.limit` - Check or enforce a rate limit We chose this approach because it maps directly to the operations developers want to perform, making the API intuitive to use. ## HTTP Methods We exclusively use POST for all operations. While this deviates from REST conventions, it provides several advantages: 1. **Consistent Request Pattern**: All requests follow the same pattern regardless of operation 2. **Rich Query Parameters**: Complex filtering and querying without URL length limitations 3. **Security and Compatibility**: Avoids issues with proxies or firewalls logging potentially sensitive parameters in the url ## Request Format All requests should: * Use the POST HTTP method * Include a Content-Type header set to application/json * Include an Authorization header (see Authentication documentation) * Send parameters as a JSON object in the request body Example: ```bash theme={"theme":"kanagawa-wave"} curl -X POST "https://api.unkey.com/v2/keys.createKey" \ -H "Authorization: Bearer root_1234567890" \ -H "Content-Type: application/json" \ -d '{ "apiId": "api_1234", "name": "Production API Key" }' ``` ## Service Namespaces Our API is organized into logical service namespaces that group related procedures: * **keys** - API key operations (create, verify, revoke) * **apis** - API configuration and settings * **ratelimit** - Rate limiting services * **analytics** - Usage and performance data * **identities** - Identity management * **permissions** - Permission management Each namespace contains multiple procedures that perform specific actions within that domain. ## Benefits of RPC Design We believe our RPC-style approach offers significant benefits: 1. **Clarity of Intent**: Endpoint names clearly communicate the action being performed 2. **Natural Code Mapping**: Endpoints naturally map to code and user intent (`keys.createKey()` instead of `POST /keys`) 3. **Complex Operations**: Supports complex operations that don't map well to REST's resource model 4. **Flexibility**: Allows for more flexible request structures without being constrained by URL parameters # Overview Source: https://unkey.com/docs/audit-log/introduction Track every change to your API keys, permissions, and workspace configuration with Unkey audit logs. Filter by actor, event type, and time. Audit logs capture every mutation in your workspace, key creation, permission changes, revocations, and more. They're essential for security compliance, debugging, and understanding who changed what and when. ## Why audit logs matter When something goes wrong, trace exactly what happened and who was responsible. SOC 2, HIPAA, and other frameworks require audit trails for sensitive operations. Know when team members create, modify, or revoke keys, especially in shared workspaces. Figure out why a key stopped working or when a permission was changed. ## What's captured Every mutation is logged automatically: | Category | Events | | --------------- | ------------------------------------------------------ | | **Keys** | Create, update, revoke, delete, verify (failures) | | **Keyspaces** | Create, update, delete | | **Permissions** | Create, update, delete, attach to key, remove from key | | **Roles** | Create, update, delete, assign, unassign | | **Rate limits** | Create namespace, update limits, set overrides | | **Workspace** | Member added, member removed, settings changed | ## Accessing audit logs 1. Sign into the [Dashboard](https://app.unkey.com/audit) 2. Click **Audit Logs** in the left navigation Audit log entries in the Unkey dashboard ## Reading the log Each entry shows: | Field | Description | | --------------- | --------------------------------------------------------- | | **Time** | When the change occurred | | **Actor** | Who made the change, a user (dashboard) or root key (API) | | **Action** | The operation: `Create`, `Update`, `Delete`, etc. | | **Event** | What was affected: key, permission, API, etc. | | **Description** | Human-readable summary | Click any row to see full details, including the complete request/response payloads: Audit log entry details showing request and response payloads ## Filtering logs Use the filters at the top of the audit log page to narrow down: * **Event type**: Show only key events, permission events, etc. * **Actor**: Filter by specific user or root key * **Time range**: Focus on a specific period ## Retention Audit logs are retained based on your plan: | Plan | Retention | | ---------- | ------------------------ | | Free | 7 days | | Pro | 90 days | | Enterprise | Custom (up to unlimited) | Need longer retention? [Contact us](mailto:support@unkey.com) about enterprise options. ## Next steps Full list of audited events and their payloads How Unkey protects your data # Event Types Source: https://unkey.com/docs/audit-log/types Complete reference of all audit log event types in Unkey including key creation, deletion, verification, permission changes, and more. ## Workspaces Workspace actions that will create a new audit log item. * `workspace.create` - A workspace is created. * `workspace.update` - A workspace is updated. * `workspace.delete` - A workspace is deleted. * `workspace.opt_in` - A workspace was opted into a feature. ## Keyspace Keyspace actions that will create a new audit log item. * `api.create` - A keyspace is created. * `api.update` - A keyspace is updated. * `api.delete` - A keyspace is deleted. ## Keys Key actions that will create a new audit log item. * `key.create` - A key is created. * `key.update` - A key is updated. * `key.delete` - A key is deleted. * `key.reroll` - A key is rerolled. ### Ratelimit Namespace Ratelimit Namespace actions that will create a new audit log item. * `ratelimitNamespace.create` - A ratelimit namespace is created. * `ratelimitNamespace.update` - A ratelimit namespace is updated. * `ratelimitNamespace.delete` - A ratelimit namespace is deleted. ### Ratelimit Ratelimit actions that will create a new audit log item. * `ratelimit.create` - A ratelimit is created. * `ratelimit.update` - A ratelimit is updated. * `ratelimit.delete` - A ratelimit is deleted. * `ratelimit.set_override` - A ratelimit override is set. * `ratelimit.read_override` - A ratelimit override is read. * `ratelimit.delete_override` - A ratelimit override is deleted. ## Role Role actions that will create a new audit log item. * `role.create` - A role is created. * `role.update` - A role is updated. * `role.delete` - A role is deleted. ## Permission Permission actions that will create a new audit log item. * `permission.create` - A permission is created. * `permission.update` - A permission is updated. * `permission.delete` - A permission is deleted. ## Authorization Authorization actions that will create a new audit log item. * `authorization.connect_role_and_permission` - A role and permission are connected. * `authorization.disconnect_role_and_permissions` - A role and permission are disconnected. * `authorization.connect_role_and_key` - A role and key are connected. * `authorization.disconnect_role_and_key` - A role and key are disconnected. * `authorization.connect_permission_and_key` - A permission and key are connected. * `authorization.disconnect_permission_and_key` - A permission and key are disconnected. ## Webhook Webhook actions that will create a new audit log item. * `webhook.create` - A webhook is created. * `webhook.update` - A webhook is updated. * `webhook.delete` - A webhook is deleted. ## Identity Identity actions that will create a new audit log item. * `identity.create` - An identity is created. * `identity.update` - An identity is updated. * `identity.delete` - An identity is deleted. # Deploy with the CLI Source: https://unkey.com/docs/build-and-deploy/cli Deploy a pre-built Docker image to Unkey with `unkey deploy`. Use from CI/CD pipelines, custom build systems, or your local machine. Deploying applications on Unkey is in public beta. To try it, open the product switcher in the top-left of the dashboard and select **Deploy**. During beta, deployed resources are free. We're eager for feedback, so let us know what you think on [Discord](https://unkey.com/discord), [X](https://x.com/unkeydev), or email [support@unkey.com](mailto:support@unkey.com). The `unkey deploy` command ships a **pre-built Docker image** to Unkey infrastructure. Unlike the [GitHub integration](/docs/build-and-deploy/github), the CLI does not build your image, you build it yourself (locally or in CI), push it to a registry Unkey can pull from, and `unkey deploy` creates the deployment and streams status until it is ready. Use the CLI when you want to own the build step: custom CI pipelines, monorepo build systems, pre-release images from a tag, or deploying without connecting a GitHub repository. ## Prerequisites * The [Unkey CLI](/docs/cli/overview) installed and authenticated (`unkey auth login`). * A [project](/docs/platform/projects/overview) created in your workspace. * A Docker image pushed to a registry Unkey can pull from (for example, a public `ghcr.io`, `docker.io`, or `quay.io` image). We currently don't support private registries on the platform, this will come in a future release. ## Quick start ```bash theme={"theme":"kanagawa-wave"} unkey deploy ghcr.io/your-org/your-app:v1.0.0 --project=my-project ``` That single command: 1. Creates a deployment for `my-project/default` using the image you supplied. 2. Monitors the deployment status in your terminal with a live spinner. 3. Prints the deployment ID and assigned domains once the deployment is **Ready**. Example output: ```text theme={"theme":"kanagawa-wave"} Deployment Progress ────────────────────────────────────────────────── Source Information: Branch: main Commit: a1b2c3d Image: ghcr.io/your-org/your-app:v1.0.0 ✓ Deployment created: dep_3f9a2b4c... ✓ Deployment completed successfully Deployment Complete Deployment ID: dep_3f9a2b4c... Status: Ready Environment: Preview Domains https://my-project-default-git-a1b2c3d-acme.unkey.app ``` ## Usage ```bash theme={"theme":"kanagawa-wave"} unkey deploy [flags] ``` The first positional argument is the fully qualified image reference, `registry/repository:tag` or `registry/repository@sha256:…`. The image must already exist in the registry before you run the command. ### Flags | Flag | Description | Default | | ---------------- | ------------------------------------------- | --------- | | `--project` | Project slug. **Required.** | | | `--app` | App slug within the project. | `default` | | `--env` | Environment slug to deploy to. | `preview` | | `--root-key` | Root key for authentication. **Required.** | | | `--api-base-url` | API base URL override (local testing only). | | All flags have matching environment variables so the command works cleanly in CI: ```bash theme={"theme":"kanagawa-wave"} export UNKEY_ROOT_KEY=unkey_xxx export UNKEY_PROJECT=my-project export UNKEY_APP=api unkey deploy ghcr.io/your-org/your-app:v1.0.0 --env=production ``` ### Git auto-detection If you run `unkey deploy` inside a Git working tree, the CLI auto-fills `--branch` and `--commit` from the current repository. Set the flags explicitly if you want to override what it detects (for example, when deploying from a CI runner with a detached HEAD). Dirty working trees are recorded with a `(dirty)` marker in the source info but still deploy. ## Deploy from GitHub Actions A typical CI flow builds the image, pushes it to a registry, then calls `unkey deploy`: ```yaml theme={"theme":"kanagawa-wave"} name: Deploy on: push: branches: [main] jobs: deploy: runs-on: ubuntu-24.04 permissions: contents: read packages: write steps: - uses: actions/checkout@v4 - uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - uses: docker/build-push-action@v5 with: context: . push: true tags: ghcr.io/${{ github.repository }}:${{ github.sha }} - name: Install Unkey CLI run: npm install -g unkey - name: Deploy to Unkey env: UNKEY_ROOT_KEY: ${{ secrets.UNKEY_ROOT_KEY }} run: | unkey deploy ghcr.io/${{ github.repository }}:${{ github.sha }} \ --project=my-project \ --app=api \ --env=production \ --commit=${{ github.sha }} \ --branch=${{ github.ref_name }} ``` Store your root key as a repository secret (`UNKEY_ROOT_KEY`) with permission to deploy to the target project. ## What happens after you run the command The deployment follows the same [lifecycle](/docs/build-and-deploy/deployments) as a GitHub-triggered deployment, with one difference: the **Building image** step is skipped because Unkey uses the image you supplied as-is. 1. Deployment record created with status **Pending**. 2. Image pulled from the registry. 3. Containers scheduled across your configured [regions](/docs/build-and-deploy/regions). 4. Health checks run (if configured in [app settings](/docs/platform/apps/settings)). 5. Domains assigned and routes updated. 6. Status moves to **Ready**. The CLI exits with a non-zero status if the deployment fails, so CI jobs fail loudly when something goes wrong. ## Troubleshoot | Symptom | Likely cause | | -------------------------------------------- | -------------------------------------------------------------------------------------------------- | | `docker image is required` | You called `unkey deploy` without a positional image argument. | | `--project is required` | Pass `--project` or set `UNKEY_PROJECT`. | | `--root-key is required` | Pass `--root-key` or set `UNKEY_ROOT_KEY`. | | Deployment fails during image pull | The registry is private or the image tag does not exist. Verify the image is pullable publicly. | | Deployment reaches **Ready** but returns 5xx | Check runtime logs in the dashboard, usually a crash on startup or a missing environment variable. | ## Next steps When to pick the CLI over the GitHub integration How deployments progress from create to serving traffic Configure regions, health checks, and variables Install the CLI and authenticate # CLI vs GitHub integration Source: https://unkey.com/docs/build-and-deploy/cli-vs-github Choose between deploying with `unkey deploy` and the GitHub integration. Understand the trade-offs in build control, automation, and CI/CD flexibility. Deploying applications on Unkey is in public beta. To try it, open the product switcher in the top-left of the dashboard and select **Deploy**. During beta, deployed resources are free. We're eager for feedback, so let us know what you think on [Discord](https://unkey.com/discord), [X](https://x.com/unkeydev), or email [support@unkey.com](mailto:support@unkey.com). Unkey gives you two first-class ways to ship code: connect a [GitHub repository](/docs/build-and-deploy/github) and let Unkey build and deploy on every push, or build the image yourself and push it with [`unkey deploy`](/docs/build-and-deploy/cli). Both produce the same deployment artifact, an immutable version running in an [environment](/docs/environments/overview) with a commit-scoped domain. The difference is who owns the build. This page helps you pick the right path. ## TL;DR You want zero-config, push-to-deploy. You're happy letting Unkey handle image builds on every commit. This is the right default for 95% of apps. You already have a CI/CD pipeline, you need control over the build (monorepo, custom build matrix, pre-release artifacts), or you don't want to connect a GitHub repository. ## Side-by-side | Capability | GitHub integration | CLI (`unkey deploy`) | | ----------------------------------------- | --------------------------------------------------------------------------------------------------- | ----------------------------------------------------------- | | **Who builds the image** | Unkey, using your `Dockerfile` on every push | You (locally, in CI, or wherever), Unkey pulls a tag | | **Trigger** | `git push` (webhook) | You run a command | | **Repository required** | Yes, connected to the Unkey GitHub App | No | | **Preview per branch** | Automatic on every non-default branch | You call `--env=preview` (or any environment) explicitly | | **Build logs** | Streamed to the dashboard in real time | Wherever you run the build (your CI logs, your terminal) | | **Build concurrency quota** | Counts against workspace [`max_concurrent_builds`](/docs/build-and-deploy/deployments#build-concurrency) | Does not count, the build happens outside Unkey | | **Watch paths** (skip irrelevant changes) | Supported, configure in app settings | Not applicable, you decide when to call the CLI | | **Fork protection** | Built-in: PRs from forks require approval | Not applicable | | **Superseded cancellation** | Queued builds auto-cancel when a newer commit lands | Not applicable, you control cadence | | **Works without GitHub** | No | Yes (any Git provider, or no Git at all) | | **Reproducible local deploys** | Indirect (push, then wait) | Direct (run the same command you'd run in CI) | | **Ideal for** | Product apps, preview URLs per PR, fast iteration | CI-driven release pipelines, monorepos, custom build graphs | ## When to pick the GitHub integration Connect a repository when you want push-to-deploy with no additional infrastructure: * **Product-style apps** where every PR should get its own preview URL automatically. * **Small teams** that don't want to run or maintain a separate CI pipeline for builds. * **Fast iteration** during early development, you push, Unkey deploys, done. * You need [fork protection](/docs/build-and-deploy/github#fork-protection) for open-source repositories. * You want [watch paths](/docs/build-and-deploy/github#watch-paths) to skip builds for doc-only changes. Setup is three clicks in the dashboard: install the GitHub App, select a repository, click **Deploy**. See [GitHub integration](/docs/build-and-deploy/github) for the full walkthrough. ## When to pick the CLI Reach for `unkey deploy` when you need control over the build step, or when connecting a repository isn't an option: ### You already have a CI/CD pipeline If you're building images in GitHub Actions, GitLab CI, CircleCI, Buildkite, or anything else, you've likely invested in: * **Custom build steps**: code generation, compilation, asset bundling, SBOM generation, vulnerability scans. * **Build caching**: layer caches, dependency caches, test fixtures. * **Test gates**: unit, integration, and end-to-end tests that must pass before shipping. * **Artifact signing**: Cosign, Sigstore, or organization-internal signing. Rebuilding the same image on Unkey's side is wasted time and compute. The CLI lets your existing pipeline produce the image once and then deploy it: ```bash theme={"theme":"kanagawa-wave"} # In your CI, after the image is built and pushed: unkey deploy $REGISTRY/$IMAGE:$SHA --project=api --env=production ``` ### You want deploys decoupled from Git pushes Some teams don't want every `main` push to ship to production: * **Release trains** where pushes go to a staging environment and a separate job promotes to production. * **Manual release gates** that require approval outside GitHub. * **Scheduled deploys** triggered by cron, not by commits. With the CLI, pushes to `main` don't automatically deploy. Deploys happen when something (a workflow, a human, a scheduler) explicitly runs `unkey deploy`. ### You don't use GitHub The CLI only needs a Docker image in a registry. That works with GitLab, Bitbucket, self-hosted Git, or no Git at all. ### You want reproducible local deploys Running `unkey deploy` from your laptop ships the exact same artifact you'd ship in CI. Useful for debugging a deployment issue end-to-end without burning CI minutes. ## Next steps Step-by-step guide for `unkey deploy`, including a GitHub Actions example Set up push-to-deploy from a connected repository # Deployment lifecycle Source: https://unkey.com/docs/build-and-deploy/deployments Understand the Unkey deployment lifecycle from queuing and building to deploying and domain assignment before your app serves traffic. Deploying applications on Unkey is in public beta. To try it, open the product switcher in the top-left of the dashboard and select **Deploy**. During beta, deployed resources are free. We're eager for feedback, so let us know what you think on [Discord](https://unkey.com/discord), [X](https://x.com/unkeydev), or email [support@unkey.com](mailto:support@unkey.com). A deployment represents a specific version of your app running in an [environment](/docs/environments/overview). Each deployment is immutable: once created, its image, configuration, and variables don't change. ## From push to live Every deployment follows the same steps, whether triggered by a GitHub push, the CLI, or the dashboard. Deployment progress Deployment progress Unkey receives a trigger: a GitHub webhook from a push, a CLI `unkey deploy` command, or a manual deployment from the dashboard. The deployment record is created with status **Pending**. The deployment enters the build queue. Unkey checks if a build slot is available for your workspace (see [Build concurrency](#build-concurrency) below); if all slots are busy the deployment waits here until capacity frees up. Production pushes always get a slot immediately, they never wait behind preview builds. If you deployed a pre-built image via the CLI, this step is skipped. Unkey fetches your source code from GitHub (using the commit SHA from the push) and runs `docker build` with your Dockerfile on remote build infrastructure. Build logs stream to the **Deployments** tab in real time. If the build fails (missing dependencies, Dockerfile errors), the deployment moves to **Failed** and stops here. The built image is scheduled across your configured [regions](/docs/build-and-deploy/regions). Unkey creates the requested number of instances in each region and waits for them to start. If you configured a [health check](/docs/platform/apps/settings#health-check), Unkey sends requests to your health endpoint and waits for healthy responses before proceeding. Unkey generates domains for the deployment: * **Commit domain**: a permanent URL tied to this specific commit * **Branch domain**: points to the latest deployment from this branch * **Environment domain**: points to the latest deployment in this environment * **Live domain** (production only): points to the current live deployment See [Wildcard domains](/docs/networking/wildcard-domains) for the full naming pattern. Routes are configured and traffic begins flowing to the new deployment. The deployment status moves to **Ready**. The previous deployment stays running as part of [instant rollbacks](#instant-rollbacks), giving you a window to roll back instantly if needed. ## Instant rollbacks When a new production deployment goes live, the previous deployment isn't torn down immediately. It stays running so you can [roll back](/docs/build-and-deploy/rollbacks) instantly if something goes wrong. No rebuild, no container startup, just an immediate domain reassignment back to the known-good version. The previous deployment stays running for 30 minutes after a new version takes over. During this window, Unkey reassigns the domains and traffic switches over in seconds. Since the previous deployment is no longer receiving traffic, it will scale down according to your [autoscaling](/docs/build-and-deploy/regions) rules to the configured minimum. After 30 minutes, the previous deployment spins down entirely. Rolling back to it is still possible after that, but takes longer because the containers need to start up again. ## Preview idle scaling Preview deployments stay running as long as they receive traffic. A preview deployment only spins down after six consecutive hours with zero requests. This keeps costs low for branches that are no longer being actively reviewed. ## Build concurrency Each workspace has a maximum number of deployments that can build at the same time (`max_concurrent_builds`, defaults to 1). When the workspace is at capacity, new deployments queue in the **Pending** state until a running build finishes. **Production gets priority, not a free pass.** Production pushes count against `max_concurrent_builds` like any other build, but they jump to the head of the queue: a hot-fix won't wait behind a backlog of preview builds, but it also won't blow past your concurrency quota. Multiple deployments within the same environment can build in parallel, there is no per-environment serialization on the build path. The only serialization gate is the workspace-wide quota. ## Superseded deployments If you push multiple commits to the same branch in quick succession, Unkey cancels older deployments **that are still queued** (haven't acquired a build slot yet) in favour of the newest one. Once a deployment starts building, it's committed, it will run to completion even if a newer commit lands on the same branch. This avoids the pathological case where rapid pushes keep cancelling builds and nothing ever finishes. You see this in the dashboard as **Superseded**, the deployment shows how far it got (it usually didn't get past the queued step) and why it stopped ("A newer commit on this branch replaced this deployment"). This is the default behaviour. Rapid pushes don't burn build minutes on commits you're never going to ship. ## Cancel a deployment You can manually abort an in-progress deployment from its detail page, click the small **Cancel deployment** button next to the page title. The cancellation stops the active build or deploy step immediately, marks the deployment as cancelled, and frees the build slot so the next queued deployment can start. Cancel is available while the deployment is still in any non-terminal state (pending, starting, building, deploying, network, finalizing, awaiting approval). Once a deployment is ready, failed, or already cancelled, the button disappears. ## Troubleshoot failed deployments When a deployment fails, check the build and runtime logs in the **Deployments** tab. Common causes: * Dockerfile syntax errors or missing dependencies during the build step * Application crash on startup (check the runtime logs) * Health check failures if your app doesn't respond on the configured port Fix the issue in your code and push again to trigger a new deployment. ## Next steps Revert to a previous deployment or promote a specific version How Unkey builds your container images # GitHub integration Source: https://unkey.com/docs/build-and-deploy/github Connect your GitHub repository for automatic deployments on every push. Configure branch mapping, fork protection, and deploy triggers. Deploying applications on Unkey is in public beta. To try it, open the product switcher in the top-left of the dashboard and select **Deploy**. During beta, deployed resources are free. We're eager for feedback, so let us know what you think on [Discord](https://unkey.com/discord), [X](https://x.com/unkeydev), or email [support@unkey.com](mailto:support@unkey.com). When you connect a GitHub repository to your project, Unkey deploys your application automatically on every push. No CI/CD pipeline needed. For the initial setup walkthrough, see [Deploy your first app](/docs/quickstart/deploy). If you'd rather build your own image in CI and push it to Unkey, see [Deploy with the CLI](/docs/build-and-deploy/cli) and the [CLI vs GitHub integration](/docs/build-and-deploy/cli-vs-github) comparison. ## Branch-to-environment mapping Unkey maps branches to environments based on your default branch: | Branch | Environment | | --------------------------------- | ----------- | | Default branch (typically `main`) | Production | | All other branches | Preview | Pushes to the default branch deploy to the production environment. Pushes to any other branch deploy to a preview environment. Custom branch-to-environment mapping is not yet available. The default branch always maps to production. ## Commit-level deployments Every push creates an immutable deployment with its own unique domain: ```text theme={"theme":"kanagawa-wave"} --git--.unkey.app ``` This domain never changes, even as newer deployments go live. You can use commit domains to test a specific version or share a link to a particular build. In addition to the commit domain, Unkey assigns sticky domains that follow the latest deployment: * **Branch domain**: points to the latest deployment from that branch * **Environment domain**: points to the latest deployment in that environment * **Live domain** (production only): points to the current live deployment See [Wildcard domains](/docs/networking/wildcard-domains) for the full domain naming pattern. ## Fork protection Pull requests from forked repositories are not deployed automatically. A forked PR could modify your Dockerfile or application code to extract environment variable secrets during the build or at runtime. When a PR comes from a fork, Unkey requires an authorized team member to approve the deployment before it runs. This is similar to how Vercel handles fork deployments. ### Deploy from a fork manually Once you've reviewed the contributor's changes, you can ship them to a preview environment from the **Create deployment** dialog in the dashboard. The reference field accepts: | Reference | What it deploys | | ------------------------------------------------- | ---------------------------------------------------------------- | | `main`, `feature/login` (any branch name) | Latest commit on that branch in the connected repo | | Full 40-character commit SHA | The exact commit (in the connected repo or a fork) | | `fork-owner:branch-name` | A branch on a contributor's fork (GitHub's standard fork syntax) | | `https://github.com///tree/` | A branch in the connected repo or a fork | | `https://github.com///commit/` | A specific commit in the connected repo or a fork | | `https://github.com///pull/` | The current head commit of a pull request, including from forks | When you paste a fork reference or a PR URL from a fork, the dialog shows a **Deploying from fork** indicator with the source repository so you can confirm what you're shipping before submitting. Unkey enforces a guardrail on the source: the repository name (the part after the `/`) must match the connected repository. For example, if your project is connected to `acme/api`, you can deploy from `contributor/api` but not from `contributor/some-other-repo`. Pull request URLs are validated the same way. Manual fork deployments always go to a preview environment — they never deploy to production, regardless of the branch name on the fork. ## Watch paths By default, every push triggers a deployment. If your repository contains code that doesn't affect your app (for example, documentation or unrelated services in a monorepo), configure watch paths to deploy only when relevant files change. Add glob patterns in your app's **Settings** under **Watch paths**. Unkey skips the deployment if none of the changed files match any pattern. Watch paths settings Watch paths settings ## Auto deploy Auto deploy is on by default for both production and preview. Turn it off per environment when you want to ship manually instead of on every push. When auto deploy is disabled for an environment, Unkey records the push as a **Skipped** deployment so you can see it happened, but no build runs. You can still deploy on demand from the dashboard, or build and push your image with the [CLI](/docs/build-and-deploy/cli). Configure auto deploy in your app's **Settings** under **Build settings → Auto deploy**. See [App settings](/docs/platform/apps/settings#auto-deploy) for the full reference. ## Troubleshoot failed triggers If a push doesn't trigger a deployment: 1. Verify the Unkey GitHub App is installed on the repository's organization or account. 2. Check that the repository is connected in your project's **Settings** tab. 3. Confirm [auto deploy](#auto-deploy) is enabled for the target environment. A disabled environment records the push as a skipped deployment. 4. If watch paths are configured, confirm that the push includes changes matching at least one pattern. 5. Check your project's **Deployments** tab for a failed deployment with error details. ## Next steps How deployments progress from build to serving traffic When to build your own images and push with the CLI instead Configure build context, Dockerfile path, and watch paths # Overview Source: https://unkey.com/docs/build-and-deploy/overview Build, deploy, and serve your applications on Unkey infrastructure. Learn about the deployment pipeline, regions, and rollback options. Deploying applications on Unkey is in public beta. To try it, open the product switcher in the top-left of the dashboard and select **Deploy**. During beta, deployed resources are free. We're eager for feedback, so let us know what you think on [Discord](https://unkey.com/discord), [X](https://x.com/unkeydev), or email [support@unkey.com](mailto:support@unkey.com). Unkey takes your repository and gives you a running application with automatic domains, multi-region routing, rollbacks, and built-in API security through [Sentinel](/docs/platform/sentinel/overview). ## What you get Every deployment gets its own URL. Branch, environment, and commit-level domains are assigned automatically. Deploy to multiple [regions](/docs/build-and-deploy/regions) and route traffic to the nearest healthy location. Previous deployments stay running so [rollbacks](/docs/build-and-deploy/rollbacks) are instant. No rebuild required. API key authentication, rate limiting, and IP rules run in front of your app before requests reach it. Stream build and runtime logs in real time from the dashboard. Every branch gets its own isolated [environment](/docs/environments/overview) with separate variables and domains. ## Requirements * A **GitHub repository** for automatic deployments, or a pre-built Docker image for CLI deploys. See [builds](/docs/builds/overview) for how your source becomes an image. ## Get started The fastest path is connecting a GitHub repository: 1. Create a [project](/docs/platform/projects/overview) in your workspace. 2. Click **Import from GitHub** and select your repository. 3. Review build settings (root directory, Dockerfile path, watch paths). 4. Click **Deploy**. Unkey builds your image, deploys it to your configured regions, and assigns domains. See [GitHub integration](/docs/build-and-deploy/github) for the full walkthrough with screenshots. Deployment progress Deployment progress ## Three ways to deploy Connect a repository and deploy automatically on every push. Default branch goes to production, other branches go to preview. Deploy a pre-built Docker image with `unkey deploy`. Useful for CI/CD pipelines and custom build systems. Trigger a deployment manually from a branch, commit, pull request URL, or [fork](/docs/build-and-deploy/github#deploy-from-a-fork-manually). Create deployment dialog Create deployment dialog ## Next steps Full setup walkthrough with screenshots Ship pre-built Docker images from CI or your machine Pick the right path for your team How deployments progress from build to serving traffic Configure where your app runs Build, runtime, and Sentinel configuration # Regions Source: https://unkey.com/docs/build-and-deploy/regions Deploy your application to multiple regions on Unkey and route traffic to the nearest location. Reduce latency with multi-region deployments. Deploying applications on Unkey is in public beta. To try it, open the product switcher in the top-left of the dashboard and select **Deploy**. During beta, deployed resources are free. We're eager for feedback, so let us know what you think on [Discord](https://unkey.com/discord), [X](https://x.com/unkeydev), or email [support@unkey.com](mailto:support@unkey.com). Unkey deploys your application to one or more geographic regions. Traffic routes to the nearest healthy region automatically, reducing latency for your users. ## Available regions | Region | Location | Provider | | -------------- | ------------------ | -------- | | `us-east-1` | N. Virginia, US | AWS | | `us-west-2` | Oregon, US | AWS | | `eu-central-1` | Frankfurt, Germany | AWS | Need a region that isn't listed? Reach out on [Discord](https://unkey.com/discord) or email [support@unkey.com](mailto:support@unkey.com) to request it. ## Configure regions Select regions for your app in the **Settings** tab under **Runtime settings > Regions**. You must select at least one region. Production and preview environments can use different region configurations. For example, you might deploy production to all three regions for global coverage while keeping preview deployments in a single region to reduce cost. Region settings Region settings ## Instances per region Each region runs one or more instances of your app. Configure the autoscaling range in **Settings > Runtime settings > Instances** as a minimum and maximum replica count. The default is one instance per region (`1 – 1`). When the minimum and maximum differ, Unkey scales each region between those bounds based on CPU load. Setting both values to the same number pins the instance count and disables autoscaling. Running multiple instances in a region provides redundancy. If one instance fails, traffic routes to the remaining healthy instances. During the beta, the maximum is four instances per region. Contact [support@unkey.com](mailto:support@unkey.com) if you need more. ## Traffic routing Unkey routes incoming requests to the nearest healthy region based on the client's location. If a region becomes unhealthy (all instances failing health checks), traffic reroutes to the next nearest region. ## Add or remove regions Changing your region configuration takes effect on the next deployment. Existing deployments continue running in their original regions until replaced. To update regions: 1. Open your project's **Settings** tab. 2. Under **Runtime settings**, add or remove regions. 3. Save the changes. 4. Trigger a new deployment to apply the updated configuration. ## Next steps Configure instances, CPU, memory, storage, and other runtime settings Serve your app from your own domain # Rollbacks & promotions Source: https://unkey.com/docs/build-and-deploy/rollbacks Revert to a previous deployment or promote a specific version to production with zero downtime. Manage rollbacks and promotions in Unkey. Deploying applications on Unkey is in public beta. To try it, open the product switcher in the top-left of the dashboard and select **Deploy**. During beta, deployed resources are free. We're eager for feedback, so let us know what you think on [Discord](https://unkey.com/discord), [X](https://x.com/unkeydev), or email [support@unkey.com](mailto:support@unkey.com). When a deployment causes issues, you can roll back to a previous version instantly. Rollbacks switch traffic atomically, so there's no downtime during the transition. ## Roll back a deployment 1. Open your project's **Deployments** tab. 2. Click the three-dot menu on the deployment you want to revert to. 3. Select **Rollback**. Deployment context menu Deployment context menu 4. Review the current deployment, affected domains, and target deployment in the confirmation dialog. 5. Click **Rollback to target version**. Rollback confirmation dialog Rollback confirmation dialog Unkey reassigns the environment and branch domains to point to the target deployment immediately. The previous deployment stays running until routes are fully switched, so no requests are dropped. ## What happens after a rollback After a rollback, the affected environment enters a rolled-back state. This only applies to the environment where you triggered the rollback (for example, production). Other environments are unaffected. In a rolled-back environment: * The target deployment is serving traffic. * New pushes to the branch still create deployments, but domains don't get reassigned to them automatically. * The dashboard indicates that the environment is running a rolled-back version. This prevents a new push from undoing your rollback. You stay on the rolled-back version until you explicitly promote a deployment. ## Promote a deployment Promoting a deployment reassigns domains to point to it and restores normal behavior. After promoting, the next successful deployment from a push automatically receives traffic again. 1. Open your project's **Deployments** tab. 2. Click the three-dot menu on the deployment you want to promote. 3. Select **Promote**. ## Rolling forward vs rolling back | Approach | When to use | | ---------------- | ---------------------------------------------------------------------------- | | **Roll back** | The issue is urgent and you need to restore service immediately | | **Roll forward** | The fix is quick, and you'd rather push a new deployment with the correction | Rolling back is faster since it reuses an existing, known-good deployment. Rolling forward (pushing a fix) creates a new deployment and goes through the full build and deploy pipeline. In production, previous deployments stay running for 30 minutes after a new version goes live, then spin down. In preview environments, deployments stay running until they've been idle (zero requests) for six hours. Rolling back to a running deployment is instant. Rolling back to a spun-down deployment is still possible, but takes longer because the containers need to start up again. See [instant rollbacks](/docs/build-and-deploy/deployments#instant-rollbacks) for details. ## Next steps How deployments progress from build to serving traffic How Unkey builds your container images # Dockerfile builds Source: https://unkey.com/docs/builds/dockerfile Take full control over your container image with a Dockerfile. Configure the build context, use build-time secrets, and understand build caching. Deploying applications on Unkey is in public beta. To try it, open the product switcher in the top-left of the dashboard and select **Deploy**. During beta, deployed resources are free. We're eager for feedback, so let us know what you think on [Discord](https://unkey.com/discord), [X](https://x.com/unkeydev), or email [support@unkey.com](mailto:support@unkey.com). By default Unkey [builds your application automatically](/docs/builds/overview); you don't need a Dockerfile. When your build needs more control, for example system packages, custom build steps, or a toolchain detection doesn't cover, add a Dockerfile and Unkey builds with it instead. If you're not comfortable writing a Dockerfile from scratch, the [AI Dockerfile prompt](/docs/builds/dockerfile-prompt) generates one tuned for Unkey's runtime. ## Switching to Dockerfile builds Configure the Dockerfile path in your [app settings](/docs/platform/apps/settings#dockerfile). Unkey auto-detects Dockerfiles in your build context and offers them in the dropdown. Once a path is set, every deployment of that app and environment builds with the Dockerfile. To switch back to automatic builds, set the Dockerfile field to **Automatic (no Dockerfile)**. Changes apply on the next deployment. ## Build configuration | Setting | Description | Default | | ------------------ | ------------------------------------------------------------------- | --------------- | | **Root directory** | The build context directory, where `COPY` and `ADD` are relative to | `.` (repo root) | | **Dockerfile** | Path to the Dockerfile within the root directory | Automatic | See [App settings](/docs/platform/apps/settings#build-settings) for the full reference. ## Build-time environment variables Some builds need access to environment variables during the build step, for example to install private packages or generate code. All [variables](/docs/platform/variables/overview) configured for the environment are available as Docker build secrets. Unkey mounts all your variables as a `.env` file at `/run/secrets/.env` inside the build container. To use them, add a `--mount=type=secret` flag to the `RUN` step that needs them. Unkey exposes a build argument called `UNKEY_SECRETS_ID` that you reference as the mount's `id` (see [Variable changes and the build cache](#variable-changes-and-the-build-cache)). Here's a complete example for a Node.js app that needs environment variables during the build step: ```dockerfile Dockerfile theme={"theme":"kanagawa-wave"} FROM node:lts-alpine AS builder ARG UNKEY_SECRETS_ID WORKDIR /app COPY . . RUN npm install # Mount the secrets file and load variables into the shell RUN --mount=type=secret,id=${UNKEY_SECRETS_ID},target=/run/secrets/.env \ set -a && . /run/secrets/.env && set +a && \ pnpm build FROM node:lts-alpine WORKDIR /app COPY --from=builder /app . CMD ["node", "dist/index.js"] ``` Declare `ARG UNKEY_SECRETS_ID` inside every stage that mounts the secret. ARG values aren't inherited across stages, so each `FROM ... AS ` that consumes variables needs its own declaration. The secrets file uses standard `.env` syntax, one variable per line: ```bash /run/secrets/.env theme={"theme":"kanagawa-wave"} DATABASE_URL=postgres://prod-db.acme.com/api NPM_TOKEN=npm_abc123 FEATURE_FLAG=true ``` The `set -a && . /run/secrets/.env && set +a` pattern loads every variable from the file into the shell environment for that `RUN` step. The secret file is not persisted in the final image. How you consume the file depends on your toolchain: * **Tools that expect environment variables** (npm, pip, go): use the `set -a` pattern above to load them into the shell. * **Frameworks that read `.env` files directly** (Node.js with dotenv, Ruby with dotenv): reference `/run/secrets/.env` in your build script instead of sourcing it. ### Why secret mounts instead of ARG or ENV? Docker's `ARG` and `ENV` instructions are stored in the image layer history. Anyone with access to the image can extract them with `docker history` or `docker inspect`. Secret mounts avoid this problem: the file is available only during the `RUN` step and is never written to a layer. ### Variable changes and the build cache `UNKEY_SECRETS_ID` is a hash of your project's variables. When you write `id=${UNKEY_SECRETS_ID}` on the mount line, BuildKit's cache key for that `RUN` includes the id, so any variable change produces a new id and a fresh execution of that step and everything after it in the same stage. Steps before the secret mount (base image pulls, dependency installs) keep caching normally. ## Troubleshoot Dockerfile builds When a build fails, check the build logs in the **Deployments** tab. Common issues: * **Missing Dockerfile**: Verify the Dockerfile path in your [app settings](/docs/platform/apps/settings#dockerfile). * **Dependency installation failures**: Check that your base image includes the tools your Dockerfile needs. * **Context too large**: If your build context includes large files (node\_modules, data files), add a `.dockerignore` to exclude them. ## Next steps A prompt that produces a Dockerfile tuned for Unkey's runtime Configure the Dockerfile path, root directory, and watch paths # Generate a Dockerfile with AI Source: https://unkey.com/docs/builds/dockerfile-prompt Use this prompt with Claude Code, Cursor, or Windsurf to generate a Dockerfile and .dockerignore tuned for Unkey's runtime. Deploying applications on Unkey is in public beta. To try it, open the product switcher in the top-left of the dashboard and select **Deploy**. During beta, deployed resources are free. We're eager for feedback, so let us know what you think on [Discord](https://unkey.com/discord), [X](https://x.com/unkeydev), or email [support@unkey.com](mailto:support@unkey.com). If you're not comfortable writing a Dockerfile from scratch, hand the prompt below to a coding agent. It tells the agent to first inspect your repository and then produce a Dockerfile plus a `.dockerignore` that respect Unkey's runtime constraints (reading `PORT`, handling `SIGTERM`, build secrets via mount). ## How to use it Open your repository in your coding agent of choice (Claude Code, Cursor, Windsurf, Codex, or similar), start a fresh conversation, and paste the prompt verbatim. The agent will ask clarifying questions if anything about your project is ambiguous (for example, which app in a monorepo you're deploying) before writing any files. Once the files land in your repo, push to the branch connected to your Unkey project. Review the [Dockerfile path](/docs/platform/apps/settings#dockerfile) and [root directory](/docs/platform/apps/settings#root-directory) settings in your app so the build picks the right context. ## The prompt ``` You're helping create a Dockerfile for an application that will be deployed on Unkey (https://unkey.com/docs/build-and-deploy/overview). Before writing anything, inspect the repository so the Dockerfile matches how the app is actually built and run. ## Rule: never assume, always ask If anything is ambiguous, stop and ask the user. Do not guess. This includes (but is not limited to): - Which app in a monorepo should be deployed. - Which Node/Python/Go/Bun/etc. version to target if the repo doesn't pin one. - Which package manager to use if multiple lockfiles exist. - Which command starts the app, if there are several plausible scripts. - Which port the app listens on, if it isn't clearly set from `PORT`. - Whether the build needs secrets, and which ones. - Whether the app writes to disk at runtime, and where. Ask one question at a time. Wait for the answer, then ask the next one. Batching several questions into a single message makes it hard for the user to answer each in detail, and they often miss or skip some. A wrong assumption here means the build succeeds but the deploy fails, and the user has to debug a running container to figure out why. ## Step 1: Inspect the repo Identify: - Language and runtime version (Node.js, Bun, Python, Go, Rust, Java, etc.). - Package manager and lockfile (npm, pnpm, yarn, poetry, uv, pip, cargo, go mod). - Build tool and output directory (next, vite, tsc, esbuild, turbo, go build, cargo build, etc.). - The start command and the port the app listens on. Check for `process.env.PORT`, `os.getenv("PORT")`, config files, or framework defaults. - Monorepo tooling (pnpm/npm/yarn workspaces, Turborepo, Nx, Lerna, Cargo workspaces, Go workspaces). If present, ask which app we're dockerizing and figure out which internal packages it depends on. - Any existing Dockerfile or container config to preserve intent from. ## Step 2: Respect Unkey's runtime constraints These are not general Docker guidance. The image will not run correctly without them. 1. Listen on the `PORT` environment variable (default 8080). Unkey injects `PORT` at startup. Do not hardcode a port. 2. Handle `SIGTERM` for graceful shutdown. Use the exec form for `CMD` (for example `CMD ["node", "dist/index.js"]`) so the app process is PID 1 and receives signals directly instead of a shell swallowing them. 3. Scratch and ephemeral storage: - `/tmp`: small, memory-backed scratch space, always available. - `/data`: available only if ephemeral storage is configured. The mount path is exposed in the `UNKEY_EPHEMERAL_DISK_PATH` env var. 4. Build-time secrets come from a mounted file, never `ARG` or `ENV` for the values themselves. If the build needs secrets (private package tokens, codegen against a real DB URL), consume them like this: ARG UNKEY_SECRETS_ID RUN --mount=type=secret,id=${UNKEY_SECRETS_ID},target=/run/secrets/.env \ set -a && . /run/secrets/.env && set +a && \ `UNKEY_SECRETS_ID` is a build argument Unkey injects. Declare `ARG UNKEY_SECRETS_ID` in every stage that mounts the secret; ARG values are not inherited across stages. `ARG` and `ENV` values for actual secret content are visible in `docker history` and leak into the final image. Don't use them that way. ## Step 3: Apply these best practices - Multi-stage build: one stage for dependencies and build, a minimal final stage with only the runtime artifacts. Keeps the image small and keeps build tools out of production. - Pin the base image to a specific version (for example `node:22-alpine`, not `node:latest`). - Cache dependency installs. Copy only the manifest files (`package.json`, lockfile, `go.mod`, `Cargo.toml`, etc.) first, install, then copy the rest of the source. Reordering kills the cache. - Prefer slim or distroless base images when the runtime is compatible. - Run as a non-root user (`USER node`, `USER nobody`, or a dedicated user you create). Non-root is cheap extra defense. - One foreground process. No supervisord, no `&`, no wrapper shell scripts that background the app. ## Step 4: Monorepo handling If the project is a monorepo: - Do not assume any folder convention. Monorepos put apps under `apps/`, `packages/`, `services/`, `cmd/`, or anywhere else. Read the repo's own workspace config (pnpm `workspace:`, npm/yarn `workspaces`, `turbo.json`, `nx.json`, `go.work`, `Cargo.toml` workspace members) to find the real paths. If more than one candidate fits what the user described, ask which one to deploy. - Ship only the target app and its workspace dependencies. Use the monorepo's pruning tool: `pnpm deploy`, `turbo prune`, Nx's `build --prod`, or equivalent. Do not COPY the whole monorepo into the image. - Place the Dockerfile next to the app you're deploying (wherever that actually lives in the repo), and keep the build context at the repo root so workspace packages resolve correctly. - In the summary, tell the user the exact **Root directory** and **Dockerfile** values to set in Unkey's app settings, using the real paths you used. ## Step 5: Write a .dockerignore Create a `.dockerignore` next to the Dockerfile's build context. Always exclude: .git .gitignore node_modules .next dist build out coverage .env .env.* *.log .DS_Store .vscode .idea README.md Dockerfile .dockerignore Add language- and framework-specific entries based on what you found (for example `target/` for Rust, `__pycache__/` and `.venv/` for Python, `vendor/` for Go if `go mod vendor` isn't used, `.turbo/` for Turborepo). A lean build context makes builds faster and prevents `.env` files or other local state from accidentally shipping in the image. ## Output Produce: 1. A `Dockerfile` at a path that matches the repo's real layout. Do not invent a folder structure. Place it next to the app you're deploying. 2. A matching `.dockerignore` at the correct build-context root. 3. A short note listing the exact values to configure in Unkey's app settings, using the real paths you used: - Root directory - Dockerfile path - Port (if the app doesn't listen on 8080) - Start command (only if the Dockerfile's `CMD` should be overridden at the platform level; usually leave this empty) ``` ## What to check before deploying After the agent finishes, skim the result for these specifics: The `CMD` instruction uses the exec form (JSON array, not a bare string), so your app receives `SIGTERM` directly when Unkey drains an instance. Any `RUN` step that needs a secret uses the `--mount=type=secret,id=${UNKEY_SECRETS_ID},target=/run/secrets/.env` pattern with `ARG UNKEY_SECRETS_ID` declared in the same stage, rather than `ARG` or `ENV` for the values themselves. The base image is pinned to a specific version and your final stage is slim enough that it doesn't carry the whole build toolchain. If any of those are wrong, ask the agent to fix them directly. The prompt is short enough to tweak and re-run if your project has an unusual shape. ## Related How Unkey turns your Dockerfile into a container image, including build-time secrets and caching. Configure the Dockerfile path, root directory, port, and runtime options that pair with this Dockerfile. # Builds Source: https://unkey.com/docs/builds/overview Learn how Unkey turns your source code into a container image. Push your repo and Unkey detects your language, installs dependencies, and builds automatically. Deploying applications on Unkey is in public beta. To try it, open the product switcher in the top-left of the dashboard and select **Deploy**. During beta, deployed resources are free. We're eager for feedback, so let us know what you think on [Discord](https://unkey.com/discord), [X](https://x.com/unkeydev), or email [support@unkey.com](mailto:support@unkey.com). A build turns your source code into a container image that Unkey can deploy. Every deployment starts with a build, whether triggered by a GitHub push, the CLI, or the dashboard. ## How builds work Unkey builds your application on remote build infrastructure. You don't need to write a Dockerfile, set up Docker locally, or manage build servers. The build process: 1. The build machine fetches your source code directly from GitHub. 2. Unkey detects your language, package manager, and framework, then installs dependencies and builds the image. 3. The resulting image is stored in Unkey's container registry. 4. The image is deployed to your configured [regions](/docs/build-and-deploy/regions). Build output streams to the **Deployments** tab in your project dashboard, so you can follow progress and diagnose failures in real time. The detection output appears at the top of the build logs, so you can see exactly which language and tool versions were chosen. ## Supported languages Builds support Node.js, Python, Go, PHP, Java, Rust, Ruby, Elixir, Deno, .NET, Gleam, C++, static sites, and shell scripts. Detection understands each ecosystem's standard files, for example `package.json` with npm, pnpm, bun, or yarn for Node.js, `go.mod` for Go, or `pyproject.toml` and `requirements.txt` for Python. ## Build configuration Most projects build with zero configuration. When you need to adjust where and how the build runs, configure it in the **Settings** tab of your project dashboard: | Setting | Description | Default | | ------------------ | ----------------------------------------------------------------------------------- | --------------- | | **Root directory** | The directory your application is built from | `.` (repo root) | | **Dockerfile** | Optional path to a [Dockerfile](/docs/builds/dockerfile) for full control over the image | Automatic | In a monorepo, point the [root directory](/docs/platform/apps/settings#root-directory) at the service you're deploying so detection picks up the right project. See [App settings](/docs/platform/apps/settings#build-settings) for the full reference. ## Pinning tool versions Versions are resolved from your repository's standard files: lockfiles, `engines` in `package.json`, version files like `.nvmrc` or `.python-version`, and language manifests like `go.mod`. To control which version your build uses, pin it in those files rather than relying on defaults. ## Variables and build secrets All [variables](/docs/platform/variables/overview) configured for the environment are available during the build automatically. Each variable is exposed to the build steps that need it, and the build cache is invalidated when a value changes. There is nothing to declare or mount. Secret values are never written into image layers or history, so they cannot be extracted from the image after the build. One constraint: variable names must be valid environment variable names (letters, digits, and underscores, not starting with a digit). A variable like `my.key` fails the build with a clear error because variables are exposed to your build as process environment variables. ## Runtime expectations Built images run under a simple contract: your application must listen on the `PORT` environment variable and should handle `SIGTERM` for graceful shutdown. The start command is inferred from your project, for example `npm start` or the compiled binary. If the inferred command is wrong, override it with the [command setting](/docs/platform/apps/settings#command). ## Build infrastructure Each build runs on a dedicated 16-CPU, 32 GB machine isolated to your project. No build shares resources with another project. By default, each workspace runs one build at a time. Additional pushes queue and run as the previous build finishes. This is a soft limit; contact [support@unkey.com](mailto:support@unkey.com) if you need a higher concurrency cap. ## Build caching Unkey caches build layers between builds. Subsequent builds reuse cached layers when possible, which significantly reduces build times for projects with stable dependencies. ## Need more control? Automatic builds cover the common paths. If your build needs system packages, custom build steps, or an exotic toolchain, add a [Dockerfile](/docs/builds/dockerfile) and Unkey builds with it instead. The [AI Dockerfile prompt](/docs/builds/dockerfile-prompt) gets you a solid starting point in minutes. ## Prebuilt images If you build images in your own CI/CD pipeline, you can skip the build step entirely and deploy a prebuilt image with the CLI: ```bash theme={"theme":"kanagawa-wave"} unkey deploy ghcr.io/acme/api:v1.0.0 --project=acme-platform ``` See the [CLI reference](/docs/cli/overview) for all available flags. ## Troubleshoot build failures When a build fails, check the build logs in the **Deployments** tab. Common issues: * **"Unkey could not determine how to build this app"**: detection didn't recognize the project layout. Check that the [root directory](/docs/platform/apps/settings#root-directory) points at your application, or add a [Dockerfile](/docs/builds/dockerfile) to take full control. * **Wrong language detected**: a stray manifest file (for example a leftover `composer.json`) can win detection over the ecosystem you expect. Remove it from the root directory, or add a Dockerfile. * **Wrong tool versions**: pin versions in your ecosystem's standard files as described above. * **Wrong start command**: override it with the [command setting](/docs/platform/apps/settings#command). ## Next steps What happens after the build completes Take full control over the image when you need it # get-verifications Source: https://unkey.com/docs/cli/analytics/get-verifications Run custom SQL queries against your key verification analytics data using the Unkey CLI. Export results for billing, reporting, or debugging. Execute custom SQL queries against your key verification analytics. Use this to inspect verification patterns, monitor outcomes, and build custom reports over your key usage data. Only `SELECT` queries are allowed. The response rows are dynamic -- fields vary based on your SQL `SELECT` clause and can include any combination of columns such as `time`, `outcome`, `count`, `key_id`, and more. For complete documentation including available tables, columns, data types, and query examples, see the schema reference in the API documentation. **Required permissions:** * `analytics.*.read_verifications` (to query verification analytics) See the [API reference](/docs/api-reference/analytics/query-key-verification-data) for the full HTTP endpoint documentation. ## Usage ```bash theme={"theme":"kanagawa-wave"} unkey api analytics get-verifications [flags] ``` ## Flags SQL `SELECT` query to execute against your analytics data. Only `SELECT` queries are allowed. The query runs against the `key_verifications_v1` table, which contains columns like `time`, `key_id`, `outcome`, and more. See the API reference for the full schema. ## Global Flags | Flag | Type | Description | | ------------ | ------ | -------------------------------------------------------- | | `--root-key` | string | Override root key (`$UNKEY_ROOT_KEY`) | | `--api-url` | string | Override API base URL (default: `https://api.unkey.com`) | | `--config` | string | Path to config file (default: `~/.unkey/config.toml`) | | `--output` | string | Output format. Use `json` for raw JSON | ## Examples ```bash Count valid verifications in the last 7 days theme={"theme":"kanagawa-wave"} unkey api analytics get-verifications --query="SELECT COUNT(*) as total FROM key_verifications_v1 WHERE outcome = 'VALID' AND time >= now() - INTERVAL 7 DAY" ``` ```bash Group by key and outcome theme={"theme":"kanagawa-wave"} unkey api analytics get-verifications --query="SELECT key_id, outcome, COUNT(*) as cnt FROM key_verifications_v1 GROUP BY key_id, outcome" ``` ```bash JSON output for scripting theme={"theme":"kanagawa-wave"} unkey api analytics get-verifications --query="SELECT outcome, COUNT(*) as total FROM key_verifications_v1 GROUP BY outcome" --output=json ``` ## Output Default output shows the request ID with latency, followed by the query results: ```text theme={"theme":"kanagawa-wave"} req_2c9a0jf23l4k567 (took 120ms) [ { "outcome": "VALID", "count": 1234, "time": 1696118400000 }, { "outcome": "RATE_LIMITED", "count": 56, "time": 1696118400000 } ] ``` With `--output=json`, the full response envelope is returned: ```json theme={"theme":"kanagawa-wave"} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "data": [ { "outcome": "VALID", "count": 1234, "time": 1696118400000 }, { "outcome": "RATE_LIMITED", "count": 56, "time": 1696118400000 } ] } ``` # create-api Source: https://unkey.com/docs/cli/apis/create-api Create an API namespace in Unkey using the CLI. Organize your keys by environment, service, or product with a single terminal command. 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:** * `api.*.create_api` (to create APIs in any workspace) See the [API reference](/docs/api-reference/apis/create-api-namespace) for the full HTTP endpoint documentation. ## Usage ```bash theme={"theme":"kanagawa-wave"} unkey api apis create-api [flags] ``` ## Flags Unique identifier for this API namespace within your workspace. Use descriptive names like `payment-service-prod` or `user-api-dev` to clearly identify purpose and environment. Must be 3-255 characters, start with a letter, and contain only letters, numbers, dots, hyphens, and underscores. ## Global Flags | Flag | Type | Description | | ------------ | ------ | -------------------------------------------------------- | | `--root-key` | string | Override root key (`$UNKEY_ROOT_KEY`) | | `--api-url` | string | Override API base URL (default: `https://api.unkey.com`) | | `--config` | string | Path to config file (default: `~/.unkey/config.toml`) | | `--output` | string | Output format. Use `json` for raw JSON | ## Examples ```bash Basic theme={"theme":"kanagawa-wave"} unkey api apis create-api --name=payment-service-prod ``` ```bash JSON output for scripting theme={"theme":"kanagawa-wave"} unkey api apis create-api --name=user-api-dev --output=json ``` ```bash Pipe the API ID to another command theme={"theme":"kanagawa-wave"} API_ID=$(unkey api apis create-api --name=my-api --output=json | jq -r '.data.id') unkey api keys create-key --api-id=$API_ID ``` ## Output Default output shows the request ID with latency, followed by the created API: ```text theme={"theme":"kanagawa-wave"} req_2c9a0jf23l4k567 (took 45ms) { "id": "api_1234abcd", "name": "payment-service-prod" } ``` With `--output=json`, the full response envelope is returned: ```json theme={"theme":"kanagawa-wave"} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "data": { "id": "api_1234abcd", "name": "payment-service-prod" } } ``` # delete-api Source: https://unkey.com/docs/cli/apis/delete-api Permanently delete an API namespace and all its associated keys using the Unkey CLI. This action invalidates every key and cannot be undone. 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:** * `api.*.delete_api` (to delete any API) * `api..delete_api` (to delete a specific API) See the [API reference](/docs/api-reference/apis/delete-api-namespace) for the full HTTP endpoint documentation. ## Usage ```bash theme={"theme":"kanagawa-wave"} unkey api apis delete-api [flags] ``` ## Flags Specifies which API namespace to permanently delete from your workspace. Must be a valid API ID that begins with `api_` and exists within your workspace. Before proceeding, ensure you have the correct API ID and understand that this action cannot be undone. If you need to migrate functionality, create replacement keys in a new API namespace and update client applications before deletion. ## Global Flags | Flag | Type | Description | | ------------ | ------ | -------------------------------------------------------- | | `--root-key` | string | Override root key (`$UNKEY_ROOT_KEY`) | | `--api-url` | string | Override API base URL (default: `https://api.unkey.com`) | | `--config` | string | Path to config file (default: `~/.unkey/config.toml`) | | `--output` | string | Output format. Use `json` for raw JSON | ## Examples ```bash Basic theme={"theme":"kanagawa-wave"} unkey api apis delete-api --api-id=api_1234abcd ``` ```bash JSON output for scripting theme={"theme":"kanagawa-wave"} unkey api apis delete-api --api-id=api_1234abcd --output=json ``` ```bash Delete after looking up by name theme={"theme":"kanagawa-wave"} API_ID=$(unkey api apis list-apis --output=json | jq -r '.data[] | select(.name=="old-service") | .id') unkey api apis delete-api --api-id=$API_ID ``` ## Output Default output shows the request ID with latency: ```text theme={"theme":"kanagawa-wave"} req_2c9a0jf23l4k567 (took 45ms) {} ``` With `--output=json`, the full response envelope is returned: ```json theme={"theme":"kanagawa-wave"} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "data": {} } ``` # get-api Source: https://unkey.com/docs/cli/apis/get-api Retrieve details about an API namespace using the Unkey CLI, including its ID, name, and associated workspace. Useful for scripting workflows. 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:** * `api.*.read_api` (to read any API) * `api..read_api` (to read a specific API) See the [API reference](/docs/api-reference/apis/get-api-namespace) for the full HTTP endpoint documentation. ## Usage ```bash theme={"theme":"kanagawa-wave"} unkey api apis get-api [flags] ``` ## Flags Specifies which API to retrieve by its unique identifier. Must be a valid API ID that begins with `api_` and exists within your workspace. ## Global Flags | Flag | Type | Description | | ------------ | ------ | -------------------------------------------------------- | | `--root-key` | string | Override root key (`$UNKEY_ROOT_KEY`) | | `--api-url` | string | Override API base URL (default: `https://api.unkey.com`) | | `--config` | string | Path to config file (default: `~/.unkey/config.toml`) | | `--output` | string | Output format. Use `json` for raw JSON | ## Examples ```bash Basic theme={"theme":"kanagawa-wave"} unkey api apis get-api --api-id=api_1234abcd ``` ```bash JSON output for scripting theme={"theme":"kanagawa-wave"} unkey api apis get-api --api-id=api_1234abcd --output=json ``` ```bash Verify an API exists before creating keys theme={"theme":"kanagawa-wave"} unkey api apis get-api --api-id=api_1234abcd && unkey api keys create-key --api-id=api_1234abcd ``` ## Output Default output shows the request ID with latency, followed by the API information: ```text theme={"theme":"kanagawa-wave"} req_2c9a0jf23l4k567 (took 45ms) { "id": "api_1234abcd", "name": "payment-service-prod" } ``` With `--output=json`, the full response envelope is returned: ```json theme={"theme":"kanagawa-wave"} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "data": { "id": "api_1234abcd", "name": "payment-service-prod" } } ``` # list-keys Source: https://unkey.com/docs/cli/apis/list-keys List all API keys in a namespace with the Unkey CLI. Retrieve paginated results including key IDs, metadata, and status for admin scripts. 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:** * `api.*.read_key` or `api..read_key` (to read keys) * `api.*.read_api` or `api..read_api` (to read the API) * `api.*.decrypt_key` or `api..decrypt_key` (additionally required when using `--decrypt`) See the [API reference](/docs/api-reference/apis/list-api-keys) for the full HTTP endpoint documentation. ## Usage ```bash theme={"theme":"kanagawa-wave"} unkey api apis list-keys [flags] ``` ## Flags The API namespace whose keys you want to list. Returns all keys in this API, subject to pagination and filters. Maximum number of keys to return per request. Balance between response size and number of pagination calls needed. Must be between 1 and 100. Pagination cursor from a previous response to fetch the next page. Use when `hasMore: true` in the previous response. Filter keys by external ID to find keys for a specific user or entity. Must exactly match the `externalId` set during key creation. When true, includes the plaintext key value in the response. Only works for keys created with `recoverable: true`. Requires the `decrypt_key` permission on the calling root key. Never enable this in user-facing applications. **Experimental.** Skip the cache and fetch keys directly from the database. Use this when you have just created a key and need to see it immediately, or when debugging cache consistency issues. This comes with a performance cost and should be used sparingly. ## Global Flags | Flag | Type | Description | | ------------ | ------ | -------------------------------------------------------- | | `--root-key` | string | Override root key (`$UNKEY_ROOT_KEY`) | | `--api-url` | string | Override API base URL (default: `https://api.unkey.com`) | | `--config` | string | Path to config file (default: `~/.unkey/config.toml`) | | `--output` | string | Output format. Use `json` for raw JSON | ## Examples ```bash List all keys in an API theme={"theme":"kanagawa-wave"} unkey api apis list-keys --api-id=api_1234abcd ``` ```bash Paginate with a custom limit theme={"theme":"kanagawa-wave"} unkey api apis list-keys --api-id=api_1234abcd --limit=50 ``` ```bash Filter keys by external ID theme={"theme":"kanagawa-wave"} unkey api apis list-keys --api-id=api_1234abcd --external-id=user_1234abcd ``` ```bash Paginate through all keys theme={"theme":"kanagawa-wave"} CURSOR="" while true; do RESULT=$(unkey api apis list-keys --api-id=api_1234abcd --limit=100 --output=json ${CURSOR:+--cursor=$CURSOR}) echo "$RESULT" | jq '.data[]' HAS_MORE=$(echo "$RESULT" | jq -r '.pagination.hasMore') [ "$HAS_MORE" != "true" ] && break CURSOR=$(echo "$RESULT" | jq -r '.pagination.cursor') done ``` ## Output Default output shows the request ID with latency, followed by the list of keys: ```text theme={"theme":"kanagawa-wave"} req_1234abcd (took 82ms) [ { "keyId": "key_1234abcd", "start": "sk_prod", "enabled": true, "name": "Production API Key", "createdAt": 1704067200000 }, { "keyId": "key_5678efgh", "start": "sk_dev", "enabled": true, "name": "Development Key", "createdAt": 1704153600000 } ] ``` With `--output=json`, the full response envelope including pagination is returned: ```json theme={"theme":"kanagawa-wave"} { "meta": { "requestId": "req_1234abcd" }, "data": [ { "keyId": "key_1234abcd", "start": "sk_prod", "enabled": true, "name": "Production API Key", "createdAt": 1704067200000 } ], "pagination": { "cursor": "key_1234abcd", "hasMore": true } } ``` # login Source: https://unkey.com/docs/cli/auth/login Authenticate the Unkey CLI by storing your root key in a local configuration file. Run this command once to enable all CLI operations. Authenticate the CLI by storing your root key locally. Use this before running other commands so you don't need to pass `--root-key` every time. The command prompts for your root key interactively (input is hidden) and saves it to `~/.unkey/config.toml` with restricted file permissions. You can create a root key from the [Unkey dashboard](/docs/platform/root-keys/overview). Your root key is stored in plaintext at `~/.unkey/config.toml`. Make sure only your user account can read this file. The CLI creates it with `0600` permissions by default. ## Usage ```bash theme={"theme":"kanagawa-wave"} unkey auth login ``` The command prompts you to enter your root key. Input is hidden for security: ```text theme={"theme":"kanagawa-wave"} Enter your root key: •••••••••••• Authentication successful. Key stored in /home/you/.unkey/config.toml ``` Once stored, all subsequent commands use this key automatically: ```bash theme={"theme":"kanagawa-wave"} # No --root-key needed unkey api keys create-key --api-id=api_1234abcd --name="My Key" ``` ## Overriding the stored key You can override the stored key on a per-command basis using the `--root-key` flag or the `UNKEY_ROOT_KEY` environment variable: ```bash theme={"theme":"kanagawa-wave"} # Flag takes highest priority unkey api apis get-api --api-id=api_1234abcd --root-key=unkey_xxx # Environment variable export UNKEY_ROOT_KEY=unkey_xxx unkey api apis get-api --api-id=api_1234abcd ``` The priority order is: `--root-key` flag > `UNKEY_ROOT_KEY` env var > `~/.unkey/config.toml`. ## Updating your key Run `unkey auth login` again to replace the stored key. The new key overwrites the previous one. # CLI authentication Source: https://unkey.com/docs/cli/authentication Store your root key locally so the Unkey CLI authenticates automatically. Configure credentials once and run commands without manual tokens. The `unkey auth login` command stores your [root key](/docs/platform/root-keys/overview) in a local config file so you don't have to pass `--root-key` on every CLI invocation. ## Prerequisites * An Unkey account with a [root key](/docs/platform/root-keys/overview) * The Unkey CLI installed (see [installation](#installation)) ## Installation Install the CLI with npm: ```bash theme={"theme":"kanagawa-wave"} npm install -g unkey ``` Or run it directly with npx: ```bash theme={"theme":"kanagawa-wave"} npx unkey auth login ``` ## Log in Run the `auth login` command and paste your root key when prompted: ```bash theme={"theme":"kanagawa-wave"} unkey auth login ``` ```text theme={"theme":"kanagawa-wave"} Enter your root key: **** Authentication successful. Key stored in ~/.unkey/config.toml ``` The key is read as hidden input, it won't be displayed in your terminal. ## How it works Your root key is saved to `~/.unkey/config.toml`. Once stored, other CLI commands (like `unkey deploy`) use it automatically instead of requiring a `--root-key` flag. ```toml ~/.unkey/config.toml theme={"theme":"kanagawa-wave"} root_key = "unkey_..." ``` The config file stores your root key in plain text. Make sure `~/.unkey/config.toml` is not committed to version control or shared. ## Best practices * **Use a dedicated root key for the CLI**: create a separate key with only the permissions your CLI workflows need. See [root key permissions](/docs/platform/root-keys/overview#create-a-root-key) for guidance. * **Don't commit the config file**: add `~/.unkey/` to your global gitignore or verify it's excluded from your project repositories. * **Rotate periodically**: run `unkey auth login` again with a new key to replace the stored one. ## Next steps Learn how to create and manage root keys Get started with Unkey for API development # create-identity Source: https://unkey.com/docs/cli/identities/create-identity Create an identity using the Unkey CLI to group multiple API keys under a single user, team, or organization entity in your workspace. 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:** External IDs must be unique within your workspace. Attempting to create an identity with a duplicate external ID returns a `409 Conflict` error. **Required permissions:** * `identity.*.create_identity` See the [API reference](/docs/api-reference/identities/create-identity) for the full HTTP endpoint documentation. ## Usage ```bash theme={"theme":"kanagawa-wave"} unkey api identities create-identity [flags] ``` ## Flags Your system's unique identifier for the user, organization, or entity. Must be unique across your workspace --- duplicate external IDs return a `409 Conflict` error. This identifier links Unkey identities to your authentication system, database records, or tenant structure. Accepts letters, numbers, underscores, dots, and hyphens (1--255 characters). JSON object of arbitrary metadata stored on the identity. This metadata is returned during key verification, eliminating additional database lookups for contextual information. Useful for subscription details, feature flags, user preferences, and organization information. Avoid storing sensitive data as it is returned in verification responses. Arbitrary key-value pairs. Values can be strings, numbers, booleans, arrays, or nested objects. Maximum of 100 properties. Keep total size under 10KB for optimal verification latency. JSON array of shared rate limit configurations for all keys under this identity. Rate limit counters are shared across all keys belonging to this identity, preventing abuse by users with multiple keys. Each named limit can have different thresholds and windows. Maximum of 50 rate limit configurations. The name of this rate limit, used to identify which limit to check during key verification. Use descriptive names like `api_requests`, `heavy_operations`, or `downloads`. Must be 3--128 characters. The maximum number of operations allowed within the specified time window. When reached, verification requests fail with `code=RATE_LIMITED` until the window resets. The duration for each rate limit window in milliseconds. Common values: `1000` (1 second), `60000` (1 minute), `3600000` (1 hour), `86400000` (24 hours). Minimum `1000`. Whether this rate limit should be automatically applied when verifying a key. Defaults to `false`. ## Global Flags | Flag | Type | Description | | ------------ | ------ | -------------------------------------------------------- | | `--root-key` | string | Override root key (`$UNKEY_ROOT_KEY`) | | `--api-url` | string | Override API base URL (default: `https://api.unkey.com`) | | `--config` | string | Path to config file (default: `~/.unkey/config.toml`) | | `--output` | string | Output format. Use `json` for raw JSON | ## Examples ```bash Basic theme={"theme":"kanagawa-wave"} unkey api identities create-identity --external-id=user_123 ``` ```bash With metadata theme={"theme":"kanagawa-wave"} unkey api identities create-identity --external-id=user_123 --meta-json='{"email":"alice@acme.com","name":"Alice Smith","plan":"premium"}' ``` ```bash With rate limits theme={"theme":"kanagawa-wave"} unkey api identities create-identity --external-id=user_123 --ratelimits-json='[{"name":"requests","limit":1000,"duration":60000,"autoApply":false}]' ``` ```bash JSON output for scripting theme={"theme":"kanagawa-wave"} unkey api identities create-identity --external-id=user_123 --output=json ``` ## Output Default output shows the request ID with latency, followed by the created identity: ```text theme={"theme":"kanagawa-wave"} req_01H9TQPP77V5E48E9SH0BG0ZQX (took 45ms) { "identityId": "id_1234567890abcdef" } ``` With `--output=json`, the full response envelope is returned: ```json theme={"theme":"kanagawa-wave"} { "meta": { "requestId": "req_01H9TQPP77V5E48E9SH0BG0ZQX" }, "data": { "identityId": "id_1234567890abcdef" } } ``` # delete-identity Source: https://unkey.com/docs/cli/identities/delete-identity Permanently delete an identity using the Unkey CLI. Remove the entity and unlink all associated API keys for cleanup or compliance purposes. Permanently delete an identity. This operation cannot be undone. Use this for data cleanup, compliance requirements, or when removing entities from your system. **Important:** * Associated API keys remain functional but lose shared resources * External ID becomes available for reuse immediately **Required permissions:** * `identity.*.delete_identity` (to delete identities in any workspace) See the [API reference](/docs/api-reference/identities/delete-identity) for the full HTTP endpoint documentation. ## Usage ```bash theme={"theme":"kanagawa-wave"} unkey api identities delete-identity [flags] ``` ## Flags The ID of the identity to delete. This can be either the external ID (from your own system that was used during identity creation) or the identity ID (the internal ID returned by the identity service). ## Global Flags | Flag | Type | Description | | ------------ | ------ | -------------------------------------------------------- | | `--root-key` | string | Override root key (`$UNKEY_ROOT_KEY`) | | `--api-url` | string | Override API base URL (default: `https://api.unkey.com`) | | `--config` | string | Path to config file (default: `~/.unkey/config.toml`) | | `--output` | string | Output format. Use `json` for raw JSON | ## Examples ```bash Basic theme={"theme":"kanagawa-wave"} unkey api identities delete-identity --identity=user_123 ``` ```bash JSON output for scripting theme={"theme":"kanagawa-wave"} unkey api identities delete-identity --identity=user_123 --output=json ``` ## Output Default output shows the request ID with latency: ```text theme={"theme":"kanagawa-wave"} req_01H9TQPP77V5E48E9SH0BG0ZQX (took 38ms) ``` With `--output=json`, the full response envelope is returned: ```json theme={"theme":"kanagawa-wave"} { "meta": { "requestId": "req_01H9TQPP77V5E48E9SH0BG0ZQX" } } ``` # get-identity Source: https://unkey.com/docs/cli/identities/get-identity Retrieve an identity by external ID or internal ID using the Unkey CLI. View associated metadata, rate limits, and linked API key details. Retrieve an identity by external ID or internal identity ID. Returns metadata, rate limits, and other associated data. Use this to check if an identity exists, view configurations, or build management dashboards. **Required permissions:** * `identity.*.read_identity` See the [API reference](/docs/api-reference/identities/get-identity) for the full HTTP endpoint documentation. ## Usage ```bash theme={"theme":"kanagawa-wave"} unkey api identities get-identity [flags] ``` ## Flags The ID of the identity to retrieve. This can be either the externalId (from your own system that was used during identity creation) or the identityId (the internal ID returned by the identity service). ## Global Flags | Flag | Type | Description | | ------------ | ------ | -------------------------------------------------------- | | `--root-key` | string | Override root key (`$UNKEY_ROOT_KEY`) | | `--api-url` | string | Override API base URL (default: `https://api.unkey.com`) | | `--config` | string | Path to config file (default: `~/.unkey/config.toml`) | | `--output` | string | Output format. Use `json` for raw JSON | ## Examples ```bash Basic theme={"theme":"kanagawa-wave"} unkey api identities get-identity --identity=user_123 ``` ```bash JSON output for scripting theme={"theme":"kanagawa-wave"} unkey api identities get-identity --identity=user_123 --output=json ``` ```bash Use an internal identity ID theme={"theme":"kanagawa-wave"} unkey api identities get-identity --identity=id_1234567890abcdef ``` ## Output Default output shows the request ID with latency, followed by the identity: ```text theme={"theme":"kanagawa-wave"} req_01H9TQPP77V5E48E9SH0BG0ZQX (took 32ms) { "id": "id_1234567890abcdef", "externalId": "user_123", "meta": { "name": "Alice Smith", "email": "alice@acme.com", "plan": "premium" }, "ratelimits": [ { "name": "requests", "limit": 1000, "duration": 60000 } ] } ``` With `--output=json`, the full response envelope is returned: ```json theme={"theme":"kanagawa-wave"} { "meta": { "requestId": "req_01H9TQPP77V5E48E9SH0BG0ZQX" }, "data": { "id": "id_1234567890abcdef", "externalId": "user_123", "meta": { "name": "Alice Smith", "email": "alice@acme.com", "plan": "premium" }, "ratelimits": [ { "name": "requests", "limit": 1000, "duration": 60000 } ] } } ``` # list-identities Source: https://unkey.com/docs/cli/identities/list-identities List all identities in your Unkey workspace using the CLI. Retrieve paginated results with metadata and linked key counts for each entity. 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. **Required permissions:** * `identity.*.read_identity` (to list identities in your workspace) See the [API reference](/docs/api-reference/identities/list-identities) for the full HTTP endpoint documentation. ## Usage ```bash theme={"theme":"kanagawa-wave"} unkey api identities list-identities [flags] ``` ## Flags The maximum number of identities to return in a single request. Use this to control response size and loading performance. Must be between 1 and 100. Defaults to 100. Pagination cursor from a previous response. Use this to fetch subsequent pages of results when the response contains a cursor value. ## Global Flags | Flag | Type | Description | | ------------ | ------ | -------------------------------------------------------- | | `--root-key` | string | Override root key (`$UNKEY_ROOT_KEY`) | | `--api-url` | string | Override API base URL (default: `https://api.unkey.com`) | | `--config` | string | Path to config file (default: `~/.unkey/config.toml`) | | `--output` | string | Output format. Use `json` for raw JSON | ## Examples ```bash Basic theme={"theme":"kanagawa-wave"} unkey api identities list-identities ``` ```bash Limit results theme={"theme":"kanagawa-wave"} unkey api identities list-identities --limit=50 ``` ```bash Paginate with cursor theme={"theme":"kanagawa-wave"} unkey api identities list-identities --limit=50 --cursor=cursor_eyJrZXkiOiJrZXlfMTIzNCJ9 ``` ```bash JSON output for scripting theme={"theme":"kanagawa-wave"} unkey api identities list-identities --limit=10 --output=json ``` ## Output Default output shows the request ID with latency, followed by the list of identities: ```text theme={"theme":"kanagawa-wave"} req_01H9TQPP77V5E48E9SH0BG0ZQX (took 120ms) { "identities": [ { "id": "id_01H9TQP8NP8JN3X8HWSKPW43JE", "externalId": "user_123", "meta": { "name": "Alice Smith", "plan": "premium" }, "ratelimits": [ { "name": "requests", "limit": 1000, "duration": 60000 } ] }, { "id": "id_02ZYR3Q9NP8JM4X8HWSKPW43JF", "externalId": "user_456", "meta": { "name": "Bob Johnson", "plan": "basic" }, "ratelimits": [ { "name": "requests", "limit": 500, "duration": 60000 } ] } ], "cursor": "cursor_eyJsYXN0SWQiOiJpZF8wMlpZUjNROU5QOEpNNFg4SFdTS1BXNDNKRiJ9", "total": 247 } ``` With `--output=json`, the full response envelope is returned: ```json theme={"theme":"kanagawa-wave"} { "meta": { "requestId": "req_01H9TQPP77V5E48E9SH0BG0ZQX" }, "data": { "identities": [ { "id": "id_01H9TQP8NP8JN3X8HWSKPW43JE", "externalId": "user_123", "meta": { "name": "Alice Smith", "plan": "premium" }, "ratelimits": [ { "name": "requests", "limit": 1000, "duration": 60000 } ] } ], "cursor": "cursor_eyJsYXN0SWQiOiJpZF8wMlpZUjNROU5QOEpNNFg4SFdTS1BXNDNKRiJ9", "total": 247 } } ``` # update-identity Source: https://unkey.com/docs/cli/identities/update-identity Update an identity's metadata and rate limit configuration using the Unkey CLI. Modify entity properties without affecting linked API keys. Update an identity's metadata and rate limits. Only specified fields are modified, others remain unchanged. Use this for subscription changes, plan upgrades, or updating user information. Changes take effect immediately. **Important:** * Rate limit changes propagate within 30 seconds across all regions * Providing `--meta-json` replaces all existing metadata; omitting it preserves current metadata * Providing `--ratelimits-json` replaces all existing rate limits; omitting it preserves current rate limits **Required permissions:** * `identity.*.update_identity` (to update identities in any workspace) See the [API reference](/docs/api-reference/identities/update-identity) for the full HTTP endpoint documentation. ## Usage ```bash theme={"theme":"kanagawa-wave"} unkey api identities update-identity [flags] ``` ## Flags The ID of the identity to update. Accepts either the externalId (your system-generated identifier) or the identityId (internal identifier returned by the identity service). JSON object of metadata to replace existing metadata. Omitting this flag preserves existing metadata, while providing an empty object `'{}'` clears all metadata. Avoid storing sensitive data here as it is returned in verification responses. Large metadata objects increase verification latency and should stay under 10KB total size. JSON array of rate limit configurations. Replaces all existing identity rate limits with this complete list. Omitting this flag preserves existing rate limits, while providing an empty array `'[]'` removes all rate limits. These limits are shared across all keys belonging to this identity, preventing abuse through multiple keys. The name of this rate limit. Used to identify which limit to check during key verification. Use descriptive, semantic names like `api_requests`, `heavy_operations`, or `downloads`. You will reference this exact name when verifying keys to check against this specific limit. The maximum number of operations allowed within the specified time window. When this limit is reached, verification requests will fail with `code=RATE_LIMITED` until the window resets. The duration for each rate limit window in milliseconds. Common values: `1000` (1 second), `60000` (1 minute), `3600000` (1 hour), `86400000` (24 hours). Whether this rate limit should be automatically applied when verifying a key. ## Global Flags | Flag | Type | Description | | ------------ | ------ | -------------------------------------------------------- | | `--root-key` | string | Override root key (`$UNKEY_ROOT_KEY`) | | `--api-url` | string | Override API base URL (default: `https://api.unkey.com`) | | `--config` | string | Path to config file (default: `~/.unkey/config.toml`) | | `--output` | string | Output format. Use `json` for raw JSON | ## Examples ```bash Update metadata theme={"theme":"kanagawa-wave"} unkey api identities update-identity --identity=user_123 --meta-json='{"plan":"premium","name":"Alice Smith"}' ``` ```bash Update rate limits theme={"theme":"kanagawa-wave"} unkey api identities update-identity --identity=user_123 --ratelimits-json='[{"name":"requests","limit":1000,"duration":3600000,"autoApply":true}]' ``` ```bash JSON output for scripting theme={"theme":"kanagawa-wave"} unkey api identities update-identity --identity=user_123 --meta-json='{"plan":"enterprise"}' --output=json ``` ## Output Default output shows the request ID with latency, followed by the updated identity: ```text theme={"theme":"kanagawa-wave"} req_2c9a0jf23l4k567 (took 52ms) { "id": "id_1234abcd", "externalId": "user_123", "meta": { "plan": "premium", "name": "Alice Smith" }, "ratelimits": [ { "id": "rl_5678efgh", "name": "requests", "limit": 1000, "duration": 3600000, "autoApply": true } ] } ``` With `--output=json`, the full response envelope is returned: ```json theme={"theme":"kanagawa-wave"} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "data": { "id": "id_1234abcd", "externalId": "user_123", "meta": { "plan": "premium", "name": "Alice Smith" }, "ratelimits": [ { "id": "rl_5678efgh", "name": "requests", "limit": 1000, "duration": 3600000, "autoApply": true } ] } } ``` # add-permissions Source: https://unkey.com/docs/cli/keys/add-permissions Add permissions to an API key using the Unkey CLI without removing existing permissions. Grant additional access rights with a single command. 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. Duplicate permissions are ignored automatically, making this operation idempotent. **Important:** Changes take effect immediately with up to 30-second propagation across regions. Any permissions that do not exist will be auto-created if the root key has the required permissions. **Required permissions:** * `api.*.update_key` (to update keys in any API) * `api..update_key` (to update keys in a specific API) See the [API reference](/docs/api-reference/keys/add-key-permissions) for the full HTTP endpoint documentation. ## Usage ```bash theme={"theme":"kanagawa-wave"} unkey api keys add-permissions [flags] ``` ## Flags The key ID to add permissions to. This is the database identifier returned from `keys.createKey` (e.g., `key_2cGKbMxRyIzhCxo1Idjz8q`). Do not confuse this with the actual API key string that users include in requests. Comma-separated list of permission names to add. Adding permissions never removes existing permissions or role-based permissions. Duplicate permissions are ignored automatically. Permissions that do not yet exist will be auto-created if the root key has permissions, otherwise the operation will fail with a 403 error. ## Global Flags | Flag | Type | Description | | ------------ | ------ | -------------------------------------------------------- | | `--root-key` | string | Override root key (`$UNKEY_ROOT_KEY`) | | `--api-url` | string | Override API base URL (default: `https://api.unkey.com`) | | `--config` | string | Path to config file (default: `~/.unkey/config.toml`) | | `--output` | string | Output format. Use `json` for raw JSON | ## Examples ```bash Basic theme={"theme":"kanagawa-wave"} unkey api keys add-permissions --key-id=key_1234abcd --permissions=documents.read,documents.write ``` ```bash JSON output for scripting theme={"theme":"kanagawa-wave"} unkey api keys add-permissions --key-id=key_1234abcd --permissions=billing.manage --output=json ``` ```bash Grant broad access theme={"theme":"kanagawa-wave"} unkey api keys add-permissions --key-id=key_1234abcd --permissions=admin.read,admin.write,admin.delete ``` ## Output Default output shows the request ID with latency, followed by the permissions now assigned to the key: ```text theme={"theme":"kanagawa-wave"} req_2c9a0jf23l4k567 (took 45ms) [ { "id": "perm_1234567890abcdef", "name": "documents.read", "slug": "documents.read" }, { "id": "perm_abcdef1234567890", "name": "documents.write", "slug": "documents.write" } ] ``` With `--output=json`, the full response envelope is returned: ```json theme={"theme":"kanagawa-wave"} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "data": [ { "id": "perm_1234567890abcdef", "name": "documents.read", "slug": "documents.read" }, { "id": "perm_abcdef1234567890", "name": "documents.write", "slug": "documents.write" } ] } ``` # add-roles Source: https://unkey.com/docs/cli/keys/add-roles Add roles to an API key using the Unkey CLI without affecting existing roles or permissions. Expand access rights with a single command. 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 propagation across regions. **Required permissions:** * `api.*.update_key` (to update keys in any API) * `api..update_key` (to update keys in a specific API) See the [API reference](/docs/api-reference/keys/add-key-roles) for the full HTTP endpoint documentation. ## Usage ```bash theme={"theme":"kanagawa-wave"} unkey api keys add-roles [flags] ``` ## Flags The key ID to add roles to. This is the database identifier returned from `createKey`, do not confuse it with the actual API key string that users include in requests. Added roles supplement existing roles and permissions without replacing them. Role assignments take effect immediately but may take up to 30 seconds to propagate across all regions. Comma-separated list of role names to add. Operations are idempotent, adding existing roles has no effect and causes no errors. All roles must already exist in the workspace; roles cannot be created automatically. Invalid roles cause the entire operation to fail atomically, ensuring consistent state. ## Global Flags | Flag | Type | Description | | ------------ | ------ | -------------------------------------------------------- | | `--root-key` | string | Override root key (`$UNKEY_ROOT_KEY`) | | `--api-url` | string | Override API base URL (default: `https://api.unkey.com`) | | `--config` | string | Path to config file (default: `~/.unkey/config.toml`) | | `--output` | string | Output format. Use `json` for raw JSON | ## Examples ```bash Basic theme={"theme":"kanagawa-wave"} unkey api keys add-roles --key-id=key_1234abcd --roles=api_admin,billing_reader ``` ```bash JSON output for scripting theme={"theme":"kanagawa-wave"} unkey api keys add-roles --key-id=key_1234abcd --roles=api_admin --output=json ``` ```bash Add multiple roles after creating a key theme={"theme":"kanagawa-wave"} KEY_ID=$(unkey api keys create-key --api-id=api_1234abcd --output=json | jq -r '.data.keyId') unkey api keys add-roles --key-id=$KEY_ID --roles=api_admin,billing_reader ``` ## Output Default output shows the request ID with latency, followed by the updated list of roles assigned to the key: ```text theme={"theme":"kanagawa-wave"} req_2c9a0jf23l4k567 (took 45ms) [ { "id": "role_1234abcd", "name": "api_admin" }, { "id": "role_5678efgh", "name": "billing_reader" } ] ``` With `--output=json`, the full response envelope is returned: ```json theme={"theme":"kanagawa-wave"} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "data": [ { "id": "role_1234abcd", "name": "api_admin" }, { "id": "role_5678efgh", "name": "billing_reader" } ] } ``` # create-key Source: https://unkey.com/docs/cli/keys/create-key Create a new API key using the Unkey CLI with options for expiration, metadata, rate limits, roles, and permissions in a single command. Create a new API key for user authentication and authorization. Use this 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. **Required permissions:** Your root key needs one of: * `api.*.create_key` (create keys in any API) * `api..create_key` (create keys in a specific API) See the [API reference](/docs/api-reference/keys/create-api-key) for the full HTTP endpoint documentation. ## Usage ```bash theme={"theme":"kanagawa-wave"} unkey api keys create-key [flags] ``` ## Flags The API namespace this key belongs to. Keys from different APIs cannot access each other. Prefix prepended to the generated key string for easier recognition in logs and dashboards (e.g., `prod_xxxxxxxxx`). Must be 1-16 characters containing only letters, numbers, and underscores. Human-readable name for the key, visible only in management interfaces and API responses. Avoid generic names like "API Key" when managing multiple keys. Cryptographic key length in bytes. The default of 16 bytes provides 2^128 possible combinations, sufficient for most applications. Minimum 16, maximum 255. Your system's user or entity identifier to link to this key. Returned during verification to identify the key owner without additional database lookups. Accepts letters, numbers, underscores, dots, and hyphens. JSON object of arbitrary metadata returned during key verification. Eliminates additional database lookups during verification, improving performance for stateless services. Avoid storing sensitive data here as it is returned in verification responses. Arbitrary key-value pairs. The object can contain strings, numbers, booleans, arrays, or nested objects. Keep total size under 10KB for best verification latency. Comma-separated list of role names to assign. Roles must already exist in your workspace. During verification, all permissions from assigned roles are checked against requested permissions. Comma-separated list of permission names to grant directly to this key. Wildcard permissions like `documents.*` grant access to all sub-permissions. Direct permissions supplement any permissions inherited from assigned roles. Unix timestamp in milliseconds when the key expires. Verification fails with `code=EXPIRED` immediately after this time passes. Omit to create a permanent key that never expires. JSON object of credit and refill configuration. Controls usage-based limits through credit consumption with optional automatic refills. Unlike rate limits which control frequency, credits control total usage with global consistency. Number of credits remaining. Set to the initial credit balance for the key. Configuration for automatic credit refill behavior. How often credits are automatically refilled. One of `daily` or `monthly`. Number of credits to add during each refill cycle. Minimum 1. Day of the month for monthly refills (1-31). Only required when interval is `monthly`. For days beyond the month's length, refill occurs on the last day of the month. JSON array of rate limit configurations. Defines time-based rate limits that protect against abuse by controlling request frequency. Unlike credits which track total usage, rate limits reset automatically after each window expires. The name of this rate limit, used to identify which limit to check during key verification. Use descriptive names like `api_requests` or `heavy_operations`. 3-128 characters. The maximum number of operations allowed within the specified time window. When reached, verification requests fail with `code=RATE_LIMITED` until the window resets. Minimum 1. The duration of each rate limit window in milliseconds. Common values: `1000` (1 second), `60000` (1 minute), `3600000` (1 hour), `86400000` (24 hours). Minimum 1000. Whether this rate limit should be automatically applied when verifying a key. Defaults to `false`. Whether the key is active for verification. When set to `false`, all verification attempts fail with `code=DISABLED`. Whether the plaintext key is stored in an encrypted vault for later retrieval. Only enable for development keys or when key recovery is absolutely necessary. ## Global Flags | Flag | Type | Description | | ------------ | ------ | -------------------------------------------------------- | | `--root-key` | string | Override root key (`$UNKEY_ROOT_KEY`) | | `--api-url` | string | Override API base URL (default: `https://api.unkey.com`) | | `--config` | string | Path to config file (default: `~/.unkey/config.toml`) | | `--output` | string | Output format. Use `json` for raw JSON | ## Examples ```bash Basic theme={"theme":"kanagawa-wave"} unkey api keys create-key --api-id=api_1234abcd ``` ```bash With prefix and name theme={"theme":"kanagawa-wave"} unkey api keys create-key --api-id=api_1234abcd --prefix=prod --name='Payment Service Key' ``` ```bash With external ID and roles theme={"theme":"kanagawa-wave"} unkey api keys create-key --api-id=api_1234abcd --external-id=user_1234abcd --roles=api_admin,billing_reader ``` ```bash With metadata theme={"theme":"kanagawa-wave"} unkey api keys create-key --api-id=api_1234abcd --meta-json='{"plan":"pro","team":"acme"}' ``` ```bash With credits and refill theme={"theme":"kanagawa-wave"} unkey api keys create-key --api-id=api_1234abcd --credits-json='{"remaining":1000,"refill":{"interval":"monthly","amount":100}}' ``` ```bash With rate limits theme={"theme":"kanagawa-wave"} unkey api keys create-key --api-id=api_1234abcd --ratelimits-json='[{"name":"requests","limit":100,"duration":60000,"autoApply":true}]' ``` ```bash JSON output for scripting theme={"theme":"kanagawa-wave"} unkey api keys create-key --api-id=api_1234abcd --output=json ``` ```bash Pipe the key ID to another command theme={"theme":"kanagawa-wave"} KEY_ID=$(unkey api keys create-key --api-id=api_1234abcd --output=json | jq -r '.data.keyId') echo "Created key: $KEY_ID" ``` ## Output Default output shows the request ID with latency, followed by the created key: ```text theme={"theme":"kanagawa-wave"} req_2c9a0jf23l4k567 (took 120ms) { "keyId": "key_2cGKbMxRyIzhCxo1Idjz8q", "key": "prod_2cGKbMxRjIzhCxo1IdjH3arELti7Sdyc8w6XYbvtcyuBowPT" } ``` With `--output=json`, the full response envelope is returned: ```json theme={"theme":"kanagawa-wave"} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "data": { "keyId": "key_2cGKbMxRyIzhCxo1Idjz8q", "key": "prod_2cGKbMxRjIzhCxo1IdjH3arELti7Sdyc8w6XYbvtcyuBowPT" } } ``` # delete-key Source: https://unkey.com/docs/cli/keys/delete-key Permanently delete an API key using the Unkey CLI. Revoke access immediately for compromised keys, user offboarding, or account cleanup. Delete API keys permanently from user accounts or for cleanup purposes. Use this for user-requested key deletion, 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 root key 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) See the [API reference](/docs/api-reference/keys/delete-api-keys) for the full HTTP endpoint documentation. ## Usage ```bash theme={"theme":"kanagawa-wave"} unkey api keys delete-key [flags] ``` ## Flags Specifies which key to delete using the database identifier returned from `createKey`. Do not confuse this with the actual API key string that users include in requests. Deletion immediately invalidates the key, causing all future verification attempts to fail with `code=NOT_FOUND`. Key deletion triggers cache invalidation across all regions but may take up to 30 seconds to fully propagate. Controls deletion behavior between recoverable soft-deletion and irreversible permanent erasure. Soft deletion (default) preserves key data for potential recovery through direct database operations. Permanent deletion completely removes all traces including hash values and metadata with no recovery option. Use permanent deletion only for regulatory compliance (GDPR), resolving hash collisions, or when reusing identical key strings. Permanent deletion cannot be undone and may affect analytics data that references the deleted key. Most applications should use soft deletion to maintain audit trails and prevent accidental data loss. ## Global Flags | Flag | Type | Description | | ------------ | ------ | -------------------------------------------------------- | | `--root-key` | string | Override root key (`$UNKEY_ROOT_KEY`) | | `--api-url` | string | Override API base URL (default: `https://api.unkey.com`) | | `--config` | string | Path to config file (default: `~/.unkey/config.toml`) | | `--output` | string | Output format. Use `json` for raw JSON | ## Examples ```bash Basic theme={"theme":"kanagawa-wave"} unkey api keys delete-key --key-id=key_1234abcd ``` ```bash Permanent deletion theme={"theme":"kanagawa-wave"} unkey api keys delete-key --key-id=key_1234abcd --permanent ``` ```bash JSON output for scripting theme={"theme":"kanagawa-wave"} unkey api keys delete-key --key-id=key_1234abcd --output=json ``` ## Output Default output shows the request ID with latency: ```text theme={"theme":"kanagawa-wave"} req_2c9a0jf23l4k567 (took 45ms) {} ``` With `--output=json`, the full response envelope is returned: ```json theme={"theme":"kanagawa-wave"} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "data": {} } ``` # get-key Source: https://unkey.com/docs/cli/keys/get-key Retrieve detailed information about an API key using the Unkey CLI including metadata, permissions, rate limits, and remaining credits. 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:** * `api.*.read_key` or `api..read_key` (to read key information) * `api.*.decrypt_key` or `api..decrypt_key` (additionally required when using `--decrypt`) See the [API reference](/docs/api-reference/keys/get-api-key) for the full HTTP endpoint documentation. ## Usage ```bash theme={"theme":"kanagawa-wave"} unkey api keys get-key [flags] ``` ## Flags The database identifier of the key to retrieve, returned from `keys.createKey`. Do not confuse this with the actual API key string that users include in requests. Find this ID in creation responses, key listings, dashboard, or verification responses. Whether to include the plaintext key value in the response. Only works for keys created with `recoverable=true` and requires the `decrypt_key` permission. Returned keys must be handled securely -- never logged, cached, or stored insecurely. ## Global Flags | Flag | Type | Description | | ------------ | ------ | -------------------------------------------------------- | | `--root-key` | string | Override root key (`$UNKEY_ROOT_KEY`) | | `--api-url` | string | Override API base URL (default: `https://api.unkey.com`) | | `--config` | string | Path to config file (default: `~/.unkey/config.toml`) | | `--output` | string | Output format. Use `json` for raw JSON | ## Examples ```bash Basic theme={"theme":"kanagawa-wave"} unkey api keys get-key --key-id=key_1234abcd ``` ```bash With decryption theme={"theme":"kanagawa-wave"} unkey api keys get-key --key-id=key_1234abcd --decrypt ``` ```bash JSON output for scripting theme={"theme":"kanagawa-wave"} unkey api keys get-key --key-id=key_1234abcd --output=json ``` ```bash Pipe key details to jq theme={"theme":"kanagawa-wave"} unkey api keys get-key --key-id=key_1234abcd --output=json | jq '.data.permissions' ``` ## Output Default output shows the request ID with latency, followed by the key details: ```text theme={"theme":"kanagawa-wave"} req_2c9a0jf23l4k567 (took 45ms) { "keyId": "key_1234abcd", "start": "sk_prod", "enabled": true, "name": "Production API Key", "createdAt": 1704067200000, "permissions": [ "documents.read", "documents.write" ], "roles": [ "editor" ] } ``` With `--output=json`, the full response envelope is returned: ```json theme={"theme":"kanagawa-wave"} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "data": { "keyId": "key_1234abcd", "start": "sk_prod", "enabled": true, "name": "Production API Key", "createdAt": 1704067200000, "permissions": [ "documents.read", "documents.write" ], "roles": [ "editor" ] } } ``` # migrate-keys Source: https://unkey.com/docs/cli/keys/migrate-keys Migrate pre-hashed API keys from an existing provider into Unkey using the CLI. Import keys in bulk without invalidating user credentials. Migrate pre-hashed API keys from an existing provider into Unkey. Use this to move keys from another system without requiring users to rotate their credentials. You provide the already-hashed keys and Unkey stores them directly, so existing API keys continue to work after migration. The endpoint returns HTTP 200 even on partial success; hashes that could not be migrated are listed in the response. **Important:** You must obtain a `migrationId` from Unkey support before using this command. Keys that already exist in the system will appear in the `failed` array rather than causing the entire request to fail. **Required permissions:** Your root key must have one of the following permissions: * `api.*.create_key` (to migrate keys to any API) * `api..create_key` (to migrate keys to a specific API) See the [API reference](/docs/api-reference/keys/migrate-api-keys) for the full HTTP endpoint documentation. ## Usage ```bash theme={"theme":"kanagawa-wave"} unkey api keys migrate-keys [flags] ``` ## Flags Identifier of the configured migration provider/strategy to use (e.g., "your\_company"). You will receive this from Unkey's support staff. Must be 3-255 characters. The ID of the API that the keys should be inserted into. Must be 3-255 characters. JSON array of key migration objects. Each object describes a single key to migrate. The current hash of the key on your side. This is the pre-hashed value that Unkey will store directly. Must be at least 3 characters. Sets a human-readable identifier for internal organization and dashboard display. Never exposed to end users, only visible in management interfaces and API responses. Must be 1-255 characters. Links this key to a user or entity in your system using your own identifier. Returned during verification to identify the key owner without additional database lookups. Must be 1-255 characters. Stores arbitrary JSON metadata returned during key verification for contextual information. Eliminates additional database lookups during verification. Avoid storing sensitive data here as it is returned in verification responses. Assigns existing roles to this key for permission management through role-based access control. Roles must already exist in your workspace before assignment. Grants specific permissions directly to this key without requiring role membership. Wildcard permissions like `documents.*` grant access to all sub-permissions. Direct permissions supplement any permissions inherited from assigned roles. Sets when this key automatically expires as a Unix timestamp in milliseconds. Verification fails with `code=EXPIRED` immediately after this time passes. Omitting this field creates a permanent key that never expires. Controls whether the key is active immediately upon migration. When set to `false`, the key exists but all verification attempts fail with `code=DISABLED`. Controls usage-based limits through credit consumption with optional automatic refills. Number of credits remaining. Use `null` for unlimited. Configuration for automatic credit refill behavior. How often credits are automatically refilled. One of `daily` or `monthly`. Number of credits to add during each refill cycle. Day of the month for monthly refills (1-31). Only required when interval is `monthly`. For days beyond the month's length, refill occurs on the last day of the month. Defines time-based rate limits that control request frequency. Multiple rate limits can control different operation types with separate thresholds and windows. The name of this rate limit, used to identify which limit to check during key verification. Must be 3-128 characters. The maximum number of operations allowed within the specified time window. The duration for each rate limit window in milliseconds. Minimum 1000 (1 second). Whether this rate limit should be automatically applied when verifying a key. ## Global Flags | Flag | Type | Description | | ------------ | ------ | -------------------------------------------------------- | | `--root-key` | string | Override root key (`$UNKEY_ROOT_KEY`) | | `--api-url` | string | Override API base URL (default: `https://api.unkey.com`) | | `--config` | string | Path to config file (default: `~/.unkey/config.toml`) | | `--output` | string | Output format. Use `json` for raw JSON | ## Examples ```bash Basic theme={"theme":"kanagawa-wave"} unkey api keys migrate-keys \ --migration-id=your_company \ --api-id=api_123456789 \ --keys-json='[{"hash":"c4fbfe7c69a067cb0841dea343346a750a69908a08ea9656d2a8c19fb0823c64","enabled":true}]' ``` ```bash With metadata and roles theme={"theme":"kanagawa-wave"} unkey api keys migrate-keys \ --migration-id=your_company \ --api-id=api_123456789 \ --keys-json='[{"hash":"c4fbfe7c69a067cb0841dea343346a750a69908a08ea9656d2a8c19fb0823c64","name":"Production API Key","externalId":"user_abc123","enabled":true,"meta":{"plan":"enterprise"},"roles":["admin"],"permissions":["api.read","api.write"]}]' ``` ```bash JSON output for scripting theme={"theme":"kanagawa-wave"} unkey api keys migrate-keys \ --migration-id=your_company \ --api-id=api_123456789 \ --keys-json='[{"hash":"abc123","enabled":true}]' \ --output=json ``` ## Output Default output shows the request ID with latency, followed by the migration results: ```text theme={"theme":"kanagawa-wave"} req_2c9a0jf23l4k567 (took 120ms) { "migrated": [ { "hash": "c4fbfe7c69a067cb0841dea343346a750a69908a08ea9656d2a8c19fb0823c64", "keyId": "key_2cGKbMxRyIzhCxo1Idjz8q" } ], "failed": [] } ``` With `--output=json`, the full response envelope is returned: ```json theme={"theme":"kanagawa-wave"} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "data": { "migrated": [ { "hash": "c4fbfe7c69a067cb0841dea343346a750a69908a08ea9656d2a8c19fb0823c64", "keyId": "key_2cGKbMxRyIzhCxo1Idjz8q" } ], "failed": [] } } ``` # remove-permissions Source: https://unkey.com/docs/cli/keys/remove-permissions Remove specific permissions from an API key using the Unkey CLI without affecting other roles or permissions. Revoke granular access rights. 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 propagation across regions. **Required 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. See the [API reference](/docs/api-reference/keys/remove-key-permissions) for the full HTTP endpoint documentation. ## Usage ```bash theme={"theme":"kanagawa-wave"} unkey api keys remove-permissions [flags] ``` ## Flags The key ID to remove permissions from. This is the database identifier returned from `keys.createKey`, do not confuse it with the actual API key string that users include in requests. Comma-separated list of permission names to remove. You can specify permissions by slug or by permission ID. After removal, verification checks for these permissions will fail unless granted through roles. ## Global Flags | Flag | Type | Description | | ------------ | ------ | -------------------------------------------------------- | | `--root-key` | string | Override root key (`$UNKEY_ROOT_KEY`) | | `--api-url` | string | Override API base URL (default: `https://api.unkey.com`) | | `--config` | string | Path to config file (default: `~/.unkey/config.toml`) | | `--output` | string | Output format. Use `json` for raw JSON | ## Examples ```bash Basic theme={"theme":"kanagawa-wave"} unkey api keys remove-permissions --key-id=key_1234abcd --permissions=documents.read,documents.write ``` ```bash JSON output for scripting theme={"theme":"kanagawa-wave"} unkey api keys remove-permissions --key-id=key_1234abcd --permissions=documents.read --output=json ``` ```bash Remove a single permission theme={"theme":"kanagawa-wave"} unkey api keys remove-permissions --key-id=key_1234abcd --permissions=billing.manage ``` ## Output Default output shows the request ID with latency, followed by the remaining direct permissions on the key: ```text theme={"theme":"kanagawa-wave"} req_2c9a0jf23l4k567 (took 45ms) [ { "id": "perm_1234abcd", "name": "documents.read" } ] ``` With `--output=json`, the full response envelope is returned: ```json theme={"theme":"kanagawa-wave"} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "data": [ { "id": "perm_1234abcd", "name": "documents.read" } ] } ``` # remove-roles Source: https://unkey.com/docs/cli/keys/remove-roles Remove specific roles from an API key using the Unkey CLI without affecting direct permissions or other assigned roles on the same key. 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 propagation across regions. **Required 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. See the [API reference](/docs/api-reference/keys/remove-key-roles) for the full HTTP endpoint documentation. ## Usage ```bash theme={"theme":"kanagawa-wave"} unkey api keys remove-roles [flags] ``` ## Flags The key ID to remove roles from. This is the database identifier returned from key creation, do not confuse it with the actual API key string that users include in requests. Removing roles only affects direct assignments, not permissions inherited from other sources. Role changes take effect immediately but may take up to 30 seconds to propagate across all regions. Comma-separated list of role names to remove. Operations are idempotent, removing non-assigned roles has no effect and causes no errors. After removal, the key loses access to permissions that were only granted through these roles. Invalid role references cause the entire operation to fail atomically, ensuring consistent state. ## Global Flags | Flag | Type | Description | | ------------ | ------ | -------------------------------------------------------- | | `--root-key` | string | Override root key (`$UNKEY_ROOT_KEY`) | | `--api-url` | string | Override API base URL (default: `https://api.unkey.com`) | | `--config` | string | Path to config file (default: `~/.unkey/config.toml`) | | `--output` | string | Output format. Use `json` for raw JSON | ## Examples ```bash Basic theme={"theme":"kanagawa-wave"} unkey api keys remove-roles --key-id=key_1234abcd --roles=api_admin,billing_reader ``` ```bash JSON output for scripting theme={"theme":"kanagawa-wave"} unkey api keys remove-roles --key-id=key_1234abcd --roles=api_admin --output=json ``` ```bash Remove a single role theme={"theme":"kanagawa-wave"} unkey api keys remove-roles --key-id=key_1234abcd --roles=temporary_access ``` ## Output Default output shows the request ID with latency, followed by the remaining roles assigned to the key: ```text theme={"theme":"kanagawa-wave"} req_2c9a0jf23l4k567 (took 45ms) [ { "id": "role_5678efgh", "name": "billing_reader" } ] ``` With `--output=json`, the full response envelope is returned: ```json theme={"theme":"kanagawa-wave"} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "data": [ { "id": "role_5678efgh", "name": "billing_reader" } ] } ``` # reroll-key Source: https://unkey.com/docs/cli/keys/reroll-key Generate a new API key string while preserving the existing key's configuration, permissions, and metadata using the Unkey CLI reroll command. 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, and recovery settings. 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). The original key will be revoked after the duration specified in `--expiration`. Set it to `0` to revoke immediately, or use a positive value to allow a graceful overlap period for key rotation. **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:** * `api.*.create_key` or `api..create_key` * `api.*.encrypt_key` or `api..encrypt_key` (only when the original key is recoverable) See the [API reference](/docs/api-reference/keys/reroll-key) for the full HTTP endpoint documentation. ## Usage ```bash theme={"theme":"kanagawa-wave"} unkey api keys reroll-key [flags] ``` ## Flags The database identifier of the key to reroll. This is the unique ID returned when creating or listing keys, NOT the actual API key token. You can find this ID in the response from `keys.createKey`, key verification responses, the Unkey dashboard, or API key listing endpoints. Duration in milliseconds until the original key is revoked, starting from now. Set to `0` to revoke the original key immediately. Positive values keep the original key active for the specified duration, allowing graceful migration by giving users time to update their credentials. Common overlap periods: 1 hour (`3600000`), 24 hours (`86400000`), 7 days (`604800000`), 30 days (`2592000000`). ## Global Flags | Flag | Type | Description | | ------------ | ------ | -------------------------------------------------------- | | `--root-key` | string | Override root key (`$UNKEY_ROOT_KEY`) | | `--api-url` | string | Override API base URL (default: `https://api.unkey.com`) | | `--config` | string | Path to config file (default: `~/.unkey/config.toml`) | | `--output` | string | Output format. Use `json` for raw JSON | ## Examples ```bash Immediate revocation theme={"theme":"kanagawa-wave"} unkey api keys reroll-key --key-id=key_1234abcd --expiration=0 ``` ```bash 24-hour grace period theme={"theme":"kanagawa-wave"} unkey api keys reroll-key --key-id=key_1234abcd --expiration=86400000 ``` ```bash JSON output for scripting theme={"theme":"kanagawa-wave"} unkey api keys reroll-key --key-id=key_1234abcd --expiration=0 --output=json ``` ```bash Capture the new key theme={"theme":"kanagawa-wave"} NEW_KEY=$(unkey api keys reroll-key --key-id=key_1234abcd --expiration=3600000 --output=json | jq -r '.data.key') ``` ## Output Default output shows the request ID with latency, followed by the new key details: ```text theme={"theme":"kanagawa-wave"} req_2c9a0jf23l4k567 (took 45ms) { "keyId": "key_5678efgh", "key": "prod_2cGKbMxRjIzhCxo1IdjH3arELti7Sdyc8w6XYbvtcyuBowPT" } ``` With `--output=json`, the full response envelope is returned: ```json theme={"theme":"kanagawa-wave"} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "data": { "keyId": "key_5678efgh", "key": "prod_2cGKbMxRjIzhCxo1IdjH3arELti7Sdyc8w6XYbvtcyuBowPT" } } ``` # set-permissions Source: https://unkey.com/docs/cli/keys/set-permissions Replace all permissions on an API key with the Unkey CLI in a single atomic operation. Overwrite existing permissions with a new exact set. 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 propagation across regions. This is a complete replacement operation -- all existing direct permissions not included in the provided list will be removed. **Required permissions:** * `api.*.update_key` (to update keys in any API) * `api..update_key` (to update keys in a specific API) See the [API reference](/docs/api-reference/keys/set-key-permissions) for the full HTTP endpoint documentation. ## Usage ```bash theme={"theme":"kanagawa-wave"} unkey api keys set-permissions [flags] ``` ## Flags The key ID to set permissions on. This is the database identifier returned from `keys.createKey` (e.g., `key_2cGKbMxRyIzhCxo1Idjz8q`). Do not confuse this with the actual API key string that users include in requests. Comma-separated list of permissions. Replaces all existing direct permissions with this new set. Providing an empty value removes all direct permissions from the key. Permissions granted through roles are not affected. Any permissions that do not already exist will be auto-created if your root key has sufficient permissions. ## Global Flags | Flag | Type | Description | | ------------ | ------ | -------------------------------------------------------- | | `--root-key` | string | Override root key (`$UNKEY_ROOT_KEY`) | | `--api-url` | string | Override API base URL (default: `https://api.unkey.com`) | | `--config` | string | Path to config file (default: `~/.unkey/config.toml`) | | `--output` | string | Output format. Use `json` for raw JSON | ## Examples ```bash Basic theme={"theme":"kanagawa-wave"} unkey api keys set-permissions --key-id=key_1234abcd --permissions=documents.read,documents.write,settings.view ``` ```bash JSON output for scripting theme={"theme":"kanagawa-wave"} unkey api keys set-permissions --key-id=key_1234abcd --permissions=documents.read,documents.write --output=json ``` ```bash Remove all direct permissions theme={"theme":"kanagawa-wave"} unkey api keys set-permissions --key-id=key_1234abcd --permissions= ``` ## Output Default output shows the request ID with latency, followed by the updated list of direct permissions on the key: ```text theme={"theme":"kanagawa-wave"} req_2c9a0jf23l4k567 (took 45ms) [ { "id": "perm_1234567890abcdef", "name": "documents.read", "slug": "documents.read" }, { "id": "perm_abcdef1234567890", "name": "documents.write", "slug": "documents.write" }, { "id": "perm_567890abcdef1234", "name": "settings.view", "slug": "settings.view" } ] ``` With `--output=json`, the full response envelope is returned: ```json theme={"theme":"kanagawa-wave"} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "data": [ { "id": "perm_1234567890abcdef", "name": "documents.read", "slug": "documents.read" }, { "id": "perm_abcdef1234567890", "name": "documents.write", "slug": "documents.write" }, { "id": "perm_567890abcdef1234", "name": "settings.view", "slug": "settings.view" } ] } ``` # set-roles Source: https://unkey.com/docs/cli/keys/set-roles Replace all roles on an API key using the Unkey CLI in a single atomic operation. Overwrite existing role assignments with a new exact set. 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 propagation across regions. This is a wholesale replacement, not an incremental update -- any existing roles not included in the request are removed. **Required permissions:** * `api.*.update_key` (to update keys in any API) * `api..update_key` (to update keys in a specific API) See the [API reference](/docs/api-reference/keys/set-key-roles) for the full HTTP endpoint documentation. ## Usage ```bash theme={"theme":"kanagawa-wave"} unkey api keys set-roles [flags] ``` ## Flags The key ID to set roles on. This is the database identifier returned from key creation (e.g., `key_2cGKbMxRyIzhCxo1Idjz8q`), not the actual API key string that users include in requests. Comma-separated list of roles. Replaces all existing roles on the key with this exact set. All roles must already exist in your workspace -- invalid role references cause the entire operation to fail atomically. Providing an empty value removes all role assignments from the key. ## Global Flags | Flag | Type | Description | | ------------ | ------ | -------------------------------------------------------- | | `--root-key` | string | Override root key (`$UNKEY_ROOT_KEY`) | | `--api-url` | string | Override API base URL (default: `https://api.unkey.com`) | | `--config` | string | Path to config file (default: `~/.unkey/config.toml`) | | `--output` | string | Output format. Use `json` for raw JSON | ## Examples ```bash Basic theme={"theme":"kanagawa-wave"} unkey api keys set-roles --key-id=key_1234abcd --roles=api_admin,billing_reader ``` ```bash Reset to a single role theme={"theme":"kanagawa-wave"} unkey api keys set-roles --key-id=key_1234abcd --roles=viewer ``` ```bash JSON output for scripting theme={"theme":"kanagawa-wave"} unkey api keys set-roles --key-id=key_1234abcd --roles=api_admin,billing_reader --output=json ``` ## Output Default output shows the request ID with latency, followed by the roles now assigned to the key: ```text theme={"theme":"kanagawa-wave"} req_2c9a0jf23l4k567 (took 45ms) [ { "id": "role_1234567890abcdef", "name": "api_admin" }, { "id": "role_abcdef1234567890", "name": "billing_reader" } ] ``` With `--output=json`, the full response envelope is returned: ```json theme={"theme":"kanagawa-wave"} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "data": [ { "id": "role_1234567890abcdef", "name": "api_admin" }, { "id": "role_abcdef1234567890", "name": "billing_reader" } ] } ``` # update-credits Source: https://unkey.com/docs/cli/keys/update-credits Update remaining credit quotas on an API key using the Unkey CLI. Adjust usage limits for plan changes, billing cycles, or purchased credits. 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 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:** 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 regions. See the [API reference](/docs/api-reference/keys/update-key-credits) for the full HTTP endpoint documentation. ## Usage ```bash theme={"theme":"kanagawa-wave"} unkey api keys update-credits [flags] ``` ## Flags The ID of the key to update (begins with `key_`). This is the database reference ID for the key, not the actual API key string that users authenticate with. This ID uniquely identifies which key's credits will be updated. Defines how to modify the key's remaining credits. Must be one of `set`, `increment`, or `decrement`. Use `set` to replace current credits with a specific value or unlimited usage, `increment` to add credits for plan upgrades or credit purchases, and `decrement` to reduce credits for refunds or policy violations. The credit value to use with the specified operation. For `set`, this becomes the new remaining credits value. For `increment`, this amount is added to current credits. For `decrement`, this amount is subtracted from current credits. Omit when using the `set` operation to make the key unlimited (removes usage restrictions entirely). When decrementing, if the result would be negative, remaining credits are automatically set to zero. Required when using `increment` or `decrement` operations. Optional for `set` operation (omitting creates unlimited usage). ## Global Flags | Flag | Type | Description | | ------------ | ------ | -------------------------------------------------------- | | `--root-key` | string | Override root key (`$UNKEY_ROOT_KEY`) | | `--api-url` | string | Override API base URL (default: `https://api.unkey.com`) | | `--config` | string | Path to config file (default: `~/.unkey/config.toml`) | | `--output` | string | Output format. Use `json` for raw JSON | ## Examples ```bash Set credits to a fixed value theme={"theme":"kanagawa-wave"} unkey api keys update-credits --key-id=key_1234abcd --operation=set --value=1000 ``` ```bash Increment credits after a purchase theme={"theme":"kanagawa-wave"} unkey api keys update-credits --key-id=key_1234abcd --operation=increment --value=500 ``` ```bash Decrement credits for a refund theme={"theme":"kanagawa-wave"} unkey api keys update-credits --key-id=key_1234abcd --operation=decrement --value=100 ``` ```bash JSON output for scripting theme={"theme":"kanagawa-wave"} unkey api keys update-credits --key-id=key_1234abcd --operation=set --value=1000 --output=json ``` ## Output Default output shows the request ID with latency, followed by the updated credit data: ```text theme={"theme":"kanagawa-wave"} req_2c9a0jf23l4k567 (took 45ms) { "remaining": 1000, "refill": { "interval": "monthly", "amount": 1000, "refillDay": 1 } } ``` With `--output=json`, the full response envelope is returned: ```json theme={"theme":"kanagawa-wave"} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "data": { "remaining": 1000, "refill": { "interval": "monthly", "amount": 1000, "refillDay": 1 } } } ``` # update-key Source: https://unkey.com/docs/cli/keys/update-key Update API key properties using the Unkey CLI. Change metadata, expiration, rate limits, remaining credits, or enabled status in one command. 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 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:** 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 regions due to cache invalidation. See the [API reference](/docs/api-reference/keys/update-key-settings) for the full HTTP endpoint documentation. ## Usage ```bash theme={"theme":"kanagawa-wave"} unkey api keys update-key [flags] ``` ## Flags Specifies which key to update using the database identifier returned from `createKey`. Do not confuse this with the actual API key string that users include in requests. Sets a human-readable name for internal organization and identification. Omitting this flag leaves the current name unchanged. Avoid generic names like "API Key" when managing multiple keys per user or service. Links this key to a user or entity in your system for ownership tracking during verification. Omitting this flag preserves the current association. Essential for user-specific analytics, billing, and key management across multiple users. JSON object of arbitrary metadata returned during key verification. Omitting this flag preserves existing metadata. Avoid storing sensitive data here as it's returned in verification responses. Large metadata objects increase verification latency and should stay under 10KB total size. Arbitrary key-value pairs. Example: `{"plan":"enterprise","team":"acme"}` Unix timestamp in milliseconds when the key automatically expires. Verification fails with `code=EXPIRED` immediately after this time passes. Avoid setting timestamps in the past as they immediately invalidate the key. Keys expire based on server time, not client time. JSON object for credit and refill configuration. Controls usage-based limits for this key through credit consumption. Setting null enables unlimited usage. Essential for implementing usage-based pricing and subscription quotas. Number of credits remaining. Set to `null` for unlimited usage. This also clears the refilling schedule. Configuration for automatic credit refill behavior. How often credits are automatically refilled. One of `daily` or `monthly`. Number of credits to add during each refill cycle. Must be at least 1. Day of the month for monthly refills (1-31). Only required when interval is `monthly`. For days beyond the month's length, refill occurs on the last day of the month. JSON array of rate limit configurations. Defines time-based rate limits that protect against abuse by controlling request frequency. Unlike credits which track total usage, rate limits reset automatically after each window expires. Multiple rate limits can control different operation types with separate thresholds and windows. The name of this rate limit, used to identify which limit to check during key verification. Use descriptive names like `api_requests`, `heavy_operations`, or `downloads`. The maximum number of operations allowed within the specified time window. When this limit is reached, verification requests will fail with `code=RATE_LIMITED` until the window resets. The duration for each rate limit window in milliseconds. Common values: `1000` (1 second), `60000` (1 minute), `3600000` (1 hour), `86400000` (24 hours). Whether this rate limit should be automatically applied when verifying a key. Defaults to `false`. Controls whether the key is currently active for verification requests. When set to `false`, all verification attempts fail with `code=DISABLED` regardless of other settings. Useful for temporarily suspending access during billing issues, security incidents, or maintenance windows without losing key configuration. Comma-separated list of role names to assign. Roles must already exist in your workspace before assignment. During verification, all permissions from assigned roles are checked against requested permissions. **Replaces all existing roles** -- use dedicated add/remove endpoints for incremental changes. Comma-separated list of permission names to grant directly to this key. Wildcard permissions like `documents.*` grant access to all sub-permissions. Direct permissions supplement any permissions inherited from assigned roles. **Replaces all existing permissions** -- use dedicated add/remove endpoints for incremental changes. ## Global Flags | Flag | Type | Description | | ------------ | ------ | -------------------------------------------------------- | | `--root-key` | string | Override root key (`$UNKEY_ROOT_KEY`) | | `--api-url` | string | Override API base URL (default: `https://api.unkey.com`) | | `--config` | string | Path to config file (default: `~/.unkey/config.toml`) | | `--output` | string | Output format -- use `json` for raw JSON | ## Examples ```bash Rename a key theme={"theme":"kanagawa-wave"} unkey api keys update-key --key-id=key_1234abcd --name='Updated Key Name' ``` ```bash Disable a key theme={"theme":"kanagawa-wave"} unkey api keys update-key --key-id=key_1234abcd --enabled=false ``` ```bash Link to identity and assign roles theme={"theme":"kanagawa-wave"} unkey api keys update-key --key-id=key_1234abcd --external-id=user_5678 --roles=api_admin,billing_reader ``` ```bash Update metadata theme={"theme":"kanagawa-wave"} unkey api keys update-key --key-id=key_1234abcd --meta-json='{"plan":"enterprise","team":"acme"}' ``` ```bash Configure credits with monthly refill theme={"theme":"kanagawa-wave"} unkey api keys update-key --key-id=key_1234abcd --credits-json='{"remaining":5000,"refill":{"interval":"monthly","amount":5000}}' ``` ```bash Set rate limits theme={"theme":"kanagawa-wave"} unkey api keys update-key --key-id=key_1234abcd --ratelimits-json='[{"name":"requests","limit":500,"duration":60000,"autoApply":true}]' ``` ```bash JSON output for scripting theme={"theme":"kanagawa-wave"} unkey api keys update-key --key-id=key_1234abcd --name='Scripted Update' --output=json ``` ## Output Default output shows the request ID with latency: ```text theme={"theme":"kanagawa-wave"} req_2c9a0jf23l4k567 (took 45ms) {} ``` With `--output=json`, the full response envelope is returned: ```json theme={"theme":"kanagawa-wave"} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "data": {} } ``` # verify-key Source: https://unkey.com/docs/cli/keys/verify-key Verify an API key's validity and check permissions using the Unkey CLI. Test key verification locally before deploying authentication logic. Verify an API key's validity and permissions for request authentication. Use this command 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. **Required permissions:** Your root key needs one of: * `api.*.verify_key` (verify keys in any API) * `api..verify_key` (verify keys in specific API) **Note:** If your root key has no verify permissions at all, you will receive a 403 Forbidden error. If your root key has verify permissions for a different API than the key you're verifying, you will receive a 200 response with `code: NOT_FOUND` to avoid leaking key existence. See the [API reference](/docs/api-reference/keys/verify-api-key) for the full HTTP endpoint documentation. ## Usage ```bash theme={"theme":"kanagawa-wave"} unkey api keys verify-key [flags] ``` ## Flags The API key to verify, exactly as provided by your user. Include any prefix -- even small changes will cause verification to fail. Metadata tags for analytics in `key=value` format. Attaches metadata for analytics and monitoring without affecting verification outcomes. Enables segmentation of API usage in dashboards by endpoint, client version, region, or custom dimensions. Avoid including sensitive data in tags as they may appear in logs and analytics reports. Permission query to check, supports `AND`/`OR` operators and parentheses for grouping. Examples: `"documents.read"`, `"documents.read AND documents.write"`, `"(documents.read OR documents.write) AND users.view"`. Verification fails if the key lacks the required permissions through direct assignment or role inheritance. JSON object for credit consumption configuration. Controls credit deduction for usage-based billing and quota enforcement. Omitting this field uses the default cost of 1 credit per verification. Sets how many credits to deduct for this verification request. Use `0` for read-only operations or free tier access, higher values for premium features. Credits are deducted after all security checks pass. Min: `0`, max: `1000000000`. JSON array of rate limit checks to enforce during verification. Omitting this field skips rate limit checks entirely, relying only on configured key rate limits. Multiple rate limits can be checked simultaneously, each with different costs and temporary overrides. References an existing rate limit by its name. Key rate limits take precedence over identifier-based limits. Optionally override how expensive this operation is and how many tokens are deducted from the current limit. Optionally override the maximum number of requests allowed within the specified interval. Optionally override the duration of the rate limit window in milliseconds. Migration provider ID for on-demand key migration from your previous system. Reach out for migration support at [support@unkey.com](mailto:support@unkey.com). ## Global Flags | Flag | Type | Description | | ------------ | ------ | -------------------------------------------------------- | | `--root-key` | string | Override root key (`$UNKEY_ROOT_KEY`) | | `--api-url` | string | Override API base URL (default: `https://api.unkey.com`) | | `--config` | string | Path to config file (default: `~/.unkey/config.toml`) | | `--output` | string | Output format. Use `json` for raw JSON | ## Examples ```bash Basic verification theme={"theme":"kanagawa-wave"} unkey api keys verify-key --key=sk_1234abcdef ``` ```bash Check permissions theme={"theme":"kanagawa-wave"} unkey api keys verify-key --key=sk_1234abcdef --permissions='documents.read AND users.view' ``` ```bash With analytics tags theme={"theme":"kanagawa-wave"} unkey api keys verify-key --key=sk_1234abcdef --tags=endpoint=/users/profile,method=GET ``` ```bash With credit cost theme={"theme":"kanagawa-wave"} unkey api keys verify-key --key=sk_1234abcdef --credits-json='{"cost":5}' ``` ```bash With rate limits theme={"theme":"kanagawa-wave"} unkey api keys verify-key --key=sk_1234abcdef --ratelimits-json='[{"name":"requests","limit":100,"duration":60000}]' ``` ```bash JSON output for scripting theme={"theme":"kanagawa-wave"} unkey api keys verify-key --key=sk_1234abcdef --output=json ``` ```bash Check valid field in scripts theme={"theme":"kanagawa-wave"} VALID=$(unkey api keys verify-key --key=$API_KEY --output=json | jq -r '.data.valid') if [ "$VALID" = "true" ]; then echo "Key is valid"; fi ``` ## Output Default output shows the request ID with latency, followed by the verification result: ```text theme={"theme":"kanagawa-wave"} req_2c9a0jf23l4k567 (took 32ms) { "valid": true, "code": "VALID", "keyId": "key_1234abcd", "name": "user-dashboard-key", "enabled": true, "permissions": ["documents.read", "documents.write", "users.view"], "roles": ["editor"], "credits": 950, "expires": 1735689600000 } ``` With `--output=json`, the full response envelope is returned: ```json theme={"theme":"kanagawa-wave"} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "data": { "valid": true, "code": "VALID", "keyId": "key_1234abcd", "name": "user-dashboard-key", "enabled": true, "permissions": ["documents.read", "documents.write", "users.view"], "roles": ["editor"], "credits": 950, "expires": 1735689600000, "meta": { "userId": "user_12345", "plan": "premium" } } } ``` When verification fails, the `valid` field is `false` and `code` indicates the reason: ```json theme={"theme":"kanagawa-wave"} { "meta": { "requestId": "req_def456ghi789" }, "data": { "valid": false, "code": "EXPIRED", "keyId": "key_5678efgh", "name": "temporary-access-key", "enabled": true, "expires": 1704067200000 } } ``` Possible `code` values: `VALID`, `NOT_FOUND`, `FORBIDDEN`, `INSUFFICIENT_PERMISSIONS`, `USAGE_EXCEEDED`, `RATE_LIMITED`, `DISABLED`, `EXPIRED`. # whoami Source: https://unkey.com/docs/cli/keys/whoami Look up API key details by providing the raw key string using the Unkey CLI whoami command. Identify which key ID a string belongs to. Look up key details by providing the full API key string. Use this to identify an unknown key, inspect its permissions, check expiration, or verify it belongs to the expected API. The key is matched by its hash, so you must provide the complete key exactly as issued -- even minor modifications will cause a not found error. **Important:** Never log, cache, or store API keys in your system as they provide full access to user resources. **Required permissions:** * `api.*.read_key` (to read keys from any API) * `api..read_key` (to read keys from a specific API) If your root key lacks permissions but the key exists, the API may return a 404 to prevent leaking the existence of a key to unauthorized clients. If you believe a key should exist but receive a 404, double check your root key has the correct permissions. See the [API reference](/docs/api-reference/keys/get-api-key-by-hash) for the full HTTP endpoint documentation. ## Usage ```bash theme={"theme":"kanagawa-wave"} unkey api keys whoami [flags] ``` ## Flags The complete API key string, including any prefix. Must be provided exactly as issued -- even minor modifications will cause a not found error. Between 1 and 512 characters. ## Global Flags | Flag | Type | Description | | ------------ | ------ | -------------------------------------------------------- | | `--root-key` | string | Override root key (`$UNKEY_ROOT_KEY`) | | `--api-url` | string | Override API base URL (default: `https://api.unkey.com`) | | `--config` | string | Path to config file (default: `~/.unkey/config.toml`) | | `--output` | string | Output format. Use `json` for raw JSON | ## Examples ```bash Basic theme={"theme":"kanagawa-wave"} unkey api keys whoami --key=sk_1234abcdef5678 ``` ```bash JSON output for scripting theme={"theme":"kanagawa-wave"} unkey api keys whoami --key=sk_1234abcdef5678 --output=json ``` ```bash Extract the key ID theme={"theme":"kanagawa-wave"} KEY_ID=$(unkey api keys whoami --key=sk_1234abcdef5678 --output=json | jq -r '.data.keyId') echo $KEY_ID ``` ## Output Default output shows the request ID with latency, followed by the key details: ```text theme={"theme":"kanagawa-wave"} req_1234abcd (took 52ms) { "keyId": "key_1234abcd", "start": "sk_prod", "enabled": true, "name": "Production API Key", "createdAt": 1704067200000, "permissions": [ "documents.read", "documents.write" ], "roles": [ "editor" ] } ``` With `--output=json`, the full response envelope is returned: ```json theme={"theme":"kanagawa-wave"} { "meta": { "requestId": "req_1234abcd" }, "data": { "keyId": "key_1234abcd", "start": "sk_prod", "enabled": true, "name": "Production API Key", "createdAt": 1704067200000, "permissions": [ "documents.read", "documents.write" ], "roles": [ "editor" ] } } ``` # CLI Source: https://unkey.com/docs/cli/overview Install and use the Unkey CLI to manage API keys, rate limits, permissions, and identities from your terminal. Supports all core operations. The Unkey CLI gives you direct access to the Unkey API from your terminal. Create and manage API keys, configure rate limits, set permissions, and query analytics, all without leaving the command line. The CLI is early and provided on a best-effort basis. There are no breaking change guarantees for commands, flags, or output format. The underlying [API](/docs/api-reference/overview) is versioned and stable. ## Installation ```bash theme={"theme":"kanagawa-wave"} npm install -g unkey ``` Download the latest binary for your platform from [GitHub Releases](https://github.com/unkeyed/unkey/releases). ```bash theme={"theme":"kanagawa-wave"} # macOS (Apple Silicon) curl -L https://github.com/unkeyed/unkey/releases/latest/download/unkey_darwin_arm64 -o unkey chmod +x unkey sudo mv unkey /usr/local/bin/ ``` ## Authentication Before using the CLI, authenticate with your root key: ```bash theme={"theme":"kanagawa-wave"} unkey auth login ``` This stores your key at `~/.unkey/config.toml`. All subsequent commands use it automatically. You can also pass a key per-command or via environment variable: ```bash theme={"theme":"kanagawa-wave"} # Flag unkey api keys create-key --root-key=unkey_xxx --api-id=api_123 # Environment variable export UNKEY_ROOT_KEY=unkey_xxx unkey api keys create-key --api-id=api_123 ``` ## Usage All API operations live under the `unkey api` command, organized by resource: ```bash theme={"theme":"kanagawa-wave"} unkey api [flags] ``` ### Resources | Resource | Description | | ------------- | -------------------------------------------------------------------------- | | `apis` | Create, get, delete API namespaces and list their keys | | `keys` | Create, verify, update, delete keys and manage their permissions and roles | | `identities` | Create, get, update, delete identities for grouping keys | | `permissions` | Create, get, delete permissions and roles | | `ratelimit` | Apply rate limits and manage overrides | | `analytics` | Query key verification data with SQL | ### Examples **Create an API and issue a key:** ```bash theme={"theme":"kanagawa-wave"} unkey api apis create-api --name=payment-service-prod unkey api keys create-key --api-id=api_1234abcd --name='Production Key' --enabled ``` **Verify a key:** ```bash theme={"theme":"kanagawa-wave"} unkey api keys verify-key --key=sk_1234abcdef ``` **Set up rate limiting:** ```bash theme={"theme":"kanagawa-wave"} unkey api ratelimit limit --namespace=api.requests --identifier=user_123 --limit=100 --duration=60000 ``` **Query analytics:** ```bash theme={"theme":"kanagawa-wave"} unkey api analytics get-verifications --query="SELECT COUNT(*) as total FROM key_verifications_v1 WHERE outcome = 'VALID' AND time >= now() - INTERVAL 7 DAY" ``` **Manage permissions:** ```bash theme={"theme":"kanagawa-wave"} unkey api permissions create-permission --name=documents.read --slug=documents-read unkey api keys add-permissions --key-id=key_1234abcd --permissions=documents.read,documents.write ``` ## Output By default, the CLI prints the request ID with latency, followed by the response data: ```text theme={"theme":"kanagawa-wave"} req_2c9a0jf23l4k567 (took 45ms) { "id": "api_1234abcd", "name": "payment-service-prod" } ``` For scripting, use `--output=json` to get the raw API response envelope (meta + data) as JSON on stdout: ```bash theme={"theme":"kanagawa-wave"} unkey api apis create-api --name=my-api --output=json | jq '.data.id' ``` ## Global Flags Every command accepts these flags: | Flag | Description | | ------------ | ------------------------------------------------------------ | | `--root-key` | Override the root key (also: `UNKEY_ROOT_KEY` env var) | | `--api-url` | Override the API base URL (default: `https://api.unkey.com`) | | `--config` | Path to config file (default: `~/.unkey/config.toml`) | | `--output` | Output format. Use `json` for raw JSON | ## Help Every command has built-in help with descriptions, flag details, and examples: ```bash theme={"theme":"kanagawa-wave"} unkey --help unkey api --help unkey api keys --help unkey api keys create-key --help ``` # create-permission Source: https://unkey.com/docs/cli/permissions/create-permission Create a new permission in your Unkey workspace using the CLI to define specific actions or capabilities for your RBAC authorization system. 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 names must be unique within the workspace. Once created, permissions are immediately available for assignment. **Required permissions:** * `rbac.*.create_permission` See the [API reference](/docs/api-reference/permissions/create-permission) for the full HTTP endpoint documentation. ## Usage ```bash theme={"theme":"kanagawa-wave"} unkey api permissions create-permission [flags] ``` ## Flags Human-readable name describing the permission's purpose. Names must be unique within your workspace to prevent conflicts during assignment. Use clear, semantic names that developers can easily understand when building authorization logic. Consider using hierarchical naming conventions like `resource.action` for better organization. Must be 1-512 characters. URL-safe identifier for use in APIs and integrations. Must start with a letter and contain only letters, numbers, periods, underscores, and hyphens. Slugs are often used in REST endpoints, configuration files, and external integrations. Must be unique within your workspace. Must be 1-128 characters. Detailed documentation of what this permission grants access to. Include information about affected resources, allowed actions, and any important limitations. This internal documentation helps team members understand permission scope and security implications. Not visible to end users. Max 512 characters. ## Global Flags | Flag | Type | Description | | ------------ | ------ | -------------------------------------------------------- | | `--root-key` | string | Override root key (`$UNKEY_ROOT_KEY`) | | `--api-url` | string | Override API base URL (default: `https://api.unkey.com`) | | `--config` | string | Path to config file (default: `~/.unkey/config.toml`) | | `--output` | string | Output format. Use `json` for raw JSON | ## Examples ```bash Basic theme={"theme":"kanagawa-wave"} unkey api permissions create-permission --name=users.read --slug=users-read ``` ```bash With description theme={"theme":"kanagawa-wave"} unkey api permissions create-permission --name=billing.write --slug=billing-write --description="Grants write access to billing resources" ``` ```bash JSON output for scripting theme={"theme":"kanagawa-wave"} unkey api permissions create-permission --name=analytics.view --slug=analytics-view --output=json ``` ```bash Pipe the permission ID to another command theme={"theme":"kanagawa-wave"} PERM_ID=$(unkey api permissions create-permission --name=documents.read --slug=documents-read --output=json | jq -r '.data.permissionId') echo "Created permission: $PERM_ID" ``` ## Output Default output shows the request ID with latency, followed by the created permission: ```text theme={"theme":"kanagawa-wave"} req_2c9a0jf23l4k567 (took 45ms) { "permissionId": "perm_1234567890abcdef" } ``` With `--output=json`, the full response envelope is returned: ```json theme={"theme":"kanagawa-wave"} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "data": { "permissionId": "perm_1234567890abcdef" } } ``` # create-role Source: https://unkey.com/docs/cli/permissions/create-role Create a new role using the Unkey CLI to group related permissions together. Simplify access management by assigning roles instead of permissions. Create a new role to group related permissions for easier management. Roles enable consistent permission assignment across multiple API keys. **Important:** Role names must be unique within the workspace. Once created, roles are immediately available for assignment. **Required permissions:** * `rbac.*.create_role` See the [API reference](/docs/api-reference/permissions/create-role) for the full HTTP endpoint documentation. ## Usage ```bash theme={"theme":"kanagawa-wave"} unkey api permissions create-role [flags] ``` ## Flags The unique name for this role. Must be unique within your workspace and clearly indicate the role's purpose. Use descriptive names like `admin`, `editor`, or `billing_manager`. Must be 1-512 characters, start with a letter, and contain only letters, numbers, dots, hyphens, and underscores. Provides comprehensive documentation of what this role encompasses and what access it grants. Include information about the intended use case, what permissions should be assigned, and any important considerations. This internal documentation helps team members understand role boundaries and security implications. Not visible to end users. Maximum 512 characters. ## Global Flags | Flag | Type | Description | | ------------ | ------ | -------------------------------------------------------- | | `--root-key` | string | Override root key (`$UNKEY_ROOT_KEY`) | | `--api-url` | string | Override API base URL (default: `https://api.unkey.com`) | | `--config` | string | Path to config file (default: `~/.unkey/config.toml`) | | `--output` | string | Output format. Use `json` for raw JSON | ## Examples ```bash Basic theme={"theme":"kanagawa-wave"} unkey api permissions create-role --name=content.editor --description="Can read and write content" ``` ```bash Without description theme={"theme":"kanagawa-wave"} unkey api permissions create-role --name=api.reader ``` ```bash JSON output for scripting theme={"theme":"kanagawa-wave"} unkey api permissions create-role --name=admin.billing --description="Full billing access" --output=json ``` ```bash Pipe the role ID to another command theme={"theme":"kanagawa-wave"} ROLE_ID=$(unkey api permissions create-role --name=support.readonly --output=json | jq -r '.data.roleId') ``` ## Output Default output shows the request ID with latency, followed by the created role: ```text theme={"theme":"kanagawa-wave"} req_2c9a0jf23l4k567 (took 45ms) { "roleId": "role_5678efgh9012wxyz" } ``` With `--output=json`, the full response envelope is returned: ```json theme={"theme":"kanagawa-wave"} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "data": { "roleId": "role_5678efgh9012wxyz" } } ``` # delete-permission Source: https://unkey.com/docs/cli/permissions/delete-permission Remove a permission from your Unkey workspace using the CLI. Delete unused permissions to keep your RBAC authorization model clean and current. 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. Any verification requests checking for the deleted permission will fail. **Required permissions:** * `rbac.*.delete_permission` See the [API reference](/docs/api-reference/permissions/delete-permission) for the full HTTP endpoint documentation. ## Usage ```bash theme={"theme":"kanagawa-wave"} unkey api permissions delete-permission [flags] ``` ## Flags The permission ID or slug to permanently delete from your workspace. Must be 3-255 characters, start with a letter, and contain only letters, numbers, dots, hyphens, and underscores. WARNING: Deleting a permission has immediate and irreversible consequences: * All API keys with this permission will lose that access immediately * All roles containing this permission will have it removed * Any verification requests checking for this permission will fail * This action cannot be undone Before deletion, ensure you have updated any keys or roles that depend on this permission, migrated to alternative permissions if needed, and notified affected users about the access changes. ## Global Flags | Flag | Type | Description | | ------------ | ------ | -------------------------------------------------------- | | `--root-key` | string | Override root key (`$UNKEY_ROOT_KEY`) | | `--api-url` | string | Override API base URL (default: `https://api.unkey.com`) | | `--config` | string | Path to config file (default: `~/.unkey/config.toml`) | | `--output` | string | Output format. Use `json` for raw JSON | ## Examples ```bash Delete by permission ID theme={"theme":"kanagawa-wave"} unkey api permissions delete-permission --permission=perm_1234567890abcdef ``` ```bash Delete by permission slug theme={"theme":"kanagawa-wave"} unkey api permissions delete-permission --permission=documents.read ``` ```bash JSON output for scripting theme={"theme":"kanagawa-wave"} unkey api permissions delete-permission --permission=perm_1234567890abcdef --output=json ``` ## Output Default output shows the request ID with latency: ```text theme={"theme":"kanagawa-wave"} req_2c9a0jf23l4k567 (took 45ms) {} ``` With `--output=json`, the full response envelope is returned: ```json theme={"theme":"kanagawa-wave"} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "data": {} } ``` # delete-role Source: https://unkey.com/docs/cli/permissions/delete-role Remove a role from your Unkey workspace using the CLI. Delete unused roles and automatically unlink them from all associated API keys. 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. All keys with this role will lose the associated permissions, and access to resources protected by this role's permissions will be denied. **Required permissions:** * `rbac.*.delete_role` See the [API reference](/docs/api-reference/permissions/delete-role) for the full HTTP endpoint documentation. ## Usage ```bash theme={"theme":"kanagawa-wave"} unkey api permissions delete-role [flags] ``` ## Flags The role ID or name to permanently delete. Must either be a valid role ID that begins with `role_` or a role name that exists within your workspace. Must be 3-255 characters and contain only letters, numbers, underscores, colons, hyphens, dots, and asterisks. ## Global Flags | Flag | Type | Description | | ------------ | ------ | -------------------------------------------------------- | | `--root-key` | string | Override root key (`$UNKEY_ROOT_KEY`) | | `--api-url` | string | Override API base URL (default: `https://api.unkey.com`) | | `--config` | string | Path to config file (default: `~/.unkey/config.toml`) | | `--output` | string | Output format. Use `json` for raw JSON | ## Examples ```bash Basic theme={"theme":"kanagawa-wave"} unkey api permissions delete-role --role=role_dns_manager ``` ```bash Delete by name theme={"theme":"kanagawa-wave"} unkey api permissions delete-role --role=admin ``` ```bash JSON output for scripting theme={"theme":"kanagawa-wave"} unkey api permissions delete-role --role=role_dns_manager --output=json ``` ## Output Default output shows the request ID with latency: ```text theme={"theme":"kanagawa-wave"} req_2c9a0jf23l4k567 (took 38ms) {} ``` With `--output=json`, the full response envelope is returned: ```json theme={"theme":"kanagawa-wave"} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "data": {} } ``` # get-permission Source: https://unkey.com/docs/cli/permissions/get-permission Retrieve details about a specific permission in your Unkey workspace using the CLI including its name, description, and creation date. Retrieve details about a specific permission including its name, description, and metadata. Use this to inspect a permission's current state, verify its configuration, or look up its name and description. The returned data includes the permission's unique ID, human-readable name, URL-safe slug, and optional description. **Required permissions:** * `rbac.*.read_permission` (to read permissions in any workspace) See the [API reference](/docs/api-reference/permissions/get-permission) for the full HTTP endpoint documentation. ## Usage ```bash theme={"theme":"kanagawa-wave"} unkey api permissions get-permission [flags] ``` ## Flags The unique identifier of the permission to retrieve. Must be a valid permission ID that begins with `perm_` and exists within your workspace. ## Global Flags | Flag | Type | Description | | ------------ | ------ | -------------------------------------------------------- | | `--root-key` | string | Override root key (`$UNKEY_ROOT_KEY`) | | `--api-url` | string | Override API base URL (default: `https://api.unkey.com`) | | `--config` | string | Path to config file (default: `~/.unkey/config.toml`) | | `--output` | string | Output format. Use `json` for raw JSON | ## Examples ```bash Basic theme={"theme":"kanagawa-wave"} unkey api permissions get-permission --permission=perm_1234567890abcdef ``` ```bash JSON output for scripting theme={"theme":"kanagawa-wave"} unkey api permissions get-permission --permission=perm_1234567890abcdef --output=json ``` ```bash Pipe the permission name to another command theme={"theme":"kanagawa-wave"} PERM_NAME=$(unkey api permissions get-permission --permission=perm_1234567890abcdef --output=json | jq -r '.data.name') echo "Permission name: $PERM_NAME" ``` ## Output Default output shows the request ID with latency, followed by the permission details: ```text theme={"theme":"kanagawa-wave"} req_1234abcd (took 38ms) { "id": "perm_1234567890abcdef", "name": "documents.read", "slug": "documents-read", "description": "Allows reading document resources" } ``` With `--output=json`, the full response envelope is returned: ```json theme={"theme":"kanagawa-wave"} { "meta": { "requestId": "req_1234abcd" }, "data": { "id": "perm_1234567890abcdef", "name": "documents.read", "slug": "documents-read", "description": "Allows reading document resources" } } ``` # get-role Source: https://unkey.com/docs/cli/permissions/get-role Retrieve details about a specific role using the Unkey CLI including its name, description, and list of assigned permissions for RBAC review. Retrieve details about a specific role including its assigned permissions. Use this to verify role configuration, check which permissions are currently assigned, or retrieve metadata for access review. Accepts either a role ID (starting with `role_`) or a role name. **Required permissions:** * `rbac.*.read_role` (to read roles in any workspace) See the [API reference](/docs/api-reference/permissions/get-role) for the full HTTP endpoint documentation. ## Usage ```bash theme={"theme":"kanagawa-wave"} unkey api permissions get-role [flags] ``` ## Flags Role ID (starting with `role_`) or role name to retrieve. Must be 3-255 characters and contain only letters, numbers, dots, hyphens, underscores, colons, and asterisks. Returns complete role information including all assigned permissions. ## Global Flags | Flag | Type | Description | | ------------ | ------ | -------------------------------------------------------- | | `--root-key` | string | Override root key (`$UNKEY_ROOT_KEY`) | | `--api-url` | string | Override API base URL (default: `https://api.unkey.com`) | | `--config` | string | Path to config file (default: `~/.unkey/config.toml`) | | `--output` | string | Output format. Use `json` for raw JSON | ## Examples ```bash By role ID theme={"theme":"kanagawa-wave"} unkey api permissions get-role --role=role_1234567890abcdef ``` ```bash By role name theme={"theme":"kanagawa-wave"} unkey api permissions get-role --role=my-role-name ``` ```bash JSON output for scripting theme={"theme":"kanagawa-wave"} unkey api permissions get-role --role=role_1234567890abcdef --output=json ``` ```bash Extract permissions from a role theme={"theme":"kanagawa-wave"} unkey api permissions get-role --role=support.readonly --output=json | jq '.data.permissions[].name' ``` ## Output Default output shows the request ID with latency, followed by the role details: ```text theme={"theme":"kanagawa-wave"} req_2c9a0jf23l4k567 (took 45ms) { "id": "role_1234567890abcdef", "name": "support.readonly", "description": "Provides read-only access for customer support representatives", "permissions": [ { "id": "perm_abc123", "name": "tickets.read" } ] } ``` With `--output=json`, the full response envelope is returned: ```json theme={"theme":"kanagawa-wave"} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "data": { "id": "role_1234567890abcdef", "name": "support.readonly", "description": "Provides read-only access for customer support representatives", "permissions": [ { "id": "perm_abc123", "name": "tickets.read" } ] } } ``` # list-permissions Source: https://unkey.com/docs/cli/permissions/list-permissions List all permissions in your Unkey workspace using the CLI. View every permission defined in your RBAC system for auditing and management. Retrieve all permissions in your workspace. Results are paginated and sorted by their id. Use the `--limit` and `--cursor` flags to traverse large permission sets efficiently. **Required permissions:** * `rbac.*.read_permission` See the [API reference](/docs/api-reference/permissions/list-permissions) for the full HTTP endpoint documentation. ## Usage ```bash theme={"theme":"kanagawa-wave"} unkey api permissions list-permissions [flags] ``` ## Flags Maximum number of permissions to return in a single response. Accepts a value between 1 and 100. Defaults to 100 when omitted. Pagination cursor from a previous response to fetch the next page of permissions. Include this value when you need to retrieve additional permissions beyond the initial response. Leave empty or omit this flag to start from the beginning of the permission list. Cursors are temporary and may expire -- always handle cases where a cursor becomes invalid. ## Global Flags | Flag | Type | Description | | ------------ | ------ | -------------------------------------------------------- | | `--root-key` | string | Override root key (`$UNKEY_ROOT_KEY`) | | `--api-url` | string | Override API base URL (default: `https://api.unkey.com`) | | `--config` | string | Path to config file (default: `~/.unkey/config.toml`) | | `--output` | string | Output format. Use `json` for raw JSON | ## Examples ```bash Basic theme={"theme":"kanagawa-wave"} unkey api permissions list-permissions ``` ```bash Limit results theme={"theme":"kanagawa-wave"} unkey api permissions list-permissions --limit=50 ``` ```bash Paginate with cursor theme={"theme":"kanagawa-wave"} unkey api permissions list-permissions --limit=50 --cursor=eyJrZXkiOiJwZXJtXzEyMzQifQ== ``` ```bash JSON output for scripting theme={"theme":"kanagawa-wave"} unkey api permissions list-permissions --output=json ``` ```bash Iterate all pages theme={"theme":"kanagawa-wave"} CURSOR="" while true; do RESP=$(unkey api permissions list-permissions --limit=50 --output=json ${CURSOR:+--cursor=$CURSOR}) echo "$RESP" | jq '.data[]' HAS_MORE=$(echo "$RESP" | jq -r '.pagination.hasMore') [ "$HAS_MORE" != "true" ] && break CURSOR=$(echo "$RESP" | jq -r '.pagination.cursor') done ``` ## Output Default output shows the request ID with latency, followed by the list of permissions: ```text theme={"theme":"kanagawa-wave"} req_2c9a0jf23l4k567 (took 32ms) [ { "id": "perm_1234567890abcdef", "name": "users.read", "slug": "users-read", "description": "Allows reading user profile information and account details" } ] ``` With `--output=json`, the full response envelope including pagination metadata is returned: ```json theme={"theme":"kanagawa-wave"} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "data": [ { "id": "perm_1234567890abcdef", "name": "users.read", "slug": "users-read", "description": "Allows reading user profile information and account details" } ], "pagination": { "hasMore": true, "cursor": "eyJrZXkiOiJwZXJtXzEyMzQiLCJ0cyI6MTY5OTM3ODgwMH0=" } } ``` # list-roles Source: https://unkey.com/docs/cli/permissions/list-roles List all roles in your Unkey workspace using the CLI, including their assigned permissions. Review your full RBAC configuration at a glance. Retrieve all roles in your workspace including their assigned permissions. Results are paginated and sorted by their id. Use this to audit your access control setup, verify role-permission mappings, or build automation that reacts to your current RBAC configuration. **Required permissions:** * `rbac.*.read_role` See the [API reference](/docs/api-reference/permissions/list-roles) for the full HTTP endpoint documentation. ## Usage ```bash theme={"theme":"kanagawa-wave"} unkey api permissions list-roles [flags] ``` ## Flags Maximum number of roles to return in a single response (1-100, default 100). Use smaller values for faster response times and better UI performance. Use larger values when you need to process many roles efficiently. Results exceeding this limit will be paginated with a cursor for continuation. Pagination cursor from a previous response to fetch the next page of roles. Include this when you need to retrieve additional roles beyond the first page. Each response containing more results will include a cursor value that can be used here. Leave empty or omit this flag to start from the beginning of the role list. ## Global Flags | Flag | Type | Description | | ------------ | ------ | -------------------------------------------------------- | | `--root-key` | string | Override root key (`$UNKEY_ROOT_KEY`) | | `--api-url` | string | Override API base URL (default: `https://api.unkey.com`) | | `--config` | string | Path to config file (default: `~/.unkey/config.toml`) | | `--output` | string | Output format. Use `json` for raw JSON | ## Examples ```bash Basic theme={"theme":"kanagawa-wave"} unkey api permissions list-roles ``` ```bash Limit results theme={"theme":"kanagawa-wave"} unkey api permissions list-roles --limit=50 ``` ```bash Paginate through results theme={"theme":"kanagawa-wave"} unkey api permissions list-roles --limit=50 --cursor=eyJrZXkiOiJyb2xlXzEyMzQifQ== ``` ```bash JSON output for scripting theme={"theme":"kanagawa-wave"} unkey api permissions list-roles --output=json ``` ## Output Default output shows the request ID with latency, followed by the list of roles and their permissions: ```text theme={"theme":"kanagawa-wave"} req_2c9a0jf23l4k567 (took 32ms) [ { "id": "role_1234567890abcdef", "name": "support.readonly", "description": "Provides read-only access for customer support representatives", "permissions": [ { "id": "perm_abc123", "name": "tickets.read" } ] } ] ``` With `--output=json`, the full response envelope is returned including pagination metadata: ```json theme={"theme":"kanagawa-wave"} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "data": [ { "id": "role_1234567890abcdef", "name": "support.readonly", "description": "Provides read-only access for customer support representatives", "permissions": [ { "id": "perm_abc123", "name": "tickets.read" } ] } ], "pagination": { "cursor": "eyJrZXkiOiJyb2xlXzEyMzQiLCJ0cyI6MTY5OTM3ODgwMH0=", "hasMore": true } } ``` # delete-override Source: https://unkey.com/docs/cli/ratelimit/delete-override Remove a rate limit override using the Unkey CLI so the affected identifier reverts to the namespace default. Clean up custom rate limits. 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. **Required permissions:** * `ratelimit.*.delete_override` (workspace-wide) * `ratelimit..delete_override` (scoped to a specific namespace) See the [API reference](/docs/api-reference/ratelimit/delete-ratelimit-override) for the full HTTP endpoint documentation. ## Usage ```bash theme={"theme":"kanagawa-wave"} unkey api ratelimit delete-override [flags] ``` ## Flags The id or name of the namespace containing the override. Must be 1-255 characters. The exact identifier pattern of the override to delete. This must match exactly as it was specified when creating the override, including any wildcards (`*`) that were part of the original pattern. Case-sensitive. Must be 1-255 characters. After deletion, any identifiers previously affected by this override will immediately revert to using the default rate limit for the namespace. ## Global Flags | Flag | Type | Description | | ------------ | ------ | -------------------------------------------------------- | | `--root-key` | string | Override root key (`$UNKEY_ROOT_KEY`) | | `--api-url` | string | Override API base URL (default: `https://api.unkey.com`) | | `--config` | string | Path to config file (default: `~/.unkey/config.toml`) | | `--output` | string | Output format. Use `json` for raw JSON | ## Examples ```bash Delete a specific override theme={"theme":"kanagawa-wave"} unkey api ratelimit delete-override --namespace=api.requests --identifier=premium_user_123 ``` ```bash Delete a wildcard pattern override theme={"theme":"kanagawa-wave"} unkey api ratelimit delete-override --namespace=api.requests --identifier="premium_*" ``` ```bash JSON output for scripting theme={"theme":"kanagawa-wave"} unkey api ratelimit delete-override --namespace=api.requests --identifier=premium_user_123 --output=json ``` ## Output Default output shows the request ID with latency, followed by the result: ```text theme={"theme":"kanagawa-wave"} req_2cGKbMxRyIzhCxo1Idjz8q (took 45ms) {} ``` With `--output=json`, the full response envelope is returned: ```json theme={"theme":"kanagawa-wave"} { "meta": { "requestId": "req_2cGKbMxRyIzhCxo1Idjz8q" }, "data": {} } ``` # get-override Source: https://unkey.com/docs/cli/ratelimit/get-override Retrieve the configuration of a rate limit override by identifier using the Unkey CLI. Inspect custom limits applied to specific users or keys. 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. This is case-sensitive -- for example, if the override was created for `premium_*`, you must use `premium_*` here, not a specific ID like `premium_user1`. **Required permissions:** * `ratelimit.*.read_override` (to read overrides in any namespace) * `ratelimit..read_override` (to read overrides in a specific namespace) See the [API reference](/docs/api-reference/ratelimit/get-ratelimit-override) for the full HTTP endpoint documentation. ## Usage ```bash theme={"theme":"kanagawa-wave"} unkey api ratelimit get-override [flags] ``` ## Flags The id or name of the namespace containing the override. Must be 1-255 characters. The exact identifier pattern of the override to retrieve. This must match exactly as it was specified when creating the override, including any wildcards (`*`) that were part of the original pattern. Must be 1-255 characters. ## Global Flags | Flag | Type | Description | | ------------ | ------ | -------------------------------------------------------- | | `--root-key` | string | Override root key (`$UNKEY_ROOT_KEY`) | | `--api-url` | string | Override API base URL (default: `https://api.unkey.com`) | | `--config` | string | Path to config file (default: `~/.unkey/config.toml`) | | `--output` | string | Output format. Use `json` for raw JSON | ## Examples ```bash Specific identifier theme={"theme":"kanagawa-wave"} unkey api ratelimit get-override --namespace=api.requests --identifier=premium_user_123 ``` ```bash Wildcard pattern theme={"theme":"kanagawa-wave"} unkey api ratelimit get-override --namespace=api.requests --identifier="premium_*" ``` ```bash JSON output for scripting theme={"theme":"kanagawa-wave"} unkey api ratelimit get-override --namespace=api.requests --identifier=premium_user_123 --output=json ``` ```bash Pipe override limit to another command theme={"theme":"kanagawa-wave"} LIMIT=$(unkey api ratelimit get-override --namespace=api.requests --identifier=premium_user_123 --output=json | jq -r '.data.limit') echo "Current limit: $LIMIT" ``` ## Output Default output shows the request ID with latency, followed by the override configuration: ```text theme={"theme":"kanagawa-wave"} req_2cGKbMxRyIzhCxo1Idjz8q (took 45ms) { "overrideId": "ovr_1234567890abcdef", "namespaceId": "ns_1234567890abcdef", "identifier": "premium_user_123", "limit": 1000, "duration": 60000 } ``` With `--output=json`, the full response envelope is returned: ```json theme={"theme":"kanagawa-wave"} { "meta": { "requestId": "req_2cGKbMxRyIzhCxo1Idjz8q" }, "data": { "overrideId": "ovr_1234567890abcdef", "namespaceId": "ns_1234567890abcdef", "identifier": "premium_user_123", "limit": 1000, "duration": 60000 } } ``` # limit Source: https://unkey.com/docs/cli/ratelimit/limit Check and enforce a rate limit for any identifier using the Unkey CLI. Test rate limiting behavior locally before deploying to production. 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. **Important:** 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. **Required permissions:** * `ratelimit.*.limit` (to check limits in any namespace) * `ratelimit..limit` (to check limits in a specific namespace) See the [API reference](/docs/api-reference/ratelimit/apply-rate-limiting) for the full HTTP endpoint documentation. ## Usage ```bash theme={"theme":"kanagawa-wave"} unkey api ratelimit limit [flags] ``` ## Flags The id or name of the namespace. Use namespaces to organize rate limits by service or purpose, such as `api.requests` or `auth.login`. Must be 1-255 characters. The entity being rate limited. Use user IDs for per-user limits, IP addresses for anonymous limiting, or API key IDs for per-key limits. The same identifier can be used across different namespaces to apply multiple rate limit types. Must be 1-255 characters. Maximum operations allowed within the duration window before requests are rejected. When this limit is reached, subsequent requests fail until the window resets. Balance user experience with resource protection when setting limits for different user tiers. Rate limit window duration in milliseconds after which the counter resets. Shorter durations enable faster recovery but may be less effective against sustained abuse. Common values include `60000` (1 minute), `3600000` (1 hour), and `86400000` (24 hours). How much of the rate limit quota this request consumes, enabling weighted rate limiting. Defaults to `1`. Use higher values for resource-intensive operations and `0` for tracking without limiting. When accumulated cost exceeds the limit within the duration window, subsequent requests are rejected. ## Global Flags | Flag | Type | Description | | ------------ | ------ | -------------------------------------------------------- | | `--root-key` | string | Override root key (`$UNKEY_ROOT_KEY`) | | `--api-url` | string | Override API base URL (default: `https://api.unkey.com`) | | `--config` | string | Path to config file (default: `~/.unkey/config.toml`) | | `--output` | string | Output format. Use `json` for raw JSON | ## Examples ```bash Basic rate limit check theme={"theme":"kanagawa-wave"} unkey api ratelimit limit --namespace=api.requests --identifier=user_abc123 --limit=100 --duration=60000 ``` ```bash IP-based rate limiting theme={"theme":"kanagawa-wave"} unkey api ratelimit limit --namespace=auth.login --identifier=203.0.113.42 --limit=5 --duration=60000 ``` ```bash Weighted cost for heavy operations theme={"theme":"kanagawa-wave"} unkey api ratelimit limit --namespace=api.heavy_operations --identifier=user_def456 --limit=50 --duration=3600000 --cost=5 ``` ```bash JSON output for scripting theme={"theme":"kanagawa-wave"} unkey api ratelimit limit --namespace=api.requests --identifier=user_abc123 --limit=100 --duration=60000 --output=json ``` ## Output Default output shows the request ID with latency, followed by the rate limit result: ```text theme={"theme":"kanagawa-wave"} req_01H9TQPP77V5E48E9SH0BG0ZQX (took 32ms) { "limit": 100, "remaining": 99, "reset": 1714582980000, "success": true } ``` With `--output=json`, the full response envelope is returned: ```json theme={"theme":"kanagawa-wave"} { "meta": { "requestId": "req_01H9TQPP77V5E48E9SH0BG0ZQX" }, "data": { "limit": 100, "remaining": 99, "reset": 1714582980000, "success": true } } ``` When a rate limit override is in effect, the response includes the override ID: ```json theme={"theme":"kanagawa-wave"} { "meta": { "requestId": "req_01H9TQPP77V5E48E9SH0BG0ZQZ" }, "data": { "limit": 1000, "remaining": 995, "reset": 1714582980000, "success": true, "overrideId": "ovr_2cGKbMxRyIzhCxo1Idjz8q" } } ``` # list-overrides Source: https://unkey.com/docs/cli/ratelimit/list-overrides List all rate limit overrides in a namespace using the Unkey CLI. View paginated results of custom limits applied to specific identifiers. 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. **Required permissions:** * `ratelimit.*.read_override` or `ratelimit..read_override` See the [API reference](/docs/api-reference/ratelimit/list-ratelimit-overrides) for the full HTTP endpoint documentation. ## Usage ```bash theme={"theme":"kanagawa-wave"} unkey api ratelimit list-overrides [flags] ``` ## Flags The ID or name of the rate limit namespace to list overrides for. Must be 1-255 characters. Maximum number of override entries to return in a single response. Use this to control response size and loading performance. * Lower values (10-20): Better for UI displays and faster response times * Higher values (50-100): Better for data exports or bulk operations * Default (10): Suitable for most dashboard views Results exceeding this limit will be paginated, with a cursor provided for fetching subsequent pages. Must be between 1 and 100. Pagination cursor from a previous response. Include this when fetching subsequent pages of results. Each response containing more results than the requested limit will include a cursor value that can be used here. ## Global Flags | Flag | Type | Description | | ------------ | ------ | -------------------------------------------------------- | | `--root-key` | string | Override root key (`$UNKEY_ROOT_KEY`) | | `--api-url` | string | Override API base URL (default: `https://api.unkey.com`) | | `--config` | string | Path to config file (default: `~/.unkey/config.toml`) | | `--output` | string | Output format. Use `json` for raw JSON | ## Examples ```bash Basic theme={"theme":"kanagawa-wave"} unkey api ratelimit list-overrides --namespace=api.requests ``` ```bash With custom page size theme={"theme":"kanagawa-wave"} unkey api ratelimit list-overrides --namespace=api.requests --limit=50 ``` ```bash Paginate through results theme={"theme":"kanagawa-wave"} unkey api ratelimit list-overrides --namespace=api.requests --cursor=cursor_eyJsYXN0SWQiOiJvdnJfM2RITGNOeVN6SnppRHlwMkpla2E5ciJ9 ``` ```bash JSON output for scripting theme={"theme":"kanagawa-wave"} unkey api ratelimit list-overrides --namespace=api.requests --output=json ``` ## Output Default output shows the request ID with latency, followed by the list of overrides: ```text theme={"theme":"kanagawa-wave"} req_2cGKbMxRyIzhCxo1Idjz8q (took 38ms) { "overrides": [ { "overrideId": "ovr_1234567890abcdef", "identifier": "premium_user_123", "limit": 1000, "duration": 60000 }, { "overrideId": "ovr_2345678901bcdefg", "identifier": "premium_*", "limit": 500, "duration": 60000 } ], "cursor": "cursor_eyJsYXN0SWQiOiJvdnJfMmhGTGNOeVN6SnppRHlwMkpla2E5ciJ9" } ``` With `--output=json`, the full response envelope is returned: ```json theme={"theme":"kanagawa-wave"} { "meta": { "requestId": "req_2cGKbMxRyIzhCxo1Idjz8q" }, "data": { "overrides": [ { "overrideId": "ovr_1234567890abcdef", "identifier": "premium_user_123", "limit": 1000, "duration": 60000 }, { "overrideId": "ovr_2345678901bcdefg", "identifier": "premium_*", "limit": 500, "duration": 60000 } ], "cursor": "cursor_eyJsYXN0SWQiOiJvdnJfMmhGTGNOeVN6SnppRHlwMkpla2E5ciJ9" } } ``` # multi-limit Source: https://unkey.com/docs/cli/ratelimit/multi-limit Check and enforce multiple rate limits in a single request using the Unkey CLI. Apply concurrent limits for different scopes or time windows. 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:** * `ratelimit.*.limit` (to check limits in any namespace) * `ratelimit..limit` (to check limits in all specific namespaces being checked) See the [API reference](/docs/api-reference/ratelimit/apply-multiple-rate-limit-checks) for the full HTTP endpoint documentation. ## Usage ```bash theme={"theme":"kanagawa-wave"} unkey api ratelimit multi-limit [flags] ``` ## Flags JSON array of rate limit check objects. Each object defines an independent rate limit check to perform. The id or name of the namespace. Use namespaces to organize rate limits by concern, such as `api.requests` or `auth.login`. Must be 1-255 characters. Defines the scope of rate limiting by identifying the entity being limited. Use user IDs for per-user limits, IP addresses for anonymous limiting, or API key IDs for per-key limits. The same identifier can be used across different namespaces to apply multiple rate limit types. Must be 1-255 characters. Sets the maximum operations allowed within the duration window before requests are rejected. When this limit is reached, subsequent requests fail until the window resets. Minimum: 1. Sets the rate limit window duration in milliseconds after which the counter resets. Common values include 60000 (1 minute), 3600000 (1 hour), and 86400000 (24 hours). Range: 1,000-2,592,000,000 (1 second to 30 days). Sets how much of the rate limit quota this request consumes, enabling weighted rate limiting. Use higher values for resource-intensive operations and 0 for tracking without limiting. Minimum: 0. ## Global Flags | Flag | Type | Description | | ------------ | ------ | -------------------------------------------------------- | | `--root-key` | string | Override root key (`$UNKEY_ROOT_KEY`) | | `--api-url` | string | Override API base URL (default: `https://api.unkey.com`) | | `--config` | string | Path to config file (default: `~/.unkey/config.toml`) | | `--output` | string | Output format -- use `json` for raw JSON | ## Examples ```bash Multiple namespace checks theme={"theme":"kanagawa-wave"} unkey api ratelimit multi-limit \ --limits-json='[{"namespace":"api.requests","identifier":"user_abc123","limit":100,"duration":60000},{"namespace":"auth.login","identifier":"user_abc123","limit":5,"duration":60000}]' ``` ```bash Weighted cost checks theme={"theme":"kanagawa-wave"} unkey api ratelimit multi-limit \ --limits-json='[{"namespace":"api.light_operations","identifier":"user_xyz789","limit":100,"duration":60000,"cost":1},{"namespace":"api.heavy_operations","identifier":"user_xyz789","limit":50,"duration":3600000,"cost":5}]' ``` ```bash JSON output for scripting theme={"theme":"kanagawa-wave"} unkey api ratelimit multi-limit \ --limits-json='[{"namespace":"api.requests","identifier":"user_abc123","limit":100,"duration":60000}]' \ --output=json ``` ## Output Default output shows the request ID with latency, followed by the multi-limit result: ```text theme={"theme":"kanagawa-wave"} req_01H9TQPP77V5E48E9SH0BG0ZQX (took 38ms) { "passed": true, "limits": [ { "namespace": "api.requests", "identifier": "user_abc123", "limit": 100, "remaining": 99, "reset": 1714582980000, "passed": true }, { "namespace": "auth.login", "identifier": "user_abc123", "limit": 5, "remaining": 4, "reset": 1714582980000, "passed": true } ] } ``` With `--output=json`, the full response envelope is returned: ```json theme={"theme":"kanagawa-wave"} { "meta": { "requestId": "req_01H9TQPP77V5E48E9SH0BG0ZQX" }, "data": { "passed": true, "limits": [ { "namespace": "api.requests", "identifier": "user_abc123", "limit": 100, "remaining": 99, "reset": 1714582980000, "passed": true }, { "namespace": "auth.login", "identifier": "user_abc123", "limit": 5, "remaining": 4, "reset": 1714582980000, "passed": true } ] } } ``` # set-override Source: https://unkey.com/docs/cli/ratelimit/set-override Create or update a custom rate limit override for a specific identifier using the Unkey CLI. Bypass namespace defaults for individual users. 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. Overrides take effect immediately and completely replace the default limit for matching identifiers. **Important:** Use wildcard patterns (e.g., `premium_*`) to match multiple identifiers. Set `--limit=0` to completely block access for an identifier. **Required permissions:** * `ratelimit.*.set_override` (to set overrides in any namespace) * `ratelimit..set_override` (to set overrides in a specific namespace) See the [API reference](/docs/api-reference/ratelimit/set-ratelimit-override) for the full HTTP endpoint documentation. ## Usage ```bash theme={"theme":"kanagawa-wave"} unkey api ratelimit set-override [flags] ``` ## Flags The ID or name of the rate limit namespace. Must be 1-255 characters. Identifier of the entity receiving this custom rate limit. This can be a specific user ID, an IP address, an email domain, or any other string that identifies the target entity. Must be 1-255 characters. Wildcards (`*`) can be used to create pattern-matching rules that apply to multiple identifiers. For example: * `premium_*` matches all identifiers starting with `premium_` * `*_admin` matches all identifiers ending with `_admin` * `*suspicious*` matches any identifier containing `suspicious` The maximum number of requests allowed for this override. This defines the custom quota for the specified identifier(s) and entirely replaces the default limit for matching identifiers. Minimum value is `0`. Special values: * **Higher than default** -- for premium or trusted entities * **Lower than default** -- for suspicious or abusive entities * **0** -- to completely block access (useful for ban implementation) The duration in milliseconds for the rate limit window. This defines how long the rate limit counter accumulates before resetting to zero. Minimum value is `1000`. Common values: `60000` (1 minute), `3600000` (1 hour), `86400000` (1 day). This can differ from the default duration for the namespace. ## Global Flags | Flag | Type | Description | | ------------ | ------ | -------------------------------------------------------- | | `--root-key` | string | Override root key (`$UNKEY_ROOT_KEY`) | | `--api-url` | string | Override API base URL (default: `https://api.unkey.com`) | | `--config` | string | Path to config file (default: `~/.unkey/config.toml`) | | `--output` | string | Output format. Use `json` for raw JSON | ## Examples ```bash Premium user with higher limit theme={"theme":"kanagawa-wave"} unkey api ratelimit set-override --namespace=api.requests --identifier=premium_user_123 --limit=1000 --duration=60000 ``` ```bash Wildcard pattern for multiple identifiers theme={"theme":"kanagawa-wave"} unkey api ratelimit set-override --namespace=api.requests --identifier='premium_*' --limit=500 --duration=60000 ``` ```bash Block a specific identifier theme={"theme":"kanagawa-wave"} unkey api ratelimit set-override --namespace=api.requests --identifier=abusive_user_456 --limit=0 --duration=60000 ``` ```bash JSON output for scripting theme={"theme":"kanagawa-wave"} unkey api ratelimit set-override --namespace=api.requests --identifier=partner_acme --limit=2000 --duration=60000 --output=json ``` ## Output Default output shows the request ID with latency, followed by the created or updated override: ```text theme={"theme":"kanagawa-wave"} req_2cGKbMxRyIzhCxo1Idjz8q (took 45ms) { "overrideId": "ovr_1234567890abcdef" } ``` With `--output=json`, the full response envelope is returned: ```json theme={"theme":"kanagawa-wave"} { "meta": { "requestId": "req_2cGKbMxRyIzhCxo1Idjz8q" }, "data": { "overrideId": "ovr_1234567890abcdef" } } ``` # Cloudflare Workers Source: https://unkey.com/docs/cookbook/cloudflare-workers Authenticate API keys in Cloudflare Workers with Unkey. A copy-paste recipe for low-latency, edge-based key verification using @unkey/api. Protect your Cloudflare Workers with Unkey's globally distributed verification. ## Install ```bash theme={"theme":"kanagawa-wave"} npm create cloudflare@latest my-api cd my-api npm install @unkey/api ``` ## Basic Worker ```typescript src/index.ts theme={"theme":"kanagawa-wave"} import { Unkey } from "@unkey/api"; export interface Env { UNKEY_ROOT_KEY: string; } export default { async fetch(request: Request, env: Env): Promise { // 1. Extract API key const authHeader = request.headers.get("Authorization"); if (!authHeader?.startsWith("Bearer ")) { return Response.json({ error: "Missing API key" }, { status: 401 }); } const unkey = new Unkey({ rootKey: env.UNKEY_ROOT_KEY }); const apiKey = authHeader.slice(7); try { // 2. Verify with Unkey (throws on transport/SDK errors — handled below) const { data } = await unkey.keys.verifyKey({ key: apiKey, }); // 3. Check validity if (!data.valid) { return Response.json( { error: data.code }, { status: data.code === "RATE_LIMITED" ? 429 : 401 }, ); } // 4. Request is authenticated return Response.json({ message: "Access granted", user: data.identity?.externalId, remaining: data.credits, }); } catch (error) { console.error("Unkey error:", error); return Response.json( { error: "Authentication service unavailable" }, { status: 503 }, ); } }, }; ``` ## Configure Secrets ```bash theme={"theme":"kanagawa-wave"} npx wrangler secret put UNKEY_ROOT_KEY # Enter your API ID when prompted ``` *** ## With Hono For a cleaner routing experience, use Hono: ```bash theme={"theme":"kanagawa-wave"} npm install hono @unkey/hono ``` ```typescript src/index.ts theme={"theme":"kanagawa-wave"} import { Hono } from "hono"; import { unkey } from "@unkey/hono"; type Bindings = { UNKEY_ROOT_KEY: string; }; const app = new Hono<{ Bindings: Bindings }>(); // Public routes app.get("/health", (c) => c.json({ status: "ok" })); // Protected routes with Unkey middleware app.use("/api/*", async (c, next) => { const handler = unkey({ rootKey: c.env.UNKEY_ROOT_KEY, getKey: (c) => c.req.header("Authorization")?.replace("Bearer ", ""), onError: (c, error) => { console.error("Unkey error:", error); return c.json({ error: "Service unavailable" }, 503); }, handleInvalidKey: (c, result) => { return c.json({ error: result.code }, 401); }, }); return handler(c, next); }); app.get("/api/data", (c) => { const auth = c.get("unkey"); return c.json({ message: "Access granted", user: auth.identity?.externalId, remaining: auth.credits, }); }); app.get("/api/users", (c) => { return c.json({ users: [] }); }); export default app; ``` *** ## Reusable Auth Middleware Create a clean middleware pattern: ```typescript src/middleware/auth.ts theme={"theme":"kanagawa-wave"} import { Unkey } from "@unkey/api"; import { UnkeyError } from "@unkey/api/models/errors"; import { V2KeysVerifyKeyResponseData } from "@unkey/api/models/components"; import { Context, Next } from "hono"; declare module "hono" { interface ContextVariableMap { auth: V2KeysVerifyKeyResponseData; } } interface AuthOptions { getKey?: (c: Context) => string | null; permissions?: string; } type Bindings = { UNKEY_ROOT_KEY: string; }; export function authMiddleware(options: AuthOptions = {}) { return async (c: Context, next: Next) => { const getKey = options.getKey ?? ((c) => c.req.header("Authorization")?.replace("Bearer ", "") ?? null); const apiKey = getKey(c); if (!apiKey) { return c.json({ error: "Missing API key" }, 401); } try { const unkey = new Unkey({ rootKey: c.env.UNKEY_ROOT_KEY }); const { data } = await unkey.keys.verifyKey({ key: apiKey, permissions: options.permissions, }); if (!data.valid) { if (data.code === "INSUFFICIENT_PERMISSIONS") { return c.json({ error: "Forbidden" }, 403); } return c.json({ error: data.code }, { status: 401 }); } c.set("auth", data); await next(); } catch (err) { return c.json({ error: "Service unavailable" }, 503); } }; } ``` Use it: ```typescript src/index.ts theme={"theme":"kanagawa-wave"} import { Hono } from "hono"; import { authMiddleware } from "./middleware/auth"; const app = new Hono(); // Basic auth app.use("/api/*", (c, next) => authMiddleware()(c, next)); // Permission-based auth for admin routes app.use("/admin/*", (c, next) => authMiddleware({ permissions: "admin", })(c, next), ); app.get("/api/data", (c) => { const auth = c.get("auth"); return c.json({ user: auth.identity?.externalId }); }); app.delete("/admin/users/:id", (c) => { // Only accessible with admin permission return c.json({ deleted: c.req.param("id") }); }); export default app; ``` *** ## Rate Limit Headers Add standard rate limit headers: ```typescript src/middleware/auth.ts theme={"theme":"kanagawa-wave"} import { Unkey } from "@unkey/api"; import { UnkeyError } from "@unkey/api/models/errors"; import { V2KeysVerifyKeyResponseData } from "@unkey/api/models/components"; export function authMiddleware(options: AuthOptions = {}) { return async (c: Context, next: Next) => { const apiKey = c.req.header("Authorization")?.replace("Bearer ", ""); if (!apiKey) { return c.json({ error: "Missing API key" }, 401); } try { const unkey = new Unkey({ rootKey: c.env.UNKEY_ROOT_KEY }); const { data } = await unkey.keys.verifyKey({ key: apiKey, }); // Set rate limit headers (v2 uses ratelimits array) if (data.ratelimits?.[0]) { const rl = data.ratelimits[0]; c.header("X-RateLimit-Limit", rl.limit.toString()); c.header("X-RateLimit-Remaining", rl.remaining.toString()); c.header("X-RateLimit-Reset", rl.reset.toString()); } if (data.credits !== undefined) { c.header("X-Credits-Remaining", data.credits.toString()); } if (!data.valid) { const status = data.code === "RATE_LIMITED" ? 429 : 401; return c.json({ error: data.code }, status); } c.set("auth", data); await next(); } catch (err) { return c.json({ error: "Service unavailable" }, 503); } }; } ``` *** ## With Durable Objects For stateful applications with Durable Objects: ```typescript src/index.ts theme={"theme":"kanagawa-wave"} import { Unkey } from "@unkey/api"; import { DurableObject } from "cloudflare:workers"; export interface Env { UNKEY_ROOT_KEY: string; } export class Counter extends DurableObject { async fetch(request: Request) { let count = ((await this.ctx.storage.get("count")) as number) || 0; count++; await this.ctx.storage.put("count", count); return Response.json({ count }); } } export default { async fetch(request: Request, env: Env): Promise { const unkey = new Unkey({ rootKey: env.UNKEY_ROOT_KEY }); // Verify API key first const apiKey = request.headers.get("Authorization")?.slice(7); if (!apiKey) { return Response.json({ error: "Missing API key" }, { status: 401 }); } try { const { data } = await unkey.keys.verifyKey({ key: apiKey, }); if (!data.valid) { return Response.json({ error: "Unauthorized" }, { status: 401 }); } // Use external ID as Durable Object ID for per-user state const id = env.COUNTER.idFromName( data.identity?.externalId ?? "anonymous", ); const counter = env.COUNTER.get(id); return counter.fetch(request); } catch (error) { return Response.json({ error: "Service unavailable" }, { status: 503 }); } }, }; ``` *** ## wrangler.toml ```toml wrangler.toml theme={"theme":"kanagawa-wave"} name = "my-api" main = "src/index.ts" compatibility_date = "2024-01-01" # Store UNKEY_ROOT_KEY as a secret, not in config # npx wrangler secret put UNKEY_ROOT_KEY ``` *** ## Deploy ```bash theme={"theme":"kanagawa-wave"} npx wrangler deploy ``` Your API is now protected globally with Unkey! 🌍 # Endpoint-Specific Rate Limits Source: https://unkey.com/docs/cookbook/endpoint-ratelimit Apply different rate limits to different API endpoints using Unkey. Configure per-route limits with separate quotas for reads and writes. Not all endpoints are equal. Your `/health` endpoint can handle thousands of requests, but your `/ai/generate` endpoint calls an expensive LLM. This recipe shows how to apply different rate limits per endpoint. ## The pattern ```typescript theme={"theme":"kanagawa-wave"} // Define limits per endpoint pattern const ENDPOINT_LIMITS = { "/api/ai/*": { limit: 10, duration: "1m" }, // Expensive AI calls "/api/export/*": { limit: 5, duration: "1h" }, // Heavy data exports "/api/*": { limit: 100, duration: "1m" }, // Default API routes }; // Match request path to limits function getLimits(path: string) { for (const [pattern, config] of Object.entries(ENDPOINT_LIMITS)) { if (matchPath(pattern, path)) return config; } return { limit: 100, duration: "1m" }; // fallback } ``` ## Full implementation ### Next.js Middleware ```typescript theme={"theme":"kanagawa-wave"} // middleware.ts import { Ratelimit } from "@unkey/ratelimit"; import { NextResponse } from "next/server"; import type { NextRequest } from "next/server"; const limiter = new Ratelimit({ rootKey: process.env.UNKEY_ROOT_KEY!, namespace: "api", limit: 100, duration: "1m", }); // Define endpoint-specific limits (most specific first) const ENDPOINT_LIMITS: Array<{ pattern: RegExp; limit: number; duration: string; namespace: string; }> = [ // Expensive AI endpoints - very tight limits { pattern: /^\/api\/ai\/.*/, limit: 10, duration: "1m", namespace: "ai", }, // Data export - limit per hour { pattern: /^\/api\/export\/.*/, limit: 5, duration: "1h", namespace: "export", }, // Write operations - moderate limits { pattern: /^\/api\/.*\/(create|update|delete)/, limit: 30, duration: "1m", namespace: "writes", }, // Default API routes { pattern: /^\/api\/.*/, limit: 100, duration: "1m", namespace: "api", }, ]; function getEndpointConfig(pathname: string) { for (const config of ENDPOINT_LIMITS) { if (config.pattern.test(pathname)) { return config; } } return ENDPOINT_LIMITS[ENDPOINT_LIMITS.length - 1]; // default } export async function middleware(request: NextRequest) { // Skip non-API routes if (!request.nextUrl.pathname.startsWith("/api")) { return NextResponse.next(); } const userId = request.headers.get("x-user-id") ?? request.ip ?? "anonymous"; const config = getEndpointConfig(request.nextUrl.pathname); // Use endpoint-specific namespace for separate counters const { success, remaining, reset } = await limiter.limit( `${config.namespace}:${userId}`, { limit: config.limit, duration: config.duration as any, }, ); if (!success) { return NextResponse.json( { error: "Rate limit exceeded", endpoint: config.namespace, retryAfter: Math.ceil((reset - Date.now()) / 1000), }, { status: 429, headers: { "X-RateLimit-Limit": config.limit.toString(), "X-RateLimit-Remaining": "0", "X-RateLimit-Reset": reset.toString(), "Retry-After": Math.ceil((reset - Date.now()) / 1000).toString(), }, }, ); } const response = NextResponse.next(); response.headers.set("X-RateLimit-Limit", config.limit.toString()); response.headers.set("X-RateLimit-Remaining", remaining.toString()); response.headers.set("X-RateLimit-Reset", reset.toString()); return response; } export const config = { matcher: "/api/:path*", }; ``` ### Express with route-specific middleware ```typescript theme={"theme":"kanagawa-wave"} // middleware/ratelimit.ts import { Ratelimit } from "@unkey/ratelimit"; import type { Request, Response, NextFunction } from "express"; const limiter = new Ratelimit({ rootKey: process.env.UNKEY_ROOT_KEY!, namespace: "api", limit: 100, duration: "1m", }); interface RateLimitOptions { limit: number; duration: string; namespace?: string; identifierFn?: (req: Request) => string; } export function rateLimit(options: RateLimitOptions) { return async (req: Request, res: Response, next: NextFunction) => { const identifier = options.identifierFn?.(req) ?? (req.headers["x-user-id"] as string) ?? req.ip ?? "anonymous"; const namespace = options.namespace ?? "api"; const { success, remaining, reset } = await limiter.limit( `${namespace}:${identifier}`, { limit: options.limit, duration: options.duration as any, }, ); res.set({ "X-RateLimit-Limit": options.limit.toString(), "X-RateLimit-Remaining": remaining.toString(), "X-RateLimit-Reset": reset.toString(), }); if (!success) { return res.status(429).json({ error: "Rate limit exceeded", retryAfter: Math.ceil((reset - Date.now()) / 1000), }); } next(); }; } // Usage in routes import express from "express"; import { rateLimit } from "./middleware/ratelimit"; const app = express(); // Expensive AI endpoint - 10 requests per minute app.post( "/api/ai/generate", rateLimit({ limit: 10, duration: "1m", namespace: "ai" }), async (req, res) => { // Call your AI provider }, ); // Data export - 5 per hour app.get( "/api/export/:type", rateLimit({ limit: 5, duration: "1h", namespace: "export" }), async (req, res) => { // Generate export }, ); // Regular endpoints - 100 per minute (default) app.use("/api", rateLimit({ limit: 100, duration: "1m" })); ``` ### Hono with route groups ```typescript theme={"theme":"kanagawa-wave"} import { Hono } from "hono"; import { Ratelimit } from "@unkey/ratelimit"; const app = new Hono(); const limiter = new Ratelimit({ rootKey: process.env.UNKEY_ROOT_KEY!, namespace: "api", limit: 100, duration: "1m", }); // Middleware factory for different limits function rateLimitMiddleware(options: { limit: number; duration: string; namespace: string; }) { return async (c: any, next: any) => { const identifier = c.req.header("x-user-id") ?? "anonymous"; const { success, remaining, reset } = await limiter.limit( `${options.namespace}:${identifier}`, { limit: options.limit, duration: options.duration as any }, ); c.header("X-RateLimit-Limit", options.limit.toString()); c.header("X-RateLimit-Remaining", remaining.toString()); c.header("X-RateLimit-Reset", reset.toString()); if (!success) { return c.json({ error: "Rate limit exceeded" }, 429); } await next(); }; } // AI routes - strict limits const ai = new Hono(); ai.use( "*", rateLimitMiddleware({ limit: 10, duration: "1m", namespace: "ai" }), ); ai.post("/generate", (c) => c.json({ result: "..." })); ai.post("/embed", (c) => c.json({ result: "..." })); // Export routes - hourly limits const exports = new Hono(); exports.use( "*", rateLimitMiddleware({ limit: 5, duration: "1h", namespace: "export" }), ); exports.get("/csv", (c) => c.json({ url: "..." })); exports.get("/json", (c) => c.json({ url: "..." })); // Mount route groups app.route("/api/ai", ai); app.route("/api/export", exports); // Default API routes app.use( "/api/*", rateLimitMiddleware({ limit: 100, duration: "1m", namespace: "api" }), ); export default app; ``` ## Cost-based limiting For endpoints where some operations are more expensive than others, use cost-based limiting: ```typescript theme={"theme":"kanagawa-wave"} app.post("/api/ai/generate", async (req, res) => { const { model, tokens } = req.body; // Different models have different costs const cost = model === "gpt-4" ? 10 : model === "gpt-3.5" ? 2 : 1; const { success } = await limiter.limit(userId, { cost }); if (!success) { return res.status(429).json({ error: "Rate limit exceeded" }); } // Process request... }); ``` With a limit of 100/minute: * 100 cheap model calls, OR * 50 gpt-3.5 calls, OR * 10 gpt-4 calls ## Best practices Different namespaces mean separate counters. A user can hit their AI limit without affecting their regular API quota. When matching paths, put more specific patterns first. `/api/ai/*` should come before `/api/*`. Tight limits on expensive endpoints are fine, but communicate them clearly in your API docs. Use Unkey analytics to see which endpoints hit limits most often, then adjust accordingly. ## Next steps Combine with user tiers for complete rate limiting Understand how rate limiting works under the hood # Express Middleware Source: https://unkey.com/docs/cookbook/express-middleware Build a reusable Express middleware for Unkey API key authentication. Verify keys automatically on every request with error handling. A production-ready middleware pattern for Express applications. ## Install ```bash theme={"theme":"kanagawa-wave"} npm install @unkey/api express ``` ## Basic Middleware ```typescript middleware/auth.ts theme={"theme":"kanagawa-wave"} import { Unkey } from "@unkey/api"; import { Request, Response, NextFunction } from "express"; const unkey = new Unkey({ rootKey: process.env.UNKEY_ROOT_KEY! }); // Extend Express Request type declare global { namespace Express { interface Request { unkey?: V2KeysVerifyKeyResponseData; } } } export async function unkeyAuth( req: Request, res: Response, next: NextFunction, ) { // 1. Extract API key const authHeader = req.headers.authorization; if (!authHeader?.startsWith("Bearer ")) { return res.status(401).json({ error: "Missing API key" }); } const apiKey = authHeader.slice(7); let data; try { // 2. Verify with Unkey (throws on transport/SDK errors) ({ data } = await unkey.keys.verifyKey({ key: apiKey })); } catch (err) { console.error("Unkey error:", err); return res .status(503) .json({ error: "Authentication service unavailable" }); } // 3. Check validity if (!data.valid) { const status = data.code === "RATE_LIMITED" ? 429 : 401; return res.status(status).json({ error: data.code }); } // 4. Attach to request and continue req.unkey = data; next(); } ``` ## Use the Middleware ```typescript app.ts theme={"theme":"kanagawa-wave"} import express from "express"; import { unkeyAuth } from "./middleware/auth"; const app = express(); // Public routes app.get("/health", (req, res) => { res.json({ status: "ok" }); }); // Protected routes app.get("/api/data", unkeyAuth, (req, res) => { // Access verification result const { identity, meta, credits } = req.unkey!; res.json({ message: "Access granted", user: identity?.externalId, plan: meta?.plan, creditsRemaining: credits, }); }); // Protect entire router const apiRouter = express.Router(); apiRouter.use(unkeyAuth); apiRouter.get("/users", (req, res) => { res.json({ users: [] }); }); apiRouter.post("/users", (req, res) => { res.json({ created: true }); }); app.use("/api/v1", apiRouter); app.listen(3000); ``` *** ## With Rate Limit Headers Include rate limit info in response headers: ```typescript middleware/auth.ts theme={"theme":"kanagawa-wave"} export async function unkeyAuth( req: Request, res: Response, next: NextFunction, ) { const authHeader = req.headers.authorization; if (!authHeader?.startsWith("Bearer ")) { return res.status(401).json({ error: "Missing API key" }); } let data; try { ({ data } = await unkey.keys.verifyKey({ key: authHeader.slice(7) })); } catch { return res.status(503).json({ error: "Service unavailable" }); } // Add rate limit headers if available (v2 uses ratelimits array) if (data.ratelimits?.[0]) { const rl = data.ratelimits[0]; res.set({ "X-RateLimit-Limit": rl.limit.toString(), "X-RateLimit-Remaining": rl.remaining.toString(), "X-RateLimit-Reset": rl.reset.toString(), }); } // Add remaining credits header if available if (data.credits !== undefined) { res.set("X-Credits-Remaining", data.credits.toString()); } if (!data.valid) { const status = data.code === "RATE_LIMITED" ? 429 : 401; return res.status(status).json({ error: data.code }); } req.unkey = data; next(); } ``` *** ## Permission-Based Access Create middleware that requires specific permissions: ```typescript middleware/permissions.ts theme={"theme":"kanagawa-wave"} import { Unkey } from "@unkey/api"; import { Request, Response, NextFunction } from "express"; const unkey = new Unkey({ rootKey: process.env.UNKEY_ROOT_KEY! }); export function requirePermission(permission: string) { return async (req: Request, res: Response, next: NextFunction) => { const authHeader = req.headers.authorization; if (!authHeader?.startsWith("Bearer ")) { return res.status(401).json({ error: "Missing API key" }); } let data; try { ({ data } = await unkey.keys.verifyKey({ key: authHeader.slice(7), permissions: permission, // Check for this permission })); } catch { return res.status(503).json({ error: "Service unavailable" }); } if (!data.valid) { if (data.code === "INSUFFICIENT_PERMISSIONS") { return res.status(403).json({ error: "Forbidden", required: permission, }); } return res.status(401).json({ error: data.code }); } req.unkey = data; next(); }; } ``` Use it: ```typescript theme={"theme":"kanagawa-wave"} // Anyone with a valid key app.get("/api/data", unkeyAuth, handler); // Only keys with "admin" permission app.delete("/api/users/:id", requirePermission("admin"), deleteUser); // Only keys with "billing.read" permission app.get("/api/invoices", requirePermission("billing.read"), getInvoices); ``` *** ## Configurable Middleware Factory Create a flexible middleware that can be configured per-route: ```typescript middleware/auth.ts theme={"theme":"kanagawa-wave"} import { Unkey } from "@unkey/api"; import { Request, Response, NextFunction } from "express"; const unkey = new Unkey({ rootKey: process.env.UNKEY_ROOT_KEY! }); interface AuthOptions { permissions?: string; getKey?: (req: Request) => string | null; onError?: (req: Request, res: Response, error: Error) => void; onInvalid?: ( req: Request, res: Response, result: V2KeysVerifyKeyResponseData, ) => void; } export function createAuthMiddleware(options: AuthOptions = {}) { const { permissions, getKey = (req) => req.headers.authorization?.slice(7) ?? null, onError = (req, res) => res.status(503).json({ error: "Service unavailable" }), onInvalid = (req, res, result) => res.status(401).json({ error: result.code }), } = options; return async (req: Request, res: Response, next: NextFunction) => { const apiKey = getKey(req); if (!apiKey) { return res.status(401).json({ error: "Missing API key" }); } let data; try { ({ data } = await unkey.keys.verifyKey({ key: apiKey, permissions, })); } catch (err) { return onError(req, res, err as Error); } if (!data.valid) { return onInvalid(req, res, data); } req.unkey = data; next(); }; } // Pre-configured middlewares export const unkeyAuth = createAuthMiddleware(); export const adminAuth = createAuthMiddleware({ permissions: "admin", onInvalid: (req, res, result) => { if (result.code === "INSUFFICIENT_PERMISSIONS") { return res.status(403).json({ error: "Admin access required" }); } return res.status(401).json({ error: result.code }); }, }); ``` *** ## Error Handling Graceful degradation when Unkey is unavailable: ```typescript middleware/auth.ts theme={"theme":"kanagawa-wave"} export async function unkeyAuth( req: Request, res: Response, next: NextFunction, ) { const apiKey = req.headers.authorization?.slice(7); if (!apiKey) { return res.status(401).json({ error: "Missing API key" }); } try { const { data } = await unkey.keys.verifyKey({ key: apiKey, }); if (!data.valid) { return res.status(401).json({ error: data.code }); } req.unkey = data; next(); } catch (e) { // Log for monitoring console.error("Unkey verification error:", e); // Option 1: Fail closed (more secure) return res.status(503).json({ error: "Authentication unavailable" }); // Option 2: Fail open (better availability, less secure) // console.warn("Allowing request due to Unkey unavailability"); // return next(); } } ``` *** ## TypeScript Setup ```typescript types/express.d.ts theme={"theme":"kanagawa-wave"} import { V2KeysVerifyKeyResponseData } from "@unkey/api/models/components"; declare global { namespace Express { interface Request { unkey?: V2KeysVerifyKeyResponseData; } } } export {}; ``` Make sure to include this in your `tsconfig.json`: ```json theme={"theme":"kanagawa-wave"} { "compilerOptions": { "typeRoots": ["./node_modules/@types", "./types"] } } ``` # FastAPI Authentication Source: https://unkey.com/docs/cookbook/fastapi-auth Implement API key authentication in FastAPI using Unkey with a dependency injection pattern. Protect endpoints with automatic key verification. A clean, reusable pattern using FastAPI's dependency injection system. ## Install ```bash theme={"theme":"kanagawa-wave"} pip install unkey.py fastapi uvicorn ``` ## Basic Dependency ```python dependencies/auth.py theme={"theme":"kanagawa-wave"} from fastapi import HTTPException, Security from fastapi.security import APIKeyHeader from unkey.py import Unkey import os # Configure API key header api_key_header = APIKeyHeader(name="X-API-Key", auto_error=False) # Initialize Unkey client unkey = Unkey(root_key=os.environ["UNKEY_ROOT_KEY"]) async def verify_api_key(api_key: str = Security(api_key_header)): """Dependency that verifies API keys and returns the verification result.""" if not api_key: raise HTTPException( status_code=401, detail="Missing API key", headers={"WWW-Authenticate": "ApiKey"}, ) try: result = await unkey.keys.verify_key_async( key=api_key, ) except Exception as e: raise HTTPException( status_code=503, detail="Authentication service unavailable" ) if not result.valid: status = 429 if result.code == "RATE_LIMITED" else 401 raise HTTPException(status_code=status, detail=result.code) return result ``` ## Use in Routes ```python main.py theme={"theme":"kanagawa-wave"} from fastapi import FastAPI, Depends from dependencies.auth import verify_api_key app = FastAPI() @app.get("/api/data") async def get_data(auth = Depends(verify_api_key)): return { "message": "Access granted", "user": auth.identity.external_id if auth.identity else None, "remaining_credits": auth.credits, "metadata": auth.meta, } @app.get("/api/users") async def get_users(auth = Depends(verify_api_key)): # Use auth.identity.external_id to scope data access return {"users": []} ``` *** ## Typed Response Model Create a Pydantic model for the auth result: ```python models/auth.py theme={"theme":"kanagawa-wave"} from pydantic import BaseModel, ConfigDict from typing import Optional, Any class Identity(BaseModel): id: str external_id: str meta: Optional[dict[str, Any]] = None class AuthResult(BaseModel): valid: bool code: str key_id: str name: Optional[str] = None identity: Optional[Identity] = None meta: Optional[dict[str, Any]] = None credits: Optional[int] = None expires: Optional[int] = None enabled: bool = True permissions: Optional[list[str]] = None roles: Optional[list[str]] = None model_config = ConfigDict(extra="allow") # Allow additional fields from Unkey ``` ```python dependencies/auth.py theme={"theme":"kanagawa-wave"} from models.auth import AuthResult async def verify_api_key(api_key: str = Security(api_key_header)) -> AuthResult: if not api_key: raise HTTPException(status_code=401, detail="Missing API key") try: result = await unkey.keys.verify_key_async(key=api_key) except Exception as e: raise HTTPException(status_code=503, detail="Unkey service unavailable") if not result.valid: raise HTTPException(status_code=401, detail=result.code) return AuthResult( valid=result.valid, code=result.code, key_id=result.key_id, identity=result.identity, meta=result.meta, credits=result.credits, ) ``` *** ## Permission-Based Access Create dependencies for different permission levels: ```python dependencies/auth.py theme={"theme":"kanagawa-wave"} from fastapi import HTTPException, Security from fastapi.security import APIKeyHeader from unkey.py import Unkey import os api_key_header = APIKeyHeader(name="X-API-Key", auto_error=False) unkey = Unkey(root_key=os.environ["UNKEY_ROOT_KEY"]) def require_permission(permission: str): """Factory that creates a dependency requiring a specific permission.""" async def verify(api_key: str = Security(api_key_header)): if not api_key: raise HTTPException(status_code=401, detail="Missing API key") # Include permission check in verification result = await unkey.keys.verify_key_async( key=api_key, permissions=permission, ) if not result.valid: if result.code == "INSUFFICIENT_PERMISSIONS": raise HTTPException( status_code=403, detail=f"Permission required: {permission}" ) raise HTTPException(status_code=401, detail=result.code) return result return verify # Pre-built permission checkers require_read = require_permission("data.read") require_write = require_permission("data.write") require_admin = require_permission("admin") ``` Use in routes: ```python main.py theme={"theme":"kanagawa-wave"} from dependencies.auth import require_read, require_write, require_admin @app.get("/api/data") async def read_data(auth = Depends(require_read)): return {"data": []} @app.post("/api/data") async def create_data(auth = Depends(require_write)): return {"created": True} @app.delete("/api/users/{user_id}") async def delete_user(user_id: str, auth = Depends(require_admin)): return {"deleted": user_id} ``` *** ## Rate Limit Headers Return rate limit info in response headers: ```python dependencies/auth.py theme={"theme":"kanagawa-wave"} from fastapi import Response async def verify_api_key( response: Response, api_key: str = Security(api_key_header) ): if not api_key: raise HTTPException(status_code=401, detail="Missing API key") result = await unkey.keys.verify_key_async( key=api_key, ) # Add rate limit headers if result.ratelimits: rl = result.ratelimits[0] response.headers["X-RateLimit-Limit"] = str(rl.limit) response.headers["X-RateLimit-Remaining"] = str(rl.remaining) response.headers["X-RateLimit-Reset"] = str(rl.reset) # Add credits header if result.credits is not None: response.headers["X-Credits-Remaining"] = str(result.credits) if not result.valid: raise HTTPException( status_code=429 if result.code == "RATE_LIMITED" else 401, detail=result.code ) return result ``` *** ## Async Client For better performance with async FastAPI: ```python dependencies/auth.py theme={"theme":"kanagawa-wave"} from contextlib import asynccontextmanager from unkey.py import Unkey import os # Global client (initialized on startup) unkey_client: Unkey = None @asynccontextmanager async def lifespan(app): global unkey_client unkey_client = Unkey(root_key=os.environ["UNKEY_ROOT_KEY"]) yield # Cleanup if needed async def verify_api_key(api_key: str = Security(api_key_header)): if not api_key: raise HTTPException(status_code=401, detail="Missing API key") # Use async method result = await unkey_client.keys.verify_key_async( key=api_key, ) if not result.valid: raise HTTPException(status_code=401, detail=result.code) return result ``` ```python main.py theme={"theme":"kanagawa-wave"} from fastapi import FastAPI from dependencies.auth import lifespan app = FastAPI(lifespan=lifespan) ``` *** ## Full Example ```python main.py theme={"theme":"kanagawa-wave"} from fastapi import FastAPI, Depends, HTTPException, Security, Response from fastapi.security import APIKeyHeader from pydantic import BaseModel from unkey.py import Unkey import os app = FastAPI(title="My API") # Auth setup api_key_header = APIKeyHeader(name="X-API-Key", auto_error=False) unkey = Unkey(root_key=os.environ["UNKEY_ROOT_KEY"]) # Models class DataResponse(BaseModel): message: str user: str | None remaining_credits: int | None # Dependencies async def get_auth( response: Response, api_key: str = Security(api_key_header) ): if not api_key: raise HTTPException(status_code=401, detail="Missing API key") result = await unkey.keys.verify_key_async(key=api_key) if result.credits is not None: response.headers["X-Credits-Remaining"] = str(result.credits) if not result.valid: raise HTTPException(status_code=401, detail=result.code) return result # Routes @app.get("/api/data", response_model=DataResponse) async def get_data(auth = Depends(get_auth)): return DataResponse( message="Access granted", user=auth.identity.external_id if auth.identity else None, remaining_credits=auth.credits, ) @app.get("/health") async def health(): return {"status": "ok"} if __name__ == "__main__": import uvicorn uvicorn.run(app, host="0.0.0.0", port=8000) ``` Run with: ```bash theme={"theme":"kanagawa-wave"} UNKEY_ROOT_KEY=... uvicorn main:app --reload ``` # Echo Framework Middleware Source: https://unkey.com/docs/cookbook/go-echo-middleware Build production-ready API key authentication middleware for the Go Echo framework using Unkey. Verify keys on every incoming request. This recipe shows how to create robust API key authentication middleware for the [Echo web framework](https://echo.labstack.com/). ## Complete Middleware Implementation ```go theme={"theme":"kanagawa-wave"} package main import ( "net/http" "os" "slices" "strings" "github.com/labstack/echo/v4" "github.com/labstack/echo/v4/middleware" unkey "github.com/unkeyed/sdks/api/go/v2" "github.com/unkeyed/sdks/api/go/v2/models/components" ) var unkeyClient *unkey.Unkey func init() { unkeyClient = unkey.New( unkey.WithSecurity(os.Getenv("UNKEY_ROOT_KEY")), ) } // UnkeyAuthMiddleware creates an Echo middleware for API key verification func UnkeyAuthMiddleware() echo.MiddlewareFunc { return func(next echo.HandlerFunc) echo.HandlerFunc { return func(c echo.Context) error { authHeader := c.Request().Header.Get("Authorization") if authHeader == "" { return c.JSON(http.StatusUnauthorized, map[string]string{ "error": "Missing Authorization header", "code": "MISSING_KEY", }) } apiKey := strings.TrimPrefix(authHeader, "Bearer ") // Verify with Unkey res, err := unkeyClient.Keys.VerifyKey(c.Request().Context(), components.V2KeysVerifyKeyRequestBody{ Key: apiKey, }) if err != nil { return c.JSON(http.StatusServiceUnavailable, map[string]string{ "error": "Verification service unavailable", "code": "SERVICE_ERROR", "message": err.Error(), }) } if !res.V2KeysVerifyKeyResponseBody.Data.Valid { code := string(res.V2KeysVerifyKeyResponseBody.Data.Code) return c.JSON(http.StatusUnauthorized, map[string]string{ "error": "Invalid API key", "code": code, }) } // Store verification result in context c.Set("unkey", &res.V2KeysVerifyKeyResponseBody.Data) return next(c) } } } // RequirePermission creates middleware to check specific permissions func RequirePermission(permission string) echo.MiddlewareFunc { return func(next echo.HandlerFunc) echo.HandlerFunc { return func(c echo.Context) error { result := c.Get("unkey") if result == nil { return c.JSON(http.StatusUnauthorized, map[string]string{ "error": "Authentication required", "code": "AUTH_REQUIRED", }) } keyResult, ok := result.(*components.V2KeysVerifyKeyResponseData) if !ok || keyResult == nil { return c.JSON(http.StatusInternalServerError, map[string]any{ "error": "Invalid authentication context", "code": "INTERNAL_ERROR", }) } if slices.Contains(keyResult.Permissions, permission) { return next(c) } return c.JSON(http.StatusForbidden, map[string]any{ "error": "Insufficient permissions", "code": "FORBIDDEN", "required": permission, }) } } } // RequireRole creates middleware to check specific roles func RequireRole(role string) echo.MiddlewareFunc { return func(next echo.HandlerFunc) echo.HandlerFunc { return func(c echo.Context) error { result := c.Get("unkey") if result == nil { return c.JSON(http.StatusUnauthorized, map[string]string{ "error": "Authentication required", "code": "AUTH_REQUIRED", }) } keyResult, ok := result.(*components.V2KeysVerifyKeyResponseData) if !ok || keyResult == nil { return c.JSON(http.StatusInternalServerError, map[string]any{ "error": "Invalid authentication context", "code": "INTERNAL_ERROR", }) } if slices.Contains(keyResult.Roles, role) { return next(c) } return c.JSON(http.StatusForbidden, map[string]any{ "error": "Insufficient role", "code": "FORBIDDEN", "required": role, }) } } } // GetUnkeyResult retrieves the Unkey verification result from context func GetUnkeyResult(c echo.Context) *components.V2KeysVerifyKeyResponseData { result := c.Get("unkey") if result == nil { return nil } r, ok := result.(*components.V2KeysVerifyKeyResponseData) if !ok { return nil } return r } // Usage example func main() { e := echo.New() // Middleware e.Use(middleware.Logger()) e.Use(middleware.Recover()) // Public routes e.GET("/health", func(c echo.Context) error { return c.JSON(http.StatusOK, map[string]string{"status": "ok"}) }) // Protected routes api := e.Group("/api") api.Use(UnkeyAuthMiddleware()) { api.GET("/data", func(c echo.Context) error { result := GetUnkeyResult(c) ownerID := "" if result.Identity != nil { ownerID = result.Identity.ExternalID } return c.JSON(http.StatusOK, map[string]any{ "message": "Access granted", "key_id": *result.KeyID, "owner": ownerID, "meta": result.Meta, }) }) api.GET("/profile", func(c echo.Context) error { result := GetUnkeyResult(c) keyID := "" if result.KeyID != nil { keyID = *result.KeyID } return c.JSON(http.StatusOK, map[string]any{ "key_id": keyID, "permissions": result.Permissions, "roles": result.Roles, }) }) } // Admin routes admin := e.Group("/api/admin") admin.Use(UnkeyAuthMiddleware(), RequirePermission("admin:read")) { admin.GET("/users", func(c echo.Context) error { return c.JSON(http.StatusOK, map[string]any{ "message": "Admin access granted", "users": []string{"user1", "user2"}, }) }) admin.POST("/config", func(c echo.Context) error { return c.JSON(http.StatusOK, map[string]string{ "message": "Config updated", }) }, RequirePermission("admin:write")) } e.Start(":8080") } ``` ## Custom Configuration Create configurable middleware: ```go theme={"theme":"kanagawa-wave"} type AuthConfig struct { HeaderName string Prefix string Optional bool } func UnkeyAuthWithConfig(config AuthConfig) echo.MiddlewareFunc { if config.HeaderName == "" { config.HeaderName = "Authorization" } if config.Prefix == "" { config.Prefix = "Bearer " } return func(next echo.HandlerFunc) echo.HandlerFunc { return func(c echo.Context) error { authHeader := c.Request().Header.Get(config.HeaderName) if authHeader == "" { if config.Optional { return next(c) } return c.JSON(http.StatusUnauthorized, map[string]string{ "error": "Missing API key", "code": "MISSING_KEY", }) } apiKey := strings.TrimPrefix(authHeader, config.Prefix) res, err := unkeyClient.Keys.VerifyKey(c.Request().Context(), components.V2KeysVerifyKeyRequestBody{ Key: apiKey, }) if err != nil { return c.JSON(http.StatusServiceUnavailable, map[string]string{ "error": "Verification failed", }) } if !res.V2KeysVerifyKeyResponseBody.Data.Valid { return c.JSON(http.StatusUnauthorized, map[string]string{ "error": "Invalid API key", }) } c.Set("unkey", &res.V2KeysVerifyKeyResponseBody.Data) return next(c) } } } // Usage with custom config e.GET("/api/public", func(c echo.Context) error { result := GetUnkeyResult(c) if result != nil { keyID := "" if result.KeyID != nil { keyID = *result.KeyID } return c.JSON(http.StatusOK, map[string]any{ "message": "Authenticated", "key_id": keyID, }) } return c.JSON(http.StatusOK, map[string]string{ "message": "Anonymous", }) }, UnkeyAuthWithConfig(AuthConfig{Optional: true})) ``` ## Group-Level Middleware Apply middleware to route groups: ```go theme={"theme":"kanagawa-wave"} // API v1 routes v1 := e.Group("/api/v1") v1.Use(UnkeyAuthMiddleware()) // Public subset public := v1.Group("/public") public.GET("/status", statusHandler) // Protected subset protected := v1.Group("/protected") protected.Use(RequirePermission("data:read")) protected.GET("/data", dataHandler) ``` ## Testing ```bash theme={"theme":"kanagawa-wave"} # Test without key curl http://localhost:8080/api/data # {"error":"Missing Authorization header","code":"MISSING_KEY"} # Test with valid key curl -H "Authorization: Bearer YOUR_API_KEY" http://localhost:8080/api/data # {"message":"Access granted","key_id":"key_..."} # Test admin route without permission curl -H "Authorization: Bearer USER_KEY" http://localhost:8080/api/admin/users # {"error":"Insufficient permissions","code":"FORBIDDEN"} # Test admin route with permission curl -H "Authorization: Bearer ADMIN_KEY" http://localhost:8080/api/admin/users # {"message":"Admin access granted","users":["user1","user2"]} ``` ## Related Get started with Go and Unkey Complete Go SDK documentation # Gin Framework Middleware Source: https://unkey.com/docs/cookbook/go-gin-middleware Build production-ready API key authentication middleware for the Go Gin framework using Unkey. Verify keys with automatic error handling. This recipe shows how to create robust API key authentication middleware for the [Gin web framework](https://gin-gonic.com/). ## Complete Middleware Implementation ```go theme={"theme":"kanagawa-wave"} package main import ( "net/http" "os" "slices" "strconv" "strings" "github.com/gin-gonic/gin" unkey "github.com/unkeyed/sdks/api/go/v2" "github.com/unkeyed/sdks/api/go/v2/models/components" ) var unkeyClient *unkey.Unkey func init() { unkeyClient = unkey.New( unkey.WithSecurity(os.Getenv("UNKEY_ROOT_KEY")), ) } // UnkeyAuth creates a Gin middleware for API key verification func UnkeyAuth() gin.HandlerFunc { return func(c *gin.Context) { authHeader := c.GetHeader("Authorization") if authHeader == "" { c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{ "error": "Missing Authorization header", "code": "MISSING_KEY", }) return } apiKey := strings.TrimPrefix(authHeader, "Bearer ") // Verify with Unkey res, err := unkeyClient.Keys.VerifyKey(c.Request.Context(), components.V2KeysVerifyKeyRequestBody{ Key: apiKey, }) if err != nil { c.AbortWithStatusJSON(http.StatusServiceUnavailable, gin.H{ "error": "Verification service unavailable", "code": "SERVICE_ERROR", "message": err.Error(), }) return } if !res.V2KeysVerifyKeyResponseBody.Data.Valid { code := string(res.V2KeysVerifyKeyResponseBody.Data.Code) c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{ "error": "Invalid API key", "code": code, }) return } // Store verification result in context (as pointer for type assertion compatibility) c.Set("unkey", &res.V2KeysVerifyKeyResponseBody.Data) c.Next() } } // RequirePermission creates middleware to check specific permissions func RequirePermission(permission string) gin.HandlerFunc { return func(c *gin.Context) { result, exists := c.Get("unkey") if !exists { c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{ "error": "Authentication required", "code": "AUTH_REQUIRED", }) return } keyResult, ok := result.(*components.V2KeysVerifyKeyResponseData) if !ok || keyResult == nil { c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{ "error": "Invalid authentication context", "code": "INTERNAL_ERROR", }) return } if slices.Contains(keyResult.Permissions, permission) { c.Next() return } c.AbortWithStatusJSON(http.StatusForbidden, gin.H{ "error": "Insufficient permissions", "code": "FORBIDDEN", "required": permission, }) } } // RequireRole creates middleware to check specific roles func RequireRole(role string) gin.HandlerFunc { return func(c *gin.Context) { result, exists := c.Get("unkey") if !exists { c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{ "error": "Authentication required", "code": "AUTH_REQUIRED", }) return } keyResult, ok := result.(*components.V2KeysVerifyKeyResponseData) if !ok || keyResult == nil { c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{ "error": "Invalid authentication context", "code": "INTERNAL_ERROR", }) return } if slices.Contains(keyResult.Roles, role) { c.Next() return } c.AbortWithStatusJSON(http.StatusForbidden, gin.H{ "error": "Insufficient role", "code": "FORBIDDEN", "required": role, }) } } // GetUnkeyResult retrieves the Unkey verification result from context func GetUnkeyResult(c *gin.Context) *components.V2KeysVerifyKeyResponseData { result, ok := c.Get("unkey") if !ok { return nil } r, ok := result.(*components.V2KeysVerifyKeyResponseData) if !ok { return nil } return r } // Usage example func main() { r := gin.Default() // Public routes r.GET("/health", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"status": "ok"}) }) // Protected API group api := r.Group("/api") api.Use(UnkeyAuth()) { api.GET("/data", func(c *gin.Context) { result := GetUnkeyResult(c) ownerID := "" if result.Identity != nil { ownerID = result.Identity.ExternalID } c.JSON(http.StatusOK, gin.H{ "message": "Access granted", "key_id": *result.KeyID, "owner": ownerID, "meta": result.Meta, }) }) api.GET("/profile", func(c *gin.Context) { result := GetUnkeyResult(c) c.JSON(http.StatusOK, gin.H{ "key_id": *result.KeyID, "permissions": result.Permissions, "roles": result.Roles, }) }) } // Admin routes with permission check admin := r.Group("/api/admin") admin.Use(UnkeyAuth(), RequirePermission("admin:read")) { admin.GET("/users", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{ "message": "Admin access granted", "users": []string{"user1", "user2"}, }) }) admin.POST("/config", RequirePermission("admin:write"), func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{ "message": "Config updated", }) }) } r.Run(":8080") } ``` ## Rate Limiting Integration Combine with Unkey's rate limiting: ```go theme={"theme":"kanagawa-wave"} func RateLimitMiddleware(namespace string) gin.HandlerFunc { return func(c *gin.Context) { result := GetUnkeyResult(c) if result == nil { c.Next() return } // Use the key's built-in rate limits from verification // These are already checked during key verification // Or use standalone rate limit API for custom limits res, err := unkeyClient.Ratelimits.Limit(c.Request.Context(), components.V2RatelimitsLimitRequestBody{ Namespace: namespace, Identifier: *result.KeyID, Limit: 100, Duration: 60000, // 100 per minute }) if err != nil { c.AbortWithStatusJSON(http.StatusServiceUnavailable, gin.H{ "error": "Rate limit check failed", }) return } if !res.V2RatelimitsLimitResponseBody.Success { c.AbortWithStatusJSON(http.StatusTooManyRequests, gin.H{ "error": "Rate limit exceeded", "reset": res.V2RatelimitsLimitResponseBody.Reset, "limit": 100, "window": "60s", }) return } // Add rate limit headers c.Header("X-RateLimit-Limit", "100") c.Header("X-RateLimit-Remaining", strconv.FormatInt(res.V2RatelimitsLimitResponseBody.Remaining, 10)) c.Next() } } ``` ## Testing ```bash theme={"theme":"kanagawa-wave"} # Test without key curl http://localhost:8080/api/data # {"error":"Missing Authorization header","code":"MISSING_KEY"} # Test with valid key curl -H "Authorization: Bearer YOUR_API_KEY" http://localhost:8080/api/data # {"message":"Access granted","key_id":"key_..."} # Test admin route without permission curl -H "Authorization: Bearer USER_KEY" http://localhost:8080/api/admin/users # {"error":"Insufficient permissions","code":"FORBIDDEN"} # Test admin route with permission curl -H "Authorization: Bearer ADMIN_KEY" http://localhost:8080/api/admin/users # {"message":"Admin access granted","users":["user1","user2"]} ``` ## Related Get started with Go and Unkey Complete Go SDK documentation # Go Standard Library Middleware Source: https://unkey.com/docs/cookbook/go-stdlib-middleware Build production-ready API key authentication middleware for Go's standard library net/http using Unkey. No external framework required. This recipe shows how to create robust API key authentication middleware for Go's standard library HTTP server. ## Complete Middleware Implementation ```go theme={"theme":"kanagawa-wave"} package main import ( "context" "encoding/json" "net/http" "os" "slices" "strings" "time" unkey "github.com/unkeyed/sdks/api/go/v2" "github.com/unkeyed/sdks/api/go/v2/models/components" ) // KeyContext stores Unkey verification result in request context type KeyContext struct { KeyID string OwnerID string Meta map[string]any Permissions []string Roles []string } // contextKey is the key type for storing Unkey context type contextKey string const unkeyContextKey contextKey = "unkey" var unkeyClient *unkey.Unkey func init() { unkeyClient = unkey.New( unkey.WithSecurity(os.Getenv("UNKEY_ROOT_KEY")), ) } // AuthMiddleware creates a middleware that verifies API keys func AuthMiddleware(opts ...AuthOption) func(http.Handler) http.Handler { options := &authOptions{ headerName: "Authorization", prefix: "Bearer ", required: true, } for _, opt := range opts { opt(options) } return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") // Extract API key authHeader := r.Header.Get(options.headerName) if authHeader == "" { if options.required { w.WriteHeader(http.StatusUnauthorized) json.NewEncoder(w).Encode(map[string]string{ "error": "Missing API key", "code": "MISSING_KEY", }) return } // Auth not required, continue without verification next.ServeHTTP(w, r) return } apiKey := strings.TrimPrefix(authHeader, options.prefix) // Verify with Unkey ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second) defer cancel() res, err := unkeyClient.Keys.VerifyKey(ctx, components.V2KeysVerifyKeyRequestBody{ Key: apiKey, }) if err != nil { w.WriteHeader(http.StatusServiceUnavailable) json.NewEncoder(w).Encode(map[string]string{ "error": "Verification service unavailable", "code": "SERVICE_ERROR", "message": err.Error(), }) return } result := res.V2KeysVerifyKeyResponseBody.Data if !result.Valid { code := string(result.Code) w.WriteHeader(http.StatusUnauthorized) json.NewEncoder(w).Encode(map[string]any{ "error": "Invalid API key", "code": code, }) return } // Build context keyCtx := &KeyContext{ KeyID: *result.KeyID, Meta: result.Meta, Permissions: result.Permissions, Roles: result.Roles, } if result.Identity != nil { keyCtx.OwnerID = result.Identity.ExternalID } // Store in request context ctx = context.WithValue(r.Context(), unkeyContextKey, keyCtx) next.ServeHTTP(w, r.WithContext(ctx)) }) } } // AuthOption configures the auth middleware type authOptions struct { headerName string prefix string required bool } type AuthOption func(*authOptions) // WithHeaderName sets a custom header name for the API key func WithHeaderName(name string) AuthOption { return func(o *authOptions) { o.headerName = name } } // WithPrefix sets a custom prefix for the API key func WithPrefix(prefix string) AuthOption { return func(o *authOptions) { o.prefix = prefix } } // WithOptional makes authentication optional func WithOptional() AuthOption { return func(o *authOptions) { o.required = false } } // GetKeyContext retrieves the Unkey context from request func GetKeyContext(r *http.Request) (*KeyContext, bool) { ctx, ok := r.Context().Value(unkeyContextKey).(*KeyContext) return ctx, ok } // RequirePermission middleware checks if the key has a specific permission func RequirePermission(permission string) func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { keyCtx, ok := GetKeyContext(r) if !ok { w.WriteHeader(http.StatusUnauthorized) json.NewEncoder(w).Encode(map[string]string{ "error": "Authentication required", "code": "AUTH_REQUIRED", }) return } if slices.Contains(keyCtx.Permissions, permission) { next.ServeHTTP(w, r) return } w.WriteHeader(http.StatusForbidden) json.NewEncoder(w).Encode(map[string]string{ "error": "Insufficient permissions", "code": "FORBIDDEN", "required": permission, }) }) } } // Usage example func main() { mux := http.NewServeMux() // Public route mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(map[string]string{"status": "ok"}) }) // Protected routes mux.Handle("/api/protected", AuthMiddleware()(http.HandlerFunc(protectedHandler))) // Protected with permission check // Compose middleware: AuthMiddleware wraps RequirePermission which wraps the handler mux.Handle("/api/admin", AuthMiddleware()(RequirePermission("admin:read")(http.HandlerFunc(adminHandler)))) // Optional auth mux.Handle("/api/public", AuthMiddleware(WithOptional())(http.HandlerFunc(optionalAuthHandler))) server := &http.Server{ Addr: ":8080", Handler: mux, ReadTimeout: 15 * time.Second, WriteTimeout: 15 * time.Second, } server.ListenAndServe() } func protectedHandler(w http.ResponseWriter, r *http.Request) { keyCtx, _ := GetKeyContext(r) json.NewEncoder(w).Encode(map[string]any{ "message": "Access granted", "key_id": keyCtx.KeyID, "owner": keyCtx.OwnerID, }) } func adminHandler(w http.ResponseWriter, r *http.Request) { keyCtx, _ := GetKeyContext(r) json.NewEncoder(w).Encode(map[string]any{ "message": "Admin access granted", "key_id": keyCtx.KeyID, }) } func optionalAuthHandler(w http.ResponseWriter, r *http.Request) { keyCtx, ok := GetKeyContext(r) if ok { json.NewEncoder(w).Encode(map[string]any{ "message": "Authenticated access", "key_id": keyCtx.KeyID, }) } else { json.NewEncoder(w).Encode(map[string]any{ "message": "Anonymous access", }) } } ``` ## Key Features * **Context propagation** - Key info stored in request context * **Permission checking** - Middleware to check specific permissions * **Optional auth** - Support for optional authentication * **Custom headers** - Configurable header names and prefixes * **Timeout handling** - Request timeouts for Unkey API calls * **Error responses** - Structured JSON error responses ## Testing ```bash theme={"theme":"kanagawa-wave"} # Start server go run main.go # Test protected route without key curl http://localhost:8080/api/protected # {"error":"Missing API key","code":"MISSING_KEY"} # Test protected route with valid key curl -H "Authorization: Bearer YOUR_API_KEY" http://localhost:8080/api/protected # {"message":"Access granted","key_id":"key_..."} # Test public route (no auth required) curl http://localhost:8080/api/public # {"message":"Anonymous access"} # Test public route with auth curl -H "Authorization: Bearer YOUR_API_KEY" http://localhost:8080/api/public # {"message":"Authenticated access","key_id":"key_..."} # Test admin route without permission curl -H "Authorization: Bearer USER_API_KEY" http://localhost:8080/api/admin # {"error":"Insufficient permissions","code":"FORBIDDEN","required":"admin:read"} # Test admin route with permission curl -H "Authorization: Bearer ADMIN_API_KEY" http://localhost:8080/api/admin # {"message":"Admin access granted","key_id":"key_..."} ``` ## Related Complete Go SDK documentation # Next.js API Routes Source: https://unkey.com/docs/cookbook/nextjs-api-routes Protect your Next.js API routes with Unkey API key authentication. Copy-paste recipe for verifying keys in App Router route handlers. Two approaches: use the `@unkey/nextjs` wrapper for simplicity, or manual verification for full control. ## Option 1: Using @unkey/nextjs (Recommended) The simplest way to protect Next.js API routes. ### Install ```bash theme={"theme":"kanagawa-wave"} npm install @unkey/nextjs ``` ### App Router ```typescript app/api/protected/route.ts theme={"theme":"kanagawa-wave"} import { withUnkey, NextRequestWithUnkeyContext } from "@unkey/nextjs"; export const POST = withUnkey( async (req: NextRequestWithUnkeyContext) => { // req.unkey contains verification result const { identity } = req.unkey.data; return Response.json({ message: "Access granted", user: identity?.externalId, }); }, { rootKey: process.env.UNKEY_ROOT_KEY! }, ); ``` ### Pages Router ```typescript pages/api/protected.ts theme={"theme":"kanagawa-wave"} import { withUnkey, NextRequestWithUnkeyContext } from "@unkey/nextjs"; import { NextApiRequest, NextApiResponse } from "next"; async function handler(req: NextApiRequest, res: NextApiResponse) { // Type assertion needed for Pages Router const unkeyReq = req as unknown as NextRequestWithUnkeyContext; return res.json({ message: "Access granted", user: unkeyReq.unkey?.data.identity?.externalId, }); } export default withUnkey(handler, { rootKey: process.env.UNKEY_ROOT_KEY! }); ``` ### Custom key extraction By default, `withUnkey` looks for a Bearer token in the `Authorization` header. Customize this: ```typescript theme={"theme":"kanagawa-wave"} export const POST = withUnkey( async (req) => { /* ... */ }, { rootKey: process.env.UNKEY_ROOT_KEY!, getKey: (req) => req.headers.get("x-api-key"), // Custom header }, ); ``` ### Custom error handling ```typescript theme={"theme":"kanagawa-wave"} export const POST = withUnkey( async (req) => { /* ... */ }, { rootKey: process.env.UNKEY_ROOT_KEY!, handleInvalidKey: (req, result) => { // result.code tells you why it failed return Response.json( { error: "Unauthorized", reason: result.code, // "NOT_FOUND", "EXPIRED", "RATE_LIMITED", etc. }, { status: 401 }, ); }, onError: (req, error) => { console.error("Unkey error:", error); return Response.json( { error: "Authentication service unavailable" }, { status: 503 }, ); }, }, ); ``` *** ## Option 2: Manual Verification For full control over the authentication flow. ```typescript app/api/protected/route.ts theme={"theme":"kanagawa-wave"} import { Unkey } from "@unkey/api"; import { UnkeyError } from "@unkey/api/models/errors"; import { NextRequest, NextResponse } from "next/server"; const unkey = new Unkey({ rootKey: process.env.UNKEY_ROOT_KEY! }); export async function POST(req: NextRequest) { // 1. Extract API key const authHeader = req.headers.get("authorization"); if (!authHeader?.startsWith("Bearer ")) { return NextResponse.json({ error: "Missing API key" }, { status: 401 }); } const apiKey = authHeader.slice(7); // 2. Verify with Unkey try { const { meta, data } = await unkey.keys.verifyKey({ key: apiKey, }); // You can reject the request because the key is invalid. if (!data.valid) { return Response.json({ error: "Invalid API key" }, { status: 401 }); } // Perform your operations return Response.json({ data: "hello world" }); } catch (err) { // handle our errors however you want. if (err instanceof UnkeyError) { console.error("Unkey API Error:", { statusCode: err.statusCode, body: err.body, message: err.message, }); return Response.json( { error: "API error occurred", details: err.message }, { status: err.statusCode }, ); } // Handle generic errors console.log("Unknown error:", err); return Response.json({ error: "Internal Server Error" }, { status: 500 }); } } ``` *** ## Reusable Middleware Pattern Create a helper for consistent auth across routes: ```typescript lib/auth.ts theme={"theme":"kanagawa-wave"} import { Unkey } from "@unkey/api"; import { UnkeyError } from "@unkey/api/models/errors"; import { NextRequest, NextResponse } from "next/server"; import { V2KeysVerifyKeyResponseData } from "@unkey/api/models/components"; type AuthenticatedHandler = ( req: NextRequest, auth: V2KeysVerifyKeyResponseData, ) => Promise; const unkey = new Unkey({ rootKey: process.env.UNKEY_ROOT_KEY! }); export function withAuth(handler: AuthenticatedHandler) { return async (req: NextRequest) => { const authHeader = req.headers.get("authorization"); if (!authHeader?.startsWith("Bearer ")) { return NextResponse.json({ error: "Missing API key" }, { status: 401 }); } try { const { meta, data } = await unkey.keys.verifyKey({ key: authHeader.slice(7), }); if (!data.valid) { return NextResponse.json({ error: data.code }, { status: 401 }); } return handler(req, data); } catch (error) { if (error instanceof UnkeyError) { return NextResponse.json( { error: error.message }, { status: error.statusCode }, ); } return NextResponse.json( { error: "Service unavailable" }, { status: 503 }, ); } }; } ``` Use it: ```typescript app/api/users/route.ts theme={"theme":"kanagawa-wave"} import { withAuth } from "@/lib/auth"; export const GET = withAuth(async (req, auth) => { // auth contains the full verification result const users = await db.users.findMany({ where: { organizationId: auth.identity?.externalId }, }); return Response.json({ users }); }); import { withAuth } from "../../lib/auth/withauth"; import { NextResponse } from "next/server"; export const GET = withAuth(async (_req, auth) => { // auth contains the full verification result so you can look at details const users = await db.users.findMany({ where: { organizationId: auth.identity?.externalId }, }); return NextResponse.json({ users }); }); ``` *** ## Check Permissions Require specific permissions for sensitive endpoints: ```typescript app/api/admin/route.ts theme={"theme":"kanagawa-wave"} import { Unkey } from "@unkey/api"; import { UnkeyError } from "@unkey/api/models/errors"; import { NextRequest, NextResponse } from "next/server"; const unkey = new Unkey({ rootKey: process.env.UNKEY_ROOT_KEY! }); export async function POST(req: NextRequest) { // 1. Extract API key const authHeader = req.headers.get("authorization"); if (!authHeader?.startsWith("Bearer ")) { return NextResponse.json({ error: "Missing API key" }, { status: 401 }); } const apiKey = authHeader.slice(7); // 2. Verify with Unkey try { const result = await unkey.keys.verifyKey({ key: apiKey, permissions: "admin.delete", }); // You can reject the request because the key is invalid. if (!result.data.valid) { if (result.data.code === "INSUFFICIENT_PERMISSIONS") { return Response.json( { error: "Admin access required" }, { status: 403 }, ); } return Response.json({ error: "Unauthorized" }, { status: 401 }); } return Response.json({ data: "hello world" }); } catch (err) { // handle our errors however you want. if (err instanceof UnkeyError) { console.error("Unkey API Error:", { statusCode: err.statusCode, body: err.body, message: err.message, }); return Response.json( { error: "API error occurred", details: err.message }, { status: err.statusCode }, ); } // Handle generic errors console.log("Unknown error:", err); return Response.json({ error: "Internal Server Error" }, { status: 500 }); } } ``` *** ## Environment Setup ```bash .env.local theme={"theme":"kanagawa-wave"} # Required for verification and key management UNKEY_ROOT_KEY=unkey_... ``` Never expose `UNKEY_ROOT_KEY` to the client. It should only be used in server-side code. # Per-User Rate Limits Source: https://unkey.com/docs/cookbook/per-user-ratelimit Apply different rate limits based on user subscription tiers using Unkey. Give free, pro, and enterprise users separate request quotas. Apply different rate limits to different users based on their subscription tier, role, or any other criteria. This recipe shows how to implement tiered rate limiting without hardcoding limits in your application. ## The pattern ```typescript theme={"theme":"kanagawa-wave"} // Define limits per tier const TIER_LIMITS = { free: { limit: 100, duration: "1h" }, pro: { limit: 1000, duration: "1h" }, enterprise: { limit: 10000, duration: "1h" }, }; // Get user's tier and apply appropriate limit const tier = await getUserTier(userId); const config = TIER_LIMITS[tier]; const { success } = await limiter.limit(userId, { limit: config.limit, duration: config.duration, }); ``` ## Full implementation ### Next.js API Route ```typescript theme={"theme":"kanagawa-wave"} // app/api/route.ts import { Ratelimit } from "@unkey/ratelimit"; import { headers } from "next/headers"; import { NextResponse } from "next/server"; // Initialize with default limits (will be overridden per-request) const limiter = new Ratelimit({ rootKey: process.env.UNKEY_ROOT_KEY!, namespace: "api", limit: 100, duration: "1h", }); const TIER_LIMITS: Record = { free: { limit: 100, duration: "1h" }, pro: { limit: 1000, duration: "1h" }, enterprise: { limit: 10000, duration: "1h" }, }; async function getUserTier(userId: string): Promise { // Replace with your actual user lookup // e.g., database query, auth provider, etc. const user = await db.users.findUnique({ where: { id: userId } }); return user?.tier ?? "free"; } export async function POST(request: Request) { const headersList = headers(); const userId = headersList.get("x-user-id"); if (!userId) { return NextResponse.json({ error: "Missing user ID" }, { status: 401 }); } // Get user's tier const tier = await getUserTier(userId); const config = TIER_LIMITS[tier] ?? TIER_LIMITS.free; // Apply tier-specific rate limit by overriding the constructor defaults const { success, remaining, reset } = await limiter.limit(userId, { limit: { limit: config.limit, duration: config.duration as any, }, }); if (!success) { return NextResponse.json( { error: "Rate limit exceeded", tier, reset: new Date(reset).toISOString(), }, { status: 429, headers: { "X-RateLimit-Limit": config.limit.toString(), "X-RateLimit-Remaining": "0", "X-RateLimit-Reset": reset.toString(), }, }, ); } // Your API logic here return NextResponse.json({ message: "Success", tier, remaining, }); } ``` ### Express Middleware ```typescript theme={"theme":"kanagawa-wave"} // middleware/ratelimit.ts import { Ratelimit } from "@unkey/ratelimit"; import type { Request, Response, NextFunction } from "express"; const limiter = new Ratelimit({ rootKey: process.env.UNKEY_ROOT_KEY!, namespace: "api", limit: 100, duration: "1h", }); const TIER_LIMITS: Record = { free: { limit: 100, duration: "1h" }, pro: { limit: 1000, duration: "1h" }, enterprise: { limit: 10000, duration: "1h" }, }; export function tieredRateLimit() { return async (req: Request, res: Response, next: NextFunction) => { const userId = req.headers["x-user-id"] as string; if (!userId) { return res.status(401).json({ error: "Missing user ID" }); } // Get tier from your auth system const tier = req.user?.tier ?? "free"; const config = TIER_LIMITS[tier] ?? TIER_LIMITS.free; const { success, remaining, reset } = await limiter.limit(userId, { limit: { limit: config.limit, duration: config.duration as any, }, }); // Always set rate limit headers res.set({ "X-RateLimit-Limit": config.limit.toString(), "X-RateLimit-Remaining": remaining.toString(), "X-RateLimit-Reset": reset.toString(), "X-RateLimit-Tier": tier, }); if (!success) { return res.status(429).json({ error: "Rate limit exceeded", tier, retryAfter: Math.ceil((reset - Date.now()) / 1000), }); } next(); }; } ``` ## Using Unkey overrides (recommended) Instead of managing limits in your code, use [Unkey overrides](/docs/platform/ratelimiting/overrides) to set per-user limits dynamically. Overrides are managed via the separate `Overrides` class: ```typescript theme={"theme":"kanagawa-wave"} import { Overrides, Ratelimit } from "@unkey/ratelimit"; const limiter = new Ratelimit({ rootKey: process.env.UNKEY_ROOT_KEY!, namespace: "api", limit: 100, // Default for free tier duration: "1h", }); const overrides = new Overrides({ rootKey: process.env.UNKEY_ROOT_KEY!, }); // When a user upgrades to Pro, set an override await overrides.setOverride({ namespace: "api", identifier: userId, limit: 1000, duration: 3600000, // 1h in ms }); // Now this user automatically gets 1000/hour instead of 100 const { success } = await limiter.limit(userId); ``` This approach means: * No code changes when limits change * Overrides can be managed via API or dashboard * Default limit applies to users without overrides ## With API key verification If you're already using Unkey for API keys, attach rate limits directly to keys: ```typescript theme={"theme":"kanagawa-wave"} import { Unkey } from "@unkey/api"; const unkey = new Unkey({ rootKey: process.env.UNKEY_ROOT_KEY! }); // Create a Pro tier key with higher limits try { const { data } = await unkey.keys.createKey({ apiId: "api_xxx", name: "Pro User Key", ratelimits: [ { name: "requests", limit: 1000, duration: 3600000, // 1 hour in ms autoApply: true, }, ], meta: { tier: "pro", }, }); } catch (err) { console.error(err); throw err; } // Verification automatically enforces the key's rate limit const { meta, data } = await unkey.keys.verifyKey({ key: userKey }); if (!data.valid) { if (data.code === "RATE_LIMITED") { // Key-specific limit exceeded } } ``` ## Best practices Always use the same identifier format (user ID, org ID) for accurate limiting across requests. Return rate limit headers so clients know their limits and can back off gracefully. Pro/Enterprise users often expect some burst capacity. Consider slightly higher limits with shorter windows. Track when users hit limits to inform pricing decisions and identify potential abuse. ## Next steps Manage per-user limits without code changes Full subscription tier implementation # Tiered Subscriptions Source: https://unkey.com/docs/cookbook/tiered-subscriptions Implement Free, Pro, and Enterprise subscription tiers with Unkey. Set different API key limits, rate limits, and permissions per plan. A complete pattern for SaaS subscription tiers with different API limits, rate limits, and features. ## The Pattern 1. Store the user's plan in key metadata 2. Configure limits based on plan at key creation 3. Optionally upgrade/downgrade by updating the key ## Define Your Tiers ```typescript lib/tiers.ts theme={"theme":"kanagawa-wave"} export const TIERS = { free: { name: "Free", credits: 100, refill: { interval: "daily" as const, amount: 100 }, rateLimit: { limit: 10, duration: 60000 }, // 10/min features: ["api_access"], }, pro: { name: "Pro", credits: 10000, refill: { interval: "monthly" as const, amount: 10000 }, rateLimit: { limit: 100, duration: 60000 }, // 100/min features: ["api_access", "webhooks", "priority_support"], }, enterprise: { name: "Enterprise", credits: null, // Unlimited refill: null, rateLimit: { limit: 1000, duration: 60000 }, // 1000/min features: [ "api_access", "webhooks", "priority_support", "sla", "custom_domain", ], }, } as const; export type Tier = keyof typeof TIERS; ``` ## Create Keys for Each Tier ```typescript lib/keys.ts theme={"theme":"kanagawa-wave"} import { Unkey } from "@unkey/api"; import { TIERS, Tier } from "./tiers"; const unkey = new Unkey({ rootKey: process.env.UNKEY_ROOT_KEY! }); export async function createKeyForUser(userId: string, tier: Tier) { const config = TIERS[tier]; const { data } = await unkey.keys.createKey({ apiId: process.env.UNKEY_API_ID!, prefix: `sk_${tier}`, externalId: userId, name: `${config.name} API Key`, // Set credits based on tier (skip for unlimited) ...(config.credits && { credits: { remaining: config.credits, refill: config.refill ?? undefined, }, }), // Set rate limits ratelimits: [ { name: "requests", limit: config.rateLimit.limit, duration: config.rateLimit.duration, }, ], // Store plan info in metadata meta: { tier, plan: config.name, features: config.features, }, }); return data; } ``` ## Check Features During Verification ```typescript middleware/auth.ts theme={"theme":"kanagawa-wave"} import { Unkey } from "@unkey/api"; const unkey = new Unkey({ rootKey: process.env.UNKEY_ROOT_KEY! }); export async function verifyAndCheckFeature( apiKey: string, requiredFeature?: string, ) { const { data } = await unkey.keys.verifyKey({ key: apiKey }); if (!data.valid) { return { valid: false, code: data.code }; } // Check feature access if (requiredFeature) { const features = (data.meta?.features as string[]) ?? []; if (!features.includes(requiredFeature)) { return { valid: false, code: "FEATURE_NOT_AVAILABLE", tier: data.meta?.tier, }; } } return { valid: true, data }; } ``` ## Upgrade/Downgrade Keys ```typescript lib/keys.ts theme={"theme":"kanagawa-wave"} export async function changeUserTier(keyId: string, newTier: Tier) { const config = TIERS[newTier]; await unkey.keys.updateKey({ keyId, // Update credits ...(config.credits ? { credits: { remaining: config.credits, refill: config.refill ?? undefined, }, } : { // Remove credits for unlimited tier // (remaining: null also clears the refill schedule) credits: { remaining: null, }, }), // Update rate limits ratelimits: [ { name: "requests", limit: config.rateLimit.limit, duration: config.rateLimit.duration, }, ], // Update metadata meta: { tier: newTier, plan: config.name, features: config.features, }, }); } ``` *** ## Express Example ```typescript app.ts theme={"theme":"kanagawa-wave"} import express from "express"; import { Unkey } from "@unkey/api"; import { TIERS } from "./lib/tiers"; const app = express(); const unkey = new Unkey({ rootKey: process.env.UNKEY_ROOT_KEY! }); // Middleware that checks tier and feature access function requireFeature(feature: string) { return async (req, res, next) => { const apiKey = req.headers.authorization?.slice(7); if (!apiKey) { return res.status(401).json({ error: "Missing API key" }); } let data; try { ({ data } = await unkey.keys.verifyKey({ key: apiKey })); } catch { return res.status(503).json({ error: "Service unavailable" }); } if (!data.valid) { return res.status(401).json({ error: data.code }); } const features = (data.meta?.features as string[]) ?? []; if (!features.includes(feature)) { return res.status(403).json({ error: "Feature not available on your plan", required: feature, currentTier: data.meta?.tier, upgrade: "https://yourapp.com/upgrade", }); } req.user = { id: data.identity?.externalId, tier: data.meta?.tier as string, features, }; next(); }; } // Basic API access (all tiers) app.get("/api/data", requireFeature("api_access"), (req, res) => { res.json({ data: [] }); }); // Webhooks (Pro and Enterprise only) app.post("/api/webhooks", requireFeature("webhooks"), (req, res) => { res.json({ webhook: "created" }); }); // SLA endpoint (Enterprise only) app.get("/api/sla-status", requireFeature("sla"), (req, res) => { res.json({ sla: "99.99%" }); }); app.listen(3000); ``` *** ## Next.js Example ```typescript app/api/webhooks/route.ts theme={"theme":"kanagawa-wave"} import { Unkey } from "@unkey/api"; import { NextRequest, NextResponse } from "next/server"; const unkey = new Unkey({ rootKey: process.env.UNKEY_ROOT_KEY! }); export async function POST(req: NextRequest) { const apiKey = req.headers.get("authorization")?.slice(7); if (!apiKey) { return NextResponse.json({ error: "Missing API key" }, { status: 401 }); } const { data } = await unkey.keys.verifyKey({ key: apiKey }); if (!data.valid) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } const features = (data.meta?.features as string[]) ?? []; if (!features.includes("webhooks")) { return NextResponse.json( { error: "Webhooks require a Pro or Enterprise plan", currentTier: data.meta?.tier, upgradeUrl: "/settings/billing", }, { status: 403 }, ); } // Process webhook creation... return NextResponse.json({ created: true }); } ``` *** ## Handling Plan Changes When a user upgrades via Stripe/billing: ```typescript webhooks/stripe.ts theme={"theme":"kanagawa-wave"} import Stripe from "stripe"; import { changeUserTier } from "@/lib/keys"; export async function handleSubscriptionChange(event: Stripe.Event) { const subscription = event.data.object as Stripe.Subscription; // Map Stripe price IDs to tiers const priceToTier: Record = { price_free: "free", price_pro: "pro", price_enterprise: "enterprise", }; const newTier = priceToTier[subscription.items.data[0].price.id]; const userId = subscription.metadata.userId; // Get user's key ID from your database const keyId = await db.users.getKeyId(userId); // Update the key await changeUserTier(keyId, newTier); // Optionally notify the user await sendEmail(userId, `You've been upgraded to ${newTier}!`); } ``` *** ## Dashboard Display Show users their current usage: ```typescript app/api/usage/route.ts theme={"theme":"kanagawa-wave"} import { Unkey } from "@unkey/api"; const unkey = new Unkey({ rootKey: process.env.UNKEY_ROOT_KEY! }); export async function GET(req: NextRequest) { const apiKey = req.headers.get("authorization")?.slice(7)!; const { data } = await unkey.keys.verifyKey({ key: apiKey }); if (!data.valid) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } const tier = TIERS[data.meta?.tier as Tier]; return NextResponse.json({ tier: data.meta?.tier, plan: tier.name, usage: { credits: { used: tier.credits ? tier.credits - (data.credits ?? 0) : null, limit: tier.credits, remaining: data.credits, unlimited: tier.credits === null, }, rateLimit: { limit: tier.rateLimit.limit, window: `${tier.rateLimit.duration / 1000}s`, }, }, features: tier.features, }); } ``` Response: ```json theme={"theme":"kanagawa-wave"} { "tier": "pro", "plan": "Pro", "usage": { "credits": { "used": 1234, "limit": 10000, "remaining": 8766, "unlimited": false }, "rateLimit": { "limit": 100, "window": "60s" } }, "features": ["api_access", "webhooks", "priority_support"] } ``` # Usage-Based Billing Source: https://unkey.com/docs/cookbook/usage-billing Track API usage per customer for metered billing with Unkey. Use the remaining credits feature to count requests and enforce usage caps. Charge customers based on how much they use your API. This recipe shows how to track usage with Unkey's credits system and integrate with billing providers like Stripe. ## The pattern ```typescript theme={"theme":"kanagawa-wave"} // Create a key with monthly credits const key = await unkey.keys.createKey({ apiId: "api_xxx", credits: { remaining: 10000, // 10,000 API calls included refill: { interval: "monthly", amount: 10000, }, }, }); // Each verification decrements remaining const { meta, data } = await unkey.keys.verifyKey({ key: userKey }); if (!data.valid && data.code === "USAGE_EXCEEDED") { // Prompt user to upgrade or pay for overage } // Check remaining credits console.log(data.credits); // 9,999 after first call ``` ## Full implementation ### Setting up usage tracking ```typescript theme={"theme":"kanagawa-wave"} // lib/unkey.ts import { Unkey } from "@unkey/api"; const unkey = new Unkey({ rootKey: process.env.UNKEY_ROOT_KEY! }); interface CreateBillingKeyOptions { customerId: string; plan: "starter" | "growth" | "scale"; stripeCustomerId?: string; } const PLAN_CREDITS = { starter: 1_000, growth: 10_000, scale: 100_000, }; export async function createBillingKey(options: CreateBillingKeyOptions) { const credits = PLAN_CREDITS[options.plan]; // The SDK throws on failure, so any error propagates to the caller. const { data } = await unkey.keys.createKey({ apiId: process.env.UNKEY_API_ID!, externalId: options.customerId, // Link to your user/org name: `${options.plan} plan`, credits: { remaining: credits, refill: { interval: "monthly", amount: credits, }, }, meta: { plan: options.plan, stripeCustomerId: options.stripeCustomerId, createdAt: new Date().toISOString(), }, }); return data; } ``` ### API route with usage tracking ```typescript theme={"theme":"kanagawa-wave"} // app/api/route.ts import { Unkey } from "@unkey/api"; import { NextResponse } from "next/server"; const unkey = new Unkey({ rootKey: process.env.UNKEY_ROOT_KEY! }); export async function POST(request: Request) { const apiKey = request.headers.get("authorization")?.replace("Bearer ", ""); if (!apiKey) { return NextResponse.json({ error: "Missing API key" }, { status: 401 }); } let data; try { ({ data } = await unkey.keys.verifyKey({ key: apiKey })); } catch { return NextResponse.json({ error: "Verification failed" }, { status: 500 }); } if (!data.valid) { if (data.code === "USAGE_EXCEEDED") { return NextResponse.json( { error: "Usage limit exceeded", remaining: 0, message: "Please upgrade your plan or wait for monthly reset", }, { status: 402 }, // Payment Required ); } return NextResponse.json( { error: "Invalid API key", code: data.code }, { status: 401 }, ); } // Include usage info in response headers const response = NextResponse.json({ success: true, // Your API response... }); response.headers.set("X-Usage-Remaining", (data.credits ?? 0).toString()); return response; } ``` ### Stripe integration for overages ```typescript theme={"theme":"kanagawa-wave"} // lib/billing.ts import Stripe from "stripe"; import { Unkey } from "@unkey/api"; const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!); const unkey = new Unkey({ rootKey: process.env.UNKEY_ROOT_KEY! }); // Record overage usage in Stripe export async function recordOverage(keyId: string, amount: number) { // Get the key to find the Stripe customer const { data: key } = await unkey.keys.getKey({ keyId }); if (!key?.meta?.stripeCustomerId) { throw new Error("No Stripe customer linked to this key"); } // Find or create a usage-based subscription item const subscriptions = await stripe.subscriptions.list({ customer: key.meta.stripeCustomerId as string, status: "active", }); const subscription = subscriptions.data[0]; if (!subscription) { throw new Error("No active subscription"); } // Find the metered price item const meteredItem = subscription.items.data.find( (item) => item.price.recurring?.usage_type === "metered", ); if (meteredItem) { // Report usage to Stripe await stripe.subscriptionItems.createUsageRecord(meteredItem.id, { quantity: amount, timestamp: Math.floor(Date.now() / 1000), action: "increment", }); } } // Webhook handler for when credits run out export async function handleUsageExceeded(keyId: string) { const { data: key } = await unkey.keys.getKey({ keyId }); if (key?.meta?.stripeCustomerId) { // Option 1: Add overage credits and bill later await unkey.keys.updateKey({ keyId, credits: { remaining: 1000, // Grant overage allowance }, }); await recordOverage(keyId, 1000); // Option 2: Or just notify and let them upgrade // await sendUpgradeEmail(key.meta.email); } } ``` ### Usage dashboard endpoint ```typescript theme={"theme":"kanagawa-wave"} // app/api/usage/route.ts import { Unkey } from "@unkey/api"; import { NextResponse } from "next/server"; const unkey = new Unkey({ rootKey: process.env.UNKEY_ROOT_KEY! }); export async function GET(request: Request) { const customerId = request.headers.get("x-customer-id"); if (!customerId) { return NextResponse.json({ error: "Missing customer ID" }, { status: 401 }); } // Get all keys for this customer const { data } = await unkey.apis.listKeys({ apiId: process.env.UNKEY_API_ID!, externalId: customerId, }); if (!data.length) { return NextResponse.json({ error: "No keys found" }, { status: 404 }); } const key = data[0]; const remaining = key.credits?.remaining ?? 0; const limit = key.credits?.refill?.amount ?? 0; return NextResponse.json({ plan: key.meta?.plan ?? "starter", usage: { remaining, limit, used: limit - remaining, refillInterval: key.credits?.refill?.interval ?? null, refillDay: key.credits?.refill?.refillDay ?? null, }, }); } ``` ## Plan upgrades When a customer upgrades, update their key: ```typescript theme={"theme":"kanagawa-wave"} export async function upgradePlan( keyId: string, newPlan: "starter" | "growth" | "scale", ) { const newCredits = PLAN_CREDITS[newPlan]; // Get current usage const { data: currentKey } = await unkey.keys.getKey({ keyId }); const currentRemaining = currentKey.credits?.remaining ?? 0; const currentRefillAmount = currentKey.credits?.refill?.amount ?? 0; // Option 1: Add the difference (pro-rated) const creditsToAdd = newCredits - currentRefillAmount; await unkey.keys.updateKey({ keyId, credits: { remaining: currentRemaining + creditsToAdd, refill: { interval: "monthly", amount: newCredits, }, }, meta: { ...currentKey.meta, plan: newPlan, upgradedAt: new Date().toISOString(), }, }); // Option 2: Reset to new plan's full credits // await unkey.keys.updateKey({ // keyId, // credits: { // remaining: newCredits, // refill: { interval: "monthly", amount: newCredits }, // }, // }); } ``` ## Multi-resource tracking Track different types of usage separately: ```typescript theme={"theme":"kanagawa-wave"} // Create a key with multiple rate limits as credit pools const { data } = await unkey.keys.createKey({ apiId: process.env.UNKEY_API_ID!, externalId: customerId, credits: { remaining: 10000, // General API calls }, ratelimits: [ { name: "ai-tokens", limit: 100000, duration: 2592000000 }, // 30 days { name: "storage-mb", limit: 5000, duration: 2592000000 }, { name: "exports", limit: 100, duration: 2592000000 }, ], meta: { plan: "growth" }, }); // Check specific resource usage on verify const { data: verification } = await unkey.keys.verifyKey({ key: userKey, ratelimits: [{ name: "ai-tokens", cost: tokenCount }], }); if ( verification.ratelimits?.find((r) => r.name === "ai-tokens")?.remaining === 0 ) { return { error: "AI token quota exceeded" }; } ``` ## Best practices Monthly refills keep usage tracking simple. For different billing cycles, adjust the interval. Don't just cut users off. Offer overage billing or soft limits with warnings. Let users see their usage in your dashboard. Nobody likes surprise bills. Use webhooks to keep Stripe/Paddle usage records in sync with Unkey. ## Next steps Complete Free/Pro/Enterprise implementation Deep dive into Unkey's credit system # Environments Source: https://unkey.com/docs/environments/overview Configure isolated deployment environments in Unkey to separate production traffic from preview and staging builds with scoped settings. Deploying applications on Unkey is in public beta. To try it, open the product switcher in the top-left of the dashboard and select **Deploy**. During beta, deployed resources are free. We're eager for feedback, so let us know what you think on [Discord](https://unkey.com/discord), [X](https://x.com/unkeydev), or email [support@unkey.com](mailto:support@unkey.com). An environment is an isolated deployment target within an [app](/docs/platform/apps/overview). Each environment has its own deployments, [variables](/docs/platform/variables/overview), [custom domains](/docs/networking/domains), and [Sentinel policies](/docs/platform/sentinel/overview). ## Production and preview Every app gets two environments: | Environment | Purpose | | -------------- | ------------------------------------------------------------------------ | | **Production** | Serves live traffic. Runs three Sentinel replicas for high availability. | | **Preview** | For testing branches before merging. Runs one Sentinel replica. | Unkey creates both environments automatically when you create a project. You don't need to set them up manually. Custom environments beyond production and preview are not yet available. ## Branch mapping When you connect a [GitHub repository](/docs/build-and-deploy/github), Unkey maps branches to environments: * Pushes to the default branch (typically `main`) deploy to **production**. * Pushes to any other branch deploy to **preview**. Each push creates a new [deployment](/docs/build-and-deploy/deployments) in the corresponding environment. ## Environment-scoped configuration Configuration is scoped per environment, so production and preview can differ: * **Variables**: Different database URLs, API keys, and feature flags for each environment * **Custom domains**: Production might use `api.acme.com` while preview uses auto-generated domains * **Regions and instances**: Run production in multiple regions with higher instance counts, and preview in a single region * **Sentinel policies**: Apply stricter rate limits or authentication rules in production ## Sticky domains Each environment has a sticky domain that always points to the latest deployment in that environment: ```text theme={"theme":"kanagawa-wave"} ---.unkey.app ``` When a new deployment reaches the Ready state, the environment domain updates automatically. You don't need to reconfigure DNS or update your clients. Production environments also get a live domain without the environment name: ```text theme={"theme":"kanagawa-wave"} --.unkey.app ``` See [Wildcard domains](/docs/networking/wildcard-domains) for the full domain naming pattern. ## Deployments per environment An environment can have many deployments, but only one is active at a time. In production, when a new deployment goes live, the previous one transitions to standby after 30 minutes. Standby deployments are available for [rollbacks](/docs/build-and-deploy/rollbacks). In preview environments, deployments stay running as long as they receive traffic. Idle preview deployments (zero requests for six hours) are automatically archived to free up resources. ## Next steps Configure environment-specific variables Serve your app from your own domain # no_running_instances Source: https://unkey.com/docs/errors/frontline/capacity/no_running_instances NoRunningInstances represents a 503 error - no deployments have running instances `err:frontline:capacity:no_running_instances` # firewall_denied Source: https://unkey.com/docs/errors/frontline/client/firewall_denied Denied represents a 403 error - request rejected by a Firewall policy `err:frontline:client:firewall_denied` # insufficient_permissions Source: https://unkey.com/docs/errors/frontline/client/insufficient_permissions InsufficientPermissions represents a 403 error - the credential lacks the permissions required by a permission_query. `err:frontline:client:insufficient_permissions` # invalid_key Source: https://unkey.com/docs/errors/frontline/client/invalid_key InvalidKey represents a 401 error - key not found, disabled, or expired. `err:frontline:client:invalid_key` # missing_credentials Source: https://unkey.com/docs/errors/frontline/client/missing_credentials MissingCredentials represents a 401 error - no credentials found in the request. `err:frontline:client:missing_credentials` # openapi_validation_failed Source: https://unkey.com/docs/errors/frontline/client/openapi_validation_failed InvalidRequest represents a 400 error - request does not conform to the OpenAPI spec `err:frontline:client:openapi_validation_failed` # rate_limited Source: https://unkey.com/docs/errors/frontline/client/rate_limited RateLimited represents a 429 error - the credential or its auto-applied rate limit was exceeded. `err:frontline:client:rate_limited` # invalid_configuration Source: https://unkey.com/docs/errors/frontline/config/invalid_configuration InvalidConfiguration represents a 422 error - the deployment's policy configuration could not be parsed. `err:frontline:config:invalid_configuration` # config_load_failed Source: https://unkey.com/docs/errors/frontline/platform/config_load_failed ConfigLoadFailed represents a 500 error - failed to load configuration `err:frontline:platform:config_load_failed` # deployment_selection_failed Source: https://unkey.com/docs/errors/frontline/platform/deployment_selection_failed DeploymentSelectionFailed represents a 500 error - failed to select an available deployment `err:frontline:platform:deployment_selection_failed` # internal_server_error Source: https://unkey.com/docs/errors/frontline/platform/internal_server_error InternalServerError represents a 500 error - internal server error `err:frontline:platform:internal_server_error` # config_not_found Source: https://unkey.com/docs/errors/frontline/routing/config_not_found ConfigNotFound represents a 404 error - no configuration found for the requested hostname `err:frontline:routing:config_not_found` # deployment_not_found Source: https://unkey.com/docs/errors/frontline/routing/deployment_not_found DeploymentNotFound represents a 404 error - the resolved deployment was not found or did not match the expected environment. `err:frontline:routing:deployment_not_found` # bad_gateway Source: https://unkey.com/docs/errors/frontline/upstream/bad_gateway BadGateway represents a 502 error - invalid response from upstream server `err:frontline:upstream:bad_gateway` # gateway_timeout Source: https://unkey.com/docs/errors/frontline/upstream/gateway_timeout GatewayTimeout represents a 504 error - upstream server timeout `err:frontline:upstream:gateway_timeout` # proxy_forward_failed Source: https://unkey.com/docs/errors/frontline/upstream/proxy_forward_failed ProxyForwardFailed represents a 502 error - failed to forward request to backend `err:frontline:upstream:proxy_forward_failed` # service_unavailable Source: https://unkey.com/docs/errors/frontline/upstream/service_unavailable ServiceUnavailable represents a 503 error - backend service is unavailable `err:frontline:upstream:service_unavailable` # Error Codes Source: https://unkey.com/docs/errors/overview Understand Unkey's structured error code system with categories for authentication, authorization, data, and application errors with fix guidance. ## Introduction Unkey's error system uses a structured approach to organize and identify errors across the platform. This system makes it easier to understand, debug, and handle errors consistently. ## Error Code Format All Unkey error codes follow a consistent URN-like format: ```text theme={"theme":"kanagawa-wave"} err:system:category:specific ``` For example: `err:unkey:authentication:missing` This format breaks down as follows: * **err**: Standard prefix for all error codes * **system**: The service area or responsibility domain (e.g., unkey, user) * **category**: The error type or classification (e.g., authentication, data) * **specific**: The exact error condition (e.g., missing, malformed) ## Systems The "system" component identifies where the error originated: * **unkey**: Errors originating from Unkey's internal systems * **github**: Errors related to GitHub integration * **aws**: Errors related to AWS integration ## Categories The "category" component provides a second level of classification, for example: * **authentication**: Errors related to the authentication process * **authorization**: Errors related to permissions and access control * **application**: Errors related to application operations and system integrity * **data**: Errors related to data operations and resources * **limits**: Rate limiting or quota-related errors ## Error Response Format When an error occurs, the API returns a consistent JSON response format: ```json theme={"theme":"kanagawa-wave"} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "error": { "detail": "Authentication credentials were not provided", "status": 401, "title": "Unauthorized", "type": "https://unkey.com/docs/errors/unkey/authentication/missing" } } ``` Key fields: * **requestId**: Unique identifier for the request (important for support) * **detail**: Human-readable explanation of the error * **status**: HTTP status code * **title**: Short summary of the error type * **type**: URL to detailed documentation about this error ## Documentation Integration All error codes have a corresponding documentation page accessible via the `type` URL in the error response. These pages provide detailed information about: * What caused the error * How to fix the issue * Common mistakes that lead to this error * Related errors you might encounter # assertion_failed Source: https://unkey.com/docs/errors/unkey/application/assertion_failed A runtime assertion or invariant check failed in Unkey. Learn what causes this internal error and how to report it if you encounter it. `err:unkey:application:assertion_failed` ```json Example theme={"theme":"kanagawa-wave"} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "error": { "detail": "A system integrity check failed while processing your request", "status": 500, "title": "Internal Server Error", "type": "https://unkey.com/docs/errors/unkey/application/assertion_failed" } } ``` ## What Happened? This error occurs when Unkey's internal system detects an inconsistency or violation of its expected invariants during the processing of your request. Unlike validation errors which occur when your input is invalid, assertion failures happen when the system's internal state doesn't match what was expected. Possible causes include: * Data corruption or inconsistency in Unkey's database * Bugs in Unkey's business logic * Race conditions or timing issues * System state that violates core assumptions * Incompatible changes between different parts of the system This type of error is generally not caused by anything you did wrong in your request, but rather indicates an internal issue with Unkey's system integrity. ## How To Fix Since this is an internal system error, there's usually nothing you can directly do to fix it. However, you can try the following: 1. **Retry the request**: Some assertion failures may be due to temporary conditions that resolve themselves 2. **Contact Unkey support**: Report the error with the request ID to help Unkey address the underlying issue 3. **Check for workarounds**: In some cases, using a different API endpoint or approach might avoid the issue When contacting support, be sure to include: * The full error response, including the request ID * The API endpoint you were calling * The request payload (with sensitive information redacted) * Any patterns you've noticed (e.g., if it happens consistently or intermittently) ## Important Notes * Assertion failures indicate bugs or data integrity issues that Unkey needs to fix * Unlike many other errors, changing your request is unlikely to resolve the issue * These errors are typically logged and monitored by Unkey's engineering team * If you encounter this error consistently, there may be an underlying issue with your account data ## 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 # invalid_input Source: https://unkey.com/docs/errors/unkey/application/invalid_input The client provided input that failed server-side validation. Review the error details to fix malformed fields, missing values, or bad types. `err:unkey:application:invalid_input` ```json Example theme={"theme":"kanagawa-wave"} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "error": { "detail": "The request contains invalid input that failed validation", "status": 400, "title": "Bad Request", "type": "https://unkey.com/docs/errors/unkey/application/invalid_input", "errors": [ { "location": "body.limit", "message": "must be greater than or equal to 1", "fix": "Provide a limit value of at least 1" } ] } } ``` ## What Happened? This error occurs when your request contains input data that doesn't meet Unkey's validation requirements. This could be due to missing required fields, values that are out of allowed ranges, incorrectly formatted data, or other validation failures. Common validation issues include: * Missing required fields * Values that exceed minimum or maximum limits * Strings that don't match required patterns * Invalid formats for IDs, emails, or other structured data * Type mismatches (e.g., providing a string where a number is expected) Here's an example of a request that would trigger this error: ```bash theme={"theme":"kanagawa-wave"} # Attempting to create a rate limit with an invalid limit value of 0 curl -X POST https://api.unkey.com/v2/ratelimit.limit \ -H "Content-Type: application/json" \ -H "Authorization: Bearer unkey_YOUR_API_KEY" \ -d '{ "namespace": "api.requests", "identifier": "user_123", "limit": 0, "duration": 60000 }' ``` ## How To Fix To fix this error, carefully review the error details provided in the response. The `errors` array contains specific information about what failed validation: 1. Check the `location` field to identify which part of your request is problematic 2. Read the `message` field for details about why validation failed 3. Look at the `fix` field (if available) for guidance on how to correct the issue 4. Modify your request to comply with the validation requirements Here's the corrected version of our example request: ```bash theme={"theme":"kanagawa-wave"} # Corrected request with a valid limit value curl -X POST https://api.unkey.com/v2/ratelimit.limit \ -H "Content-Type: application/json" \ -H "Authorization: Bearer unkey_YOUR_API_KEY" \ -d '{ "namespace": "api.requests", "identifier": "user_123", "limit": 100, "duration": 60000 }' ``` ## Common Mistakes * **Ignoring schema requirements**: Not checking the API documentation for field requirements * **Range violations**: Providing values outside of allowed ranges (too small, too large) * **Format errors**: Not following the required format for IDs, emails, or other structured data * **Missing fields**: Omitting required fields in API requests * **Type errors**: Sending the wrong data type (e.g., string instead of number) ## Related Errors * [err:unkey:application:assertion\_failed](./assertion_failed) - When a runtime assertion or invariant check fails * [err:unkey:application:protected\_resource](./protected_resource) - When attempting to modify a protected resource # precondition_failed Source: https://unkey.com/docs/errors/unkey/application/precondition_failed A precondition check failed before the operation could proceed. Learn common causes like conflicting state or unmet resource requirements. `err:unkey:application:precondition_failed` ```json Example theme={"theme":"kanagawa-wave"} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "error": { "detail": "Vault hasn't been set up.", "status": 412, "title": "Precondition Failed", "type": "https://unkey.com/docs/errors/unkey/application/precondition_failed" } } ``` ## What Happened? This error occurs when your request is valid, but a precondition required to fulfill it is not met. Unlike validation errors where your input is invalid, precondition failures indicate that the system or resource is not configured correctly to handle your request. Common scenarios that trigger this error: * **API not configured for keys**: The API you're trying to create keys for doesn't have key authentication set up * **Vault not configured**: You're trying to create recoverable keys or decrypt keys, but the vault service isn't set up for your workspace * **Encryption not enabled**: You're requesting key encryption/decryption on an API that doesn't have encryption enabled * **Rate limit configuration missing**: You're checking a rate limit that doesn't exist for the key or its associated identity Here's an example of a request that would trigger this error: ```bash theme={"theme":"kanagawa-wave"} # Attempting to create a recoverable key when vault isn't configured curl -X POST https://api.unkey.com/v2/keys.createKey \ -H "Content-Type: application/json" \ -H "Authorization: Bearer unkey_YOUR_ROOT_KEY" \ -d '{ "apiId": "api_1234567890", "prefix": "test", "recoverable": true }' ``` ## How To Fix The fix depends on which precondition failed. Check the error's `detail` field for specific information: ### Vault Not Set Up If you see "Vault hasn't been set up", you have two options: 1. **Configure the vault service** for your workspace (contact Unkey support if you need assistance) 2. **Remove the encryption requirement** from your request: ```bash theme={"theme":"kanagawa-wave"} # Create a non-recoverable key instead curl -X POST https://api.unkey.com/v2/keys.createKey \ -H "Content-Type: application/json" \ -H "Authorization: Bearer unkey_YOUR_ROOT_KEY" \ -d '{ "apiId": "api_1234567890", "prefix": "test", "recoverable": false }' ``` ### API Not Set Up for Keys If you see "The requested API is not set up to handle keys", you need to: 1. Verify the API ID is correct 2. Ensure the API has key authentication configured in your Unkey dashboard 3. Create or configure the key authentication settings for your API ### API Not Set Up for Encryption If you see "This API does not support key encryption", either: 1. Enable encrypted key storage for the API in your Unkey dashboard settings 2. Remove the `recoverable: true` option or `decrypt: true` parameter from your request ### Rate Limit Not Found If you see a message about a requested rate limit not existing: 1. Verify the rate limit name is correct 2. Create the rate limit configuration for the key or its identity 3. Ensure the rate limit is associated with the correct resource ## Common Mistakes * **Assuming features are enabled by default**: Features like vault encryption and rate limits require explicit configuration * **Wrong API configuration**: Trying to use encryption features on an API that wasn't set up for it * **Missing rate limit setup**: Referencing rate limits that haven't been created yet * **Workspace-level configuration issues**: Some features need to be enabled at the workspace level before they can be used ## Related Errors * [err:unkey:application:invalid\_input](./invalid_input) - When your request input fails validation * [err:unkey:data:api\_not\_found](../data/api_not_found) - When the API itself doesn't exist * [err:unkey:application:service\_unavailable](./service_unavailable) - When a required service is temporarily unavailable # protected_resource Source: https://unkey.com/docs/errors/unkey/application/protected_resource You attempted to modify or delete a resource with delete protection enabled. Disable protection in the dashboard before retrying the operation. `err:unkey:application:protected_resource` ```json Example theme={"theme":"kanagawa-wave"} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "error": { "detail": "The resource you are attempting to modify is protected and cannot be changed", "status": 403, "title": "Forbidden", "type": "https://unkey.com/docs/errors/unkey/application/protected_resource" } } ``` ## What Happened? This error occurs when you attempt to modify or delete a resource that is marked as protected in the Unkey system. Protected resources have a special status that prevents them from being changed or removed, typically because they are system resources, defaults, or otherwise critical to proper system operation. Common scenarios that trigger this error: * Attempting to delete a default API or workspace * Trying to modify system-created roles or permissions * Attempting to change protected settings or configurations * Trying to remove or alter resources that are required for system integrity Here's an example of a request that might trigger this error: ```bash theme={"theme":"kanagawa-wave"} # Attempting to delete a protected default API curl -X DELETE https://api.unkey.com/v2/apis.deleteApi \ -H "Content-Type: application/json" \ -H "Authorization: Bearer unkey_YOUR_API_KEY" \ -d '{ "apiId": "api_default_protected" }' ``` ## How To Fix Since protected resources are deliberately shielded from modification, the solution is usually to work with or around them rather than trying to change them: 1. **Work with the protected resource**: Use the resource as-is and build your workflows around it 2. **Create a new resource**: Instead of modifying a protected resource, create a new one with your desired configuration 3. **Use alternatives**: Look for alternative ways to achieve your goal without modifying protected resources 4. **Contact support**: If you believe you have a legitimate need to modify a protected resource, contact Unkey support For example, instead of deleting a protected API, you might create a new one: ```bash theme={"theme":"kanagawa-wave"} curl -X POST https://api.unkey.com/v2/apis.createApi \ -H "Content-Type: application/json" \ -H "Authorization: Bearer unkey_YOUR_API_KEY" \ -d '{ "name": "My Custom API" }' ``` ## Important Notes * Protected resources are designated as such for system stability and security reasons * Even with admin or owner permissions, protected resources typically cannot be modified * This protection is separate from permission-based restrictions and applies even to workspace owners * The protection status of a resource is not typically exposed in API responses until you try to modify it ## Related Errors * [err:unkey:authorization:forbidden](../authorization/forbidden) - When an operation is not allowed for policy reasons * [err:unkey:authorization:insufficient\_permissions](../authorization/insufficient_permissions) - When you lack permissions for an operation # service_unavailable Source: https://unkey.com/docs/errors/unkey/application/service_unavailable An Unkey service is temporarily unavailable. Learn about automatic retries, backoff strategies, and how to check Unkey's system status page. `err:unkey:application:service_unavailable` ```json Example theme={"theme":"kanagawa-wave"} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "error": { "detail": "The service is temporarily unavailable. Please try again later.", "status": 503, "title": "Service Unavailable", "type": "https://unkey.com/docs/errors/unkey/application/service_unavailable" } } ``` ## What Happened? This error occurs when a component of the Unkey platform is temporarily unavailable or unable to process your request. Unlike an unexpected error, this is a known state where the system has detected that it cannot currently provide the requested service. Possible causes of this error: * Scheduled maintenance * High load or capacity issues * Dependent service outages * Regional infrastructure problems * Database overload or maintenance Here's an example of a request that might receive this error during a service disruption: ```bash theme={"theme":"kanagawa-wave"} curl -X POST https://api.unkey.com/v2/keys.createKey \ -H "Content-Type: application/json" \ -H "Authorization: Bearer unkey_YOUR_API_KEY" \ -d '{ "apiId": "api_123abc", "name": "My API Key" }' ``` ## How To Fix Since this is a temporary service issue, the best approach is to wait and retry. Here are some strategies: 1. **Implement retry logic**: Add automatic retries with exponential backoff to your code 2. **Check service status**: Visit the Unkey status page for updates on service availability 3. **Try alternate regions**: If Unkey offers region-specific endpoints, try an alternate region 4. **Wait and retry manually**: If it's a one-time operation, simply try again later Here's an example of a robust retry strategy: ```bash theme={"theme":"kanagawa-wave"} # Bash script with retry logic max_attempts=5 attempt=0 backoff_time=1 while [ $attempt -lt $max_attempts ]; do response=$(curl -s -w "\n%{http_code}" \ -X POST https://api.unkey.com/v2/keys.createKey \ -H "Content-Type: application/json" \ -H "Authorization: Bearer unkey_YOUR_API_KEY" \ -d '{ "apiId": "api_123abc", "name": "My API Key" }') http_code=$(echo "$response" | tail -n1) body=$(echo "$response" | sed '$ d') if [ $http_code -eq 503 ]; then attempt=$((attempt+1)) if [ $attempt -eq $max_attempts ]; then echo "Service still unavailable after $max_attempts attempts" exit 1 fi echo "Service unavailable, retrying in $backoff_time seconds... (Attempt $attempt/$max_attempts)" sleep $backoff_time backoff_time=$((backoff_time*2)) else echo "$body" exit 0 fi done ``` ## Important Notes * This error is temporary, and the service will typically recover automatically * For critical applications, implement circuit breakers to prevent cascading failures * If the service remains unavailable for an extended period, check Unkey's status page or contact support * Include the `requestId` from the error response when contacting support ## Related Errors * [err:unkey:application:unexpected\_error](./unexpected_error) - When an unhandled error occurs * [err:unkey:authorization:workspace\_disabled](../authorization/workspace_disabled) - When the workspace is disabled (a different type of unavailability) # unexpected_error Source: https://unkey.com/docs/errors/unkey/application/unexpected_error An unhandled or unexpected error occurred in Unkey. Learn how to report this issue and what information to include for faster resolution. `err:unkey:application:unexpected_error` ```json Example theme={"theme":"kanagawa-wave"} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "error": { "detail": "An unexpected error occurred while processing your request", "status": 500, "title": "Internal Server Error", "type": "https://unkey.com/docs/errors/unkey/application/unexpected_error" } } ``` ## What Happened? This error occurs when the Unkey system encounters an internal error that wasn't anticipated or couldn't be handled gracefully. This is generally not caused by anything you did wrong in your request, but rather indicates an issue within Unkey's systems. Possible causes of this error: * Temporary infrastructure issues * Database connectivity problems * Bugs in the Unkey service * Resource constraints or timeouts * Unexpected edge cases not handled by the application logic Here's an example of a request that might trigger this error if there's an internal issue: ```bash theme={"theme":"kanagawa-wave"} # A valid request that could trigger an unexpected error if there's an internal issue curl -X POST https://api.unkey.com/v2/keys.createKey \ -H "Content-Type: application/json" \ -H "Authorization: Bearer unkey_YOUR_API_KEY" \ -d '{ "apiId": "api_123abc", "name": "My API Key" }' ``` ## How To Fix Since this is an internal error, there's usually little you can do to directly fix it, but you can try the following: 1. **Retry the request**: Many unexpected errors are temporary and will resolve on a retry 2. **Check Unkey status**: Visit the Unkey status page to see if there are any ongoing service issues 3. **Contact support**: If the error persists, contact Unkey support with your request ID 4. **Implement retry logic**: For critical operations, implement exponential backoff retry logic in your code Here's an example of implementing retry logic with exponential backoff: ```javascript theme={"theme":"kanagawa-wave"} // Pseudocode for retry with exponential backoff async function retryWithBackoff(fn, maxRetries = 3, baseDelay = 300) { let retries = 0; while (true) { try { return await fn(); } catch (error) { if (error.status !== 500 || retries >= maxRetries) { throw error; // Either not a 500 error or we've exceeded retries } // Exponential backoff with jitter const delay = baseDelay * Math.pow(2, retries) * (0.8 + Math.random() * 0.4); console.log( `Retrying after ${delay}ms (attempt ${retries + 1}/${maxRetries})`, ); await new Promise((resolve) => setTimeout(resolve, delay)); retries++; } } } ``` ## Important Notes * Always include the `requestId` when contacting support about this error * This error may indicate a bug in Unkey's systems that needs to be fixed * Unlike most other errors, this one usually can't be resolved by changing your request * If you encounter this error consistently with a specific API call, there may be an edge case that Unkey's team needs to address ## Related Errors * [err:unkey:application:service\_unavailable](./service_unavailable) - When a service is temporarily unavailable # key_not_found Source: https://unkey.com/docs/errors/unkey/authentication/key_not_found The authentication key was not found in the Unkey database. This occurs when a key has been deleted, expired, or was never created. Verify the key. `err:unkey:authentication:key_not_found` ```json Example theme={"theme":"kanagawa-wave"} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "error": { "detail": "The provided API key was not found", "status": 401, "title": "Unauthorized", "type": "https://unkey.com/docs/errors/unkey/authentication/key_not_found" } } ``` ## What Happened? This error occurs when you've provided a properly formatted API key in your request to the Unkey API, but the key doesn't exist in Unkey's system. The key might have been deleted, revoked, or you might be using an incorrect key. Common causes include: * Using an API key that has been deleted * Using an API key from a different workspace or environment * Typographical errors when entering the key * Using a test key in production or vice versa Here's an example of a request with a non-existent API key: ```bash theme={"theme":"kanagawa-wave"} # Request to Unkey API with a non-existent key curl -X POST https://api.unkey.com/v2/keys.listKeys \ -H "Content-Type: application/json" \ -H "Authorization: Bearer unkey_NONEXISTENT_KEY" ``` ## How To Fix To fix this error, you need to use a valid API key when making requests to the Unkey API: 1. **Check your Unkey dashboard**: Verify you're using the correct Unkey API key from the [Unkey dashboard](https://app.unkey.com) 2. **Create a new key if needed**: If your key was deleted, create a new one 3. **Use the correct environment**: Make sure you're using the appropriate key for your environment (development, production, etc.) Here's how to check and use the correct Unkey API key: 1. Log in to your Unkey dashboard 2. Navigate to the API keys section 3. Copy the appropriate API key for your environment 4. Use the key in your request as shown below ```bash theme={"theme":"kanagawa-wave"} curl -X POST https://api.unkey.com/v2/keys.listKeys \ -H "Content-Type: application/json" \ -H "Authorization: Bearer unkey_YOUR_VALID_API_KEY" ``` ## Common Mistakes * **Using revoked Unkey keys**: API keys that have been revoked will return this error * **Environment mismatch**: Using development keys in production or vice versa * **Workspace confusion**: Using keys from one workspace in another workspace's API calls * **Copy-paste errors**: Inadvertently omitting part of the key when copying * **Expired keys**: Keys that have expired will return this error * **Using demo keys**: Using example keys from documentation ## Related Errors * [err:unkey:authentication:missing](./missing) - When no authentication credentials are provided * [err:unkey:authentication:malformed](./malformed) - When the API key is provided but formatted incorrectly # malformed Source: https://unkey.com/docs/errors/unkey/authentication/malformed Authentication credentials were incorrectly formatted. Check that your Bearer token or API key header follows the expected format for Unkey. `err:unkey:authentication:malformed` ```json Example theme={"theme":"kanagawa-wave"} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "error": { "detail": "Authentication credentials were incorrectly formatted", "status": 401, "title": "Unauthorized", "type": "https://unkey.com/docs/errors/unkey/authentication/malformed" } } ``` ## What Happened? This error occurs when your request includes authentication credentials, but they are not formatted correctly. The Unkey API expects API keys to be provided in a specific format in the Authorization header. Common causes include: * Missing the "Bearer" prefix before your API key * Including extra spaces or characters * Using incorrect casing (e.g., "bearer" instead of "Bearer") * Providing a malformed or truncated API key Here's an example of a request with incorrectly formatted credentials: ```bash theme={"theme":"kanagawa-wave"} # Missing the "Bearer" prefix curl -X POST https://api.unkey.com/v2/keys.listKeys \ -H "Content-Type: application/json" \ -H "Authorization: unkey_YOUR_API_KEY" ``` ## How To Fix To fix this error, ensure your Authorization header follows the correct format: 1. **Use the correct format**: Ensure your Authorization header follows the format `Bearer unkey_YOUR_API_KEY` 2. **Check for extra spaces or characters**: Make sure there are no invisible characters or line breaks 3. **Verify the API key format**: Your Unkey API key should start with `unkey_` Here's the correctly formatted request: ```bash theme={"theme":"kanagawa-wave"} curl -X POST https://api.unkey.com/v2/keys.listKeys \ -H "Content-Type: application/json" \ -H "Authorization: Bearer unkey_YOUR_API_KEY" ``` When properly authenticated, you'll receive a successful response like this: ```json theme={"theme":"kanagawa-wave"} { "meta": { "requestId": "req_8f7g6h5j4k3l2m1n" }, "data": { "keys": [ { "keyId": "key_123abc456def", "name": "Production API Key" } ] } } ``` ## Common Mistakes * **Authorization header format**: Must be exactly `Bearer unkey_YOUR_API_KEY` with a single space after "Bearer" * **Incorrect casing**: Using "bearer" instead of "Bearer" * **API key format**: Your Unkey API key should start with `unkey_` and contain no spaces * **Using wrong key type**: Ensure you're using a root key for management API calls * **Copying errors**: Check for invisible characters or line breaks that might have been copied * **Extra characters**: Including quotes or other characters around the API key * **Truncated keys**: Accidentally cutting off part of the API key when copying ## Related Errors * [err:unkey:authentication:missing](./missing) - When no authentication credentials are provided * [err:unkey:authentication:key\_not\_found](./key_not_found) - When the provided API key doesn't exist # missing Source: https://unkey.com/docs/errors/unkey/authentication/missing Authentication credentials were not provided in the request. Add your root key as a Bearer token in the Authorization header for Unkey API calls. `err:unkey:authentication:missing` ```json Example theme={"theme":"kanagawa-wave"} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "error": { "detail": "Authentication credentials were not provided", "status": 401, "title": "Unauthorized", "type": "https://unkey.com/docs/errors/unkey/authentication/missing" } } ``` ## What Happened? This error occurs when you make a request to the Unkey API without including your API key in the Authorization header. The Unkey API requires authentication for most endpoints to verify your identity and permissions. Here's an example of a request that would trigger this error: ```bash theme={"theme":"kanagawa-wave"} # Request to Unkey API without an API key curl -X POST https://api.unkey.com/v2/keys.listKeys \ -H "Content-Type: application/json" ``` Authentication is required to: * Verify your identity * Ensure you have permission to perform the requested operation * Track usage and apply appropriate rate limits * Maintain security and audit trails ## How To Fix To fix this error, you need to include your Unkey API key in the Authorization header of your request: 1. **Get your Unkey API key**: Obtain your API key from the [Unkey dashboard](https://app.unkey.com) 2. **Add the Authorization header**: Include your Unkey API key with the format `Bearer unkey_YOUR_API_KEY` Here's the corrected request: ```bash theme={"theme":"kanagawa-wave"} curl -X POST https://api.unkey.com/v2/keys.listKeys \ -H "Content-Type: application/json" \ -H "Authorization: Bearer unkey_YOUR_API_KEY" ``` When properly authenticated, you'll receive a successful response like this: ```json theme={"theme":"kanagawa-wave"} { "meta": { "requestId": "req_8f7g6h5j4k3l2m1n" }, "data": { "keys": [ { "keyId": "key_123abc456def", "name": "Production API Key" } ] } } ``` ## Common Mistakes * **Missing the `Bearer` prefix**: Unkey requires the format `Bearer unkey_YOUR_API_KEY` with a space after "Bearer" * **Headers lost in proxies**: Some proxy servers or API sentinels might strip custom headers * **Expired or revoked keys**: Using keys that are no longer valid * **Wrong environment**: Using development keys in production or vice versa ## Related Errors * [err:unkey:authentication:malformed](./malformed) - When the API key is provided but formatted incorrectly * [err:unkey:authentication:key\_not\_found](./key_not_found) - When the provided API key doesn't exist # portal_session_not_found Source: https://unkey.com/docs/errors/unkey/authentication/portal_session_not_found The provided portal session was not found, has expired, or has already been exchanged. Create a new session from your backend and redirect the user again. `err:unkey:authentication:portal_session_not_found` ```json Example theme={"theme":"kanagawa-wave"} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "error": { "detail": "Session is invalid, expired, or has already been used.", "status": 401, "title": "Unauthorized", "type": "https://unkey.com/docs/errors/unkey/authentication/portal_session_not_found" } } ``` ## What Happened? This error is returned by `POST /v2/portal.exchangeSession` and any portal-authenticated endpoint when the supplied session identifier cannot be resolved. There are three common reasons: * **Expired session ID** — The short-lived session ID returned by `portal.createSession` is valid for **15 minutes**. After that it can no longer be exchanged. * **Already-used session ID** — Session IDs are **single-use**. Once a browser exchanges it, the same ID cannot be exchanged again. * **Expired browser session** — After exchange, the browser session is valid for **24 hours**. Once it expires, requests using that token return this error. ## How To Fix Create a fresh session from your backend and redirect the user again: ```bash theme={"theme":"kanagawa-wave"} curl -X POST https://api.unkey.com/v2/portal.createSession \ -H "Authorization: Bearer YOUR_ROOT_KEY" \ -H "Content-Type: application/json" \ -d '{ "slug": "my-portal", "externalId": "user_123", "permissions": ["api.*.read_key", "api.*.read_analytics"] }' ``` Then redirect the user to the returned `url`. The portal will exchange the new session ID for a 24-hour browser cookie. If you have configured a `return_url` on your portal, expired browser sessions will automatically redirect there with `?reason=session_expired`. Use that hook to re-mint a session and bounce the user back into the portal seamlessly. ```typescript theme={"theme":"kanagawa-wave"} // In your backend route handler if (request.url.searchParams.get("reason") === "session_expired") { const { data } = await createPortalSession(currentUser); return Response.redirect(data.url, 302); } ``` ## Common Mistakes * **Reusing a session ID**: Session IDs are single-use. Generate a new one for every redirect. * **Storing session IDs**: Don't persist session IDs — they are short-lived and meant to be consumed immediately. * **Skipping the exchange**: The portal frontend must call `portal.exchangeSession` to convert the session ID into a browser session. * **Long-running tabs**: Users who keep the portal open beyond 24 hours need a fresh session. ## Related Errors * [err:unkey:authentication:portal\_token\_missing](./portal_token_missing) - When no portal session token is supplied at all * [err:unkey:data:portal\_config\_not\_found](../data/portal_config_not_found) - When the portal configuration referenced by `slug` does not exist * [err:unkey:authentication:missing](./missing) - When no authentication credentials are provided to a non-portal endpoint # portal_token_missing Source: https://unkey.com/docs/errors/unkey/authentication/portal_token_missing A request to a portal-authenticated endpoint was made without a portal session token. Provide the session cookie or session header from the portal. `err:unkey:authentication:portal_token_missing` ```json Example theme={"theme":"kanagawa-wave"} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "error": { "detail": "A portal session token is required for this request.", "status": 401, "title": "Unauthorized", "type": "https://unkey.com/docs/errors/unkey/authentication/portal_token_missing" } } ``` ## What Happened? This error occurs when a request was made to an endpoint that requires a [Customer Portal](/docs/quickstart/portal) session, but no session token was supplied. Portal-authenticated endpoints expect either: * An `httpOnly` session cookie set by the portal after a successful session exchange, or * An `Authorization: Bearer ` header on direct API calls from a browser session. Common causes include: * The user's browser has cookies disabled or blocked for the portal domain. * The session was never created — the user landed on the portal without going through `POST /v2/portal.exchangeSession`. * A backend integration is calling a portal-only endpoint with a root key instead of a portal session token. * The session cookie was cleared or the user opened the portal in a private/incognito window with stripped state. ## How To Fix Make sure the user has an active portal session before calling portal endpoints: 1. From your backend, call `POST /v2/portal.createSession` with your root key to create a session. 2. Redirect the user to the returned `url`. The portal will exchange the short-lived session ID for a 24-hour browser session. 3. Subsequent requests from the browser must include the portal session cookie or token. ```bash theme={"theme":"kanagawa-wave"} curl -X POST https://api.unkey.com/v2/portal.createSession \ -H "Authorization: Bearer YOUR_ROOT_KEY" \ -H "Content-Type: application/json" \ -d '{ "slug": "my-portal", "externalId": "user_123", "permissions": ["api.*.read_key"] }' ``` If you are calling the portal API directly from JavaScript, ensure your fetch includes credentials so the session cookie is sent: ```typescript theme={"theme":"kanagawa-wave"} await fetch("https://api.unkey.com/v2/...", { credentials: "include", }); ``` ## Common Mistakes * **Calling portal endpoints with a root key**: Root keys authenticate backend requests, not portal endpoints. Use a portal session. * **Missing `credentials: "include"`**: Cross-origin browser requests omit cookies by default. * **Expired session not refreshed**: After 24 hours the session expires — your backend must create a new one. * **Direct navigation to the portal**: Users must arrive via your backend redirect, not by visiting the portal URL directly. ## Related Errors * [err:unkey:authentication:portal\_session\_not\_found](./portal_session_not_found) - When a portal session token is provided but invalid or expired * [err:unkey:authentication:missing](./missing) - When no authentication credentials are provided to a non-portal endpoint * [err:unkey:data:portal\_config\_not\_found](../data/portal_config_not_found) - When the portal configuration referenced by `slug` does not exist # forbidden Source: https://unkey.com/docs/errors/unkey/authorization/forbidden The requested operation is not allowed for this entity in Unkey. Check your root key permissions or workspace access level and try again. `err:unkey:authorization:forbidden` ```json Example theme={"theme":"kanagawa-wave"} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "error": { "detail": "This operation is not allowed", "status": 403, "title": "Forbidden", "type": "https://unkey.com/docs/errors/unkey/authorization/forbidden" } } ``` ## What Happened? This error occurs when you attempt an operation that is prohibited by Unkey's platform policies, even if your API key has high-level permissions. Unlike the "insufficient\_permissions" error which relates to permission roles, this error indicates that the operation itself is not allowed regardless of permissions. Common scenarios that trigger this error: * Trying to perform operations on protected or system resources * Attempting to modify resources that are in a state that doesn't allow modifications * Trying to exceed account limits or quotas * Performing operations that violate platform policies Here's an example of a request that might trigger this error: ```bash theme={"theme":"kanagawa-wave"} # Attempting to delete a protected system resource curl -X POST https://api.unkey.com/v2/apis.deleteApi \ -H "Content-Type: application/json" \ -H "Authorization: Bearer unkey_ADMIN_KEY" \ -d '{ "apiId": "api_system_protected" }' ``` ## How To Fix This error indicates a fundamental restriction rather than a permission issue. The operation you're trying to perform may be: 1. **Not supported by the Unkey platform**: Some operations are simply not available 2. **Blocked due to your account's current state or limitations**: Your account may not have access to certain features 3. **Prevented by safety mechanisms**: System protections may prevent certain destructive operations Possible solutions include: * **Check Unkey's documentation**: Understand which operations have fundamental restrictions * **Consider your account state**: Some operations may be blocked due to your account state or plan * **Use alternative approaches**: Find supported ways to accomplish similar goals * **If you're trying to modify a resource in a specific state**: check if it needs to be in a different state first * **If you're hitting account limits**: consider upgrading your plan * **Contact Unkey support** if you believe this restriction shouldn't apply to your use case ## Common Mistakes * **Attempting to modify system resources**: Some Unkey resources are protected and cannot be modified * **Order-dependent operations**: Trying to perform operations out of their required sequence * **Plan limitations**: Attempting operations not available on your current plan * **Resource state issues**: Trying to modify resources that are in a state that doesn't allow changes * **Ignoring documentation warnings**: Not reading warnings about restricted operations * **Testing security boundaries**: Deliberately trying to access protected resources * **Outdated documentation**: Following outdated documentation that suggests now-forbidden operations ## Related Errors * [err:unkey:authorization:insufficient\_permissions](./insufficient_permissions) - When the authenticated entity lacks specific permissions * [err:unkey:authorization:key\_disabled](./key_disabled) - When the authentication key is disabled * [err:unkey:authorization:workspace\_disabled](./workspace_disabled) - When the associated workspace is disabled # insufficient_permissions Source: https://unkey.com/docs/errors/unkey/authorization/insufficient_permissions The authenticated entity lacks sufficient permissions for the requested Unkey API operation. Add the required root key permission and retry. `err:unkey:authorization:insufficient_permissions` ```json Example theme={"theme":"kanagawa-wave"} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "error": { "detail": "The authenticated API key does not have permission to perform this operation", "status": 403, "title": "Forbidden", "type": "https://unkey.com/docs/errors/unkey/authorization/insufficient_permissions" } } ``` ## What Happened? This error occurs when your API key is valid and properly authenticated, but it doesn't have the necessary permissions to perform the requested operation. In Unkey, different API keys can have different permission levels. Common scenarios that trigger this error: * Using a read-only key to perform write operations * Using a key limited to specific resources to access other resources * Attempting to access resources across workspaces with a workspace-scoped key * Using a key with limited permissions to perform administrative actions Here's an example of a request using a key with insufficient permissions: ```bash theme={"theme":"kanagawa-wave"} # Using a read-only key to create a new API key curl -X POST https://api.unkey.com/v2/keys.createKey \ -H "Content-Type: application/json" \ -H "Authorization: Bearer unkey_READ_ONLY_KEY" \ -d '{ "apiId": "api_123", "name": "New API Key" }' ``` ## How To Fix You need to use an API key with the appropriate permissions for the operation you're trying to perform. Here are some steps to resolve this issue: 1. **Check permissions**: Verify the permissions of your current Unkey API key in the [Unkey dashboard](https://app.unkey.com) 2. **Create a new key**: If needed, create a new Unkey API key with the required permissions 3. **Use role-based keys**: Consider using separate keys for different operations based on their permission requirements Here's an example using a key with the appropriate permissions: ```bash theme={"theme":"kanagawa-wave"} curl -X POST https://api.unkey.com/v2/keys.createKey \ -H "Content-Type: application/json" \ -H "Authorization: Bearer unkey_ADMIN_KEY" \ -d '{ "apiId": "api_123", "name": "New API Key" }' ``` ## Common Mistakes * **Using development keys in production**: Keys may have different permissions across environments * **Mixing key scopes**: Using a key scoped to one resource to access another * **Role misunderstanding**: Not understanding the specific permissions granted to each role * **Workspace boundaries**: Attempting to cross workspace boundaries with a limited key * **Permission level confusion**: Not understanding what operations require elevated permissions * **Expired or downgraded privileges**: Using a key whose permissions have been reduced since it was issued ## Related Errors * [err:unkey:authorization:forbidden](./forbidden) - When the operation is not allowed for policy reasons * [err:unkey:authorization:key\_disabled](./key_disabled) - When the authentication key is disabled # key_disabled Source: https://unkey.com/docs/errors/unkey/authorization/key_disabled The API key used for authentication is currently disabled in Unkey. Re-enable the key in the dashboard or create a new key to restore access. `err:unkey:authorization:key_disabled` ```json Example theme={"theme":"kanagawa-wave"} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "error": { "detail": "The API key used for authentication has been disabled", "status": 403, "title": "Forbidden", "type": "https://unkey.com/docs/errors/unkey/authorization/key_disabled" } } ``` ## What Happened? This error occurs when you try to use a disabled Unkey API key (one that starts with `unkey_`) to authenticate with the Unkey API. The key exists in the system but has been disabled and can no longer be used for authentication. Here's an example of a request that would trigger this error: ```bash theme={"theme":"kanagawa-wave"} # Request to Unkey API with a disabled key curl -X POST https://api.unkey.com/v2/keys.listKeys \ -H "Content-Type: application/json" \ -H "Authorization: Bearer unkey_DISABLED_KEY" ``` API keys can be disabled for various reasons: * Administrative action to revoke access * Security concerns or suspected compromise * Temporary deactivation during maintenance or investigation * Automated disabling due to suspicious activity * Usage policy violations ## How To Fix If you encounter this error when using the Unkey API, you have two options: 1. **Get a new Unkey root key**: If your key was permanently disabled, create a new API key with the appropriate permissions in the [Unkey dashboard](https://app.unkey.com/settings/root-keys) 2. **Re-enable your existing key**: If you have administrative access and the key was temporarily disabled, you can re-enable it through the dashboard To re-enable your Unkey root key: 1. Log in to your Unkey dashboard 2. Navigate to the API keys section 3. Search for the key you want to re-enable 4. Click "Enable" Then update your API calls to use the re-enabled key: ```bash theme={"theme":"kanagawa-wave"} curl -X POST https://api.unkey.com/v2/keys.listKeys \ -H "Content-Type: application/json" \ -H "Authorization: Bearer unkey_REACTIVATED_KEY" ``` ## Common Mistakes * **Using old or archived root keys**: Keys from previous projects or configurations may have been disabled * **Shared root keys**: When keys are shared among team members, they may be disabled by another administrator * **Security triggers**: Unusual usage patterns may automatically disable keys as a security precaution * **Environment confusion**: Using disabled staging/development keys in production environments * **Account status changes**: Keys may be disabled due to billing or account status changes * **Rotation policies**: Keys that should have been rotated according to security policies ## Related Errors * [err:unkey:authorization:insufficient\_permissions](./insufficient_permissions) - When the authenticated entity lacks sufficient permissions * [err:unkey:authorization:workspace\_disabled](./workspace_disabled) - When the associated workspace is disabled * [err:unkey:authentication:key\_not\_found](../authentication/key_not_found) - When the provided API key doesn't exist at all # workspace_disabled Source: https://unkey.com/docs/errors/unkey/authorization/workspace_disabled The workspace associated with this request is disabled in Unkey. Contact support or check your billing status to restore workspace access. `err:unkey:authorization:workspace_disabled` ```json Example theme={"theme":"kanagawa-wave"} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "error": { "detail": "The workspace associated with this API key has been disabled", "status": 403, "title": "Forbidden", "type": "https://unkey.com/docs/errors/unkey/authorization/workspace_disabled" } } ``` ## What Happened? This error occurs when you attempt to use an Unkey API key that belongs to a disabled workspace. When a workspace is disabled in Unkey, all API keys associated with that workspace stop working, regardless of their individual status. Here's an example of a request that would trigger this error: ```bash theme={"theme":"kanagawa-wave"} # Request to Unkey API with a key from a disabled workspace curl -X POST https://api.unkey.com/v2/keys.listKeys \ -H "Content-Type: application/json" \ -H "Authorization: Bearer unkey_KEY_FROM_DISABLED_WORKSPACE" ``` A workspace might be disabled for various reasons: * Billing issues or unpaid invoices * Administrative action due to terms of service violations * At the workspace owner's request * During investigation of suspicious activity * As part of account closure process * Exceeding usage limits or quotas ## How To Fix If you encounter this error when using the Unkey API, you need to address the workspace issue: 1. **Check billing status**: If the workspace was disabled due to billing issues, settle any outstanding payments in the [Unkey dashboard](https://app.unkey.com/settings/billing) 2. **Contact workspace administrator**: If you're not the workspace administrator, contact them to determine why the workspace was disabled 3. **Contact Unkey support**: If you believe the workspace was disabled in error, or you need assistance resolving the issue, contact [Unkey support](mailto:support@unkey.com) 4. **Use a key from a different workspace**: If you have access to multiple workspaces, you can temporarily use a key from an active workspace while resolving the issue Once the workspace is re-enabled, all API keys associated with it should become usable again (unless individually disabled). ## Common Mistakes * **Billing oversights**: Missed payment notifications can lead to workspace suspension * **Usage violations**: Excessive usage or pattern violations may trigger workspace disabling * **Administrative changes**: Organizational changes might lead to workspaces being temporarily disabled * **Using old workspaces**: Attempting to use keys from deprecated or archived workspaces * **Plan limitation violations**: Exceeding the limits of your current plan * **Account transfer issues**: Workspaces may be temporarily disabled during ownership transfers ## Related Errors * [err:unkey:authorization:key\_disabled](./key_disabled) - When the specific authentication key is disabled * [err:unkey:authorization:insufficient\_permissions](./insufficient_permissions) - When the authenticated entity lacks sufficient permissions * [err:unkey:data:workspace\_not\_found](../data/workspace_not_found) - When the requested workspace doesn't exist # analytics_connection_failed Source: https://unkey.com/docs/errors/unkey/data/analytics_connection_failed The connection to the Unkey analytics database failed. Learn about retry strategies, timeout settings, and how to check analytics service status. `err:unkey:data:analytics_connection_failed` # analytics_not_configured Source: https://unkey.com/docs/errors/unkey/data/analytics_not_configured Analytics is not configured for this workspace in Unkey. Request access to the analytics feature and ensure your workspace plan supports queries. `err:unkey:data:analytics_not_configured` # api_not_found Source: https://unkey.com/docs/errors/unkey/data/api_not_found The requested API namespace was not found in Unkey. Verify the API ID is correct, the API exists in your workspace, and has not been deleted. err:unkey:data:api\_not\_found ```json Example theme={"theme":"kanagawa-wave"} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "error": { "detail": "The requested API could not be found", "status": 404, "title": "Not Found", "type": "https://unkey.com/docs/errors/unkey/data/api_not_found" } } ``` ## What Happened? This error occurs when you're trying to perform an operation on an API that doesn't exist in the Unkey system. In Unkey, APIs are resources that you create to organize and manage your keys. Common scenarios that trigger this error: * Using an incorrect API ID in your requests * Referencing an API that has been deleted * Attempting to access an API in a workspace you don't have access to * Typos in API names when using name-based lookups Here's an example of a request that would trigger this error: ```bash theme={"theme":"kanagawa-wave"} # Attempting to create a key for a non-existent API curl -X POST https://api.unkey.com/v2/keys.createKey \ -H "Content-Type: application/json" \ -H "Authorization: Bearer unkey_YOUR_API_KEY" \ -d '{ "apiId": "api_nonexistent", "name": "hello world" }' ``` ## How To Fix Verify that you're using the correct API ID and that the API still exists in your workspace: 1. List all APIs in your workspace to find the correct ID 2. Check if the API has been deleted and recreate it if necessary 3. Verify you're working in the correct workspace 4. Ensure proper permissions to access the API Here's how to list all APIs in your workspace: ```bash theme={"theme":"kanagawa-wave"} curl -X POST https://api.unkey.com/v2/apis.listApis \ -H "Authorization: Bearer unkey_YOUR_API_KEY" ``` If you need to create a new API, use the `apis.createApi` endpoint: ```bash theme={"theme":"kanagawa-wave"} curl -X POST https://api.unkey.com/v2/apis.createApi \ -H "Content-Type: application/json" \ -H "Authorization: Bearer unkey_YOUR_API_KEY" \ -d '{ "name": "My New API" }' ``` ## Common Mistakes * **Copy-paste errors**: Using incorrect API IDs from documentation examples * **Deleted APIs**: Attempting to reference APIs that have been deleted * **Environment confusion**: Looking for an API in production that only exists in development * **Workspace boundaries**: Trying to access an API that exists in another workspace ## Related Errors * [err:unkey:data:workspace\_not\_found](./workspace_not_found) - When the requested workspace doesn't exist * [err:unkey:data:key\_not\_found](./key_not_found) - When the requested key doesn't exist * [err:unkey:authorization:insufficient\_permissions](../authorization/insufficient_permissions) - When you don't have permission to access an API # audit_log_not_found Source: https://unkey.com/docs/errors/unkey/data/audit_log_not_found The requested audit log entry was not found in Unkey. Verify the audit log ID is correct, and that the entry exists within your retention period. err:unkey:data:audit\_log\_not\_found ```json Example theme={"theme":"kanagawa-wave"} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "error": { "detail": "The requested audit log could not be found", "status": 404, "title": "Not Found", "type": "https://unkey.com/docs/errors/unkey/data/audit_log_not_found" } } ``` ## What Happened? This error occurs when you're trying to retrieve or operate on a specific audit log entry that doesn't exist in the Unkey system. Audit logs record important actions and events that occur within your workspace. Common scenarios that trigger this error: * Using an incorrect audit log ID * Requesting an audit log entry that has been deleted or expired * Trying to access audit logs from a different workspace * Typographical errors in audit log identifiers Here's an example of a request that would trigger this error: ```bash theme={"theme":"kanagawa-wave"} # Attempting to get a non-existent audit log entry curl -X POST https://api.unkey.com/v2/audit.getLog \ -H "Authorization: Bearer unkey_YOUR_API_KEY" \ -d '{ "logId": "log_nonexistent" }' ``` ## How To Fix Verify that you're using the correct audit log ID and that the log entry still exists in your workspace: 1. Check the audit log ID in your request for typos or formatting errors 2. Use the list audit logs endpoint to find valid log IDs 3. Verify you're working in the correct workspace 4. Consider that audit logs might have a retention period after which they're automatically deleted Here's how to list recent audit logs in your workspace: ```bash theme={"theme":"kanagawa-wave"} curl -X POST https://api.unkey.com/v2/audit.listLogs \ -H "Authorization: Bearer unkey_YOUR_API_KEY" \ -d '{ "limit": 10 }' ``` ## Common Mistakes * **Expired logs**: Trying to access audit logs beyond the retention period * **Copy-paste errors**: Using incorrect log IDs from documentation examples * **Workspace boundaries**: Attempting to access logs from another workspace * **Permission issues**: Trying to access logs you don't have permission to view ## Related Errors * [err:unkey:authorization:insufficient\_permissions](../authorization/insufficient_permissions) - When you don't have permission to access audit logs * [err:unkey:data:workspace\_not\_found](./workspace_not_found) - When the requested workspace doesn't exist # identity_already_exists Source: https://unkey.com/docs/errors/unkey/data/identity_already_exists An identity with this external ID already exists in your Unkey workspace. Use the existing identity or choose a different unique external ID. err:unkey:data:identity\_already\_exists ```json Example theme={"theme":"kanagawa-wave"} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "error": { "detail": "An identity with this external ID already exists", "status": 409, "title": "Conflict", "type": "https://unkey.com/docs/errors/unkey/data/identity_already_exists" } } ``` ## What Happened? This error occurs when you're trying to create an identity with an external ID that already exists in your Unkey workspace. External IDs must be unique within a workspace to avoid confusion and maintain data integrity. Common scenarios that trigger this error: * Creating an identity with an external ID that's already in use * Re-creating a previously deleted identity with the same external ID * Migration or import processes that don't check for existing identities * Duplicate API calls due to retries or network issues Here's an example of a request that would trigger this error: ```bash theme={"theme":"kanagawa-wave"} # Attempting to create an identity with an external ID that already exists curl -X POST https://api.unkey.com/v2/identities.createIdentity \ -H "Content-Type: application/json" \ -H "Authorization: Bearer unkey_YOUR_API_KEY" \ -d '{ "externalId": "user_123", "meta": { "name": "John Doe", "email": "john@acme.com" } }' ``` ## How To Fix When you encounter this error, you have several options: 1. **Use a different external ID**: If creating a new identity, use a unique external ID 2. **Update the existing identity**: If you want to modify an existing identity, use the update endpoint instead 3. **Get the existing identity**: If you just need the identity information, retrieve it rather than creating it 4. **Implement upsert logic**: Use a get-or-create pattern in your code Here's how to update an existing identity: ```bash theme={"theme":"kanagawa-wave"} curl -X POST https://api.unkey.com/v2/identities.updateIdentity \ -H "Content-Type: application/json" \ -H "Authorization: Bearer unkey_YOUR_API_KEY" \ -d '{ "externalId": "user_123", "meta": { "name": "John Doe", "email": "updated_email@acme.com" } }' ``` Or implement a get-or-create pattern in your code: ```javascript theme={"theme":"kanagawa-wave"} // Pseudocode for get-or-create pattern async function getOrCreateIdentity(externalId, meta) { try { // Try to create the identity return await createIdentity(externalId, meta); } catch (error) { // If it already exists (409 error), get it instead if (error.status === 409) { return await getIdentity(externalId); } // Otherwise, rethrow the error throw error; } } ``` ## Common Mistakes * **Not checking for existing identities**: Failing to check if an identity already exists before creating it * **Retry loops**: Repeatedly trying to create the same identity after a failure * **Case sensitivity**: Not accounting for case sensitivity in external IDs * **Cross-environment duplication**: Using the same external IDs across development and production environments ## Related Errors * [err:unkey:data:identity\_not\_found](./identity_not_found) - When the requested identity doesn't exist * [err:unkey:authorization:insufficient\_permissions](../authorization/insufficient_permissions) - When you don't have permission to perform operations on identities # identity_not_found Source: https://unkey.com/docs/errors/unkey/data/identity_not_found The requested identity was not found in Unkey. Verify the identity ID or external ID is correct and the identity exists in your workspace. err:unkey:data:identity\_not\_found ```json Example theme={"theme":"kanagawa-wave"} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "error": { "detail": "The requested identity could not be found", "status": 404, "title": "Not Found", "type": "https://unkey.com/docs/errors/unkey/data/identity_not_found" } } ``` ## What Happened? This error occurs when you're trying to perform an operation on an identity that doesn't exist in the Unkey system. Identities in Unkey are used to represent users or entities that own or use API keys. Common scenarios that trigger this error: * Using an incorrect identity ID or external ID * Referencing an identity that has been deleted * Trying to update or get information about a non-existent identity * Typos in identity identifiers Here's an example of a request that would trigger this error: ```bash theme={"theme":"kanagawa-wave"} # Attempting to get a non-existent identity curl -X POST https://api.unkey.com/v2/identities.getIdentity \ -H "Content-Type: application/json" \ -H "Authorization: Bearer unkey_YOUR_API_KEY" \ -d '{ "identityId": "ident_nonexistent" }' ``` ## How To Fix Verify that you're using the correct identity ID or external ID and that the identity still exists in your workspace: 1. Check the identity ID in your request for typos or formatting errors 2. List all identities in your workspace to find the correct ID 3. If the identity has been deleted, you may need to recreate it If you need to create a new identity, use the `identities.createIdentity` endpoint: ```bash theme={"theme":"kanagawa-wave"} curl -X POST https://api.unkey.com/v2/identities.createIdentity \ -H "Content-Type: application/json" \ -H "Authorization: Bearer unkey_YOUR_API_KEY" \ -d '{ "externalId": "user_123", "meta": { "name": "John Doe", "email": "john@acme.com" } }' ``` ## Common Mistakes * **Incorrect identifiers**: Using wrong identity IDs or external IDs * **Deleted identities**: Attempting to reference identities that have been removed * **Case sensitivity**: External IDs might be case-sensitive * **Workspace boundaries**: Trying to access identities from another workspace ## Related Errors * [err:unkey:data:identity\_already\_exists](./identity_already_exists) - When trying to create an identity that already exists * [err:unkey:data:key\_not\_found](./key_not_found) - When the requested key doesn't exist * [err:unkey:authorization:insufficient\_permissions](../authorization/insufficient_permissions) - When you don't have permission to perform operations on identities # key_auth_not_found Source: https://unkey.com/docs/errors/unkey/data/key_auth_not_found The requested key authentication namespace was not found in Unkey. Verify the key auth ID is correct and exists within your workspace config. err:unkey:data:key\_auth\_not\_found ```json Example theme={"theme":"kanagawa-wave"} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "error": { "detail": "The requested key authentication could not be found", "status": 404, "title": "Not Found", "type": "https://unkey.com/docs/errors/unkey/data/key_auth_not_found" } } ``` ## What Happened? This error occurs when you're trying to perform an operation on a key authentication record that doesn't exist in the Unkey system. Key authentication records contain information about how API keys are authenticated. Common scenarios that trigger this error: * Using an incorrect key authentication ID * Referencing a key authentication record that has been deleted * Attempting to update authentication settings for a non-existent record * Typos in identifiers ## How To Fix Verify that you're using the correct key authentication ID and that the record still exists: 1. Check the key authentication ID in your request for typos or formatting errors 2. Verify the key authentication record exists by looking up the associated key 3. If the record has been deleted, you may need to recreate the key or its authentication settings Here's how to get information about a key's authentication settings: ```bash theme={"theme":"kanagawa-wave"} curl -X POST https://api.unkey.com/v2/keys.getKey \ -H "Content-Type: application/json" \ -H "Authorization: Bearer unkey_YOUR_API_KEY" \ -d '{ "keyId": "key_your_key_id" }' ``` ## Common Mistakes * **Copy-paste errors**: Incorrect IDs due to copy-paste mistakes * **Deleted records**: Attempting to reference authentication records for deleted keys * **Misunderstanding relationships**: Confusing key IDs with key authentication IDs * **Workspace boundaries**: Trying to access authentication records from another workspace ## Related Errors * [err:unkey:data:key\_not\_found](./key_not_found) - When the requested key doesn't exist * [err:unkey:authentication:key\_not\_found](../authentication/key_not_found) - When an API key used for authentication doesn't exist * [err:unkey:authorization:key\_disabled](../authorization/key_disabled) - When the authentication key is disabled # key_not_found Source: https://unkey.com/docs/errors/unkey/data/key_not_found The requested API key was not found in Unkey. Verify the key ID is correct, the key has not been deleted, and it belongs to your workspace. err:unkey:data:key\_not\_found ```json Example theme={"theme":"kanagawa-wave"} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "error": { "detail": "The requested API key could not be found", "status": 404, "title": "Not Found", "type": "https://unkey.com/docs/errors/unkey/data/key_not_found" } } ``` ## What Happened? This error occurs when you're trying to perform an operation on a specific API key using its ID, but the key with that ID doesn't exist in the system. This is different from the authentication error `err:unkey:authentication:key_not_found`, which occurs during the authentication process. Common scenarios that trigger this error: * Attempting to update, delete, or get information about a key that has been deleted * Using an incorrect or malformed key ID * Trying to access a key that exists in a different workspace * Reference to a key that hasn't been created yet Here's an example of a request that would trigger this error: ```bash theme={"theme":"kanagawa-wave"} # Attempting to get details for a non-existent key curl -X POST https://api.unkey.com/v2/keys.getKey \ -H "Content-Type: application/json" \ -H "Authorization: Bearer unkey_YOUR_API_KEY" \ -d '{ "keyId": "key_nonexistent" }' ``` ## How To Fix Verify that you're using the correct key ID and that the key still exists in your workspace: 1. Check the key ID in your request for typos or formatting errors 2. Confirm the key exists by listing all keys in your workspace via the [Unkey dashboard](https://unkey.com/dashboard) or the API 3. Verify you're working in the correct workspace 4. If you need to create a new key, use the `keys.createKey` endpoint Here's how to list all keys to find the correct ID: ```bash theme={"theme":"kanagawa-wave"} curl -X POST https://api.unkey.com/v2/apis.listKeys \ -H "Content-Type: application/json" \ -H "Authorization: Bearer unkey_YOUR_API_KEY" \ -d '{ "apiId": "api_your_api_id" }' ``` ## Common Mistakes * **Copy-paste errors**: Incorrect key IDs due to copy-paste mistakes * **Deleted keys**: Attempting to reference keys that have been deleted * **Environment confusion**: Looking for a key in production that only exists in development * **Workspace boundaries**: Trying to access a key that exists in another workspace ## Related Errors * [err:unkey:authentication:key\_not\_found](../authentication/key_not_found) - When an API key used for authentication doesn't exist * [err:unkey:data:api\_not\_found](./api_not_found) - When the requested API doesn't exist * [err:unkey:data:workspace\_not\_found](./workspace_not_found) - When the requested workspace doesn't exist # key_space_not_found Source: https://unkey.com/docs/errors/unkey/data/key_space_not_found The requested key space was not found in Unkey. Verify the key space ID is correct and that it exists within your workspace configuration. `err:unkey:data:key_space_not_found` # migration_not_found Source: https://unkey.com/docs/errors/unkey/data/migration_not_found The requested key migration was not found in Unkey. Verify the migration ID is correct and the migration exists within your current workspace. err:unkey:data:migration\_not\_found ```json Example theme={"theme":"kanagawa-wave"} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "error": { "detail": "The requested Migration could not be found", "status": 404, "title": "Not Found", "type": "https://unkey.com/docs/errors/unkey/data/migration_not_found" } } ``` ## What Happened? This error occurs when you're trying to migrate API keys for a migration that doesn't exist in the Unkey system. Common scenarios that trigger this error: * Using an incorrect or expired migrationId * The migration was deleted * The migration belongs to a different workspace * Typos in the migrationId Here's an example of a request that would trigger this error: ```bash theme={"theme":"kanagawa-wave"} # Attempting to migrate keys with a non-existent migrationId curl -X POST https://api.unkey.com/v2/keys.migrateKeys \ -H "Content-Type: application/json" \ -H "Authorization: Bearer " \ -d '{ "apiId": "api_123", "migrationId": "migration_456", "keys": [{ "hash": "deadbeef" }] }' ``` ## How To Fix If you're unsure about your migrationId or setup, contact [support@unkey.com](mailto:support@unkey.com). # permission_already_exists Source: https://unkey.com/docs/errors/unkey/data/permission_already_exists A permission with this slug already exists in your Unkey workspace. Use the existing permission or choose a different unique slug for the new one. err:unkey:data:permission\_already\_exists ```json Example theme={"theme":"kanagawa-wave"} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "error": { "detail": "A permission with slug \"admin\" already exists in this workspace", "status": 409, "title": "Conflict", "type": "https://unkey.com/docs/errors/unkey/data/permission/duplicate" } } ``` ## What Happened? This error occurs when you're trying to create a permission with a name that already exists in your Unkey workspace. Permission names must be unique within a workspace to avoid confusion and maintain proper access control. Common scenarios that trigger this error: * Creating a permission with a name that's already in use * Re-creating a previously deleted permission with the same name * Migration or import processes that don't check for existing permissions * Duplicate API calls due to retries or network issues Here's an example of a request that would trigger this error: ```bash theme={"theme":"kanagawa-wave"} # Attempting to create a permission with a name that already exists curl -X POST https://api.unkey.com/v2/permissions.createPermission \ -H "Content-Type: application/json" \ -H "Authorization: Bearer unkey_YOUR_API_KEY" \ -d '{ "name": "admin", "slug": "admin-access", "description": "Administrator access" }' ``` ## How To Fix When you encounter this error, you have several options: 1. **Use a different name**: If creating a new permission, use a unique name 2. **Get the existing permission**: If you just need the permission information, retrieve it rather than creating it 3. **List existing permissions**: Check what permissions already exist before creating new ones 4. **Implement idempotent creation**: Use a get-or-create pattern in your code Here's how to list existing permissions: ```bash theme={"theme":"kanagawa-wave"} curl -X POST https://api.unkey.com/v2/permissions.listPermissions \ -H "Content-Type: application/json" \ -H "Authorization: Bearer unkey_YOUR_API_KEY" \ -d '{}' ``` Or implement a get-or-create pattern in your code: ```javascript theme={"theme":"kanagawa-wave"} // Pseudocode for get-or-create pattern async function getOrCreatePermission(name, slug, description) { try { // Try to create the permission return await createPermission(name, slug, description); } catch (error) { // If it already exists (409 error), get it instead if (error.status === 409) { // Extract the permission name from the error message and get it const permissions = await listPermissions(); return permissions.find( (p) => p.name.toLowerCase() === name.toLowerCase(), ); } // Otherwise, rethrow the error throw error; } } ``` ## Common Mistakes * **Not checking for existing permissions**: Failing to check if a permission already exists before creating it * **Case sensitivity**: Permission names are case-insensitive - "Admin" and "admin" are the same * **Retry loops**: Repeatedly trying to create the same permission after a failure * **Cross-environment duplication**: Using the same permission names across development and production without proper namespacing ## Related Errors * [err:unkey:data:permission\_not\_found](./permission_not_found) - When the requested permission doesn't exist * [err:unkey:authorization:insufficient\_permissions](../authorization/insufficient_permissions) - When you don't have permission to manage permissions # permission_not_found Source: https://unkey.com/docs/errors/unkey/data/permission_not_found The requested permission was not found in Unkey. Verify the permission ID is correct and it exists within your workspace's RBAC configuration. err:unkey:data:permission\_not\_found ```json Example theme={"theme":"kanagawa-wave"} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "error": { "detail": "The requested permission could not be found", "status": 404, "title": "Not Found", "type": "https://unkey.com/docs/errors/unkey/data/permission_not_found" } } ``` ## What Happened? This error occurs when you're trying to perform an operation on a permission that doesn't exist in the Unkey system. Permissions in Unkey are used to control access to resources and operations. Common scenarios that trigger this error: * Using an incorrect permission ID or name * Referencing a permission that has been deleted * Trying to assign a permission that doesn't exist in the current workspace * Typos in permission names when using name-based lookups ## How To Fix Verify that you're using the correct permission ID or name and that the permission still exists in your workspace: 1. List all permissions in your workspace to find the correct ID 2. Check if the permission has been deleted and recreate it if necessary 3. Verify you're working in the correct workspace Here's how to list all permissions in your workspace: ```bash theme={"theme":"kanagawa-wave"} curl -X POST https://api.unkey.com/v2/permissions.listPermissions \ -H "Authorization: Bearer unkey_YOUR_API_KEY" ``` If you need to create a new permission, use the appropriate API endpoint: ```bash theme={"theme":"kanagawa-wave"} curl -X POST https://api.unkey.com/v2/permissions.createPermission \ -H "Content-Type: application/json" \ -H "Authorization: Bearer unkey_YOUR_API_KEY" \ -d '{ "name": "read:keys", "description": "Allows reading key information" }' ``` ## Common Mistakes * **Incorrect identifiers**: Using wrong permission IDs or names * **Deleted permissions**: Referencing permissions that have been removed * **Case sensitivity**: Permissions names might be case-sensitive * **Workspace boundaries**: Trying to use permissions from another workspace ## Related Errors * [err:unkey:data:role\_not\_found](./role_not_found) - When the requested role doesn't exist * [err:unkey:data:api\_not\_found](./api_not_found) - When the requested API doesn't exist * [err:unkey:authorization:insufficient\_permissions](../authorization/insufficient_permissions) - When you don't have permission to perform operations on permissions # portal_config_not_found Source: https://unkey.com/docs/errors/unkey/data/portal_config_not_found No portal configuration matched the provided slug for your workspace. Verify the slug or contact Unkey to provision a portal configuration. `err:unkey:data:portal_config_not_found` ```json Example theme={"theme":"kanagawa-wave"} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "error": { "detail": "Portal configuration not found.", "status": 404, "title": "Not Found", "type": "https://unkey.com/docs/errors/unkey/data/portal_config_not_found" } } ``` ## What Happened? This error occurs when you call `POST /v2/portal.createSession` with a `slug` that does not match a portal configuration in your workspace. Common causes include: * Typo in the `slug` value sent from your backend. * The portal configuration belongs to a different workspace than the root key you authenticated with. * The portal has not yet been provisioned for your workspace. The [Customer Portal](/docs/quickstart/portal) is in early access, and during that period configurations are created by the Unkey team on request. ## How To Fix 1. **Verify the slug**: Double-check that the `slug` in your request matches the slug configured for your portal. Slugs are 3–64 characters, lowercase alphanumeric and hyphens, with no leading or trailing hyphen. 2. **Check the workspace**: Make sure the root key you are using belongs to the same workspace as the portal configuration. 3. **Request a configuration**: If you have not yet been onboarded to the Customer Portal, contact the Unkey team to provision one for your workspace. Example of a correct request: ```bash theme={"theme":"kanagawa-wave"} curl -X POST https://api.unkey.com/v2/portal.createSession \ -H "Authorization: Bearer YOUR_ROOT_KEY" \ -H "Content-Type: application/json" \ -d '{ "slug": "my-portal", "externalId": "user_123", "permissions": ["api.*.read_key"] }' ``` ## Common Mistakes * **Slug case sensitivity**: Slugs are lowercase. `My-Portal` will not match `my-portal`. * **Wrong root key**: Using a root key from a different workspace returns this error rather than a 401, because the slug lookup is workspace-scoped. * **Hyphen rules**: Slugs cannot start or end with a hyphen, and cannot contain underscores or other punctuation. ## Related Errors * [err:unkey:authentication:portal\_token\_missing](../authentication/portal_token_missing) - When a portal session token is required but not provided * [err:unkey:authentication:portal\_session\_not\_found](../authentication/portal_session_not_found) - When a portal session is invalid, expired, or already used * [err:unkey:data:workspace\_not\_found](./workspace_not_found) - When the workspace itself cannot be resolved # project_not_found Source: https://unkey.com/docs/errors/unkey/data/project_not_found The requested project was not found in Unkey. Verify the project ID is correct and that the project exists within your current workspace. err:unkey:data:project\_not\_found ```json Example theme={"theme":"kanagawa-wave"} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "error": { "detail": "The requested project could not be found", "status": 404, "title": "Not Found", "type": "https://unkey.com/docs/errors/unkey/data/project_not_found" } } ``` ## What Happened? This error occurs when you're trying to perform an operation on a project that doesn't exist in the Unkey system. Common scenarios that trigger this error: * Using an incorrect project ID in your requests * Referencing a project that has been deleted * Attempting to access a project in a workspace you don't have access to ## How To Fix Verify that you're using the correct project ID and that the project still exists in your workspace: 1. Check the project ID in the Unkey dashboard under **Projects** 2. Verify the project has not been deleted 3. Confirm you're working in the correct workspace ## Related Errors * [err:unkey:data:api\_not\_found](./api_not_found) - When the requested API doesn't exist * [err:unkey:data:workspace\_not\_found](./workspace_not_found) - When the requested workspace doesn't exist # Ratelimit Namespace Gone Source: https://unkey.com/docs/errors/unkey/data/ratelimit_namespace_gone Returned when you reference a rate limit namespace that was previously deleted. Learn why this 410 Gone error occurs and how to resolve it. ## Description This error occurs when you attempt to use a ratelimit namespace that has been deleted. Once a namespace is deleted, it cannot be restored through the API or dashboard. ## Error Code `unkey/data/ratelimit_namespace_gone` ## HTTP Status Code `410 Gone` ## Cause The ratelimit namespace you're trying to access was previously deleted and is no longer available through the API or dashboard. ## Resolution Contact [support@unkey.com](mailto:support@unkey.com) with your workspace ID and namespace name if you need this namespace restored. ## Prevention To avoid accidentally deleting namespaces: * Restrict namespace deletion via workspace permissions * Carefully review namespace-deletion requests before confirming ## Related * [Ratelimit Namespace Not Found](/docs/errors/unkey/data/ratelimit_namespace_not_found) * [Rate Limiting Documentation](/docs/platform/ratelimiting/introduction) # ratelimit_namespace_not_found Source: https://unkey.com/docs/errors/unkey/data/ratelimit_namespace_not_found The requested rate limit namespace was not found in Unkey. Verify the namespace ID or name is correct and the namespace has not been deleted. err:unkey:data:ratelimit\_namespace\_not\_found ```json Example theme={"theme":"kanagawa-wave"} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "error": { "detail": "The requested rate limit namespace could not be found", "status": 404, "title": "Not Found", "type": "https://unkey.com/docs/errors/unkey/data/ratelimit_namespace_not_found" } } ``` ## What Happened? This error occurs when you're trying to perform an operation on a rate limit namespace that doesn't exist in the Unkey system. Rate limit namespaces are used to organize and manage rate limits for different resources or operations. Common scenarios that trigger this error: * Using an incorrect namespace ID or name * Referencing a namespace that has been deleted * Trying to modify a namespace that doesn't exist in the current workspace * Typos in namespace names when using name-based lookups Here's an example of a request that would trigger this error: ```bash theme={"theme":"kanagawa-wave"} # Attempting to get overrides for a non-existent namespace curl -X POST https://api.unkey.com/v2/ratelimit.listOverrides \ -H "Content-Type: application/json" \ -H "Authorization: Bearer unkey_YOUR_API_KEY" \ -d '{ "namespaceName": "nonexistent_namespace" }' ``` ## How To Fix Verify that you're using the correct namespace ID or name and that the namespace still exists in your workspace: 1. Check the namespace ID or name in your request for typos or formatting errors 2. List all namespaces in your workspace to find the correct ID or name 3. If the namespace has been deleted, you may need to recreate it Here's how to use the correct namespace in a rate limit operation: ```bash theme={"theme":"kanagawa-wave"} # Creating a rate limit using a valid namespace curl -X POST https://api.unkey.com/v2/ratelimit.limit \ -H "Content-Type: application/json" \ -H "Authorization: Bearer unkey_YOUR_API_KEY" \ -d '{ "namespace": "your_valid_namespace", "identifier": "user_123", "limit": 100, "duration": 60000 }' ``` ## Common Mistakes * **Typos in namespace names**: Small typographical errors in namespace names * **Case sensitivity**: Namespace names might be case-sensitive * **Deleted namespaces**: Referencing namespaces that have been removed * **Workspace boundaries**: Trying to use namespaces from another workspace ## Related Errors * [err:unkey:data:ratelimit\_override\_not\_found](./ratelimit_override_not_found) - When the requested rate limit override doesn't exist * [err:unkey:authorization:insufficient\_permissions](../authorization/insufficient_permissions) - When you don't have permission to perform operations on rate limits * [err:unkey:data:workspace\_not\_found](./workspace_not_found) - When the requested workspace doesn't exist # ratelimit_override_not_found Source: https://unkey.com/docs/errors/unkey/data/ratelimit_override_not_found The requested rate limit override was not found in Unkey. Verify the override identifier and namespace are correct and the override exists. err:unkey:data:ratelimit\_override\_not\_found ```json Example theme={"theme":"kanagawa-wave"} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "error": { "detail": "The requested rate limit override could not be found", "status": 404, "title": "Not Found", "type": "https://unkey.com/docs/errors/unkey/data/ratelimit_override_not_found" } } ``` ## What Happened? This error occurs when you're trying to perform an operation on a rate limit override that doesn't exist in the Unkey system. Rate limit overrides are used to create custom rate limits for specific identifiers within a namespace. Common scenarios that trigger this error: * Using an incorrect override ID * Referencing an override that has been deleted * Trying to get or modify an override for an identifier that doesn't have one * Using the wrong namespace when looking up an override Here's an example of a request that would trigger this error: ```bash theme={"theme":"kanagawa-wave"} # Attempting to get a non-existent rate limit override curl -X POST https://api.unkey.com/v2/ratelimit.getOverride \ -H "Content-Type: application/json" \ -H "Authorization: Bearer unkey_YOUR_API_KEY" \ -d '{ "namespaceId": "ns_123abc", "identifier": "user_without_override" }' ``` ## How To Fix Verify that you're using the correct namespace and identifier, and that the override still exists: 1. Check the namespace ID and identifier in your request for typos 2. List all overrides in the namespace to confirm if the one you're looking for exists 3. If the override has been deleted or never existed, you may need to create it Here's how to list overrides in a namespace: ```bash theme={"theme":"kanagawa-wave"} curl -X POST https://api.unkey.com/v2/ratelimit.listOverrides \ -H "Content-Type: application/json" \ -H "Authorization: Bearer unkey_YOUR_API_KEY" \ -d '{ "namespaceId": "ns_123abc" }' ``` If you need to create a new override, use the `ratelimit.setOverride` endpoint: ```bash theme={"theme":"kanagawa-wave"} curl -X POST https://api.unkey.com/v2/ratelimit.setOverride \ -H "Content-Type: application/json" \ -H "Authorization: Bearer unkey_YOUR_API_KEY" \ -d '{ "namespaceId": "ns_123abc", "identifier": "user_123", "limit": 200, "duration": 60000 }' ``` ## Common Mistakes * **Wrong identifier**: Using an incorrect user identifier when looking up overrides * **Deleted overrides**: Attempting to reference overrides that have been removed * **Namespace mismatch**: Looking in the wrong namespace for an override * **Assuming defaults are overrides**: Trying to get an override for an identifier that's using default limits ## Related Errors * [err:unkey:data:ratelimit\_namespace\_not\_found](./ratelimit_namespace_not_found) - When the requested rate limit namespace doesn't exist * [err:unkey:authorization:insufficient\_permissions](../authorization/insufficient_permissions) - When you don't have permission to perform operations on rate limit overrides * [err:unkey:data:workspace\_not\_found](./workspace_not_found) - When the requested workspace doesn't exist # role_already_exists Source: https://unkey.com/docs/errors/unkey/data/role_already_exists A role with this name already exists in your Unkey workspace. Use the existing role or choose a different unique name when creating a new one. err:unkey:data:role\_already\_exists ```json Example theme={"theme":"kanagawa-wave"} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "error": { "detail": "A role with name \"admin\" already exists in this workspace", "status": 409, "title": "Conflict", "type": "https://unkey.com/docs/errors/unkey/data/role/duplicate" } } ``` ## What Happened? This error occurs when you're trying to create a role with a name that already exists in your Unkey workspace. Role names must be unique within a workspace to avoid confusion and maintain proper access control. Common scenarios that trigger this error: * Creating a role with a name that's already in use * Re-creating a previously deleted role with the same name * Migration or import processes that don't check for existing roles * Duplicate API calls due to retries or network issues Here's an example of a request that would trigger this error: ```bash theme={"theme":"kanagawa-wave"} # Attempting to create a role with a name that already exists curl -X POST https://api.unkey.com/v2/permissions.createRole \ -H "Content-Type: application/json" \ -H "Authorization: Bearer unkey_YOUR_API_KEY" \ -d '{ "name": "admin", "description": "Administrator role with full access" }' ``` ## How To Fix When you encounter this error, you have several options: 1. **Use a different name**: If creating a new role, use a unique name 2. **Get the existing role**: If you just need the role information, retrieve it rather than creating it 3. **List existing roles**: Check what roles already exist before creating new ones 4. **Implement idempotent creation**: Use a get-or-create pattern in your code Here's how to list existing roles: ```bash theme={"theme":"kanagawa-wave"} curl -X POST https://api.unkey.com/v2/permissions.listRoles \ -H "Content-Type: application/json" \ -H "Authorization: Bearer unkey_YOUR_API_KEY" \ -d '{}' ``` Or implement a get-or-create pattern in your code: ```javascript theme={"theme":"kanagawa-wave"} // Pseudocode for get-or-create pattern async function getOrCreateRole(name, description) { try { // Try to create the role return await createRole(name, description); } catch (error) { // If it already exists (409 error), get it instead if (error.status === 409) { // Extract the role name from the error message and get it const roles = await listRoles(); return roles.find((r) => r.name.toLowerCase() === name.toLowerCase()); } // Otherwise, rethrow the error throw error; } } ``` ## Common Mistakes * **Not checking for existing roles**: Failing to check if a role already exists before creating it * **Case sensitivity**: Role names are case-insensitive - "Admin" and "admin" are the same * **Retry loops**: Repeatedly trying to create the same role after a failure * **Cross-environment duplication**: Using the same role names across development and production without proper namespacing ## Related Errors * [err:unkey:data:role\_not\_found](./role_not_found) - When the requested role doesn't exist * [err:unkey:authorization:insufficient\_permissions](../authorization/insufficient_permissions) - When you don't have permission to manage roles # role_not_found Source: https://unkey.com/docs/errors/unkey/data/role_not_found The requested role was not found in Unkey. Verify the role ID is correct and the role has not been deleted from your workspace RBAC config. err:unkey:data:role\_not\_found ```json Example theme={"theme":"kanagawa-wave"} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "error": { "detail": "The requested role could not be found", "status": 404, "title": "Not Found", "type": "https://unkey.com/docs/errors/unkey/data/role_not_found" } } ``` ## What Happened? This error occurs when you're trying to perform an operation on a role that doesn't exist in the Unkey system. Roles in Unkey are collections of permissions that can be assigned to users or API keys. Common scenarios that trigger this error: * Using an incorrect role ID or name * Referencing a role that has been deleted * Trying to assign a role that doesn't exist in the current workspace * Typos in role names when using name-based lookups ## How To Fix Verify that you're using the correct role ID or name and that the role still exists in your workspace: 1. List all roles in your workspace to find the correct ID 2. Check if the role has been deleted and recreate it if necessary 3. Verify you're working in the correct workspace Here's how to list all roles in your workspace: ```bash theme={"theme":"kanagawa-wave"} curl -X POST https://api.unkey.com/v2/permissions.listRoles \ -H "Authorization: Bearer unkey_YOUR_API_KEY" ``` If you need to create a new role, use the appropriate API endpoint: ```bash theme={"theme":"kanagawa-wave"} curl -X POST https://api.unkey.com/v2/permissions.createRole \ -H "Content-Type: application/json" \ -H "Authorization: Bearer unkey_YOUR_API_KEY" \ -d '{ "name": "API Reader", "description": "Can read API information" }' ``` ## Common Mistakes * **Incorrect identifiers**: Using wrong role IDs or names * **Deleted roles**: Referencing roles that have been removed * **Case sensitivity**: Role names might be case-sensitive * **Workspace boundaries**: Trying to use roles from another workspace ## Related Errors * [err:unkey:data:permission\_not\_found](./permission_not_found) - When the requested permission doesn't exist * [err:unkey:data:api\_not\_found](./api_not_found) - When the requested API doesn't exist * [err:unkey:authorization:insufficient\_permissions](../authorization/insufficient_permissions) - When you don't have permission to perform operations on roles # workspace_not_found Source: https://unkey.com/docs/errors/unkey/data/workspace_not_found The requested workspace was not found in Unkey. Verify the workspace ID is correct in your root key and that your workspace has not been deleted. err:unkey:data:workspace\_not\_found ```json Example theme={"theme":"kanagawa-wave"} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "error": { "detail": "The requested workspace could not be found", "status": 404, "title": "Not Found", "type": "https://unkey.com/docs/errors/unkey/data/workspace_not_found" } } ``` ## What Happened? This error occurs when you're trying to perform an operation on a workspace that doesn't exist in the Unkey system. This can happen when referencing a workspace by ID or name in API calls. Common scenarios that trigger this error: * Using an incorrect workspace ID * Referencing a workspace that has been deleted * Attempting to access a workspace you don't have permission to see * Typos in workspace names when using name-based lookups ## How To Fix Verify that you're using the correct workspace ID or name and that the workspace still exists: 1. Check your Unkey dashboard to see a list of workspaces you have access to 2. Verify the workspace ID or name in your API calls 3. Ensure you have permission to access the workspace 4. If needed, create a new workspace through the dashboard or API ## Common Mistakes * **Deleted workspaces**: Attempting to reference workspaces that have been deleted * **Copy-paste errors**: Using incorrect IDs from documentation examples * **Permission issues**: Trying to access workspaces you've been removed from * **Case sensitivity**: Using incorrect casing in workspace name lookups ## Related Errors * [err:unkey:authorization:workspace\_disabled](../authorization/workspace_disabled) - When the workspace exists but is disabled * [err:unkey:data:api\_not\_found](./api_not_found) - When the requested API doesn't exist * [err:unkey:data:key\_not\_found](./key_not_found) - When the requested key doesn't exist # client_closed_request Source: https://unkey.com/docs/errors/user/bad_request/client_closed_request The client cancelled the request before the Unkey server could finish processing. Learn about connection timeouts and how to increase them. `err:user:bad_request:client_closed_request` ```json Example theme={"theme":"kanagawa-wave"} { "meta": { "requestId": "req_4dgzrNP3Je5mU1tD" }, "error": { "detail": "Client closed request", "status": 499, "title": "Client Closed Request", "type": "https://unkey.com/docs/errors/user/bad_request/client_closed_request", "errors": [] } } ``` ## What Happened? Your client cancelled the request before our server finished processing it. This happens when: * Your client timeout is shorter than the processing time * Network connection was lost during the request * Your application cancelled the request (user navigated away, etc.) * Load balancer or proxy terminated the connection **HTTP Status 499** is a non-standard but widely used status code that means "Client Closed Request". It was originally defined by nginx and is now used by many web servers to indicate when a client disconnects before receiving the full response. ## Common Causes ### 1. Client Timeout Too Short Your HTTP client may have a timeout that's shorter than our processing time. Make sure your client timeout allows enough time for the operation to complete (typically 30+ seconds). ### 2. Command Line Tools Tools like `curl` or `timeout` commands may have short default timeouts. Use `--max-time 30` with curl or longer timeout values with other tools. ### 3. User Interface Cancellations In web applications, users might navigate away or close tabs before requests complete. This is normal user behavior and should be handled gracefully in your application. ## How to Fix It ### 1. Increase Client Timeouts Make sure your client timeout is reasonable for the operation (typically 30+ seconds). Check your HTTP client documentation for timeout configuration options. ### 2. Handle Network Issues If you're experiencing network connectivity problems that cause your client to disconnect, check your network stability and consider increasing timeout values. ### 3. Check Your Infrastructure If you're using proxies, load balancers, or CDNs: * Verify their timeout settings * Check if they're terminating long-running requests * Ensure they're configured to handle the expected request duration ## When You See This Error ### It's Usually Not Our Fault Status 499 means your client cancelled the request, so the issue is typically: * Your client timeout settings * Network connectivity problems * User behavior (closing browser, etc.) ### Check Your Logs Look for patterns: * Is it happening at specific times? * Are certain operations more affected? * Are there network errors in your client logs? ### You Won't See 499 Errors in Your Client **Important:** Since the client cancels the request before getting a response, your application typically won't receive a 499 status code. Instead, you'll see: * Network timeout errors * Request cancelled/aborted errors * Connection reset errors You can only see 499 errors in server logs, not in your client application. This is why this error mainly helps server operators understand why requests failed. ## Difference from Other Errors * **408 Request Timeout**: Server took too long (server-side issue) * **499 Client Closed Request**: Client cancelled early (client-side issue) * **504 Sentinel Timeout**: Upstream service timeout (infrastructure issue) If you're seeing mostly 499 errors, focus on your client configuration and network connectivity rather than server performance. # invalid_analytics_function Source: https://unkey.com/docs/errors/user/bad_request/invalid_analytics_function Your analytics query uses a SQL function not allowed for security reasons in Unkey. Review the list of supported aggregate and scalar functions. `err:user:bad_request:invalid_analytics_function` ```json Example theme={"theme":"kanagawa-wave"} { "meta": { "requestId": "req_4dgzrNP3Je5mU1tD" }, "error": { "detail": "Function 'file' is not allowed", "status": 400, "title": "Bad Request", "type": "https://unkey.com/docs/errors/user/bad_request/invalid_analytics_function" } } ``` ## What Happened? Your query tried to use a function that's blocked for security reasons! For security, only safe functions are allowed in analytics queries. Functions that could: * Read files from the server (`file`, `executable`) * Make network requests (`url`, `remote`) * Access external systems (`mysql`, `postgresql`, `s3`, `hdfs`) * Modify data or system state ...are all blocked. ## How to Fix It ### 1. Use Allowed Functions Stick to standard analytics functions: ```sql Wrong - Blocked function theme={"theme":"kanagawa-wave"} SELECT file('/etc/passwd') FROM key_verifications_v1 ``` ```sql Correct - Safe analytics functions theme={"theme":"kanagawa-wave"} SELECT toStartOfHour(time) as hour, COUNT(*) as total, AVG(response_time) as avg_response FROM key_verifications_v1 WHERE time >= now() - INTERVAL 7 DAY GROUP BY hour ``` ### 2. Common Safe Functions These are examples of allowed functions: **Aggregate functions:** * `COUNT()`, `SUM()`, `AVG()`, `MIN()`, `MAX()` * `uniq()`, `groupArray()` **Date/time functions:** * `now()`, `today()`, `yesterday()` * `toStartOfHour()`, `toStartOfDay()`, `toStartOfWeek()` * `toDate()`, `toDateTime()` **String functions:** * `concat()`, `substring()`, `lower()`, `upper()` * `length()`, `position()` **Mathematical functions:** * `round()`, `floor()`, `ceil()` * `abs()`, `sqrt()`, `pow()` **Conditional functions:** * `if()`, `multiIf()` * `CASE WHEN ... THEN ... END` ### 3. Remove Dangerous Functions ```sql Blocked - File access theme={"theme":"kanagawa-wave"} SELECT file('/path/to/file') FROM key_verifications_v1 ``` ```sql Blocked - Network access theme={"theme":"kanagawa-wave"} SELECT * FROM url('http://acme.com/data') ``` ```sql Blocked - External DB theme={"theme":"kanagawa-wave"} SELECT * FROM mysql('host:port', 'db', 'table', 'user', 'pass') ``` ```sql Safe Alternative - Use only your analytics data theme={"theme":"kanagawa-wave"} SELECT api_id, COUNT(*) as verifications FROM key_verifications_v1 WHERE time >= now() - INTERVAL 1 DAY GROUP BY api_id ``` ## Commonly Blocked Functions These functions are blocked for security: | Function | Why Blocked | | -------------------------------------- | ------------------------ | | `file()`, `executable()` | File system access | | `url()`, `remote()` | Network requests | | `mysql()`, `postgresql()`, `mongodb()` | External database access | | `s3()`, `hdfs()`, `azureBlobStorage()` | External storage access | | `dictGet()`, `dictGetOrDefault()` | Dictionary access | Need a specific function that's blocked? [Contact support](mailto:support@unkey.com) to discuss your use case - we may be able to safely enable it! # invalid_analytics_query Source: https://unkey.com/docs/errors/user/bad_request/invalid_analytics_query Your SQL analytics query has a syntax error in Unkey. Review your query for typos, missing clauses, or invalid column names and try again. `err:user:bad_request:invalid_analytics_query` ```json Example theme={"theme":"kanagawa-wave"} { "meta": { "requestId": "req_4dgzrNP3Je5mU1tD" }, "error": { "detail": "Syntax error: Expected identifier, got 'FROM' at position 15", "status": 400, "title": "Bad Request", "type": "https://unkey.com/docs/errors/user/bad_request/invalid_analytics_query" } } ``` ## What Happened? Your SQL query has a syntax error! The query parser found invalid SQL syntax that prevents it from being executed. Common causes include: * Missing or extra commas * Unclosed quotes or parentheses * Typos in SQL keywords * Invalid column or table names ## How to Fix It ### 1. Check for Missing Commas ```sql Wrong - Missing comma theme={"theme":"kanagawa-wave"} SELECT key_space_id COUNT(*) as total FROM key_verifications_v1 ``` ```sql Correct theme={"theme":"kanagawa-wave"} SELECT key_space_id, COUNT(*) as total FROM key_verifications_v1 ``` ### 2. Match Quotes and Parentheses ```sql Wrong - Unclosed quote theme={"theme":"kanagawa-wave"} SELECT key_space_id, outcome FROM key_verifications_v1 WHERE key_space_id = 'ks_123 ``` ```sql Correct theme={"theme":"kanagawa-wave"} SELECT key_space_id, outcome FROM key_verifications_v1 WHERE key_space_id = 'ks_123' ``` ### 3. Use Correct SQL Keywords ```sql Wrong - Typo in SELECT theme={"theme":"kanagawa-wave"} SELCT time, outcome FROM key_verifications_v1 ``` ```sql Correct theme={"theme":"kanagawa-wave"} SELECT time, outcome FROM key_verifications_v1 ``` ### 4. Verify Column Names Make sure you're using valid column names from your analytics tables: ```sql theme={"theme":"kanagawa-wave"} -- ✓ Valid columns SELECT time, key_space_id, outcome, key_id FROM key_verifications_v1 WHERE time >= now() - INTERVAL 7 DAY ``` ## Need Help? If you're stuck with a syntax error: 1. **Check the error message** - It usually tells you exactly where the problem is 2. **Test incrementally** - Start with a simple query like `SELECT time, outcome FROM table_name LIMIT 10` and add complexity step by step 3. **Use a SQL validator** - Many online tools can help spot syntax errors 4. **Check the schema** - Refer to the [Schema Reference](/docs/platform/analytics/schema-reference) for valid column names # invalid_analytics_query_type Source: https://unkey.com/docs/errors/user/bad_request/invalid_analytics_query_type Only SELECT queries are allowed for Unkey analytics. You attempted an INSERT, UPDATE, DELETE, or other unsupported write operation. `err:user:bad_request:invalid_analytics_query_type` ```json Example theme={"theme":"kanagawa-wave"} { "meta": { "requestId": "req_4dgzrNP3Je5mU1tD" }, "error": { "detail": "Only SELECT queries are allowed", "status": 400, "title": "Bad Request", "type": "https://unkey.com/docs/errors/user/bad_request/invalid_analytics_query_type" } } ``` ## What Happened? You tried to run a query that modifies data or isn't a SELECT! Analytics queries are read-only - you can only SELECT data, not modify it. Blocked query types: * `INSERT` - Adding new data * `UPDATE` - Modifying existing data * `DELETE` - Removing data * `DROP` - Deleting tables * `CREATE` - Creating tables * `ALTER` - Modifying table structure * `TRUNCATE` - Clearing tables ## How to Fix It ### 1. Use SELECT Instead Analytics is for querying data, not modifying it: ```sql Wrong - INSERT not allowed theme={"theme":"kanagawa-wave"} INSERT INTO key_verifications_v1 VALUES (...) ``` ```sql Wrong - UPDATE not allowed theme={"theme":"kanagawa-wave"} UPDATE key_verifications_v1 SET outcome = 'VALID' ``` ```sql Wrong - DELETE not allowed theme={"theme":"kanagawa-wave"} DELETE FROM key_verifications_v1 WHERE time < now() - INTERVAL 30 DAY ``` ```sql Correct - SELECT queries only theme={"theme":"kanagawa-wave"} SELECT api_id, outcome, COUNT(*) as total FROM key_verifications_v1 WHERE time >= now() - INTERVAL 7 DAY GROUP BY api_id, outcome ``` ### 2. Query Your Data The analytics endpoint is for analyzing your verification data: ```sql theme={"theme":"kanagawa-wave"} -- ✓ Count verifications by outcome SELECT outcome, COUNT(*) as total FROM key_verifications_v1 WHERE time >= now() - INTERVAL 1 DAY GROUP BY outcome -- ✓ Get hourly verification rates SELECT toStartOfHour(time) as hour, COUNT(*) as verifications FROM key_verifications_v1 WHERE time >= now() - INTERVAL 24 HOUR GROUP BY hour ORDER BY hour -- ✓ Find most active APIs SELECT api_id, COUNT(*) as requests FROM key_verifications_v1 WHERE time >= now() - INTERVAL 7 DAY GROUP BY api_id ORDER BY requests DESC LIMIT 10 ``` ### 3. Use the Correct API for Data Modification If you need to modify data, use the appropriate Unkey API endpoints: | What You Want | Use This API | | ------------------ | ------------------------------ | | Create a key | `POST /v2/keys.createKey` | | Update a key | `PATCH /v2/keys.updateKey` | | Delete a key | `POST /v2/keys.deleteKey` | | Modify permissions | `POST /v2/keys.addPermissions` | ## Why Read-Only? Analytics queries are read-only for several reasons: 1. **Data integrity** - Verification history should never be modified 2. **Performance** - Read-only queries can be heavily optimized 3. **Security** - Prevents accidental or malicious data corruption 4. **Audit trail** - Preserves accurate historical records Analytics is for understanding your data, not changing it. Use the main Unkey API for creating, updating, or deleting resources. # invalid_analytics_table Source: https://unkey.com/docs/errors/user/bad_request/invalid_analytics_table Your analytics query references a table that does not exist or is not accessible in Unkey. Check available tables in the schema reference. `err:user:bad_request:invalid_analytics_table` ```json Example theme={"theme":"kanagawa-wave"} { "meta": { "requestId": "req_4dgzrNP3Je5mU1tD" }, "error": { "detail": "Access to table 'system.tables' is not allowed", "status": 400, "title": "Bad Request", "type": "https://unkey.com/docs/errors/user/bad_request/invalid_analytics_table" } } ``` ## What Happened? Your query tried to access a table that either doesn't exist or isn't allowed for security reasons. For security, only specific analytics tables are accessible: * `key_verifications_v1` - Raw key verification events * `key_verifications_per_minute_v1` - Minute-level aggregates * `key_verifications_per_hour_v1` - Hour-level aggregates * `key_verifications_per_day_v1` - Day-level aggregates * `key_verifications_per_month_v1` - Month-level aggregates System tables (like `system.*`) and other database tables are blocked. ## How to Fix It ### 1. Use the Correct Table Name ```sql Wrong - System table theme={"theme":"kanagawa-wave"} SELECT * FROM system.tables ``` ```sql Correct - Analytics table theme={"theme":"kanagawa-wave"} SELECT * FROM key_verifications_v1 WHERE time >= now() - INTERVAL 7 DAY ``` ### 2. Fix Typos in Table Names ```sql Wrong - Typo theme={"theme":"kanagawa-wave"} SELECT * FROM key_verification WHERE time >= now() - INTERVAL 1 DAY ``` ```sql Correct theme={"theme":"kanagawa-wave"} SELECT * FROM key_verifications_v1 WHERE time >= now() - INTERVAL 1 DAY ``` ## Available Tables | Table Name | Description | | --------------------------------- | ----------------------- | | `key_verifications_v1` | Raw verification events | | `key_verifications_per_minute_v1` | Minute-level aggregates | | `key_verifications_per_hour_v1` | Hour-level aggregates | | `key_verifications_per_day_v1` | Day-level aggregates | | `key_verifications_per_month_v1` | Month-level aggregates | # missing_required_header Source: https://unkey.com/docs/errors/user/bad_request/missing_required_header A required HTTP header is missing from your Unkey API request. Check the API reference for required headers like Authorization and Content-Type. `err:user:bad_request:missing_required_header` # permissions_query_syntax_error Source: https://unkey.com/docs/errors/user/bad_request/permissions_query_syntax_error Your verifyKey permissions query contains invalid syntax or unsupported characters. Review the permissions query format and fix the expression. `err:user:bad_request:permissions_query_syntax_error` ```json Example theme={"theme":"kanagawa-wave"} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "error": { "detail": "Syntax error in permission query: unexpected token 'AND' at position 15. Expected permission name or opening parenthesis.", "status": 400, "title": "Bad Request", "type": "https://unkey.com/docs/errors/user/bad_request/permissions_query_syntax_error", "errors": [ { "location": "body.permissions", "message": "unexpected token 'AND' at position 15", "fix": "Check your query syntax. AND/OR operators must be between permissions, not at the start or end" } ] } } ``` ## What Happened? This error occurs when the permissions query in your `verifyKey` request contains invalid syntax or characters. This can happen due to: * **Invalid characters** in permission names (lexical errors) * **Incorrect query structure** like missing operands or unmatched parentheses (syntax errors) * **Malformed expressions** that don't follow the expected grammar ## Permissions Query Requirements The `verifyKey` endpoint accepts a permissions query string that must follow these rules: ### Valid Characters The query parser accepts these characters: * **Permissions**: Must follow the permission slug format (alphanumeric, dots, underscores, hyphens) * **Letters**: `a-z`, `A-Z` * **Numbers**: `0-9` * **Dots**: `.` for permission namespacing * **Underscores**: `_` in identifiers * **Hyphens**: `-` in identifiers * **Query operators**: `AND`, `OR` (case insensitive) * **Grouping**: `(` `)` for parentheses * **Whitespace**: Spaces, tabs and new lines for separation (ignored by parser) Everything else is not allowed. ### Query Structure A permissions query can be: 1. **A single permission**: `permission_1` 2. **Multiple permissions with AND**: `permission_1 AND permission_2` 3. **Multiple permissions with OR**: `permission_1 OR permission_2` 4. **Grouped expressions**: `(permission_1 OR permission_2) AND permission_3` **Key rules:** * Permission names must be valid permission slugs (letters, numbers, dots, underscores, hyphens) * Use `AND` when all permissions are required * Use `OR` when any of the permissions is sufficient * Use parentheses `()` to group expressions and control precedence * Operators are case insensitive: `AND`, `AnD`, `and` all work. ## Common Errors and Solutions ### 1. Invalid Characters ```bash theme={"theme":"kanagawa-wave"} # ❌ Invalid - contains special characters curl -X POST https://api.unkey.com/v2/keys.verifyKey \ -H "Content-Type: application/json" \ -H "Authorization: Bearer unkey_XXXX" \ -d '{ "key": "sk_123", "permissions": "permission$1 OR permission@2" }' # ✅ Valid - use underscores or hyphens curl -X POST https://api.unkey.com/v2/keys.verifyKey \ -H "Content-Type: application/json" \ -H "Authorization: Bearer unkey_XXXX" \ -d '{ "key": "sk_123", "permissions": "permission_1 OR permission_2" }' ``` ### 2. Missing Operands ```bash theme={"theme":"kanagawa-wave"} # ❌ Invalid - AND without right operand curl -X POST https://api.unkey.com/v2/keys.verifyKey \ -H "Content-Type: application/json" \ -H "Authorization: Bearer unkey_XXXX" \ -d '{ "key": "sk_123", "permissions": "permission_1 AND" }' # ✅ Valid - complete AND expression curl -X POST https://api.unkey.com/v2/keys.verifyKey \ -H "Content-Type: application/json" \ -H "Authorization: Bearer unkey_XXXX" \ -d '{ "key": "sk_123", "permissions": "permission_1 AND permission_2" }' ``` ### 3. Unmatched Parentheses ```bash theme={"theme":"kanagawa-wave"} # ❌ Invalid - missing closing parenthesis curl -X POST https://api.unkey.com/v2/keys.verifyKey \ -H "Content-Type: application/json" \ -H "Authorization: Bearer unkey_XXXX" \ -d '{ "key": "sk_123", "permissions": "(permission_1 AND permission_2" }' # ✅ Valid - balanced parentheses curl -X POST https://api.unkey.com/v2/keys.verifyKey \ -H "Content-Type: application/json" \ -H "Authorization: Bearer unkey_XXXX" \ -d '{ "key": "sk_123", "permissions": "(permission_1 AND permission_2)" }' ``` ### 4. Empty Parentheses ```bash theme={"theme":"kanagawa-wave"} # ❌ Invalid - empty parentheses curl -X POST https://api.unkey.com/v2/keys.verifyKey \ -H "Content-Type: application/json" \ -H "Authorization: Bearer unkey_XXXX" \ -d '{ "key": "sk_123", "permissions": "permission_1 AND ()" }' # ✅ Valid - parentheses with content curl -X POST https://api.unkey.com/v2/keys.verifyKey \ -H "Content-Type: application/json" \ -H "Authorization: Bearer unkey_XXXX" \ -d '{ "key": "sk_123", "permissions": "permission_1 AND (permission_2 OR permission_3)" }' ``` ### 5. Incorrect Operator Placement ```bash theme={"theme":"kanagawa-wave"} # ❌ Invalid - operator at start curl -X POST https://api.unkey.com/v2/keys.verifyKey \ -H "Content-Type: application/json" \ -H "Authorization: Bearer unkey_XXXX" \ -d '{ "key": "sk_123", "permissions": "OR permission_1" }' # ✅ Valid - operators between permissions curl -X POST https://api.unkey.com/v2/keys.verifyKey \ -H "Content-Type: application/json" \ -H "Authorization: Bearer unkey_XXXX" \ -d '{ "key": "sk_123", "permissions": "permission_1 OR permission_2" }' ``` ## Valid Query Examples ### Simple Permission ```bash theme={"theme":"kanagawa-wave"} curl -X POST https://api.unkey.com/v2/keys.verifyKey \ -H "Content-Type: application/json" \ -H "Authorization: Bearer unkey_XXXX" \ -d '{ "key": "sk_123", "permissions": "permission_1" }' ``` ### AND Operation ```bash theme={"theme":"kanagawa-wave"} curl -X POST https://api.unkey.com/v2/keys.verifyKey \ -H "Content-Type: application/json" \ -H "Authorization: Bearer unkey_XXXX" \ -d '{ "key": "sk_123", "permissions": "permission_1 AND permission_2" }' ``` ### OR Operation ```bash theme={"theme":"kanagawa-wave"} curl -X POST https://api.unkey.com/v2/keys.verifyKey \ -H "Content-Type: application/json" \ -H "Authorization: Bearer unkey_XXXX" \ -d '{ "key": "sk_123", "permissions": "permission_1 OR permission_2" }' ``` ### Complex Expressions ```bash theme={"theme":"kanagawa-wave"} curl -X POST https://api.unkey.com/v2/keys.verifyKey \ -H "Content-Type: application/json" \ -H "Authorization: Bearer unkey_XXXX" \ -d '{ "key": "sk_123", "permissions": "(permission_1 OR permission_2) AND permission_3" }' ``` ### Nested Expressions ```bash theme={"theme":"kanagawa-wave"} curl -X POST https://api.unkey.com/v2/keys.verifyKey \ -H "Content-Type: application/json" \ -H "Authorization: Bearer unkey_XXXX" \ -d '{ "key": "sk_123", "permissions": "((permission_1 OR permission_2) AND permission_3) OR permission_4" }' ``` ## Valid Permission Formats ### Simple Names * `permission_1` * `user_read` * `admin-access` ### Namespaced Permissions * `api.users.read` * `billing.invoices.create` * `workspace.settings.update` ### Mixed Formats * `user_management.create` * `billing-service.view` * `service123.feature_a.read` ## Context This error is specific to the `verifyKey` endpoint's permissions query parsing. The query is validated at the application level to ensure it conforms to the expected permission query language syntax. Basic validation like empty strings and length limits are handled at the OpenAPI level before reaching this parser. # query_range_exceeds_retention Source: https://unkey.com/docs/errors/user/bad_request/query_range_exceeds_retention Your query requests data older than your workspace's retention period in Unkey. Narrow the time range to stay within your plan's limits. `err:user:bad_request:query_range_exceeds_retention` ## What does this error mean? This error occurs when your analytics query attempts to access data older than your workspace's configured data retention period. By default, Unkey retains analytics data for **30 days**. **Note**: If you don't provide a time filter in your query, the system automatically adds one to scope your query to the full retention period (e.g., `time >= now() - INTERVAL 30 DAY`). This error only occurs when you explicitly specify a time range that goes beyond the retention period. ## Common causes 1. **Querying historical data beyond retention**: Your query's time filter includes dates older than your retention period ```sql theme={"theme":"kanagawa-wave"} -- If your retention is 30 days, this will fail if querying > 30 days ago SELECT COUNT(*) FROM key_verifications_v1 WHERE time >= 1234567890000 ``` ## How to fix 1. **Add a time filter within retention**: Ensure your query only accesses data within your retention period ```sql theme={"theme":"kanagawa-wave"} -- Query last 7 days (within 30-day retention) SELECT COUNT(*) FROM key_verifications_v1 WHERE time >= now() - INTERVAL 7 DAY ``` 2. **Adjust your query range**: If you need to analyze trends, query within your available retention window ```sql theme={"theme":"kanagawa-wave"} -- Query the full 30-day retention period SELECT COUNT(*) FROM key_verifications_v1 WHERE time >= now() - INTERVAL 30 DAY ``` ## Need longer retention? If your use case requires data retention beyond 30 days, please [contact our support team](https://unkey.com/support) to discuss upgrading your retention period. We can configure custom retention periods based on your needs. ## Related * [Analytics documentation](/docs/platform/analytics/overview) * [Analytics query examples](/docs/platform/analytics/query-examples) # request_body_too_large Source: https://unkey.com/docs/errors/user/bad_request/request_body_too_large The request body exceeds the maximum allowed size limit for the Unkey API. Reduce your payload size or paginate large batch operations. `err:user:bad_request:request_body_too_large` ```json Example theme={"theme":"kanagawa-wave"} { "meta": { "requestId": "req_4dgzrNP3Je5mU1tD" }, "error": { "detail": "The request body exceeds the maximum allowed size of 100 bytes.", "status": 413, "title": "Request Entity Too Large", "type": "https://unkey.com/docs/errors/user/bad_request/request_body_too_large", "errors": [] } } ``` ## What Happened? Your request was too big! We limit how much data you can send in a single API request to keep everything running smoothly. This usually happens when you're trying to send a lot of data at once - like huge metadata objects or really long strings in your request. ## How to Fix It ### 1. Trim Down Your Request The most common cause is putting too much data in the `meta` field or other parts of your request. ```bash Too Big theme={"theme":"kanagawa-wave"} curl -X POST https://api.unkey.com/v2/keys.create \ -H "Content-Type: application/json" \ -H "Authorization: Bearer unkey_XXXX" \ -d '{ "apiId": "api_123", "name": "My Key", "meta": { "userProfile": "... really long user profile data ...", "settings": { /* huge nested object with tons of properties */ } } }' ``` ```bash Just Right theme={"theme":"kanagawa-wave"} curl -X POST https://api.unkey.com/v2/keys.create \ -H "Content-Type: application/json" \ -H "Authorization: Bearer unkey_XXXX" \ -d '{ "apiId": "api_123", "name": "My Key", "meta": { "userId": "user_123", "tier": "premium" } }' ``` ### 2. Store Big Data Elsewhere Instead of cramming everything into your API request: * Store large data in your own database * Only send IDs or references to Unkey * Fetch the full data when you need it ## Need a Higher Limit? **Got a special use case?** If you have a legitimate need to send larger requests, we'd love to hear about it! [Contact our support team](mailto:support@unkey.com) and include: * What you're building * Why you need to send large requests * An example of the data you're trying to send We'll work with you to find a solution that works for your use case. # request_body_unreadable Source: https://unkey.com/docs/errors/user/bad_request/request_body_unreadable The request body could not be read due to malformed JSON, encoding issues, or a dropped connection. Validate your payload format and retry. `err:user:bad_request:request_body_unreadable` ```json Example theme={"theme":"kanagawa-wave"} { "meta": { "requestId": "req_4dgzrNP3Je5mU1tD" }, "error": { "detail": "The request body could not be read.", "status": 400, "title": "Bad Request", "type": "https://unkey.com/docs/errors/user/bad_request/request_body_unreadable", "errors": [] } } ``` ## What Happened? The server couldn't read your request body. This typically happens when there's an issue with how the request was formed or transmitted. Common causes include: * Malformed HTTP request structure * Network connection closed prematurely during transmission * Invalid or mismatched `Content-Length` header * Data corruption during transmission * Client-side network interruption ## How to Fix It ### 1. Check Your Request Structure Make sure your HTTP request is properly formatted and all required headers are present: ```bash theme={"theme":"kanagawa-wave"} curl -X POST https://api.unkey.com/v2/keys.create \ -H "Content-Type: application/json" \ -H "Authorization: Bearer unkey_XXXX" \ -d '{ "apiId": "api_123", "name": "My Key" }' ``` ### 2. Check Network Stability If you're experiencing intermittent failures: * Implement retry logic with exponential backoff * Check your network connection stability * Consider timeout values that may be too aggressive ### 3. Avoid Partial Uploads Ensure you're sending the complete request body in one go, not streaming partial data that might get interrupted. ## Still Having Issues? If you continue to see this error after checking the above: 1. Check your HTTP client library documentation for known issues 2. Try a different HTTP client to isolate the problem 3. [Contact our support team](mailto:support@unkey.com) with: * Your request ID (from the error response) * The HTTP client/library you're using * Any network logs or error messages # request_timeout Source: https://unkey.com/docs/errors/user/bad_request/request_timeout The request exceeded the Unkey server timeout before processing could complete. Simplify your request, reduce payload size, or retry later. `err:user:bad_request:request_timeout` ```json Example theme={"theme":"kanagawa-wave"} { "meta": { "requestId": "req_4dgzrNP3Je5mU1tD" }, "error": { "detail": "Request timeout", "status": 408, "title": "Request Timeout", "type": "https://unkey.com/docs/errors/user/bad_request/request_timeout", "errors": [] } } ``` ## What Happened? Your request took too long to process and was automatically terminated by the server. This typically happens when: * Database queries are running slowly * External services are responding slowly * The API is under heavy load * Network connectivity issues are causing delays ## How to Fix It ### 1. Retry the Request Most timeout errors are temporary. Simply retry your request after a short delay with exponential backoff (wait longer after each failed attempt). ### 2. Check Your Network Slow or unstable network connections can cause timeouts: * Test from a different network or location * Check if you're experiencing high latency to our servers ## When This Happens Often If you're seeing frequent timeout errors: ### Check Our Status Page Visit [status.unkey.com](https://status.unkey.com) to see if we're experiencing any service issues. ### Contact Support **Still having issues?** We're here to help! [Contact our support team](mailto:support@unkey.com) and include: * Your request IDs from the error responses * The approximate time the errors occurred * Your typical request patterns and volume We can investigate what might be causing the slowdowns and help optimize your integration. ## Difference from Other Timeout Errors * **408 Request Timeout**: The server took too long to process your request (this error) * **499 Client Closed Request**: Your client cancelled the request before the server finished * **504 Sentinel Timeout**: An upstream service (like a database) timed out If you're seeing 408 errors, the issue is usually on our side or with network connectivity. # query_quota_exceeded Source: https://unkey.com/docs/errors/user/too_many_requests/query_quota_exceeded You have exceeded your workspace's analytics query quota for the current time window in Unkey. Wait for the quota to reset or upgrade your plan. `err:user:too_many_requests:query_quota_exceeded` ```json Example theme={"theme":"kanagawa-wave"} { "meta": { "requestId": "req_4dgzrNP3Je5mU1tD" }, "error": { "detail": "Workspace has exceeded the analytics query quota of 1000 queries per hour", "status": 429, "title": "Too Many Requests", "type": "https://unkey.com/docs/errors/user/too_many_requests/query_quota_exceeded" } } ``` ## What Happened? Your workspace has made too many analytics queries in a short period! We limit the number of queries you can run per hour to keep the analytics service fast and reliable for everyone. This is a rate limit on the **number of queries**, not about individual query complexity. ## How to Fix It ### 1. Wait and Retry The quota resets every hour. Wait a bit and try your query again. ### 2. Cache Your Results Instead of running the same query repeatedly, cache the results in your application: ```typescript Bad - Queries on Every Request theme={"theme":"kanagawa-wave"} app.get("/dashboard", async (req, res) => { // This runs a query EVERY time someone loads the dashboard const stats = await fetch("https://api.unkey.com/v2/analytics.getVerifications", { method: "POST", headers: { Authorization: "Bearer unkey_XXX" }, body: JSON.stringify({ query: "SELECT COUNT(*) FROM key_verifications" }), }); res.json(stats); }); ``` ```typescript Better - Cache for 5 Minutes theme={"theme":"kanagawa-wave"} import { Cache } from "your-cache-library"; const cache = new Cache(); app.get("/dashboard", async (req, res) => { // Check cache first let stats = cache.get("dashboard-stats"); if (!stats) { // Only query if cache is empty stats = await fetch("https://api.unkey.com/v2/analytics.getVerifications", { method: "POST", headers: { Authorization: "Bearer unkey_XXX" }, body: JSON.stringify({ query: "SELECT COUNT(*) FROM key_verifications" }), }); // Cache for 5 minutes cache.set("dashboard-stats", stats, { ttl: 300 }); } res.json(stats); }); ``` ### 3. Batch Your Queries If you're making multiple queries, try to combine them into a single query with JOINs or subqueries. ### 4. Use Webhooks Instead For real-time updates, consider using webhooks instead of polling the analytics API repeatedly. ## Default Quota | Plan | Queries per Hour | | ---------- | ---------------- | | Free | 1,000 | | Pro | 10,000 | | Enterprise | Custom | ## Need a Higher Quota? **Running into limits often?** We can increase your quota! [Contact our support team](mailto:support@unkey.com) and tell us: * What you're building * Why you need more queries per hour * Your current usage patterns We'll work with you to find the right quota for your needs, or help optimize your query patterns. # workspace_rate_limited Source: https://unkey.com/docs/errors/user/too_many_requests/workspace_rate_limited Your workspace has exceeded the Unkey API rate limit for the current time window. Implement backoff and retry logic or upgrade your plan. `err:user:too_many_requests:workspace_rate_limited` ```json Example theme={"theme":"kanagawa-wave"} { "meta": { "requestId": "req_4dgzrNP3Je5mU1tD" }, "error": { "detail": "This workspace has exceeded its API rate limit of 100/10s. Please try again later.", "status": 429, "title": "Too Many Requests", "type": "https://unkey.com/docs/errors/user/too_many_requests/workspace_rate_limited" } } ``` ## What Happened? Your workspace has been configured with an API rate limit and you've exceeded it. Most workspaces don't have a rate limit by default, this is only applied when needed. The response includes standard rate limit headers (`RateLimit-Limit`, `RateLimit-Remaining`, `RateLimit-Reset`, `Retry-After`) so your client knows when to retry. ## Need Help? If you're hitting this limit unexpectedly, [contact us](mailto:support@unkey.com) and we'll sort it out. # query_execution_timeout Source: https://unkey.com/docs/errors/user/unprocessable_entity/query_execution_timeout Your analytics query exceeded the maximum execution time allowed by Unkey. Simplify the query, reduce the time range, or add filters. `err:user:unprocessable_entity:query_execution_timeout` ```json Example theme={"theme":"kanagawa-wave"} { "meta": { "requestId": "req_4dgzrNP3Je5mU1tD" }, "error": { "detail": "Query exceeded the maximum execution time of 30 seconds", "status": 422, "title": "Unprocessable Entity", "type": "https://unkey.com/docs/errors/user/unprocessable_entity/query_execution_timeout" } } ``` ## What Happened? Your analytics query took too long to execute! We limit queries to 30 seconds to keep the analytics service responsive for everyone. This usually happens when you're querying a large time range or complex data without enough filters. ## How to Fix It ### 1. Query Smaller Time Ranges The most common fix is to reduce the time range: ```sql Too Long theme={"theme":"kanagawa-wave"} SELECT COUNT(*) FROM key_verifications WHERE time >= now() - INTERVAL 1 YEAR GROUP BY toStartOfDay(time) ``` ```sql Better theme={"theme":"kanagawa-wave"} SELECT COUNT(*) FROM key_verifications WHERE time >= now() - INTERVAL 7 DAY GROUP BY toStartOfDay(time) ``` ### 2. Add More Filters Filter your data to reduce the amount of work the query needs to do: ```sql Slow theme={"theme":"kanagawa-wave"} SELECT api_id, COUNT(*) as total FROM key_verifications GROUP BY api_id ``` ```sql Faster theme={"theme":"kanagawa-wave"} SELECT api_id, COUNT(*) as total FROM key_verifications WHERE time >= now() - INTERVAL 1 DAY AND outcome = 'VALID' GROUP BY api_id ``` ### 3. Use Aggregated Tables For historical data, use pre-aggregated tables instead of raw events: ```sql Slow - Scans millions of raw events theme={"theme":"kanagawa-wave"} SELECT toStartOfHour(time) as hour, COUNT(*) as total FROM key_verifications_raw_v2 WHERE time >= now() - INTERVAL 30 DAY GROUP BY hour ``` ```sql Fast - Uses pre-aggregated data theme={"theme":"kanagawa-wave"} SELECT time as hour, SUM(count) as total FROM key_verifications_per_hour_v1 WHERE time >= now() - INTERVAL 30 DAY GROUP BY hour ``` ### 4. Limit Result Size Add a LIMIT clause to stop processing once you have enough data: ```sql theme={"theme":"kanagawa-wave"} SELECT api_id, COUNT(*) as total FROM key_verifications WHERE time >= now() - INTERVAL 7 DAY GROUP BY api_id ORDER BY total DESC LIMIT 100 ``` ## Need Longer Execution Time? **Have a legitimate need for longer-running queries?** Contact our support team! [Reach out to support](mailto:support@unkey.com) and tell us: * What you're trying to analyze * Why the query needs more than 30 seconds * An example of the query you're running We'll review your use case and see if we can accommodate your needs. # query_memory_limit_exceeded Source: https://unkey.com/docs/errors/user/unprocessable_entity/query_memory_limit_exceeded Your analytics query consumed more memory than Unkey allows. Reduce the result set size by narrowing filters, limiting rows, or simplifying joins. `err:user:unprocessable_entity:query_memory_limit_exceeded` ```json Example theme={"theme":"kanagawa-wave"} { "meta": { "requestId": "req_4dgzrNP3Je5mU1tD" }, "error": { "detail": "Query exceeded the maximum memory limit of 2GB", "status": 422, "title": "Unprocessable Entity", "type": "https://unkey.com/docs/errors/user/unprocessable_entity/query_memory_limit_exceeded" } } ``` ## What Happened? Your query tried to use more than 2GB of memory! We limit memory usage to keep the analytics service stable and fast for everyone. This typically happens when you're selecting too many rows, using large GROUP BY operations, or performing complex JOINs without enough filtering. ## How to Fix It ### 1. Use Aggregations Instead of Raw Data Instead of fetching all rows, aggregate the data: ```sql Memory Intensive - Fetches all rows theme={"theme":"kanagawa-wave"} SELECT * FROM key_verifications_v1 WHERE time >= now() - INTERVAL 7 DAY ``` ```sql Memory Efficient - Aggregates data theme={"theme":"kanagawa-wave"} SELECT toStartOfHour(time) as hour, api_id, COUNT(*) as total, AVG(response_time) as avg_response FROM key_verifications_v1 WHERE time >= now() - INTERVAL 7 DAY GROUP BY hour, api_id ``` ### 2. Add More Filters Reduce the amount of data the query needs to process: ```sql Too Much Data theme={"theme":"kanagawa-wave"} SELECT api_id, key_id, outcome, time FROM key_verifications_v1 WHERE time >= now() - INTERVAL 30 DAY ``` ```sql Filtered Query theme={"theme":"kanagawa-wave"} SELECT api_id, key_id, outcome, time FROM key_verifications_v1 WHERE time >= now() - INTERVAL 1 DAY AND api_id = 'api_123' AND outcome = 'VALID' ``` ### 3. Limit Result Size Add a LIMIT to cap the number of rows: ```sql theme={"theme":"kanagawa-wave"} SELECT api_id, key_id, outcome, time FROM key_verifications_v1 WHERE time >= now() - INTERVAL 7 DAY ORDER BY time DESC LIMIT 10000 ``` ### 4. Avoid Large GROUP BY Cardinality GROUP BY on high-cardinality columns (like `key_id`) uses a lot of memory. Instead, group by lower-cardinality columns: ```sql High Memory - Millions of unique keys theme={"theme":"kanagawa-wave"} SELECT key_id, COUNT(*) as total FROM key_verifications_v1 WHERE time >= now() - INTERVAL 30 DAY GROUP BY key_id ``` ```sql Lower Memory - Hundreds of APIs theme={"theme":"kanagawa-wave"} SELECT api_id, COUNT(*) as total FROM key_verifications_v1 WHERE time >= now() - INTERVAL 30 DAY GROUP BY api_id ``` ## Need More Memory? **Have a legitimate need for higher memory limits?** Contact our support team! [Reach out to support](mailto:support@unkey.com) and tell us: * What you're trying to analyze * Why the query needs more than 2GB of memory * An example of the query you're running We'll review your use case and see if we can accommodate your needs. # query_rows_limit_exceeded Source: https://unkey.com/docs/errors/user/unprocessable_entity/query_rows_limit_exceeded Your analytics query attempted to scan more rows than the Unkey limit allows. Add time range filters or WHERE clauses to reduce the scan scope. `err:user:unprocessable_entity:query_rows_limit_exceeded` ```json Example theme={"theme":"kanagawa-wave"} { "meta": { "requestId": "req_4dgzrNP3Je5mU1tD" }, "error": { "detail": "Query exceeded the maximum rows to scan limit of 100 million", "status": 422, "title": "Unprocessable Entity", "type": "https://unkey.com/docs/errors/user/unprocessable_entity/query_rows_limit_exceeded" } } ``` ## What Happened? Your query tried to scan more than 100 million rows! We limit the number of rows that can be scanned to keep queries fast and prevent resource exhaustion. This happens when you query large time ranges or don't filter your data enough, causing ClickHouse to scan millions of rows even if the final result is small. ## How to Fix It ### 1. Add Time Range Filters Always filter by time to limit the number of rows scanned: ```sql Scans Too Many Rows theme={"theme":"kanagawa-wave"} SELECT COUNT(*) FROM key_verifications_v1 WHERE outcome = 'VALID' ``` ```sql Limited Scan theme={"theme":"kanagawa-wave"} SELECT COUNT(*) FROM key_verifications_v1 WHERE time >= now() - INTERVAL 7 DAY AND outcome = 'VALID' ``` ### 2. Use More Selective Filters Add filters that reduce the data before aggregation: ```sql Scans Everything theme={"theme":"kanagawa-wave"} SELECT api_id, COUNT(*) as total FROM key_verifications_v1 WHERE time >= now() - INTERVAL 90 DAY GROUP BY api_id ``` ```sql Scans Less theme={"theme":"kanagawa-wave"} SELECT api_id, COUNT(*) as total FROM key_verifications_v1 WHERE time >= now() - INTERVAL 7 DAY AND api_id IN ('api_123', 'api_456') GROUP BY api_id ``` ### 3. Use Pre-Aggregated Tables For historical queries, use aggregated tables that have fewer rows: ```sql Raw Table - 100M+ rows theme={"theme":"kanagawa-wave"} SELECT toStartOfDay(time) as day, COUNT(*) as total FROM key_verifications_v1 WHERE time >= now() - INTERVAL 90 DAY GROUP BY day ``` ```sql Aggregated Table - 2K rows theme={"theme":"kanagawa-wave"} SELECT time as day, SUM(count) as total FROM key_verifications_v1_per_day_v1 WHERE time >= now() - INTERVAL 90 DAY GROUP BY day ``` ### 4. Query in Smaller Batches Instead of one large query, break it into smaller time windows: ```javascript theme={"theme":"kanagawa-wave"} // Instead of querying 90 days at once const results = []; for (let i = 0; i < 90; i += 7) { const start = `now() - INTERVAL ${i + 7} DAY`; const end = `now() - INTERVAL ${i} DAY`; const result = await query(` SELECT COUNT(*) as total FROM key_verifications_v1 WHERE time >= ${start} AND time < ${end} `); results.push(result); } ``` ## Need Higher Row Limits? **Have a legitimate need to scan more rows?** Contact our support team! [Reach out to support](mailto:support@unkey.com) and tell us: * What you're trying to analyze * Why you need to scan more than 100 million rows * An example of the query you're running We'll review your use case and help optimize your query or adjust limits if needed. # Glossary Source: https://unkey.com/docs/glossary Definitions of key terms and concepts used throughout the Unkey platform including workspaces, keyspaces, keys, identities, rate limiting, and root keys. ## Core Concepts ### Workspace A workspace is your top-level account container. Everything in Unkey belongs to a workspace. * Can be personal or team-based * Contains keyspaces, keys, identities, rate limit namespaces * One owner who can invite members and assign roles * Billing is per-workspace ### Keyspace A keyspace is a container for related API keys. Use keyspaces to organize keys by: * **Product**: `payments-api`, `analytics-api` * **Environment**: `production`, `staging`, `development` * **Tier**: `free-tier`, `pro-tier`, `enterprise` Each key belongs to exactly one keyspace. ### API Key The credential your users include in requests to authenticate with your API. Unkey manages the full lifecycle: * **Creation**: Generate with custom prefix, expiration, limits * **Verification**: Validate and check permissions in \~30ms globally * **Revocation**: Disable or delete when needed Example: `sk_live_abc123...` ### Root Key A special key that grants access to Unkey's management API. Use root keys to: * Create, update, delete API keys * Manage rate limits and permissions * Access analytics Root keys have full access to your workspace. Never expose them in client-side code or public repositories. ## Key Features ### Identity A way to link multiple keys to a single user or entity. Identities let you: * Track usage across all of a user's keys * Share rate limits between keys * Store user metadata once, not per-key Use `externalId` to link to your own user IDs: `externalId: "user_123"` ### Credits (Remaining) Limit how many times a key can be used. Different from rate limiting: | Feature | Credits | Rate Limits | | -------- | ----------------------------- | --------------------- | | Resets? | No (unless refill configured) | Yes, every window | | Use case | Total quota | Requests per time | | Example | "1000 requests total" | "100 requests/minute" | Credits decrement with each verification. When they hit 0, the key is invalid. ### Refill Automatically restore credits on a schedule: * **Daily**: Reset at midnight UTC * **Monthly**: Reset on a specific day Example: "10,000 credits, refills on the 1st of each month" ### Rate Limit Restrict requests within a time window. Prevents abuse and ensures fair usage. * **Limit**: Maximum requests allowed * **Duration**: Time window (e.g., 60 seconds) * **Namespace**: Grouping for related limits Example: "100 requests per minute" ### Rate Limit Namespace A container for related rate limits. Namespaces let you: * Apply limits across keys or identities * Set overrides for specific identifiers * Track analytics per namespace Example namespaces: `api-requests`, `auth-attempts`, `file-uploads` ### Override Custom rate limit for a specific identifier. Overrides let you: * Give VIP users higher limits * Throttle suspicious users * Test with elevated limits Configured in the dashboard, no code changes needed. ## Permissions & Authorization ### Permission A specific action a key can perform. Examples: * `documents.read` * `documents.write` * `billing.manage` Permissions are checked during verification. ### Role A named collection of permissions. Examples: * `admin` → all permissions * `editor` → read + write permissions * `viewer` → read-only permissions Attach roles to keys instead of individual permissions for easier management. ## Configuration ### Prefix A string prepended to generated keys for identification: * `sk_live_` → production key * `sk_test_` → test environment key * `pk_` → publishable key Prefixes help users identify key types at a glance. ### Metadata Custom JSON data attached to a key. Store anything relevant: ```json theme={"theme":"kanagawa-wave"} { "environment": "production", "plan": "enterprise", "customerId": "cust_123" } ``` Metadata is returned during verification. ### External ID Your identifier for a user or entity. Links Unkey identities to your system: * `user_123` (your user ID) * `org_456` (your org ID) * `team_789` (your team ID) ## Verification Responses ### Valid Key exists and passed all checks (not expired, not disabled, has credits, not rate limited, has required permissions). ### Code The reason a verification succeeded or failed: | Code | Meaning | | -------------------------- | --------------------------- | | `VALID` | Key is valid | | `NOT_FOUND` | Key doesn't exist | | `EXPIRED` | Key past expiration date | | `DISABLED` | Key manually disabled | | `USAGE_EXCEEDED` | No credits remaining | | `RATE_LIMITED` | Rate limit exceeded | | `INSUFFICIENT_PERMISSIONS` | Missing required permission | | `FORBIDDEN` | IP whitelist violation | ## Analytics ### Verification A single key validation request. Each verification is logged with: * Timestamp * Key ID * Result (valid/invalid + code) * Tags (custom dimensions you add) * Geographic origin ### Tags Custom strings attached to verifications for analytics: ```text theme={"theme":"kanagawa-wave"} path=/v1/users method=POST version=2024-01 ``` Use tags to segment analytics by endpoint, feature, version, etc. # What is Unkey? Source: https://unkey.com/docs/introduction Unkey is a globally distributed API platform that ships your repo to production with built-in key issuance, rate limiting, and analytics. **Ship APIs, not infrastructure.** Push your repo, get a production API. Unkey builds the image, runs it across your chosen regions, and routes every request through Sentinel for API key authentication, rate limiting, and IP rules before it hits your code. Domains, rollbacks, and analytics are handled. You write the application; we run it. Push to GitHub or run `unkey deploy`. Multi-region, instant rollbacks, automatic domains, preview environments. API key authentication, rate limiting, and IP policies running in front of your app. No middleware to write. Issue, verify, revoke. Usage limits, expiration, metadata, identities. Use standalone or attached to a deployment. Who's using your API, how much, when. Built in, queryable, no pipeline to build. ## Why Unkey? Shipping an API used to mean stitching together a runtime, a CDN, a key store, a Redis for rate limits, an analytics pipeline, and the auth middleware to glue them. Unkey is one platform that runs all of it on servers we operate, so you can focus on the actual API. | Without Unkey | With Unkey | | ------------------------------------------------------- | -------------------------------------- | | Provision servers, configure load balancers, manage TLS | `git push` or `unkey deploy` | | Build key storage, hashing, rotation, revocation | Attached to your app or called via SDK | | Stand up Redis for rate limiting | `unkey.ratelimit.limit()` | | Wire up usage tracking and dashboards | Built-in analytics | Unkey never stores API keys in plain text. Keys are SHA-256 hashed before storage, so even if our database were compromised your keys stay safe. ## What can you build? Connect a GitHub repository or run `unkey deploy`. Unkey builds your image, runs it across your chosen regions, and assigns domains automatically. Every push to your default branch goes to production; every other branch gets a preview environment. ```bash theme={"theme":"kanagawa-wave"} unkey deploy ghcr.io/acme/payments-api:v1.2.3 \ --project payments \ --root-key $UNKEY_ROOT_KEY ``` Sentinel sits in front of your deployment and enforces API key checks, rate limits, and IP rules. No SDK call required from inside your app: invalid requests are rejected before they reach your code. ```ts theme={"theme":"kanagawa-wave"} // Inside your app, the request is already authenticated. // Identity and quota info arrive on request headers. const userId = req.headers["x-unkey-identity"]; ``` Issue keys with prefixes, expiration, usage credits, rate limits, and arbitrary metadata. Verify them with one SDK call, attached to a deployment or standalone. ```ts theme={"theme":"kanagawa-wave"} const { meta, data } = await unkey.keys.verifyKey({ key: "sk_..." }); if (!data.valid) return new Response("Unauthorized", { status: 401 }); ``` Per-user, per-IP, per-endpoint, per-anything. Globally distributed, no Redis, attached to a key or completely standalone. ```ts theme={"theme":"kanagawa-wave"} const { success } = await unkey.ratelimit.limit({ namespace: "api.requests", identifier: userId, limit: 100, duration: 60_000, }); if (!success) return new Response("Too many requests", { status: 429 }); ``` Issue keys with monthly credits and automatic refills. Verification consumes credits, so usage-based pricing is one config object away. ```ts theme={"theme":"kanagawa-wave"} const { meta, data } = await unkey.keys.createKey({ apiId: "api_...", credits: { remaining: 10000, refill: { interval: "monthly", amount: 10000 } } }); ``` ## Get started Connect a GitHub repository (or push a Docker image) and have a live URL in minutes. Create a keyspace, generate a key, and verify it with curl or your favorite SDK. Step-by-step recipes for Next.js, Bun, Express, Hono, FastAPI, Go. Ready-to-copy patterns: per-user limits, tiered subscriptions, usage billing. ## Choose your path Deploy from GitHub or CLI. Multi-region, rollbacks, preview environments, Sentinel in front. Use the SDK directly from any backend, no deployment required. Bring existing keys over without disrupting your users. Start by deploying. Add API key auth and rate limits as policies on the deployment. ## Join the community Questions, ideas, or just want to talk to other API builders? Chat with the team and community Star the repo and file issues Follow for updates # unkey-go Source: https://unkey.com/docs/libraries/go/api Reference for the unkey-go SDK. Create, verify, update, and revoke API keys programmatically from your Go applications and microservices. ## SDK Installation To add the SDK as a dependency to your project: ```bash theme={"theme":"kanagawa-wave"} go get github.com/unkeyed/sdks/api/go/v2@latest ``` ## SDK Example Usage ### Example ```go theme={"theme":"kanagawa-wave"} package main import ( "context" unkey "github.com/unkeyed/sdks/api/go/v2" "github.com/unkeyed/sdks/api/go/v2/models/components" "log" "os" ) func main() { ctx := context.Background() s := unkey.New( unkey.WithSecurity(os.Getenv("UNKEY_ROOT_KEY")), ) res, err := s.Apis.CreateAPI(ctx, components.V2ApisCreateAPIRequestBody{ Name: "payment-service-production", }) if err != nil { log.Fatal(err) } if res.V2ApisCreateAPIResponseBody != nil { // handle response } } ``` ## Repository The full autogenerated documentation can be found on GitHub. # Overview Source: https://unkey.com/docs/libraries/go/overview Complete guide to using the Unkey Go SDK for issuing keys, verifying requests, and rate limiting in your Go applications and services. The `unkey-go` SDK provides full access to Unkey's API for managing keys, verifying requests, and rate limiting. ## Installation ```bash theme={"theme":"kanagawa-wave"} go get github.com/unkeyed/sdks/api/go/v2@latest ``` ## Quick Start ### Initialize the client ```go theme={"theme":"kanagawa-wave"} package main import ( "os" unkey "github.com/unkeyed/sdks/api/go/v2" ) func main() { client := unkey.New( unkey.WithSecurity(os.Getenv("UNKEY_ROOT_KEY")), ) // Use client... } ``` Never expose your root key in client-side code or commit it to version control. *** ## Verify an API Key Check if a user's API key is valid: ```go theme={"theme":"kanagawa-wave"} package main import ( "context" "fmt" "log" "os" unkey "github.com/unkeyed/sdks/api/go/v2" "github.com/unkeyed/sdks/api/go/v2/models/components" ) func main() { ctx := context.Background() client := unkey.New( unkey.WithSecurity(os.Getenv("UNKEY_ROOT_KEY")), ) apiKey := "sk_live_..." // From request header res, err := client.Keys.VerifyKey(ctx, components.V2KeysVerifyKeyRequestBody{ Key: apiKey, }) if err != nil { log.Fatalf("Verification failed: %v", err) } result := res.V2KeysVerifyKeyResponseBody if !result.Valid { fmt.Printf("Key invalid: %s\n", *result.Code) return } fmt.Println("Key is valid!") if result.OwnerID != nil { fmt.Printf("Owner: %s\n", *result.OwnerID) } if result.Credits != nil { fmt.Printf("Credits remaining: %d\n", *result.Credits) } } ``` ### HTTP middleware example ```go theme={"theme":"kanagawa-wave"} package main import ( "context" "net/http" "os" "strings" unkey "github.com/unkeyed/sdks/api/go/v2" "github.com/unkeyed/sdks/api/go/v2/models/components" ) var unkeyClient *unkey.Unkey func init() { unkeyClient = unkey.New( unkey.WithSecurity(os.Getenv("UNKEY_ROOT_KEY")), ) } // AuthMiddleware verifies API keys on incoming requests func AuthMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Extract API key from header authHeader := r.Header.Get("Authorization") if authHeader == "" { http.Error(w, "Missing Authorization header", http.StatusUnauthorized) return } apiKey := strings.TrimPrefix(authHeader, "Bearer ") // Verify with Unkey res, err := unkeyClient.Keys.VerifyKey(r.Context(), components.V2KeysVerifyKeyRequestBody{ Key: apiKey, }) if err != nil { http.Error(w, "Verification service unavailable", http.StatusServiceUnavailable) return } if !res.V2KeysVerifyKeyResponseBody.Valid { http.Error(w, "Invalid API key", http.StatusUnauthorized) return } // Add user info to context if needed ctx := context.WithValue(r.Context(), "keyId", res.V2KeysVerifyKeyResponseBody.KeyID) next.ServeHTTP(w, r.WithContext(ctx)) }) } func main() { mux := http.NewServeMux() mux.HandleFunc("/api/protected", func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("Access granted!")) }) // Wrap with auth middleware http.ListenAndServe(":8080", AuthMiddleware(mux)) } ``` ### Gin middleware example ```go theme={"theme":"kanagawa-wave"} package main import ( "net/http" "os" "strings" "github.com/gin-gonic/gin" unkey "github.com/unkeyed/sdks/api/go/v2" "github.com/unkeyed/sdks/api/go/v2/models/components" ) var unkeyClient *unkey.Unkey func init() { unkeyClient = unkey.New( unkey.WithSecurity(os.Getenv("UNKEY_ROOT_KEY")), ) } func UnkeyAuth() gin.HandlerFunc { return func(c *gin.Context) { authHeader := c.GetHeader("Authorization") if authHeader == "" { c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Missing API key"}) return } apiKey := strings.TrimPrefix(authHeader, "Bearer ") res, err := unkeyClient.Keys.VerifyKey(c.Request.Context(), components.V2KeysVerifyKeyRequestBody{ Key: apiKey, }) if err != nil { c.AbortWithStatusJSON(http.StatusServiceUnavailable, gin.H{"error": "Verification failed"}) return } if !res.V2KeysVerifyKeyResponseBody.Valid { c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{ "error": "Invalid API key", "code": res.V2KeysVerifyKeyResponseBody.Code, }) return } // Store verification result in context c.Set("unkeyResult", res.V2KeysVerifyKeyResponseBody) c.Next() } } func main() { r := gin.Default() // Protected routes api := r.Group("/api", UnkeyAuth()) { api.GET("/data", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"message": "Access granted"}) }) } r.Run(":8080") } ``` *** ## Create API Keys Issue new keys for your users: ```go theme={"theme":"kanagawa-wave"} package main import ( "context" "fmt" "log" "os" "time" unkey "github.com/unkeyed/sdks/api/go/v2" "github.com/unkeyed/sdks/api/go/v2/models/components" ) func main() { ctx := context.Background() client := unkey.New( unkey.WithSecurity(os.Getenv("UNKEY_ROOT_KEY")), ) // Optional values prefix := "sk_live" externalID := "user_123" name := "Production key" expires := time.Now().Add(30 * 24 * time.Hour).UnixMilli() remaining := int64(1000) res, err := client.Keys.CreateKey(ctx, components.V2KeysCreateKeyRequestBody{ APIID: "api_...", Prefix: &prefix, ExternalID: &externalID, Name: &name, Expires: &expires, Credits: &components.KeyCreditsData{ Remaining: &remaining, }, Meta: map[string]any{ "plan": "pro", }, }) if err != nil { log.Fatalf("Failed to create key: %v", err) } result := res.V2KeysCreateKeyResponseBody // Send this to your user - only time you'll see the full key! fmt.Printf("New key: %s\n", result.Key) fmt.Printf("Key ID: %s\n", result.KeyID) } ``` The full API key is only returned once at creation. Unkey stores only a hash. *** ## Update Keys Modify an existing key: ```go theme={"theme":"kanagawa-wave"} enabled := true name := "Updated name" _, err := client.Keys.UpdateKey(ctx, components.V2KeysUpdateKeyRequestBody{ KeyID: "key_...", Name: &name, Enabled: &enabled, Meta: map[string]any{ "plan": "enterprise", }, }) if err != nil { log.Fatalf("Failed to update key: %v", err) } ``` *** ## Delete Keys Permanently revoke a key: ```go theme={"theme":"kanagawa-wave"} _, err := client.Keys.DeleteKey(ctx, components.V2KeysDeleteKeyRequestBody{ KeyID: "key_...", }) if err != nil { log.Fatalf("Failed to delete key: %v", err) } ``` *** ## Rate Limiting Use the rate limit API directly: ```go theme={"theme":"kanagawa-wave"} res, err := client.Ratelimit.Limit(ctx, components.V2RatelimitsLimitRequestBody{ Namespace: "my-app", Identifier: "user_123", Limit: 100, Duration: 60000, // 60 seconds }) if err != nil { log.Fatalf("Rate limit check failed: %v", err) } if !res.V2RatelimitsLimitResponseBody.Success { fmt.Printf("Rate limited. Reset at: %d\n", res.V2RatelimitsLimitResponseBody.Reset) } else { fmt.Printf("Allowed. %d requests remaining\n", res.V2RatelimitsLimitResponseBody.Remaining) } ``` *** ## Error Handling ```go theme={"theme":"kanagawa-wave"} res, err := client.Keys.CreateKey(ctx, components.V2KeysCreateKeyRequestBody{ APIID: "api_...", }) if err != nil { // Check for specific error types var apiErr *components.APIError if errors.As(err, &apiErr) { fmt.Printf("API Error: %s - %s\n", apiErr.Code, apiErr.Message) } else { fmt.Printf("Unexpected error: %v\n", err) } return } // Safe to use result fmt.Printf("Created key: %s\n", res.V2KeysCreateKeyResponseBody.Key) ``` *** ## Full Reference Complete auto-generated API reference # Nuxt Module Source: https://unkey.com/docs/libraries/nuxt/overview Integrate Unkey API key authentication into your Nuxt application with the official Nuxt module. Protect server routes and API endpoints. If you are using Nuxt, you can benefit from an almost zero-config experience with the `@unkey/nuxt` module. ## Install ```bash npm theme={"theme":"kanagawa-wave"} npm install @unkey/nuxt ``` ```bash pnpm theme={"theme":"kanagawa-wave"} pnpm add @unkey/nuxt ``` ```bash yarn theme={"theme":"kanagawa-wave"} yarn add @unkey/nuxt ``` ```bash bun theme={"theme":"kanagawa-wave"} bun install @unkey/nuxt ``` ## Configuration `@unkey/nuxt` just requires your root key. Create an `.env` file in your project and add the following: ```env theme={"theme":"kanagawa-wave"} NUXT_UNKEY_TOKEN= ``` This can also be configured at runtime by setting the `NUXT_UNKEY_TOKEN` environment variable. From this point onward, `@unkey/nuxt` will automatically: 1. verify any API requests with an `Authorization: Bearer xxx` header. 2. register a `useUnkey()` helper that allows access to an automatically configured unkey instance. ## Usage ### Automatic verification You can access the automatically-verified `unkey` context on the server with `event.context.unkey` in your server routes or `useRequestEvent().context.unkey` in the Vue part of your app. For example: ```ts theme={"theme":"kanagawa-wave"} export default defineEventHandler(async (event) => { if (!event.context.unkey.valid) { throw createError({ statusCode: 403, message: "Invalid API key" }) } // return authorised information return { // ... }; }); ``` ```html theme={"theme":"kanagawa-wave"} ``` ## Unkey helper For more about how to use the configured helper provided by `useUnkey()`, you can see the API docs for [the TypeScript client](/docs/libraries/ts/api). For example: ```ts theme={"theme":"kanagawa-wave"} const unkey = useUnkey(); try { const { meta, data } = await unkey.keys.createKey({ apiId: "api_7oKUUscTZy22jmVf9THxDA", prefix: "xyz", byteLength: 16, externalId: "user_123", // Link to your user meta: { hello: "world", }, expires: Date.now() + 30 * 24 * 60 * 60 * 1000, // 30 days from now ratelimit: { limit: 10, duration: 1000, // 10 requests per second }, }); console.log(data.key); } catch (err) { console.error(err); throw err; } ``` ### Disable telemetry By default, Unkey collects anonymous telemetry data to help us understand how our SDKs are used. If you wish to disable this, you can do so by passing a boolean flag to the constructor: ```ts theme={"theme":"kanagawa-wave"} const unkey = useUnkey({ disableTelemetry: true }); ``` # unkey.py Source: https://unkey.com/docs/libraries/py/api Reference for the unkey.py Python SDK. Create, verify, update, and revoke API keys programmatically from your Python application or service. ## SDK Installation > \[!NOTE] > **Python version upgrade policy** > > Once a Python version reaches its [official end of life date](https://devguide.python.org/versions/), a 3-month grace period is provided for users to upgrade. Following this grace period, the minimum python version supported in the SDK will be updated. The SDK can be installed with either *pip* or *poetry* package managers. ### PIP *PIP* is the default package installer for Python, enabling easy installation and management of packages from PyPI via the command line. ```bash theme={"theme":"kanagawa-wave"} pip install unkey.py ``` ### Poetry *Poetry* is a modern tool that simplifies dependency management and package publishing by using a single `pyproject.toml` file to handle project metadata and dependencies. ```bash theme={"theme":"kanagawa-wave"} poetry add unkey.py ``` ### Shell and script usage with `uv` You can use this SDK in a Python shell with [uv](https://docs.astral.sh/uv/) and the `uvx` command that comes with it like so: ```shell theme={"theme":"kanagawa-wave"} uvx --from unkey.py python ``` It's also possible to write a standalone Python script without needing to set up a whole project like so: ```python theme={"theme":"kanagawa-wave"} #!/usr/bin/env -S uv run --script # /// script # requires-python = ">=3.9" # dependencies = [ # "unkey.py", # ] # /// from unkey.py import Unkey sdk = Unkey( # SDK arguments ) # Rest of script here... ``` Once that is saved to a file, you can run it with `uv run script.py` where `script.py` can be replaced with the actual file name. ## SDK Example Usage ### Example ```python theme={"theme":"kanagawa-wave"} # Synchronous Example from unkey.py import Unkey with Unkey( root_key="", ) as unkey: res = unkey.apis.create_api(name="payment-service-production") # Handle response print(res) ``` The same SDK client can also be used to make asynchronous requests by importing asyncio. ```python theme={"theme":"kanagawa-wave"} # Asynchronous Example import asyncio from unkey.py import Unkey async def main(): async with Unkey( root_key="", ) as unkey: res = await unkey.apis.create_api_async(name="payment-service-production") # Handle response print(res) asyncio.run(main()) ``` ## Repository The full autogenerated documentation can be found on GitHub. # Overview Source: https://unkey.com/docs/libraries/py/overview Complete guide to using the Unkey Python SDK for issuing keys, verifying requests, and rate limiting in your Python applications and APIs. The `unkey.py` SDK provides full access to Unkey's API for managing keys, verifying requests, and rate limiting. ## Installation ```bash pip theme={"theme":"kanagawa-wave"} pip install unkey.py ``` ```bash poetry theme={"theme":"kanagawa-wave"} poetry add unkey.py ``` ```bash uv theme={"theme":"kanagawa-wave"} uv add unkey.py ``` **Requirements:** Python 3.9+ ## Quick Start ### Initialize the client ```python theme={"theme":"kanagawa-wave"} import os from unkey import Unkey unkey = Unkey(root_key="unkey_...") # Or use environment variable unkey = Unkey(root_key=os.environ["UNKEY_ROOT_KEY"]) ``` Never expose your root key in client-side code or commit it to version control. *** ## Verify an API Key Check if a user's API key is valid: ```python theme={"theme":"kanagawa-wave"} from typing import Optional from unkey import Unkey, ApiError unkey = Unkey(root_key=os.environ["UNKEY_ROOT_KEY"]) def verify_request(api_key: str) -> Optional[dict]: """Verify an API key and return the result or None if invalid.""" try: result = unkey.keys.verify_key(key=api_key) if not result.valid: print(f"Key invalid: {result.code}") return None return { "valid": True, "key_id": result.key_id, "meta": result.meta, "remaining": result.credits, } except ApiError as e: print(f"Unkey error: {e.message}") return None ``` ### Verification response fields | Field | Type | Description | | ------------- | ------------- | ------------------------------------------------------------------ | | `valid` | `bool` | Whether the key passed all checks | | `code` | `str` | Status code (`VALID`, `NOT_FOUND`, `RATE_LIMITED`, etc.) | | `key_id` | `str` | The key's unique identifier | | `name` | `str?` | Human-readable name of the key | | `meta` | `dict?` | Custom metadata associated with the key | | `expires` | `int?` | Unix timestamp (in milliseconds) when the key will expire (if set) | | `credits` | `int?` | Remaining uses (if usage limits set) | | `enabled` | `bool` | Whether the key is enabled | | `roles` | `list[str]?` | Roles attached to the key | | `permissions` | `list[str]?` | Permissions attached to the key | | `identity` | `dict?` | Identity info if `external_id` was set when creating the key | | `ratelimits` | `list[dict]?` | Rate limit states (if rate limiting configured) | ### FastAPI example ```python theme={"theme":"kanagawa-wave"} from fastapi import FastAPI, Header, HTTPException from unkey import Unkey import os app = FastAPI() unkey = Unkey(root_key=os.environ["UNKEY_ROOT_KEY"]) @app.get("/api/protected") async def protected_route(x_api_key: str = Header(...)): result = unkey.keys.verify_key(key=x_api_key) if not result.valid: raise HTTPException( status_code=401 if result.code == "NOT_FOUND" else 403, detail=f"Unauthorized: {result.code}" ) return { "message": "Access granted", "key_id": result.key_id, "remaining_credits": result.credits } ``` ### Flask example ```python theme={"theme":"kanagawa-wave"} from flask import Flask, request, jsonify from unkey import Unkey import os app = Flask(__name__) unkey = Unkey(root_key=os.environ["UNKEY_ROOT_KEY"]) @app.route("/api/protected") def protected_route(): api_key = request.headers.get("X-API-Key") if not api_key: return jsonify({"error": "Missing API key"}), 401 result = unkey.keys.verify_key(key=api_key) if not result.valid: return jsonify({"error": result.code}), 401 return jsonify({ "message": "Access granted", "key_id": result.key_id }) ``` *** ## Create API Keys Issue new keys for your users: ```python theme={"theme":"kanagawa-wave"} from datetime import datetime, timedelta result = unkey.keys.create_key( api_id="api_...", # Optional but recommended prefix="sk_live", external_id="user_123", # Link to your user name="Production key", # Optional: Expiration expires=(datetime.now() + timedelta(days=30)).timestamp() * 1000, # Optional: Usage limits remaining=1000, refill={ "amount": 1000, "interval": "monthly", }, # Optional: Rate limits ratelimit={ "limit": 100, "duration": 60000, # 100 per minute }, # Optional: Custom metadata meta={ "plan": "pro", "created_by": "admin", }, ) # Send result.key to your user (only time you'll see it!) print(f"New key: {result.key}") print(f"Key ID: {result.key_id}") ``` The full API key is only returned once at creation. Unkey stores only a hash. *** ## Update Keys Modify an existing key: ```python theme={"theme":"kanagawa-wave"} unkey.keys.update_key( key_id="key_...", name="Updated name", meta={"plan": "enterprise"}, enabled=True, # Update rate limit ratelimit={ "limit": 1000, "duration": 60000, }, ) ``` *** ## Delete Keys Permanently revoke a key: ```python theme={"theme":"kanagawa-wave"} unkey.keys.delete_key(key_id="key_...") ``` Or disable temporarily: ```python theme={"theme":"kanagawa-wave"} unkey.keys.update_key( key_id="key_...", enabled=False, ) ``` *** ## Async Support All methods have async variants with `_async` suffix: ```python theme={"theme":"kanagawa-wave"} import asyncio import os from unkey import Unkey async def main(): async with Unkey(root_key=os.environ["UNKEY_ROOT_KEY"]) as unkey: # Async verification result = await unkey.keys.verify_key_async(key="sk_live_...") if result.valid: print("Key is valid!") # Async key creation new_key = await unkey.keys.create_key_async( api_id="api_...", prefix="sk_live", ) print(f"Created: {new_key.key}") asyncio.run(main()) ``` ### Async FastAPI ```python theme={"theme":"kanagawa-wave"} from fastapi import FastAPI, Header, HTTPException from unkey import Unkey import os app = FastAPI() @app.get("/api/protected") async def protected_route(x_api_key: str = Header(...)): async with Unkey(root_key=os.environ["UNKEY_ROOT_KEY"]) as unkey: result = await unkey.keys.verify_key_async(key=x_api_key) if not result.valid: raise HTTPException(status_code=401, detail=result.code) return {"message": "Access granted", "key_id": result.key_id} ``` *** ## Error Handling ```python theme={"theme":"kanagawa-wave"} import os from unkey import Unkey, ApiError unkey = Unkey(root_key=os.environ["UNKEY_ROOT_KEY"]) try: result = unkey.keys.create_key(api_id="api_...") print(f"Created: {result.key}") except ApiError as e: print(f"API Error: {e.status_code} - {e.message}") # e.status_code - HTTP status # e.message - Human readable error # e.raw_response - Full response for debugging ``` *** ## Rate Limiting Rate limiting is included in key verification, but you can also use the standalone rate limit API: ```python theme={"theme":"kanagawa-wave"} result = unkey.ratelimit.limit( namespace="my-app", identifier="user_123", limit=100, duration=60000, # 60 seconds ) if not result.success: print(f"Rate limited. Reset at: {result.reset}") else: print(f"Allowed. {result.remaining} requests left") ``` *** ## Full Reference Complete auto-generated API reference # @unkey/api Source: https://unkey.com/docs/libraries/ts/api Reference for the @unkey/api TypeScript SDK. Create, verify, update, and revoke API keys programmatically from your Node.js application. ## SDK Installation The SDK can be installed with either [npm](https://www.npmjs.com/), [pnpm](https://pnpm.io/), [bun](https://bun.sh/) or [yarn](https://classic.yarnpkg.com/en/) package managers. ```bash npm theme={"theme":"kanagawa-wave"} npm add @unkey/api ``` ```bash pnpm theme={"theme":"kanagawa-wave"} pnpm add @unkey/api ``` ```bash bun theme={"theme":"kanagawa-wave"} bun add @unkey/api ``` ```bash yarn theme={"theme":"kanagawa-wave"} yarn add @unkey/api zod # Note that Yarn does not install peer dependencies automatically. You will need # to install zod as shown above. ``` This package is published with CommonJS and ES Modules (ESM) support. ## Requirements For supported JavaScript runtimes, please consult [RUNTIMES.md](https://github.com/unkeyed/sdks/blob/main/api/ts/RUNTIMES.md). ## SDK Example Usage ### Example ```typescript theme={"theme":"kanagawa-wave"} import { Unkey } from "@unkey/api"; const unkey = new Unkey({ rootKey: process.env["UNKEY_ROOT_KEY"], }); async function run() { const { meta, data } = await unkey.apis.createApi({ name: "payment-service-production", }); console.log(data); } run(); ``` ## Repository The full autogenerated documentation can be found on GitHub. # @unkey/cache Source: https://unkey.com/docs/libraries/ts/cache/overview Use @unkey/cache for type-safe, multi-tier caching in TypeScript. Compose memory, Cloudflare, and custom stores with automatic tiering. ## Motivation Everyone needs caching, but it's often poorly implemented. Not from a technical perspective but from a usability perspective. Caching should be easy to use, typesafe, and composable. How caching looks like in many applications: ```ts theme={"theme":"kanagawa-wave"} const cache = new Some3rdPartyCache(...) type User = { email: string }; let user = await cache.get("userId") as User | undefined | null; if (!user){ user = await database.get(...) await cache.set("userId", user, Date.now() + 60_000) } // use user ``` There are a few annoying things about this code: * Manual type casting * No support for stale-while-revalidate * Only checks a single cache Most people would build a small wrapper around this to make it easier to use and so did we: This library is the result of a rewrite of our own caching layer after some developers were starting to replicate it. It's used in production by Unkey and others. ## Features * **Typescript**: Fully typesafe * **Tiered Cache**: Multiple caches in series to fall back on * **Metrics**: Middleware for collecting metrics * **Stale-While-Revalidate**: Async loading of data from your origin * **Encryption**: Middleware for automatic encryption of cache values * **Composable**: Mix and match primitives to build what you need ## Quickstart ```bash npm theme={"theme":"kanagawa-wave"} npm install @unkey/cache ``` ```bash pnpm theme={"theme":"kanagawa-wave"} pnpm add @unkey/cache ``` ```bash yarn theme={"theme":"kanagawa-wave"} yarn add @unkey/cache ``` ```bash bun theme={"theme":"kanagawa-wave"} bun install @unkey/cache ``` ```ts Hello World theme={"theme":"kanagawa-wave"} import { createCache, DefaultStatefulContext, Namespace } from "@unkey/cache"; import { MemoryStore } from "@unkey/cache/stores"; /** * Define the type of your data, * or perhaps generate the types from your database */ type User = { id: string; email: string; }; /** * In serverless you'd get this from the request handler * See /docs/libraries/ts/cache/overview#context */ const ctx = new DefaultStatefulContext(); const memory = new MemoryStore({ persistentMap: new Map() }); const cache = createCache({ user: new Namespace(ctx, { stores: [memory], fresh: 60_000, // Data is fresh for 60 seconds stale: 300_000, // Data is stale for 300 seconds }) }); await cache.user.set("userId", { id: "userId", email: "user@email.com" }); const user = await cache.user.get("userId") console.log(user) ``` ```ts Tiered Caches theme={"theme":"kanagawa-wave"} import { createCache, DefaultStatefulContext, Namespace } from "@unkey/cache"; import { CloudflareStore, MemoryStore } from "@unkey/cache/stores"; /** * In serverless you'd get this from the request handler * See /docs/libraries/ts/cache/overview#context */ const ctx = new DefaultStatefulContext(); /** * Define the type of your data, or perhaps generate the types from your database */ type User = { id: string; email: string; }; const memory = new MemoryStore({ persistentMap: new Map() }); const cloudflare = new CloudflareStore({ domain: "cache.unkey.dev", zoneId: env.CLOUDFLARE_ZONE_ID!, cloudflareApiKey: env.CLOUDFLARE_API_KEY!, }); const cache = createCache({ user: new Namespace(ctx, { /** * Specifying first `memory`, then `cloudflare` will automatically check both stores * in order. * If a value is found in memory, it is returned, else it will check cloudflare, * and if it's found in cloudflare, the value is backfilled to memory. */ stores: [memory, cloudflare], fresh: 60_000, // Data is fresh for 60 seconds stale: 300_000, // Data is stale for 300 seconds }); }); async function main() { await cache.user.set("userId", { id: "userId", email: "user@email.com" }); const user = await cache.user.get("userId"); console.info(user); } main(); ``` ```ts Multiple Namespaces theme={"theme":"kanagawa-wave"} import { createCache, DefaultStatefulContext, Namespace } from "@unkey/cache"; import { CloudflareStore, MemoryStore } from "@unkey/cache/stores"; /** * In serverless you'd get this from the request handler * See /docs/libraries/ts/cache/overview#context */ const ctx = new DefaultStatefulContext(); /** * Define the type of your data, or perhaps generate the types from your database */ type User = { id: string; email: string; }; const memory = new MemoryStore({ persistentMap: new Map() }); const cloudflare = new CloudflareStore({ domain: "cache.unkey.dev", zoneId: env.CLOUDFLARE_ZONE_ID!, cloudflareApiKey: env.CLOUDFLARE_API_KEY!, }); type ApiKey = { hash: string; ownerId: string; permissions: string[]; }; const cache = createCache({ user: new Namespace(ctx, { stores: [memory, cloudflare], fresh: 60_000, // Data is fresh for 60 seconds stale: 300_000, // Data is stale for 300 seconds }), apiKey: new Namespace(ctx, { stores: [memory], fresh: 10_000, // Data is fresh for 10 seconds stale: 60_000, // Data is stale for 60 seconds }), }); async function main() { await cache.user.set("userId", { id: "userId", email: "user@email.com" }); const user = await cache.user.get("userId"); console.info(user); await cache.apiKey.set("hash", { hash: "hash", ownerId: "me", permissions: ["do_many_things"], }); } main(); ``` ```ts theme={"theme":"kanagawa-wave"} import { Namespace, createCache } from "@unkey/cache"; import { MemoryStore, CloudflareStore } from "@unkey/cache/stores"; /** * Define your data types. * You can hopefully reuse some of these from your database models. */ type User = { email: string; }; type Account = { name: string; }; /** * Configure the swr cache defaults. */ const fresh = 60_000; // fresh for 1 minute const stale = 900_000; // stale for 15 minutes /** * Create your store instances */ const memory = new MemoryStore({ persistentMap: new Map(), }); const cloudflare = new CloudflareStore({ cloudflareApiKey: "", zoneId: "", domain: "", }); /** * Create your cache instance */ const cache = createCache({ account: new Namespace(ctx, { stores: [memory], fresh, // use the defaults defined above or a custom value stale, }), user: new Namespace(ctx, { // tiered cache, checking memory first, then cloudflare stores: [memory, cloudflare], fresh, stale, }), }); await cache.account.set("key", { name: "x" }); const user = await cache.user.get("user_123"); // typescript error, because `email` is not a key of `Account` await cache.account.set("key", { email: "x" }); ``` ## Concepts ### Namespaces Namespaces are a way to define the type of data in your cache and apply settings to it. They are used to ensure that you don't accidentally store the wrong type of data in a cache, which otherwise can happen easily when you're changing your data structures. Each namespace requires a type parameter and is instantiated with a set of stores and cache settings. ```ts Constructor theme={"theme":"kanagawa-wave"} new Namespace(ctx, opts); ``` The type of data stored in this namespace, for example: ```ts theme={"theme":"kanagawa-wave"} type User = { email: string; }; ``` An execution context, such as a request or a worker instance. [Read more](/docs/libraries/ts/cache/overview#context) ```ts theme={"theme":"kanagawa-wave"} interface Context { waitUntil: (p: Promise) => void; } ``` On Cloudflare workers or Vercel edge functions, you receive a context from the `fetch` handler. Otherwise you can use this: ```ts theme={"theme":"kanagawa-wave"} import { DefaultStatefulContext } from "@unkey/cache"; const ctx = new DefaultStatefulContext(); ``` An array of stores to use for this namespace. When providing multiple stores, the cache will be checked in order of the array until a value is found or all stores have been checked. You should order the stores from fastest to slowest, so that the fastest store is checked first. The time in milliseconds that a value is considered fresh. Cache hits within this time will return the cached value. Must be less than `stale`. The time in milliseconds that a value is considered stale. Cache hits within this time will return the cached value and trigger a background refresh. Must be greater than `fresh`. ```ts Example namespace with two stores theme={"theme":"kanagawa-wave"} import { Namespace, DefaultStatefulContext, MemoryStore, CloudflareStore, } from "@unkey/cache"; type User = { email: string; }; const memory = new MemoryStore({ persistentMap: new Map(), }); const cloudflare = new CloudflareStore({ cloudflareApiKey: c.env.CLOUDFLARE_API_KEY, zoneId: c.env.CLOUDFLARE_ZONE_ID, domain: "cache.unkey.dev", }); const ctx = new DefaultStatefulContext(); const namespace = new Namespace(ctx, { stores: [memory, cloudflare], fresh: 60_000, stale: 900_000, }); ``` ### Tiered Cache Different caches have different characteristics, some may be fast but volatile, others may be slow but persistent. By using a tiered cache, you can combine the best of both worlds. In almost every case, you want to use a fast in-memory cache as the first tier. There is no reason not to use it, as it doesn't add any latency to your application. The goal of this implementation is that it's invisible to the user. Everything behaves like a single cache. You can add as many tiers as you want. #### Reading from the cache When using a tiered cache, all stores will be checked in order until a value is found or all stores have been checked. If a value is found in a store, it will be backfilled to the previous stores in the list asynchronously. ```mermaid theme={"theme":"kanagawa-wave"} sequenceDiagram autonumber App->>Cache: get key Cache->>+Tier1: get key Tier1->>-Cache: undefined Cache->>+Tier2: get key Tier2->>-Cache: value Cache->>App: value Cache-->>Tier1: async set key value ``` #### Writing to the cache When setting or deleting a key, every store will be updated in parallel. ```mermaid theme={"theme":"kanagawa-wave"} sequenceDiagram autonumber App->>Cache: set key value par Cache->>Tier1: set key value Tier1->>Cache: ack and Cache->>Tier2: set key value Tier2->>Cache: ack end Cache->>App: ack ``` #### Example ```ts theme={"theme":"kanagawa-wave"} import { DefaultStatefulContext, Namespace, createCache } from "@unkey/cache"; import { CloudflareStore, MemoryStore } from "@unkey/cache/stores"; /** * In serverless you'd get this from the request handler * See https://unkey.com/docs/libraries/ts/cache/overview#context */ const ctx = new DefaultStatefulContext(); /** * Define the type of your data, or perhaps generate the types from your database */ type User = { id: string; email: string; }; const memory = new MemoryStore({ persistentMap: new Map() }); /** * @see https://unkey.com/docs/libraries/ts/cache/overview#cloudflare */ const cloudflare = new CloudflareStore({ domain: "cache.unkey.dev", zoneId: env.CLOUDFLARE_ZONE_ID!, cloudflareApiKey: env.CLOUDFLARE_API_KEY!, }); const userNamespace = new Namespace(ctx, { /** * Specifying first `memory`, then `cloudflare` will automatically check both stores in order * If a value is found in memory, it is returned, else it will check cloudflare, and if it's found * in cloudflare, the value is backfilled to memory. */ stores: [memory, cloudflare], fresh: 60_000, // Data is fresh for 60 seconds stale: 300_000, // Data is stale for 300 seconds }); const cache = createCache({ user: userNamespace }); async function main() { await cache.user.set("userId", { id: "userId", email: "user@email.com" }); const user = await cache.user.get("userId"); console.log(user); } main(); ``` ### Stale-While-Revalidate To make data fetching as easy as possible, the cache offers a `swr` method, that acts as a pull through cache. If the data is fresh, it will be returned from the cache, if it's stale it will be returned from the cache and a background refresh will be triggered and if it's not in the cache, the data will be synchronously fetched from the origin. ```ts theme={"theme":"kanagawa-wave"} const user = await cache.user.swr("userId", async (userId) => { return database.exec("SELECT * FROM users WHERE id = ?", userId); }); ``` The cache key to fetch, just like when using `.get(key)` A callback function that will be called to fetch the data from the origin if it's stale or not in the cache. To understand what's happening under the hood, let's look at the different scenarios. `swr` works with tiered caches, but for simplicity, these charts may only show a single store. ```mermaid theme={"theme":"kanagawa-wave"} sequenceDiagram autonumber App->>Cache: swr(key, loadFromOrigin) Cache->>+Tier1: get key Tier1->>-Cache: fresh value Cache->>App: value ``` ```mermaid theme={"theme":"kanagawa-wave"} sequenceDiagram autonumber App->>Cache: swr(key, loadFromOrigin) Cache->>+Tier1: get key Tier1->>-Cache: stale value Cache->>App: value alt async Cache->>Origin: loadFromOrigin Origin->>Cache: value Cache->>Tier1: set key value end ``` ```mermaid theme={"theme":"kanagawa-wave"} sequenceDiagram autonumber App->>Cache: swr(key, loadFromOrigin) Cache->>+Tier1: get key Tier1->>-Cache: undefined Cache->>+Tier2: get key Tier2->>-Cache: undefined Cache->>Origin: loadFromOrigin Origin->>Cache: value Cache->>App: value alt async Cache->>Tier1: set key value Cache->>Tier2: set key value end ``` #### Example ```ts theme={"theme":"kanagawa-wave"} import { DefaultStatefulContext, Namespace, createCache } from "@unkey/cache"; import { CloudflareStore, MemoryStore } from "@unkey/cache/stores"; /** * In serverless you'd get this from the request handler * See https://unkey.com/docs/libraries/ts/cache/overview#context */ const ctx = new DefaultStatefulContext(); /** * Define the type of your data, or perhaps generate the types from your database */ type User = { id: string; email: string; }; const memory = new MemoryStore({ persistentMap: new Map() }); /** * @see https://unkey.com/docs/libraries/ts/cache/overview#cloudflare */ const cloudflare = new CloudflareStore({ domain: "cache.unkey.dev", zoneId: env.CLOUDFLARE_ZONE_ID!, cloudflareApiKey: env.CLOUDFLARE_API_KEY!, }); const userNamespace = new Namespace(ctx, { /** * Specifying first `memory`, then `cloudflare` will automatically check both stores in order * If a value is found in memory, it is returned, else it will check cloudflare, and if it's found * in cloudflare, the value is backfilled to memory. */ stores: [memory, cloudflare], fresh: 60_000, // Data is fresh for 60 seconds stale: 300_000, // Data is stale for 300 seconds }); const cache = createCache({ user: userNamespace }); async function main() { await cache.user.set("userId", { id: "userId", email: "user@email.com" }); const user = await cache.user.swr("userId", async (userId) => { // @ts-expect-error we don't have a db in this example return db.getUser(userId); }); console.info(user); } main(); ``` ### Context In serverless functions it's not always trivial to run some code after you have returned a response. This is where the context comes in. It allows you to register promises that should be awaited before the function is considered done. Fortunately many providers offer a way to do this. In order to be used in this cache library, the context must implement the following interface: ```ts theme={"theme":"kanagawa-wave"} export interface Context { waitUntil: (p: Promise) => void; } ``` For stateful applications, you can use the `DefaultStatefulContext`: ```ts theme={"theme":"kanagawa-wave"} import { DefaultStatefulContext } from "@unkey/cache"; const ctx = new DefaultStatefulContext(); ``` Vendor specific documentation: * [Cloudflare Workers](https://developers.cloudflare.com/workers/runtime-apis/context/) * [Vercel Serverless](https://vercel.com/docs/functions/functions-api-reference#waituntil) * [Vercel Edge and Middleware](https://vercel.com/docs/functions/edge-middleware/middleware-api#waituntil) ## Primitives ### Stores Stores are the underlying storage mechanisms for your cache. They can be in-memory, on-disk, or remote. You can use multiple stores in a namespace to create a tiered cache. The order of stores in a namespace is important. The cache will check the stores in order until it finds a value or all stores have been checked. You can create your own store by implementing the `Store` interface. [Read more.](/docs/libraries/ts/cache/interface/store) Below are the available stores: #### Memory The memory store is an in-memory cache that is fast but only as persistent as your memory. In serverless environments, this means that the cache is lost when the function is cold-started. ```ts theme={"theme":"kanagawa-wave"} import { MemoryStore } from "@unkey/cache/stores"; const memory = new MemoryStore({ persistentMap: new Map(), }); ``` Ensure that the `Map` is instantiated in a persistent scope of your application. For Cloudflare workers or serverless functions in general, this is the global scope. #### Cloudflare The Cloudflare store uses cloudflare's [`Cache` API](https://developers.cloudflare.com/workers/runtime-apis/cache/) to store cache values. This is a remote cache that is shared across all instances of your worker but isolated per datacenter. It's still pretty fast, but needs a network request to access the cache. ```ts theme={"theme":"kanagawa-wave"} import { CloudflareStore } from "@unkey/cache/stores"; const cloudflare = new CloudflareStore({ cloudflareApiKey: "", zoneId: "", domain: "", cacheBuster: "", }); ``` The Cloudflare API key to use for cache purge operations. The api key must have the `Cache Purge` permission. You can create a new API token with this permission in the [Cloudflare dashboard](https://dash.cloudflare.com/profile/api-tokens). The Cloudflare zone ID where the cache is stored. You can find this in the Cloudflare dashboard. The domain to use for the cache. This must be a valid domain within the zone specified by `zoneId`. If the domain is not valid in the specified zone, the cache will not work and cloudflare does not provide an error message. You will just get cache misses. For example, we use `domain: "cache.unkey.dev"` in our API. As your data changes, it is important to keep backwards compatibility in mind. If your cached values are no longer backwards compatible, it can cause problems. For example when a value changes from optional to required. In these cases you should purge the entire cache by setting a new `cacheBuster` value. The `cacheBuster` is used as part of the cache key and changes ensure you are not reading old data anymore. #### Upstash Redis The Upstash Redis store uses the [Serverless Redis](https://upstash.com/docs/redis/overall/getstarted) offering from Upstash to store cache values. This is a serverless database with Redis compatibility. ```ts theme={"theme":"kanagawa-wave"} import { UpstashRedisStore } from "@unkey/cache/stores"; import { Redis } from "@upstash/redis"; const redis = new Redis({ url: , token: , }) const redisStore = new UpstashRedisStore({ redis }) ``` The Upstash Redis client to use for cache operations. #### libSQL (Turso) The libSQL store can use an [embedded SQLite database](https://docs.turso.tech/features/embedded-replicas/introduction), or a remote [Turso](https://turso.tech) database to store cache values. You must create a table in your Turso database with the following schema: ```sql theme={"theme":"kanagawa-wave"} CREATE TABLE IF NOT EXISTS cache ( key TEXT PRIMARY KEY, value TEXT NOT NULL, freshUntil INTEGER NOT NULL, staleUntil INTEGER NOT NULL ); ``` ```ts Remote Only theme={"theme":"kanagawa-wave"} import { LibSQLStore } from "@unkey/cache/stores"; import { createClient } from "@libsql/client"; const client = createClient({ url: "libsql://...", authToken: "...", }); const store = new LibSQLStore({ client, }); ``` ```ts Embedded Replicas theme={"theme":"kanagawa-wave"} import { LibSQLStore } from "@unkey/cache/stores"; import { createClient } from "@libsql/client"; const client = createClient({ url: "file:dev.db", syncUrl: "libsql://...", authToken: "...", }); const store = new LibSQLStore({ client, }); ``` The [libSQL client](https://docs.turso.tech/sdk/ts) to use for cache operations. The name of the database table name to use for cache operations. ### Middlewares #### Metrics The metrics middleware collects metrics about cache hits, misses, and backfills. It's useful for debugging and monitoring your cache usage. Using the metrics middleware requires a metrics sink. You can build your own sink by implementing the `Metrics` interface. For example we are using [axiom](https://axiom.co?ref=unkey). ```ts theme={"theme":"kanagawa-wave"} interface Metrics< TMetric extends Record = Record, > { /** * Emit a new metric event * */ emit(metric: TMetric): void; /** * flush persists all metrics to durable storage. * You must call this method before your application exits, metrics are not persisted automatically. */ flush(): Promise; } ``` Wrap your store with the metrics middleware to start collecting metrics. ```ts theme={"theme":"kanagawa-wave"} import { withMetrics } from "@unkey/cache/middleware"; const metricsSink = // your metrics sink const metricsMiddleware = withMetrics(metricsSink); const memory = new MemoryStore({ persistentMap: new Map() }); new Namespace(ctx, { // Wrap the store with the metrics middleware stores: [metricsMiddleware.wrap(memory)], // ... }); ``` The following metrics are emitted: ```ts theme={"theme":"kanagawa-wave"} type Metric = | { metric: "metric.cache.read"; key: string; hit: boolean; status?: "fresh" | "stale"; latency: number; tier: string; namespace: string; } | { metric: "metric.cache.write"; key: string; latency: number; tier: string; namespace: string; } | { metric: "metric.cache.remove"; key: string; latency: number; tier: string; namespace: string; }; ``` #### Encryption When dealing with sensitive data, you might want to encrypt your cache values at rest. You can encrypt a store by wrapping it with the `EncryptedStore`. All you need is a 32 byte base64 encoded key. You can generate one with openssl: ```bash Generate a new encryption key theme={"theme":"kanagawa-wave"} openssl rand -base64 32 ``` ```ts Example theme={"theme":"kanagawa-wave"} import { withEncryption } from "@unkey/cache/middleware"; const encryptionKey = "" const encryptionMiddleware = await withEncryption(encryptionKey) const memory = new Memory({..}) // or any other store const store = encryptionMiddleware.wrap(memory); ``` Values will be encrypted using `AES-256-GCM` and persisted in the underlying store. You can rotate your encryption key at any point, but this will essentially purge the cache. A SHA256 hash of the encryption key is used in the cache key, to allow for rotation without causing decryption errors. ## Contributing If you have a store or middleware you'd like to see in this library, please open an [issue](https://github.com/unkeyed/unkey/issues/new) or a pull request. # @unkey/hono Source: https://unkey.com/docs/libraries/ts/hono Use the @unkey/hono middleware to authenticate API keys in your Hono.js application. Automatic key verification with typed context injection. > Hono - \[炎] means flame🔥 in Japanese - is a small, simple, and ultrafast web framework for the Edges. It works on any JavaScript runtime: Cloudflare Workers, Fastly Compute\@Edge, Deno, Bun, Vercel, Netlify, Lagon, AWS Lambda, Lambda\@Edge, and Node.js. `@unkey/hono` offers a middleware for authenticating API keys with [unkey](https://unkey.com). ## Install ```bash npm theme={"theme":"kanagawa-wave"} npm install @unkey/hono ``` ```bash pnpm theme={"theme":"kanagawa-wave"} pnpm add @unkey/hono ``` ```bash yarn theme={"theme":"kanagawa-wave"} yarn add @unkey/hono ``` ```bash bun theme={"theme":"kanagawa-wave"} bun install @unkey/hono ``` Let's dive straight in. The minimal setup looks like this. You need a root key with permission to verify keys. Go to [/settings/root-keys](https://app.unkey.com/settings/root-keys) and create a key with the `verify_key` permission. By default it tries to grab the API key from the `Authorization` header and then verifies it with Unkey. The result of the verification will be written to the context and can be accessed with `c.get("unkey")`. ```ts theme={"theme":"kanagawa-wave"} 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("/somewhere", (c) => { // access the unkey response here to get metadata of the key etc const keyInfo = c.get("unkey"); return c.text(`Hello ${keyInfo.identity?.externalId ?? "user"}!`); }); ``` ## Customizing the middleware ### Header By default the middleware tries to grab the API key from the `Authorization` header. You can change this by passing a custom header name to the middleware. ```ts theme={"theme":"kanagawa-wave"} app.use( "*", unkey({ rootKey: process.env.UNKEY_ROOT_KEY!, getKey: (c) => c.req.header("x-api-key"), }), ); ``` If the header is missing the middleware will return a `401` error response like this: ```ts theme={"theme":"kanagawa-wave"} c.json({ error: "unauthorized" }, { status: 401 }); ``` To customize the response in case the header is missing, just return a response from the `getKey` function. ```ts theme={"theme":"kanagawa-wave"} app.use( "*", unkey({ rootKey: process.env.UNKEY_ROOT_KEY!, getKey: (c) => { const key = c.req.header("x-api-key"); if (!key) { return c.text("missing api key", 401); } return key; }, }), ); ``` ### Handle errors ```ts theme={"theme":"kanagawa-wave"} app.use( "*", unkey({ rootKey: process.env.UNKEY_ROOT_KEY!, onError: (c, err) => { // handle error return c.text("unauthorized", 401); }, }), ); ``` ### Handle invalid keys By default the middleware will not do anything with the verification response other than writing it to the context. However you most likely would like to just return a `401` response if the key is invalid and not continue with the request. To do this you can pass a `handleInvalidKey` handler to the middleware. See the [key verification docs](/docs/platform/apis/keys#verify-a-key) for the full `response` object. ```ts theme={"theme":"kanagawa-wave"} app.use( "*", unkey({ rootKey: process.env.UNKEY_ROOT_KEY!, handleInvalidKey: (c, result) => { return c.json( { error: "unauthorized", reason: result.code, }, 401, ); }, }), ); ``` ### Pass verification tags You can pass tags to the verification request to help you filter keys later. ```ts theme={"theme":"kanagawa-wave"} (c, next) => unkey({ rootKey: process.env.UNKEY_ROOT_KEY!, tags: [`path=${c.req.path}`], })(c, next); ``` # @unkey/nextjs Source: https://unkey.com/docs/libraries/ts/nextjs Use the @unkey/nextjs SDK to protect Next.js API routes and server actions with Unkey API key authentication. Includes withUnkey wrapper. The official Next.js SDK for Unkey. Use this within your [route handlers](https://nextjs.org/docs/app/building-your-application/routing/route-handlers) as a simple, type-safe way to verify API keys. ## Install ```bash npm theme={"theme":"kanagawa-wave"} npm install @unkey/nextjs ``` ```bash pnpm theme={"theme":"kanagawa-wave"} pnpm add @unkey/nextjs ``` ```bash yarn theme={"theme":"kanagawa-wave"} yarn add @unkey/nextjs ``` ```bash bun theme={"theme":"kanagawa-wave"} bun add @unkey/nextjs ``` Protecting API routes is as simple as wrapping them with the `withUnkey` handler: ```ts theme={"theme":"kanagawa-wave"} import { NextRequestWithUnkeyContext, withUnkey } from "@unkey/nextjs"; export const POST = withUnkey( async (req: NextRequestWithUnkeyContext) => { // The key has already been verified at this point // Access verification details via req.unkey.data if (!req.unkey.data.valid) { return new Response("Unauthorized", { status: 401 }); } // Your API logic here return Response.json({ message: "Hello!", keyId: req.unkey.data.keyId, // If you set an externalId when creating the key: externalId: req.unkey.data.identity?.externalId, }); }, { rootKey: process.env.UNKEY_ROOT_KEY! }, ); ``` ### What's in `req.unkey`? The `req.unkey.data` object contains the verification result: | Field | Type | Description | | ------------- | ----------- | ------------------------------------------------------------------ | | `valid` | `boolean` | Whether the key passed all checks | | `code` | `string` | Status code (`VALID`, `NOT_FOUND`, `RATE_LIMITED`, etc.) | | `keyId` | `string` | The key's unique identifier | | `name` | `string?` | Human-readable name of the key | | `meta` | `object?` | Custom metadata associated with the key | | `expires` | `number?` | Unix timestamp (in milliseconds) when the key will expire (if set) | | `credits` | `number?` | Remaining uses (if usage limits set) | | `enabled` | `boolean` | Whether the key is enabled | | `roles` | `string[]?` | Roles attached to the key | | `permissions` | `string[]?` | Permissions attached to the key | | `identity` | `object?` | Identity info if `externalId` was set when creating the key | | `ratelimits` | `object[]?` | Rate limit states (if rate limiting configured) | Access these via `req.unkey.data.valid`, `req.unkey.data.keyId`, etc. If you want to customize how `withUnkey` processes incoming requests, you can do so as follows: ### `getKey` By default, withUnkey will look for a bearer token located in the `authorization` header. If you want to customize this, you can do so by passing a getter in the configuration object: ```ts theme={"theme":"kanagawa-wave"} export const GET = withUnkey( async (req) => { // ... }, { rootKey: process.env.UNKEY_ROOT_KEY!, getKey: (req) => new URL(req.url).searchParams.get("key"), }, ); ``` ### `onError` You can specify custom error handling. By default errors will be logged to the console, and `withUnkey` will return a NextResponse with status 500. ```ts theme={"theme":"kanagawa-wave"} export const GET = withUnkey( async (req) => { // ... }, { rootKey: process.env.UNKEY_ROOT_KEY!, onError: async (req, res) => { await analytics.trackEvent(`Error ${res.code}: ${res.message}`); return new NextResponse("Unkey error", { status: 500 }); }, }, ); ``` ### `handleInvalidKey` Specify what to do if Unkey reports that your key is invalid. ```ts theme={"theme":"kanagawa-wave"} export const GET = withUnkey( async (req) => { // ... }, { rootKey: process.env.UNKEY_ROOT_KEY!, handleInvalidKey: (req, res) => { return new Response("Unauthorized", { status: 401 }); }, }, ); ``` # Overview Source: https://unkey.com/docs/libraries/ts/overview Complete guide to Unkey's TypeScript and JavaScript SDKs including @unkey/api, @unkey/hono, @unkey/nextjs, @unkey/cache, and @unkey/ratelimit. Unkey provides two TypeScript packages: * **@unkey/api**: Manage keys, APIs, and rate limits (server-side, requires root key) * **@unkey/ratelimit**: Standalone rate limiting (server-side, requires root key) For framework-specific wrappers, see [@unkey/nextjs](/docs/libraries/ts/nextjs) and [@unkey/hono](/docs/libraries/ts/hono). ## Installation ```bash npm theme={"theme":"kanagawa-wave"} npm install @unkey/api @unkey/ratelimit ``` ```bash pnpm theme={"theme":"kanagawa-wave"} pnpm add @unkey/api @unkey/ratelimit ``` ```bash bun theme={"theme":"kanagawa-wave"} bun add @unkey/api @unkey/ratelimit ``` ## Quick Start ### Initialize the client ```typescript theme={"theme":"kanagawa-wave"} import { Unkey } from "@unkey/api"; import { Ratelimit } from "@unkey/ratelimit"; // For key management const unkey = new Unkey({ rootKey: process.env.UNKEY_ROOT_KEY!, }); // For rate limiting const limiter = new Ratelimit({ rootKey: process.env.UNKEY_ROOT_KEY!, namespace: "my-app", limit: 10, duration: "60s", }); ``` Never expose your root key in client-side code. These SDKs are for server-side use only. *** ## Verify an API Key The most common operation, check if a user's API key is valid: ```typescript theme={"theme":"kanagawa-wave"} import { Unkey } from "@unkey/api"; const unkey = new Unkey({ rootKey: process.env.UNKEY_ROOT_KEY! }); async function handleRequest(request: Request) { const apiKey = request.headers.get("x-api-key"); if (!apiKey) { return new Response("Missing API key", { status: 401 }); } try { const { meta, data } = await unkey.keys.verifyKey({ key: apiKey, }); if (!data.valid) { // Key is invalid, expired, rate limited, etc. return new Response(`Unauthorized: ${data.code}`, { status: 401 }); } // Key is valid, access granted // data.identity?.externalId, data.meta, data.credits, etc. available return handleAuthenticatedRequest(request, data); } catch (err) { console.error(err); return new Response("Service unavailable", { status: 503 }); } } ``` ### Verification response | Field | Type | Description | | ------------- | ----------- | ------------------------------------------------------------------ | | `valid` | `boolean` | Whether the key passed all checks | | `code` | `string` | Status code (`VALID`, `NOT_FOUND`, `RATE_LIMITED`, etc.) | | `keyId` | `string` | The key's unique identifier | | `name` | `string?` | Human-readable name of the key | | `meta` | `object?` | Custom metadata associated with the key | | `expires` | `number?` | Unix timestamp (in milliseconds) when the key will expire (if set) | | `credits` | `number?` | Remaining uses (if usage limits set) | | `enabled` | `boolean` | Whether the key is enabled | | `roles` | `string[]?` | Roles attached to the key | | `permissions` | `string[]?` | Permissions attached to the key | | `identity` | `object?` | Identity info if `externalId` was set when creating the key | | `ratelimits` | `object[]?` | Rate limit states (if rate limiting configured) | ### Check permissions during verification ```typescript theme={"theme":"kanagawa-wave"} try { const { meta, data } = await unkey.keys.verifyKey({ key: apiKey, permissions: "documents.write", // Required permission }); if (!data.valid && data.code === "INSUFFICIENT_PERMISSIONS") { return new Response("Forbidden", { status: 403 }); } } catch (err) { console.error(err); return new Response("Service unavailable", { status: 503 }); } ``` *** ## Create API Keys Issue new keys for your users: ```typescript theme={"theme":"kanagawa-wave"} try { const { meta, data } = await unkey.keys.createKey({ apiId: "api_...", // Optional but recommended prefix: "sk_live", // Visible prefix externalId: "user_123", // Link to your user name: "Production key", // Human-readable name // Optional limits expires: Date.now() + 30 * 24 * 60 * 60 * 1000, // 30 days credits: { remaining: 1000, // Usage limit refill: { amount: 1000, interval: "monthly", // Auto-refill }, }, ratelimits: [ { name: "requests", limit: 100, duration: 60000, // 100/minute }, ], // Optional metadata meta: { plan: "pro", createdBy: "admin", }, }); // Send data.key to your user (only time you'll see the full key!) console.log("New key:", data.key); console.log("Key ID:", data.keyId); } catch (err) { console.error(err); throw new Error("Failed to create key"); } ``` The full API key is only returned once at creation. Unkey stores only a hash. Make sure to display it to your user immediately. *** ## Update Keys Modify an existing key's configuration: ```typescript theme={"theme":"kanagawa-wave"} try { await unkey.keys.updateKey({ keyId: "key_...", // Any fields you want to change name: "Updated name", meta: { plan: "enterprise" }, enabled: true, expires: Date.now() + 90 * 24 * 60 * 60 * 1000, ratelimits: [ { name: "requests", limit: 1000, // Upgraded limit duration: 60000, }, ], }); } catch (err) { console.error(err); throw new Error("Failed to update key"); } ``` *** ## Delete Keys Permanently revoke a key: ```typescript theme={"theme":"kanagawa-wave"} try { await unkey.keys.deleteKey({ keyId: "key_...", }); } catch (err) { console.error(err); throw new Error("Failed to delete key"); } ``` Or disable temporarily (can re-enable later): ```typescript theme={"theme":"kanagawa-wave"} try { await unkey.keys.updateKey({ keyId: "key_...", enabled: false, }); } catch (err) { console.error(err); throw new Error("Failed to disable key"); } ``` *** ## Rate Limiting Use `@unkey/ratelimit` for standalone rate limiting: ```typescript theme={"theme":"kanagawa-wave"} import { Ratelimit } from "@unkey/ratelimit"; const limiter = new Ratelimit({ rootKey: process.env.UNKEY_ROOT_KEY!, namespace: "api", limit: 100, duration: "60s", }); async function handleRequest(request: Request) { const userId = request.headers.get("x-user-id") ?? "anonymous"; try { const { success, remaining, reset } = await limiter.limit(userId); if (!success) { return new Response("Rate limit exceeded", { status: 429, headers: { "Retry-After": Math.ceil((reset - Date.now()) / 1000).toString(), "X-RateLimit-Remaining": "0", }, }); } // Process request... } catch (err) { console.error(err); return new Response("Rate limit service unavailable", { status: 503 }); } } ``` ### Cost-based rate limiting Expensive operations can consume more of the limit: ```typescript theme={"theme":"kanagawa-wave"} // Normal request costs 1 await limiter.limit(userId); // Expensive request costs 10 await limiter.limit(userId, { cost: 10 }); ``` *** ## Error Handling Use try/catch to handle errors: ```typescript theme={"theme":"kanagawa-wave"} try { const { meta, data } = await unkey.keys.createKey({ ... }); console.log("Request ID:", meta.requestId); console.log("Key ID:", data.keyId); } catch (err) { console.error(err); throw new Error("Failed to create key"); } ``` ### Resilient verification For production, handle edge cases: ```typescript theme={"theme":"kanagawa-wave"} import { Unkey } from "@unkey/api"; const unkey = new Unkey({ rootKey: process.env.UNKEY_ROOT_KEY! }); async function verifyApiKey(key: string): Promise { try { const { meta, data } = await unkey.keys.verifyKey({ key }); return data; } catch (err) { // Network error, timeout, etc. console.error("Unkey verification error:", err); return null; // Or return a default allow/deny } } ``` *** ## TypeScript Types The SDK is fully typed. Import types as needed: ```typescript theme={"theme":"kanagawa-wave"} import type { VerifyKeyResult, Key, Api, RatelimitResponse } from "@unkey/api"; ``` *** ## Framework Guides withUnkey wrapper for API routes Middleware for Hono apps Step-by-step Express guide Step-by-step Bun guide *** ## Full Reference Complete auto-generated API reference # Delete Override Source: https://unkey.com/docs/libraries/ts/ratelimit/override/delete-override Delete a rate limit override for a specific identifier using the @unkey/ratelimit SDK. Revert the identifier to namespace default limits. ## Request Identifier of your user, this can be their userId, an email, an ip or anything else. Wildcards ( \* ) can be used to match multiple identifiers. More info can be found at [https://www.unkey.com/docs/ratelimiting/overrides#wildcard-rules](https://www.unkey.com/docs/ratelimiting/overrides#wildcard-rules) Either `namespaceId` or `namespaceName` is required. Not both. The id of the namespace. Either namespaceId or namespaceName must be provided Namespaces group different limits together for better analytics. You might have a namespace for your public API and one for internal tRPC routes. Wildcards can also be used, more info can be found at [https://www.unkey.com/docs/ratelimiting/overrides#wildcard-rules](https://www.unkey.com/docs/ratelimiting/overrides#wildcard-rules) ## Response No response, but if no error is returned the override has been deleted successfully. ```ts theme={"theme":"kanagawa-wave"} try { const { meta, data } = await unkey.deleteOverride({ identifier: "user_123", namespaceName: "email.outbound", }); } catch (err) { console.error(err); throw err; } ``` ```ts theme={"theme":"kanagawa-wave"} try { const { meta, data } = await unkey.deleteOverride({ identifier: "user_123", namespaceId:"rlns_12345", }); } catch (err) { console.error(err); throw err; } ``` # Get Override Source: https://unkey.com/docs/libraries/ts/ratelimit/override/get-override Retrieve the configuration of a specific rate limit override by identifier using the @unkey/ratelimit SDK. Check current custom limits. ## Request Identifier of your user, this can be their userId, an email, an ip or anything else. Wildcards ( \* ) can be used to match multiple identifiers. More info can be found at [https://www.unkey.com/docs/ratelimiting/overrides#wildcard-rules](https://www.unkey.com/docs/ratelimiting/overrides#wildcard-rules) Either `namespaceId` or `namespaceName` is required. Not both. The id of the namespace. Either namespaceId or namespaceName must be provided Namespaces group different limits together for better analytics. You might have a namespace for your public API and one for internal tRPC routes. Wildcards can also be used, more info can be found at [https://www.unkey.com/docs/ratelimiting/overrides#wildcard-rules](https://www.unkey.com/docs/ratelimiting/overrides#wildcard-rules) ## Response Identifier of the override requested Identifier of your user, this can be their userId, an email, an ip or anything else. Wildcards ( \* ) can be used to match multiple identifiers. More info can be found at [https://www.unkey.com/docs/ratelimiting/overrides#wildcard-rules](https://www.unkey.com/docs/ratelimiting/overrides#wildcard-rules) How many requests may pass in a given window. The window duration in milliseconds. ```ts theme={"theme":"kanagawa-wave"} const { meta, data } = await unkey.getOverride({ identifier:"user.example", namespaceName: "email.outbound" }); ``` ```ts theme={"theme":"kanagawa-wave"} const { meta, data } = await unkey.getOverride({ identifier:"user.example", namespaceId: "rlns_1234", }); ``` ```ts theme={"theme":"kanagawa-wave"} { result: { id: "rlor_4567", identifier: "user.example", limit: 10, duration: 60000, async: false } } ``` # List Overrides Source: https://unkey.com/docs/libraries/ts/ratelimit/override/list-overrides List all rate limit overrides in a namespace using the @unkey/ratelimit SDK. Retrieve paginated results of custom limits for identifiers. ## Request Either `namespaceId` or `namespaceName` is required. Not both. The id of the namespace. Either namespaceId or namespaceName must be provided Namespaces group different limits together for better analytics. You might have a namespace for your public API and one for internal tRPC routes. Wildcards can also be used, more info can be found at [https://www.unkey.com/docs/ratelimiting/overrides#wildcard-rules](https://www.unkey.com/docs/ratelimiting/overrides#wildcard-rules) ## Response Identifier of the override requested Identifier of your user, this can be their userId, an email, an ip or anything else. Wildcards ( \* ) can be used to match multiple identifiers. More info can be found at [https://www.unkey.com/docs/ratelimiting/overrides#wildcard-rules](https://www.unkey.com/docs/ratelimiting/overrides#wildcard-rules) How many requests may pass in a given window. The window duration in milliseconds. The total number of overrides The cursor to use for pagination ```ts theme={"theme":"kanagawa-wave"} const { meta, data } = await unkey.listOverrides({ namespaceName: "email.outbound" }); ``` ```ts theme={"theme":"kanagawa-wave"} const { meta, data } = await unkey.listOverrides({ nameSpaceId: "rlns_12345", }); ``` ```ts theme={"theme":"kanagawa-wave"} { result: { overrides: [ { id: 'rlor_1234', identifier: 'customer_123', limit: 10, duration: 50000, async: false } ], total: 1, cursor: 'rlor_1234' } } ``` # Rate Limit Overrides Source: https://unkey.com/docs/libraries/ts/ratelimit/override/overview Set custom rate limit overrides for specific identifiers using @unkey/ratelimit. Grant higher or lower limits per user, key, or tenant. Ratelimit overrides are a way to override the ratelimit for specific users or group using an identifier. ## Configure your override ```ts theme={"theme":"kanagawa-wave"} import { Override } from "@unkey/ratelimit"; const unkey = new Override({ rootKey: process.env.UNKEY_ROOT_KEY, }); ``` ## Use it ```ts theme={"theme":"kanagawa-wave"} async function handler(request) { const identifier = request.getUserId(); // or ip or anything else you want try { const { meta, data } = await unkey.setOverride({ identifier: identifier, limit: 10, duration: 60000, namespaceName: "email.outbound", }); if (data.error) { // handle the error here console.error(data.error.message); return; } } catch (err) { console.error(err); return; } // handle the request here } ``` There are four main functions to interact with overrides: * [setOverride](/docs/libraries/ts/ratelimit/override/set-override) Sets an override for a ratelimit. * [getOverride](/docs/libraries/ts/ratelimit/override/get-override) Gets a ratelimit override. * [deleteOverride](/docs/libraries/ts/ratelimit/override/delete-override) Deletes an override. * [listOverrides](/docs/libraries/ts/ratelimit/override/list-overrides) Lists all overrides for a namnespace. # Set Override Source: https://unkey.com/docs/libraries/ts/ratelimit/override/set-override Create or update a rate limit override for a specific identifier using the @unkey/ratelimit SDK. Bypass namespace defaults per user or key. ## Request Identifier of your user, this can be their userId, an email, an ip or anything else. Wildcards ( \* ) can be used to match multiple identifiers. More info can be found at [https://www.unkey.com/docs/ratelimiting/overrides#wildcard-rules](https://www.unkey.com/docs/ratelimiting/overrides#wildcard-rules) How many requests may pass in a given window. The window duration in milliseconds. Either `namespaceId` or `namespaceName` is required. Not both. The id of the namespace. Either namespaceId or namespaceName must be provided Namespaces group different limits together for better analytics. You might have a namespace for your public API and one for internal tRPC routes. Wildcards can also be used, more info can be found at [https://www.unkey.com/docs/ratelimiting/overrides#wildcard-rules](https://www.unkey.com/docs/ratelimiting/overrides#wildcard-rules) ## Response The id of the override that was set. ```ts theme={"theme":"kanagawa-wave"} const { meta, data } = await unkey.setOverride({ identifier: "user_123", limit: 10, duration: 60000, namespaceName: "email.outbound", async: true }) ``` ```ts theme={"theme":"kanagawa-wave"} const { meta, data } = await unkey.setOverride({ identifier: "user_123", limit: 5, duration: 50000, namespaceId: "rlns_1234", async: false, }); ``` ```ts theme={"theme":"kanagawa-wave"} { result: { overrideId: 'rlor_12345' } } ``` # Ratelimit Source: https://unkey.com/docs/libraries/ts/ratelimit/ratelimit Use the @unkey/ratelimit TypeScript SDK to add fast, globally distributed rate limiting to Node.js, Bun, Deno, and Cloudflare Workers — no Redis required. `@unkey/ratelimit` is a library for fast global ratelimiting that runs in any **server-side** JavaScript runtime — Node.js, Deno, Bun, Cloudflare Workers, AWS Lambda, and other serverless or edge functions. **Server-side only.** This SDK requires your `UNKEY_ROOT_KEY`, which grants full access to your Unkey workspace. Never import or call `@unkey/ratelimit` from browser code, mobile apps, or any other client that ships to end users — doing so leaks the root key. Always invoke it from a trusted server, load the key from an environment variable (e.g. `process.env.UNKEY_ROOT_KEY`), and keep it out of bundles that reach the client. ## Install ```bash npm theme={"theme":"kanagawa-wave"} npm install @unkey/ratelimit ``` ```bash pnpm theme={"theme":"kanagawa-wave"} pnpm add @unkey/ratelimit ``` ```bash yarn theme={"theme":"kanagawa-wave"} yarn add @unkey/ratelimit ``` ```bash bun theme={"theme":"kanagawa-wave"} bun install @unkey/ratelimit ``` ## Configure your ratelimiter ```ts theme={"theme":"kanagawa-wave"} import { Ratelimit } from "@unkey/ratelimit"; const unkey = new Ratelimit({ rootKey: process.env.UNKEY_ROOT_KEY, namespace: "my-app", limit: 10, duration: "30s", }); ``` ## Use it ```ts theme={"theme":"kanagawa-wave"} async function handler(request) { const identifier = request.getUserId(); // or ip or anything else you want const ratelimit = await unkey.limit(identifier); if (!ratelimit.success) { return new Response("try again later", { status: 429 }); } // handle the request here } ``` ## Making it bullet proof Everything we do is built for scale and stability. We built on some of the world's most stable platforms ([Planetscale](https://planetscale.com/) and [Cloudflare](https://www.cloudflare.com)) and run an extensive test suite before and after every deployment. Even so, we would be fools if we wouldn't explain how you can put in safe guards along the way. In case of severe network degradations or other unforeseen events, you might want to put an upper bound on how long you are willing to wait for a response from unkey. By default the SDK will reject a request if it hasn't received a response from unkey within 5 seconds. You can tune this via the `timeout` config in the constructor (see below). The SDK captures most errors and handles them on its own, but we also encourage you to add a `onError` handler to configure what happens in case something goes wrong. Both `fallback` property of the `timeout` config and `onError` config are callback functions. They receive the original request identifier as one of their parameters, which you can use to determine whether to reject the request. ```ts theme={"theme":"kanagawa-wave"} import { Ratelimit } from "@unkey/ratelimit"; // In this example we decide to let requests pass, in case something goes wrong. // But you can of course also reject them if you want. const fallback = (identifier: string) => ({ success: true, limit: 0, reset: 0, remaining: 0, }); const unkey = new Ratelimit({ // ... standard stuff timeout: { ms: 3000, // only wait 3s at most before returning the fallback fallback, }, onError: (err, identifier) => { console.error(`${identifier} - ${err.message}`); return fallback(identifier); }, }); const { success } = await unkey.limit(identifier); ``` *** ## API ### `new Ratelimit(config: RatelimitConfig)` Create a new instance for ratelimiting by providing the necessary configuration. How many requests may pass in the given duration. How long the window should be. Either a type string literal like `60s`, `20m` or plain milliseconds. The unkey root key. You can create one at [app.unkey.com/settings/root-keys](https://app.unkey.com/settings/root-keys) Make sure the root key has permissions to use ratelimiting. Namespaces allow you to separate different areas of your app and have isolated limits. Make sure the root key has permissions to use ratelimiting. Configure a timeout to prevent network issues from blocking your function for too long. Disable it by setting `timeout: false` Timeouts rely on `Date.now()`. In cloudflare workers time doesn't progress unless there is some io happening, which means the timeout might not work as expected. Other runtimes are working. Time in milliseconds until the response is returned. A custom response to return when the timeout is reached. The important bit is the `success` value, choose whether you want to let requests pass or not. Configure what happens for unforeseen errors Example letting requests pass: ```ts theme={"theme":"kanagawa-wave"} onError: () => ({ success: true, limit: 0, remaining: 0, reset: 0 }); ``` Example rejecting the request: ```ts theme={"theme":"kanagawa-wave"} onError: () => ({ success: true, limit: 0, remaining: 0, reset: 0 }); ``` ### `.limit(identifier: string, opts: LimitOptions): Promise` Check whether a specific identifier is currently allowed to do something or if they have currently exceeded their limit. Expensive requests may use up more resources. You can specify a cost to the request and we'll deduct this many tokens in the current window. If there are not enough tokens left, the request is denied. **Example:** 1. You have a limit of 10 requests per second you already used 4 of them in the current window. 2. Now a new request comes in with a higher cost: ```ts theme={"theme":"kanagawa-wave"} const res = await rl.limit("identifier", { cost: 4 }); ``` 3. The request passes and the current limit is now at `8` 4. The same request happens again, but would not be rejected, because it would exceed the limit in the current window: `8 + 4 > 10` ### `RatelimitResponse` Whether the request may pass(true) or exceeded the limit(false). Maximum number of requests allowed within a window. How many requests the user has left within the current window. Unix timestamp in milliseconds when the limits are reset. # Custom domains Source: https://unkey.com/docs/networking/domains Attach your own custom domain names to route production traffic to your Unkey deployments. Configure DNS records and TLS certificates. Deploying applications on Unkey is in public beta. To try it, open the product switcher in the top-left of the dashboard and select **Deploy**. During beta, deployed resources are free. We're eager for feedback, so let us know what you think on [Discord](https://unkey.com/discord), [X](https://x.com/unkeydev), or email [support@unkey.com](mailto:support@unkey.com). Custom domains let you serve your app from your own domain name (for example, `api.acme.com`) instead of a `*.unkey.app` subdomain. Unkey handles TLS certificate provisioning and renewal automatically. ## Add a custom domain Navigate to your project in the dashboard and click **Settings**. Scroll to the **Custom domains** section. Select the environment and enter the fully qualified domain name you want to use (for example, `api.acme.com`). Custom domains settings showing environment and domain input After you add a domain, Unkey checks whether your DNS provider supports automatic setup. If it does, you can configure your DNS records with one click. Otherwise, you add the records manually. If your DNS provider supports the [Domain Connect](https://www.domainconnect.org/) protocol, an **Automatic setup available** card appears with your provider's name. Click **Connect** to open your DNS provider's consent page, approve the changes, and the required DNS records are created for you automatically. After you approve, Unkey begins verification immediately. No manual DNS configuration is needed. The following providers are supported for automatic setup: * Cloudflare * Vercel DNS Automatic setup works for subdomains out of the box. For apex (root) domains, some providers like Cloudflare (via CNAME flattening) and Vercel (via ALIAS records through Domain Connect) also support automatic setup. If your provider does not support these features, configure DNS manually instead. If automatic setup is not available, Unkey generates DNS records for you to add at your DNS provider: DNS records to add for domain verification showing TXT and CNAME entries A **TXT** record proves ownership of the domain. A **CNAME** record routes traffic to your deployment. Each domain receives a unique CNAME target. Add both records at your DNS provider. Unkey checks for them automatically and verifies within minutes once the records propagate. Both DNS records must be verified within 24 hours. If verification doesn't complete in time, the domain enters a failed state. Click the **Retry** button to restart verification, or remove the domain and add it again. ## Certificate provisioning After DNS verification succeeds, Unkey provisions a TLS certificate from Let's Encrypt using an ACME HTTP-01 challenge. Frontline serves the challenge token automatically during this process. Certificates renew before expiration without any action from you. If Let's Encrypt rate limits are reached, certificate issuance retries automatically with backoff. This can add up to two hours of delay in rare cases. ## DNS provider examples If your provider supports [automatic setup](#configure-dns-records), you can skip these steps and use the one-click **Connect** button instead. 1. Open your domain in the Cloudflare dashboard. 2. Click **DNS** in the sidebar. 3. Click **Add record**. 4. Add the TXT record with name `_unkey.{your-subdomain}` and the verification value. 5. Add the CNAME record with name `{your-subdomain}` and the target from your Unkey dashboard. 6. Set the CNAME proxy status to **DNS only** (gray cloud) so Unkey can terminate TLS directly. 1. Open your hosted zone in the Route53 console. 2. Click **Create record**. 3. Add the TXT record with name `_unkey.{your-subdomain}` and the verification value wrapped in quotes. 4. Add the CNAME record with name `{your-subdomain}` and the target from your Unkey dashboard. 1. Open your domain in the Vercel dashboard under **Settings** > **Domains**. 2. Click **Manage** next to your domain. 3. Navigate to the **DNS Records** tab. 4. Add the TXT record with name `_unkey.{your-subdomain}` and the verification value. 5. Add the CNAME record with name `{your-subdomain}` and the target from your Unkey dashboard. ## Troubleshooting Confirm your DNS records have propagated. TXT records can take up to 48 hours to propagate, depending on your DNS provider. You can check propagation with: ```bash theme={"theme":"kanagawa-wave"} dig TXT _unkey.api.acme.com ``` Certificate provisioning starts automatically after both DNS records are verified. If the certificate isn't issued within 30 minutes, check that your CNAME proxy status is set to **DNS only** (not proxied) at your DNS provider. A domain can only be added once per workspace. If you see a duplicate domain error, check whether the domain already exists in your workspace, possibly in a different project or environment. Remove the existing entry before adding it again. # Private networking Source: https://unkey.com/docs/networking/private-networking Enable secure service-to-service communication within your Unkey project using private networking. Route internal traffic without exposure. Deploying applications on Unkey is in public beta. To try it, open the product switcher in the top-left of the dashboard and select **Deploy**. During beta, deployed resources are free. We're eager for feedback, so let us know what you think on [Discord](https://unkey.com/discord), [X](https://x.com/unkeydev), or email [support@unkey.com](mailto:support@unkey.com). Each deployment instance runs in an isolated network namespace. Instances cannot communicate with other instances directly. ## Coming soon Private networking will enable instances of the same deployment to communicate with each other over internal DNS without traversing the public internet. # Request lifecycle Source: https://unkey.com/docs/networking/public-networking Trace the full request lifecycle from when a request hits your URL through Sentinel policies, routing, and load balancing to your app. Deploying applications on Unkey is in public beta. To try it, open the product switcher in the top-left of the dashboard and select **Deploy**. During beta, deployed resources are free. We're eager for feedback, so let us know what you think on [Discord](https://unkey.com/discord), [X](https://x.com/unkeydev), or email [support@unkey.com](mailto:support@unkey.com). This page walks through every stage a request passes through on its way to your app. ## DNS Unkey uses latency-based geolocation routing to resolve both `*.unkey.app` [wildcard domains](/docs/networking/wildcard-domains) and [custom domains](/docs/networking/domains) to the closest region to the client. ## Frontline The request arrives at Frontline, Unkey's global network layer. Frontline terminates TLS close to the client, so your app never needs to manage certificates. All connections enforce TLS 1.2 or higher, with TLS 1.3 preferred. HTTP requests are redirected to HTTPS automatically. Certificates are provisioned and renewed automatically for both wildcard and custom domains. After terminating TLS, Frontline resolves the requested domain to a deployment. It consults a globally replicated metadata store that maps every domain to a deployment ID. If the target deployment runs in a different region, Frontline forwards the request to that region automatically. ## Sentinel Frontline forwards the request to Sentinel, the application gateway. Sentinel runs the policies configured for the deployment before the request reaches your code: * [Authentication](/docs/platform/sentinel/authentication) verifies the caller's identity * [Rate limiting](/docs/platform/sentinel/policies/rate-limiting) enforces request quotas * [Custom policies](/docs/platform/sentinel/policies) apply additional rules Requests that fail a policy receive an error response from Sentinel without reaching your app. After policies pass, Sentinel selects a healthy instance of your deployment and proxies the request to your app. The response flows back through Sentinel and Frontline to the client. ### Request headers Sentinel adds headers to every proxied request so your app can identify the original client and request context: | Header | Description | | ------------------- | -------------------------------------------------------------------------- | | `X-Forwarded-For` | The original client IP address | | `X-Forwarded-Host` | The original `Host` header from the client request | | `X-Forwarded-Proto` | The protocol used by the client (`https`) | | `X-Deployment-Id` | The deployment ID that Frontline resolved from the domain | | `X-Unkey-Principal` | The authenticated principal, populated by Sentinel's authentication policy | # WebSockets Source: https://unkey.com/docs/networking/websockets Deploy WebSocket servers on Unkey with long-lived connections, no request timeouts, and Sentinel auth and rate limits applied to the upgrade. Deploying applications on Unkey is in public beta. To try it, open the product switcher in the top-left of the dashboard and select **Deploy**. During beta, deployed resources are free. We're eager for feedback, so let us know what you think on [Discord](https://unkey.com/discord), [X](https://x.com/unkeydev), or email [support@unkey.com](mailto:support@unkey.com). ## Using WebSockets WebSockets work out of the box. There is nothing to enable in the dashboard or the CLI. Bind your WebSocket server to the port your app exposes and deploy as usual. A minimal Node.js example using [`ws`](https://github.com/websockets/ws): ```ts theme={"theme":"kanagawa-wave"} import { WebSocketServer } from "ws"; const wss = new WebSocketServer({ port: 8080 }); wss.on("connection", (socket) => { socket.on("message", (data) => { socket.send(`echo: ${data}`); }); }); ``` Connect from a client using either the wildcard or custom domain assigned to your deployment. Always use `wss://` — plaintext `ws://` is not redirected. ```ts theme={"theme":"kanagawa-wave"} const ws = new WebSocket("wss://myapp-api-acme.unkey.app/ws"); ws.onopen = () => ws.send("hello"); ws.onmessage = (event) => console.log(event.data); ``` ## Headers and routing The standard proxy headers documented in [Request lifecycle](/docs/networking/public-networking#request-headers) are added to the upgrade request, so your app can identify the original client IP, host, and deployment ID before it accepts the WebSocket. Routing rules apply to the upgrade request the same way they apply to any HTTP request: the `Host` header determines which deployment receives the connection, and Sentinel policies run before the upgrade is forwarded. A request that fails [authentication](/docs/platform/sentinel/authentication) or [rate limiting](/docs/platform/sentinel/policies/rate-limiting) is rejected before it ever becomes a WebSocket. ## Limits * Only HTTP/1.1 upgrades are forwarded today. Clients that negotiate WebSockets over HTTP/2 fall back to HTTP/1.1 automatically. * If the upstream instance is unreachable, the upgrade fails with `503 Service Unavailable` instead of hanging. * Idle connections are still subject to your app's own keepalive and timeout configuration. Send periodic ping frames if you want to keep an otherwise-idle session alive. # Wildcard domains Source: https://unkey.com/docs/networking/wildcard-domains Every Unkey deployment receives a wildcard *.unkey.app domain automatically. Access any deployment instantly without manual DNS setup. Deploying applications on Unkey is in public beta. To try it, open the product switcher in the top-left of the dashboard and select **Deploy**. During beta, deployed resources are free. We're eager for feedback, so let us know what you think on [Discord](https://unkey.com/discord), [X](https://x.com/unkeydev), or email [support@unkey.com](mailto:support@unkey.com). Every deployment on Unkey receives automatic `*.unkey.app` subdomains with no DNS configuration required. These domains are available as soon as the deployment completes. ## Immutable vs sticky Wildcard domains fall into two categories based on how they resolve over time. **Immutable** domains are locked to a specific commit or deployment and never change. The same URL always returns the same version of your app. Use immutable domains when you need a permanent reference to an exact version, for example in incident reports, audit trails, or rollback verification. **Sticky** domains follow the latest deployment that matches a condition (a branch, an environment, or production). The URL stays the same, but the deployment behind it changes as you push new code. Use sticky domains for bookmarks, CI/CD integrations, and sharing preview links that stay up to date. ## Immutable domains Tied to a specific commit. Once created, it always resolves to the deployment built from that commit. Example: `myapp-api-git-a1b2c3d-acme.unkey.app` Deployments created via CLI upload include a random suffix to prevent collisions when deploying from a dirty git state. Tied to a specific deployment ID. Similar to commit domains, but references the deployment rather than the commit. Example: `myapp-api-dep-xyz789-acme.unkey.app` ## Sticky domains Follows the latest deployment on a given branch. Each time you push to the branch, this domain updates to point to the new deployment. Example: `myapp-api-git-main-acme.unkey.app` Branch names are slugified: non-alphanumeric characters become hyphens, and the name is truncated to 80 characters. Follows the latest deployment promoted to a specific environment. When you promote a new deployment or roll back, this domain updates automatically. Example: `myapp-api-staging-acme.unkey.app` A short-form URL that always resolves to the current production deployment. Unkey creates this domain only for the production environment. Example: `myapp-api-acme.unkey.app` ## When to use each type | Domain | Use case | | ----------- | ----------------------------------------------------------------------- | | Commit | Link to an exact commit's deployment in an incident report or changelog | | Deployment | Reference a specific deployment for debugging or comparison | | Branch | Share a preview URL in a pull request that stays current | | Environment | Point integration tests at a stable staging URL | | Live | Give internal tools a short, memorable production URL | # Logs Source: https://unkey.com/docs/observability/logs View and filter runtime logs from your Unkey deployments. Search application output, debug errors, and monitor container stdout and stderr. Deploying applications on Unkey is in public beta. To try it, open the product switcher in the top-left of the dashboard and select **Deploy**. During beta, deployed resources are free. We're eager for feedback, so let us know what you think on [Discord](https://unkey.com/discord), [X](https://x.com/unkeydev), or email [support@unkey.com](mailto:support@unkey.com). The **Logs** tab in your project shows runtime output (stdout and stderr) from every deployment. Anything your app prints to the console appears here. ## How logs are captured Unkey captures all stdout and stderr output from your app automatically. There is nothing to install or configure. If your app writes JSON to stdout, Unkey parses the JSON and populates the **Attributes** column with the structured data. This makes individual fields filterable and easier to scan. Plain text output appears in the **Message** column as-is. ## Log fields Each log entry includes a timestamp, a severity level (`ERROR`, `WARN`, `INFO`, or `DEBUG`), the region where the instance is running, and the log message. If the output is structured JSON, Unkey populates an additional **Attributes** column with the parsed fields. ## Filter logs Use the control bar at the top of the page to narrow results by severity, deployment, environment, region, instance, message content, and time range. Filters can be combined and are persisted in the URL, so you can bookmark or share a filtered view. Region and instance filters are useful when debugging multi-region deployments. Select a specific region to see logs from instances in that location, or filter by instance ID to isolate output from a single replica. Logs page Logs page ## Structured logging To get the most out of the Attributes column, emit structured JSON from your app: ```json theme={"theme":"kanagawa-wave"} {"level": "info", "message": "payment processed", "amount": 49.99, "currency": "USD", "customer_id": "cus_abc123"} ``` Unkey parses each key into a separate attribute, making it easier to locate specific entries when scanning a large volume of logs. ## Retention Runtime logs are retained for 90 days. # Metrics Source: https://unkey.com/docs/observability/metrics Track requests per second, latency percentiles, and runtime resource usage for each Unkey deployment. Monitor performance with built-in metric charts. Deploying applications on Unkey is in public beta. To try it, open the product switcher in the top-left of the dashboard and select **Deploy**. During beta, deployed resources are free. We're eager for feedback, so let us know what you think on [Discord](https://unkey.com/discord), [X](https://x.com/unkeydev), or email [support@unkey.com](mailto:support@unkey.com). Each deployment's overview page includes real-time charts for throughput, latency, CPU, and memory across all instances. Per-instance runtime metrics (CPU, memory, disk, and network) are also available from the deployment's network view. Navigate to a deployment from your project's **Deployments** tab to view them. ## Deployment overview charts Four metric cards at the top of the deployment overview show the last six hours of activity, aggregated across every instance in the deployment. Each card updates automatically every ten seconds. ### Requests per second The RPS chart shows the number of requests your deployment handles over time. Use it to spot traffic spikes, confirm scaling behavior, or correlate with incidents. ### Latency The latency chart displays response time distributions over time. You can switch between p50, p75, p90, p95, and p99 percentiles. Check the [Requests](/docs/observability/requests) tab to find individual slow requests and inspect their latency breakdown. ### CPU The CPU chart shows used CPU as a percentage of the deployment's total allocated CPU. Hover over a point to see the underlying millicore value. Use this to confirm headroom against your [CPU allocation](/docs/platform/apps/settings#cpu) and to predict when [autoscaling](/docs/platform/instances/overview#autoscaling) will kick in. ### Memory The memory chart shows used memory as a percentage of the deployment's total allocated memory. Hover over a point to see the raw value in MiB. Use this to detect leaks, validate sizing, and decide when to raise the [memory limit](/docs/platform/apps/settings#memory). ## Per-instance runtime metrics Open the **Network** tab on a deployment and click an instance node to open the details panel. The panel shows live charts for the selected instance, refreshing every few seconds: * **CPU.** Used vs. allocated CPU in millicores. * **Memory.** Used vs. allocated memory in bytes. * **Disk.** Used vs. allocated disk in bytes. Only shown when the instance has provisioned disk storage. * **Network.** Egress and ingress traffic over time. Use the time window selector at the top of the panel to switch between the past 15 minutes, 1 hour, 6 hours, or 24 hours. When you select a deployment node instead of a single instance, the charts aggregate across all instances in the deployment. # Observability Source: https://unkey.com/docs/observability/overview Monitor your Unkey deployments with built-in observability tools including HTTP request logs, runtime application logs, and performance metrics. Deploying applications on Unkey is in public beta. To try it, open the product switcher in the top-left of the dashboard and select **Deploy**. During beta, deployed resources are free. We're eager for feedback, so let us know what you think on [Discord](https://unkey.com/discord), [X](https://x.com/unkeydev), or email [support@unkey.com](mailto:support@unkey.com). Every project on Unkey includes built-in observability so you can inspect what your app is doing without adding extra tooling. ## What you get Each project provides three types of observability data: * [Logs](/docs/observability/logs). Runtime output (stdout and stderr) from your app, with filtering by severity, deployment, environment, and message content * [Requests](/docs/observability/requests). Every HTTP request that passes through [Sentinel](/docs/platform/sentinel/overview) to your app, including method, path, status code, headers, bodies, and a full latency breakdown * [Metrics](/docs/observability/metrics). RPS and latency charts on each deployment's overview page ## Where to find it Navigate to your project in the dashboard. The **Logs** and **Requests** tabs are at the top of the project overview. Deployment-level metrics are on each deployment's detail page. ## Retention Runtime logs are retained for 90 days. Request logs are retained for 30 days. Metrics are available for the lifetime of the deployment. # Requests Source: https://unkey.com/docs/observability/requests Inspect every HTTP request passing through the Sentinel to your deployment. View headers, status codes, latency, and request body details. Deploying applications on Unkey is in public beta. To try it, open the product switcher in the top-left of the dashboard and select **Deploy**. During beta, deployed resources are free. We're eager for feedback, so let us know what you think on [Discord](https://unkey.com/discord), [X](https://x.com/unkeydev), or email [support@unkey.com](mailto:support@unkey.com). The **Requests** tab in your project shows every HTTP request that [Sentinel](/docs/platform/sentinel/overview) processes on behalf of your deployments. Use it to debug failed requests, trace slow responses, and understand traffic patterns. ## Request fields Each row in the table shows the timestamp, the region that handled the request, the HTTP status code (color-coded by class), the HTTP method, the hostname, the request path, and the total end-to-end latency. ## Filter requests Combine filters to narrow results by status code, HTTP method, path, deployment, environment, and time range. Filters persist in the URL, so you can bookmark or share a filtered view. ### Status code filter The status code filter lets you narrow requests by HTTP response status. You can filter by class or by a specific code. **Filter by class**, select one or more status code ranges to show all requests in that class: | Range | Label | | ----- | -------- | | 2xx | Success | | 3xx | Redirect | | 4xx | Warning | | 5xx | Error | Use the **Select All** checkbox to toggle every range at once. **Filter by specific code**, enter an exact status code (100-599) in the custom code input to isolate a single response type, for example `429` for rate-limited requests or `503` for service unavailable responses. When you type a custom code, range selections are cleared automatically. Click **Apply Filter** to update the results. Requests page Requests page ## Live mode Toggle **Live** in the top-right corner to stream incoming requests in real time. New requests appear at the top of the table as they arrive. Toggle it off to freeze the view and browse historical data. ## Request detail panel Click any row to open the detail panel on the right side. The panel shows: * Request headers and body * Response headers and body * Latency breakdown (see below) * Deployment info: deployment ID, git branch, commit SHA, commit author, commit message, environment, and deployment status * Meta: request ID, timestamp, client IP, user agent, host, region, query string, and query parameters ## Latency breakdown The detail panel splits total latency into two components. Instance latency is the time your app spent processing the request. Sentinel latency is the time Sentinel spent on routing, policy evaluation, and proxying. If instance latency dominates, the bottleneck is in your application code. If Sentinel latency is high relative to total latency, the overhead is in the gateway layer. This distinction helps you focus debugging in the right place. ## Retention Request logs are retained for 30 days. # Getting Started Source: https://unkey.com/docs/platform/analytics/getting-started Request access to Unkey Analytics and run your first SQL query against key verification data. Set up credentials and explore your data. ## Request Access **Analytics is currently in private beta and available by request only.** To get started: 1. **Find your workspace ID** in the Unkey dashboard settings 2. **Email us** at [support@unkey.com](mailto:support@unkey.com) with: * Your workspace ID * Your use case (billing, dashboards, reporting, etc.) * Expected query volume We'll enable analytics for your workspace and send you confirmation. ## Authentication Analytics queries require a root key with analytics permissions. Create one in your dashboard: 1. Go to **Settings** → **Root Keys** 2. Click **Create New Root Key** 3. Select permissions: `api.*.read_analytics` OR `api..read_analytics` 4. Copy and securely store your root key Root keys have powerful permissions. Store them securely and never commit them to version control. ## Your First Query Once you have access, execute your first analytics query using the `/v2/analytics.getVerifications` endpoint. ### Count Total Verifications Count the total number of key verifications in the last 7 days across all your keyspaces to get a high-level view of your overall usage volume. ```sql SQL theme={"theme":"kanagawa-wave"} SELECT SUM(count) as total FROM key_verifications_per_day_v1 WHERE time >= now() - INTERVAL 7 DAY ``` ```bash cURL theme={"theme":"kanagawa-wave"} curl -X POST https://api.unkey.com/v2/analytics.getVerifications \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "query": "SELECT SUM(count) as total FROM key_verifications_per_day_v1 WHERE time >= now() - INTERVAL 7 DAY" }' ``` ### Break Down by Outcome Group verifications by their outcome (`VALID`, `RATE_LIMITED`, `USAGE_EXCEEDED`, etc.) over the last 24 hours to understand the distribution of successful vs. failed requests. ```sql SQL theme={"theme":"kanagawa-wave"} SELECT outcome, SUM(count) as count FROM key_verifications_per_hour_v1 WHERE time >= now() - INTERVAL 24 HOUR GROUP BY outcome ORDER BY count DESC ``` ```bash cURL theme={"theme":"kanagawa-wave"} curl -X POST https://api.unkey.com/v2/analytics.getVerifications \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "query": "SELECT outcome, SUM(count) as count FROM key_verifications_per_hour_v1 WHERE time >= now() - INTERVAL 24 HOUR GROUP BY outcome ORDER BY count DESC" }' ``` ### Top Users by Usage Identify your most active users by counting their total verifications over the last 30 days to spot power users or potential abuse patterns. ```sql SQL theme={"theme":"kanagawa-wave"} SELECT external_id, SUM(count) as verifications FROM key_verifications_per_day_v1 WHERE time >= now() - INTERVAL 30 DAY GROUP BY external_id ORDER BY verifications DESC LIMIT 10 ``` ```bash cURL theme={"theme":"kanagawa-wave"} curl -X POST https://api.unkey.com/v2/analytics.getVerifications \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "query": "SELECT external_id, SUM(count) as verifications FROM key_verifications_per_day_v1 WHERE time >= now() - INTERVAL 30 DAY GROUP BY external_id ORDER BY verifications DESC LIMIT 10" }' ``` **Performance tip:** For longer time ranges, use pre-aggregated tables instead of the raw table: * `key_verifications_per_minute_v1` - For queries spanning hours * `key_verifications_per_hour_v1` - For queries spanning days * `key_verifications_per_day_v1` - For queries spanning weeks/months * `key_verifications_per_month_v1` - For queries spanning years Use `SUM(count)` instead of `COUNT(*)` with aggregated tables. They scan far fewer rows and are much faster. Check out the [Query Examples](/docs/platform/analytics/query-examples) page for 30+ ready-to-use queries covering billing, monitoring, and analytics use cases. ## Understanding the Response Analytics queries return data as an array of objects: ```json theme={"theme":"kanagawa-wave"} { "meta": { "requestId": "req_xxx" }, "data": [ { "outcome": "VALID", "count": 1234 }, { "outcome": "RATE_LIMITED", "count": 56 }, { "outcome": "USAGE_EXCEEDED", "count": 12 } ] } ``` Each object in the `data` array contains fields from your SELECT clause. The field names match the column names or aliases you specified in your query. ## Filtering by keyspace or user You can filter queries to specific keyspaces or users. Use `key_space_id` to filter by keyspace (find this identifier in your Keyspace Settings) and `external_id` to filter by user. These fields support standard SQL operators: `=`, `!=`, `IN`, `NOT IN`, `<`, `>`, etc. **Automatic filtering:** All queries are automatically filtered based on your root key's permissions: * **Workspace filtering:** All queries are scoped to your workspace. You **do not need** to filter by `workspace_id`. * **Keyspace filtering:** If your root key has `api..read_analytics` permissions (scoped to a specific keyspace), queries are automatically filtered to that keyspace's `key_space_id`. If your root key has `api.*.read_analytics` (all keyspaces), you should filter by `key_space_id` yourself to query specific keyspaces. Queries are subject to resource limits (execution time, memory, result size, and quota). See [Query Restrictions](/docs/platform/analytics/query-restrictions) for complete details on limits and error codes. # Overview Source: https://unkey.com/docs/platform/analytics/overview Query your API key verification data with SQL using Unkey Analytics. Run custom queries to analyze usage patterns, billing, and trends. **Analytics is currently in private beta and available by request only.** See [Getting Started](/docs/platform/analytics/getting-started) for access instructions. ## What is Unkey Analytics? Unkey Analytics provides a powerful SQL interface to query your API key verification data. Instead of building your own analytics pipeline, you can leverage Unkey's built-in data warehouse to: * **Build custom dashboards** for internal teams or end-users * **Power usage-based billing** by querying verification counts per user/organization * **Generate reports** on API usage patterns, top users, and performance metrics * **Monitor and alert** on verification outcomes, rate limits, and errors **Automatic filtering:** All queries are scoped to your workspace automatically. If your root key is scoped to a specific API (`api..read_analytics`), queries are also filtered to that API's `key_space_id`. ## How it Works Every key verification request is automatically stored and aggregated across multiple time-series tables: ```mermaid theme={"theme":"kanagawa-wave"} graph LR A[Verify Key Request] --> B[Raw Events Table] B --> C[Minute Aggregates] C --> D[Hour Aggregates] D --> E[Day Aggregates] E --> F[Month Aggregates] ``` You can query these tables using standard SQL to: * Aggregate verification counts by time period * Group by keyspace, user, or outcome * Filter by region, tags, or custom criteria * Calculate metrics for billing or monitoring ## Available Data Every verification event contains: | Field | Type | Description | | --------------- | ------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | | `request_id` | String | Unique identifier for each request | | `time` | Int64 | Unix millisecond timestamp | | `workspace_id` | String | Your workspace identifier (**automatically filtered** - you don't need to filter by this) | | `key_space_id` | String | Your KeySpace identifier (e.g., `ks_1234`). **Automatically filtered** if your root key is scoped to a single keyspace. | | `external_id` | String | Your user's identifier (e.g., `user_abc`) | | `key_id` | String | Individual key identifier | | `outcome` | String | Verification result: `VALID`, `RATE_LIMITED`, `INVALID`, `EXPIRED`, `DISABLED`, `INSUFFICIENT_PERMISSIONS`, `FORBIDDEN`, `USAGE_EXCEEDED` | | `region` | String | Unkey region that handled the verification | | `tags` | Array(String) | Custom tags added during verification | | `spent_credits` | Int64 | Number of credits spent on this verification (0 if no credits were spent) | ## Use Cases Usage-based billing and credit tracking API health and performance monitoring User behavior and engagement insights # Query Examples Source: https://unkey.com/docs/platform/analytics/query-examples Common SQL query patterns for Unkey Analytics including daily verification counts, per-key usage, billing aggregations, and error rates. This guide provides SQL query examples for common analytics scenarios covering all the use cases from the legacy API and more. All examples use ClickHouse SQL syntax and work with the `/v2/analytics.getVerifications` endpoint. ## Using Queries in API Requests When making API requests, you need to format the SQL query as a JSON string on a single line. Here's how: ```sql SQL theme={"theme":"kanagawa-wave"} SELECT COUNT(*) as total FROM key_verifications_v1 WHERE time >= now() - INTERVAL 7 DAY ``` ```bash cURL theme={"theme":"kanagawa-wave"} curl -X POST https://api.unkey.com/v2/analytics.getVerifications \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "query": "SELECT COUNT(*) as total FROM key_verifications_v1 WHERE time >= now() - INTERVAL 7 DAY" }' ``` Each example below shows both the readable multi-line SQL and the single-line JSON format you can copy directly into your API requests. ## Usage Analytics **Use this for:** High-level usage metrics, health monitoring, and trend analysis. **Key patterns:** Total counts, outcome breakdowns, time series analysis. ### Total verifications in the last 7 days Count total verifications across all keyspaces in the last 7 days. ```sql SQL theme={"theme":"kanagawa-wave"} SELECT SUM(count) as total_verifications FROM key_verifications_per_day_v1 WHERE time >= now() - INTERVAL 7 DAY ``` ```bash cURL theme={"theme":"kanagawa-wave"} curl -X POST https://api.unkey.com/v2/analytics.getVerifications \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "query": "SELECT SUM(count) as total_verifications FROM key_verifications_per_day_v1 WHERE time >= now() - INTERVAL 7 DAY" }' ``` ### Verifications by outcome Break down verifications by outcome to understand success vs failure rates. ```sql SQL theme={"theme":"kanagawa-wave"} SELECT outcome, SUM(count) as count FROM key_verifications_per_day_v1 WHERE time >= now() - INTERVAL 30 DAY GROUP BY outcome ORDER BY count DESC ``` ```bash cURL theme={"theme":"kanagawa-wave"} curl -X POST https://api.unkey.com/v2/analytics.getVerifications \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "query": "SELECT outcome, SUM(count) as count FROM key_verifications_per_day_v1 WHERE time >= now() - INTERVAL 30 DAY GROUP BY outcome ORDER BY count DESC" }' ``` ### All outcomes in a single row Get all verification outcomes in one row with individual columns for each outcome type. ```sql SQL theme={"theme":"kanagawa-wave"} SELECT sumIf(count, outcome = 'VALID') AS valid, sumIf(count, outcome = 'RATE_LIMITED') AS rateLimited, sumIf(count, outcome = 'INVALID') AS invalid, sumIf(count, outcome = 'NOT_FOUND') AS notFound, sumIf(count, outcome = 'FORBIDDEN') AS forbidden, sumIf(count, outcome = 'USAGE_EXCEEDED') AS usageExceeded, sumIf(count, outcome = 'UNAUTHORIZED') AS unauthorized, sumIf(count, outcome = 'DISABLED') AS disabled, sumIf(count, outcome = 'INSUFFICIENT_PERMISSIONS') AS insufficientPermissions, sumIf(count, outcome = 'EXPIRED') AS expired, SUM(count) AS total FROM key_verifications_per_day_v1 WHERE time >= now() - INTERVAL 30 DAY ``` ```bash cURL theme={"theme":"kanagawa-wave"} curl -X POST https://api.unkey.com/v2/analytics.getVerifications \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "query": "SELECT sumIf(count, outcome = '\''VALID'\'') AS valid, sumIf(count, outcome = '\''RATE_LIMITED'\'') AS rateLimited, sumIf(count, outcome = '\''INVALID'\'') AS invalid, sumIf(count, outcome = '\''NOT_FOUND'\'') AS notFound, sumIf(count, outcome = '\''FORBIDDEN'\'') AS forbidden, sumIf(count, outcome = '\''USAGE_EXCEEDED'\'') AS usageExceeded, sumIf(count, outcome = '\''UNAUTHORIZED'\'') AS unauthorized, sumIf(count, outcome = '\''DISABLED'\'') AS disabled, sumIf(count, outcome = '\''INSUFFICIENT_PERMISSIONS'\'') AS insufficientPermissions, sumIf(count, outcome = '\''EXPIRED'\'') AS expired, SUM(count) AS total FROM key_verifications_per_day_v1 WHERE time >= now() - INTERVAL 30 DAY" }' ``` ### All outcomes per key Get outcome breakdown for each API key in a single row per key. ```sql SQL theme={"theme":"kanagawa-wave"} SELECT key_id, sumIf(count, outcome = 'VALID') AS valid, sumIf(count, outcome = 'RATE_LIMITED') AS rateLimited, sumIf(count, outcome = 'INVALID') AS invalid, sumIf(count, outcome = 'NOT_FOUND') AS notFound, sumIf(count, outcome = 'FORBIDDEN') AS forbidden, sumIf(count, outcome = 'USAGE_EXCEEDED') AS usageExceeded, sumIf(count, outcome = 'UNAUTHORIZED') AS unauthorized, sumIf(count, outcome = 'DISABLED') AS disabled, sumIf(count, outcome = 'INSUFFICIENT_PERMISSIONS') AS insufficientPermissions, sumIf(count, outcome = 'EXPIRED') AS expired, SUM(count) AS total FROM key_verifications_per_day_v1 WHERE time >= now() - INTERVAL 30 DAY GROUP BY key_id ORDER BY total DESC LIMIT 100 ``` ```bash cURL theme={"theme":"kanagawa-wave"} curl -X POST https://api.unkey.com/v2/analytics.getVerifications \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "query": "SELECT key_id, sumIf(count, outcome = '\''VALID'\'') AS valid, sumIf(count, outcome = '\''RATE_LIMITED'\'') AS rateLimited, sumIf(count, outcome = '\''INVALID'\'') AS invalid, sumIf(count, outcome = '\''NOT_FOUND'\'') AS notFound, sumIf(count, outcome = '\''FORBIDDEN'\'') AS forbidden, sumIf(count, outcome = '\''USAGE_EXCEEDED'\'') AS usageExceeded, sumIf(count, outcome = '\''UNAUTHORIZED'\'') AS unauthorized, sumIf(count, outcome = '\''DISABLED'\'') AS disabled, sumIf(count, outcome = '\''INSUFFICIENT_PERMISSIONS'\'') AS insufficientPermissions, sumIf(count, outcome = '\''EXPIRED'\'') AS expired, SUM(count) AS total FROM key_verifications_per_day_v1 WHERE time >= now() - INTERVAL 30 DAY GROUP BY key_id ORDER BY total DESC LIMIT 100" }' ``` ### Daily verification trend Track daily verification patterns over the last 30 days. ```sql SQL theme={"theme":"kanagawa-wave"} SELECT time as date, SUM(count) as verifications FROM key_verifications_per_day_v1 WHERE time >= now() - INTERVAL 30 DAY GROUP BY date ORDER BY date ``` ```bash cURL theme={"theme":"kanagawa-wave"} curl -X POST https://api.unkey.com/v2/analytics.getVerifications \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "query": "SELECT time as date, SUM(count) as verifications FROM key_verifications_per_day_v1 WHERE time >= now() - INTERVAL 30 DAY GROUP BY date ORDER BY date" }' ``` ### Hourly breakdown for today Analyze hourly verification patterns for today with outcome breakdown. ```sql SQL theme={"theme":"kanagawa-wave"} SELECT time as hour, outcome, SUM(count) as verifications FROM key_verifications_per_hour_v1 WHERE time >= toStartOfDay(now()) GROUP BY time, outcome ORDER BY time, outcome ``` ```bash cURL theme={"theme":"kanagawa-wave"} curl -X POST https://api.unkey.com/v2/analytics.getVerifications \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "query": "SELECT time as hour, outcome, SUM(count) as verifications FROM key_verifications_per_hour_v1 WHERE time >= toStartOfDay(now()) GROUP BY time, outcome ORDER BY time, outcome" }' ``` ## Usage by User **Use this for:** Understanding user behavior, identifying power users, tracking user activity over time. **Key patterns:** User ranking, activity trends, specific user analysis. ### All users ranked by usage Rank all users by their total verification usage over the last 30 days. ```sql SQL theme={"theme":"kanagawa-wave"} SELECT external_id, SUM(count) as total_verifications, SUM(CASE WHEN outcome = 'VALID' THEN count ELSE 0 END) as successful, SUM(CASE WHEN outcome = 'RATE_LIMITED' THEN count ELSE 0 END) as rate_limited FROM key_verifications_per_day_v1 WHERE time >= now() - INTERVAL 30 DAY AND external_id != '' GROUP BY external_id ORDER BY total_verifications DESC LIMIT 100 ``` ```bash cURL theme={"theme":"kanagawa-wave"} curl -X POST https://api.unkey.com/v2/analytics.getVerifications \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "query": "SELECT external_id, SUM(count) as total_verifications, SUM(CASE WHEN outcome = '\''VALID'\'' THEN count ELSE 0 END) as successful, SUM(CASE WHEN outcome = '\''RATE_LIMITED'\'' THEN count ELSE 0 END) as rate_limited FROM key_verifications_per_day_v1 WHERE time >= now() - INTERVAL 30 DAY AND external_id != '\'''\'' GROUP BY external_id ORDER BY total_verifications DESC LIMIT 100" }' ``` ### Usage for a specific user Analyze usage patterns for a specific user over the last 30 days. ```sql SQL theme={"theme":"kanagawa-wave"} SELECT SUM(count) as total_verifications, SUM(CASE WHEN outcome = 'VALID' THEN count ELSE 0 END) as successful, SUM(CASE WHEN outcome = 'RATE_LIMITED' THEN count ELSE 0 END) as rate_limited FROM key_verifications_per_day_v1 WHERE external_id = 'user_123' AND time >= now() - INTERVAL 30 DAY ``` ```bash cURL theme={"theme":"kanagawa-wave"} curl -X POST https://api.unkey.com/v2/analytics.getVerifications \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "query": "SELECT SUM(count) as total_verifications, SUM(CASE WHEN outcome = '\''VALID'\'' THEN count ELSE 0 END) as successful, SUM(CASE WHEN outcome = '\''RATE_LIMITED'\'' THEN count ELSE 0 END) as rate_limited FROM key_verifications_per_day_v1 WHERE external_id = '\''user_123'\'' AND time >= now() - INTERVAL 30 DAY" }' ``` ### Top 10 users by API usage Identify your most active users by verification count. ```sql SQL theme={"theme":"kanagawa-wave"} SELECT external_id, SUM(count) as total_verifications FROM key_verifications_per_day_v1 WHERE time >= now() - INTERVAL 30 DAY AND external_id != '' GROUP BY external_id ORDER BY total_verifications DESC LIMIT 10 ``` ```bash cURL theme={"theme":"kanagawa-wave"} curl -X POST https://api.unkey.com/v2/analytics.getVerifications \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "query": "SELECT external_id, SUM(count) as total_verifications FROM key_verifications_per_day_v1 WHERE time >= now() - INTERVAL 30 DAY AND external_id != '\'''\'' GROUP BY external_id ORDER BY total_verifications DESC LIMIT 10" }' ``` ### Daily usage per user Track daily verification patterns for each user over 30 days. ```sql SQL theme={"theme":"kanagawa-wave"} SELECT external_id, time as date, SUM(count) as verifications FROM key_verifications_per_day_v1 WHERE time >= now() - INTERVAL 30 DAY GROUP BY external_id, date ORDER BY external_id, date ``` ```bash cURL theme={"theme":"kanagawa-wave"} curl -X POST https://api.unkey.com/v2/analytics.getVerifications \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "query": "SELECT external_id, time as date, SUM(count) as verifications FROM key_verifications_per_day_v1 WHERE time >= now() - INTERVAL 30 DAY GROUP BY external_id, date ORDER BY external_id, date" }' ``` ## Keyspace Analytics **Use this for:** Comparing keyspace performance, usage across different keyspaces, keyspace-specific analysis. **Key patterns:** Keyspace comparison, success rates, per-keyspace breakdowns. ### Usage per keyspace Compare usage across all keyspaces to identify most active endpoints. ```sql SQL theme={"theme":"kanagawa-wave"} SELECT key_space_id, SUM(count) as total_verifications, SUM(CASE WHEN outcome = 'VALID' THEN count ELSE 0 END) as successful FROM key_verifications_per_day_v1 WHERE time >= now() - INTERVAL 30 DAY GROUP BY key_space_id ORDER BY total_verifications DESC ``` ```bash cURL theme={"theme":"kanagawa-wave"} curl -X POST https://api.unkey.com/v2/analytics.getVerifications \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "query": "SELECT key_space_id, SUM(count) as total_verifications, SUM(CASE WHEN outcome = '\''VALID'\'' THEN count ELSE 0 END) as successful FROM key_verifications_per_day_v1 WHERE time >= now() - INTERVAL 30 DAY GROUP BY key_space_id ORDER BY total_verifications DESC" }' ``` ### Usage for a specific keyspace Analyze detailed usage patterns for a specific keyspace over 30 days. ```sql SQL theme={"theme":"kanagawa-wave"} SELECT SUM(count) as total_verifications, SUM(CASE WHEN outcome = 'VALID' THEN count ELSE 0 END) as successful, SUM(CASE WHEN outcome = 'RATE_LIMITED' THEN count ELSE 0 END) as rate_limited, SUM(CASE WHEN outcome = 'INVALID' THEN count ELSE 0 END) as invalid FROM key_verifications_per_day_v1 WHERE key_space_id = 'ks_1234' AND time >= now() - INTERVAL 30 DAY ``` ```bash cURL theme={"theme":"kanagawa-wave"} curl -X POST https://api.unkey.com/v2/analytics.getVerifications \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "query": "SELECT SUM(count) as total_verifications, SUM(CASE WHEN outcome = '\''VALID'\'' THEN count ELSE 0 END) as successful, SUM(CASE WHEN outcome = '\''RATE_LIMITED'\'' THEN count ELSE 0 END) as rate_limited, SUM(CASE WHEN outcome = '\''INVALID'\'' THEN count ELSE 0 END) as invalid FROM key_verifications_per_day_v1 WHERE key_space_id = '\''ks_1234'\'' AND time >= now() - INTERVAL 30 DAY" }' ``` ### Compare multiple keyspaces Calculate success rates for multiple keyspaces to compare performance. ```sql SQL theme={"theme":"kanagawa-wave"} SELECT key_space_id, SUM(count) as verifications, round(SUM(CASE WHEN outcome = 'VALID' THEN count ELSE 0 END) / SUM(count) * 100, 2) as success_rate FROM key_verifications_per_day_v1 WHERE key_space_id IN ('ks_1234', 'ks_5678') AND time >= now() - INTERVAL 7 DAY GROUP BY key_space_id ``` ```bash cURL theme={"theme":"kanagawa-wave"} curl -X POST https://api.unkey.com/v2/analytics.getVerifications \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "query": "SELECT key_space_id, SUM(count) as verifications, round(SUM(CASE WHEN outcome = '\''VALID'\'' THEN count ELSE 0 END) / SUM(count) * 100, 2) as success_rate FROM key_verifications_per_day_v1 WHERE key_space_id IN ('\''ks_1234'\'', '\''ks_5678'\'') AND time >= now() - INTERVAL 7 DAY GROUP BY key_space_id" }' ``` ## Key Analytics **Use this for:** Individual API key analysis, identifying problematic keys, key-specific usage patterns. **Key patterns:** Key ranking, error analysis, specific key monitoring. ### Usage per key Identify your most frequently used API keys over the last 30 days. ```sql SQL theme={"theme":"kanagawa-wave"} SELECT key_id, SUM(count) as total_verifications, SUM(CASE WHEN outcome = 'VALID' THEN count ELSE 0 END) as successful FROM key_verifications_per_day_v1 WHERE time >= now() - INTERVAL 30 DAY GROUP BY key_id ORDER BY total_verifications DESC LIMIT 100 ``` ```bash cURL theme={"theme":"kanagawa-wave"} curl -X POST https://api.unkey.com/v2/analytics.getVerifications \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "query": "SELECT key_id, SUM(count) as total_verifications, SUM(CASE WHEN outcome = '\''VALID'\'' THEN count ELSE 0 END) as successful FROM key_verifications_per_day_v1 WHERE time >= now() - INTERVAL 30 DAY GROUP BY key_id ORDER BY total_verifications DESC LIMIT 100" }' ``` ### Usage for a specific key Analyze detailed usage patterns for a specific API key. ```sql SQL theme={"theme":"kanagawa-wave"} SELECT SUM(count) as total_verifications, SUM(CASE WHEN outcome = 'VALID' THEN count ELSE 0 END) as successful, SUM(CASE WHEN outcome = 'RATE_LIMITED' THEN count ELSE 0 END) as rate_limited FROM key_verifications_per_day_v1 WHERE key_id = 'key_1234' AND time >= now() - INTERVAL 30 DAY ``` ```bash cURL theme={"theme":"kanagawa-wave"} curl -X POST https://api.unkey.com/v2/analytics.getVerifications \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "query": "SELECT SUM(count) as total_verifications, SUM(CASE WHEN outcome = '\''VALID'\'' THEN count ELSE 0 END) as successful, SUM(CASE WHEN outcome = '\''RATE_LIMITED'\'' THEN count ELSE 0 END) as rate_limited FROM key_verifications_per_day_v1 WHERE key_id = '\''key_1234'\'' AND time >= now() - INTERVAL 30 DAY" }' ``` ### Keys with most errors Find API keys that are generating the most errors. ```sql SQL theme={"theme":"kanagawa-wave"} SELECT key_id, SUM(count) as total_errors, groupArray(DISTINCT outcome) as error_types FROM key_verifications_per_day_v1 WHERE outcome != 'VALID' AND time >= now() - INTERVAL 7 DAY GROUP BY key_id ORDER BY total_errors DESC LIMIT 20 ``` ```bash cURL theme={"theme":"kanagawa-wave"} curl -X POST https://api.unkey.com/v2/analytics.getVerifications \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "query": "SELECT key_id, SUM(count) as total_errors, groupArray(DISTINCT outcome) as error_types FROM key_verifications_per_day_v1 WHERE outcome != '\''VALID'\'' AND time >= now() - INTERVAL 7 DAY GROUP BY key_id ORDER BY total_errors DESC LIMIT 20" }' ``` ## Tag-Based Analytics **Use this for:** Custom metadata filtering, endpoint analysis, user segmentation using tags. **Key patterns:** Tag filtering, endpoint breakdowns, custom attribute analysis. Tags allow you to add custom metadata to verification requests for filtering and aggregation. ### Filter by single tag Count verifications for requests with a specific tag. ```sql SQL theme={"theme":"kanagawa-wave"} SELECT SUM(count) as total FROM key_verifications_per_day_v1 WHERE has(tags, 'path=/api/v1/users') AND time >= now() - INTERVAL 7 DAY ``` ```bash cURL theme={"theme":"kanagawa-wave"} curl -X POST https://api.unkey.com/v2/analytics.getVerifications \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "query": "SELECT SUM(count) as total FROM key_verifications_per_day_v1 WHERE has(tags, '\''path=/api/v1/users'\'') AND time >= now() - INTERVAL 7 DAY" }' ``` ### Filter by multiple tags (OR) Count verifications matching any of multiple tags. ```sql SQL theme={"theme":"kanagawa-wave"} SELECT SUM(count) as total FROM key_verifications_per_day_v1 WHERE hasAny(tags, ['path=/api/v1/users', 'path=/api/v1/posts']) AND time >= now() - INTERVAL 7 DAY ``` ```bash cURL theme={"theme":"kanagawa-wave"} curl -X POST https://api.unkey.com/v2/analytics.getVerifications \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "query": "SELECT SUM(count) as total FROM key_verifications_per_day_v1 WHERE hasAny(tags, ['\''path=/api/v1/users'\'', '\''path=/api/v1/posts'\'']) AND time >= now() - INTERVAL 7 DAY" }' ``` ### Filter by multiple tags (AND) Count verifications matching all specified tags. ```sql SQL theme={"theme":"kanagawa-wave"} SELECT SUM(count) as total FROM key_verifications_per_day_v1 WHERE hasAll(tags, ['environment=production', 'team=backend']) AND time >= now() - INTERVAL 7 DAY ``` ```bash cURL theme={"theme":"kanagawa-wave"} curl -X POST https://api.unkey.com/v2/analytics.getVerifications \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "query": "SELECT SUM(count) as total FROM key_verifications_per_day_v1 WHERE hasAll(tags, ['\''environment=production'\'', '\''team=backend'\'']) AND time >= now() - INTERVAL 7 DAY" }' ``` ### Group by tag Aggregate verifications by individual tags to see usage patterns. ```sql SQL theme={"theme":"kanagawa-wave"} SELECT arrayJoin(tags) as tag, SUM(count) as verifications FROM key_verifications_per_day_v1 WHERE time >= now() - INTERVAL 7 DAY GROUP BY tag ORDER BY verifications DESC LIMIT 20 ``` ```bash cURL theme={"theme":"kanagawa-wave"} curl -X POST https://api.unkey.com/v2/analytics.getVerifications \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "query": "SELECT arrayJoin(tags) as tag, SUM(count) as verifications FROM key_verifications_per_day_v1 WHERE time >= now() - INTERVAL 7 DAY GROUP BY tag ORDER BY verifications DESC LIMIT 20" }' ``` ### Breakdown by endpoint (using path tag) Analyze request volume by API endpoint over the last 24 hours. This query uses the raw table for detailed tag analysis. For longer time ranges, consider using aggregated tables and pre-filtered tags. ```sql SQL theme={"theme":"kanagawa-wave"} SELECT arrayJoin(arrayFilter(x -> startsWith(x, 'path='), tags)) as endpoint, COUNT(*) as requests FROM key_verifications_v1 WHERE time >= now() - INTERVAL 24 HOUR GROUP BY endpoint ORDER BY requests DESC ``` ```bash cURL theme={"theme":"kanagawa-wave"} curl -X POST https://api.unkey.com/v2/analytics.getVerifications \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "query": "SELECT arrayJoin(arrayFilter(x -> startsWith(x, '\''path='\''), tags)) as endpoint, COUNT(*) as requests FROM key_verifications_v1 WHERE time >= now() - INTERVAL 24 HOUR GROUP BY endpoint ORDER BY requests DESC" }' ``` ## Billing & Usage-Based Pricing **Use this for:** Usage-based billing implementation, credit tracking, user tier calculation. **Key patterns:** Credit aggregation, billing cycles, tier determination, cost analysis. ### Monthly credits per user Calculate monthly credit consumption per user for billing. ```sql SQL theme={"theme":"kanagawa-wave"} SELECT external_id, toStartOfMonth(time) as month, SUM(spent_credits) as total_credits FROM key_verifications_per_day_v1 WHERE external_id != '' AND time >= toStartOfMonth(now()) GROUP BY external_id, month ORDER BY total_credits DESC ``` ```bash cURL theme={"theme":"kanagawa-wave"} curl -X POST https://api.unkey.com/v2/analytics.getVerifications \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "query": "SELECT external_id, toStartOfMonth(time) as month, SUM(spent_credits) as total_credits FROM key_verifications_per_day_v1 WHERE external_id != '\'''\'' AND time >= toStartOfMonth(now()) GROUP BY external_id, month ORDER BY total_credits DESC" }' ``` ### Current billing period credits Calculate credit usage for a specific billing period. ```sql SQL theme={"theme":"kanagawa-wave"} SELECT external_id, SUM(spent_credits) as credits_this_period FROM key_verifications_per_day_v1 WHERE external_id = 'user_123' AND time >= 1704067200000 -- Start of billing period (Unix millis) AND time < 1706745600000 -- End of billing period (Unix millis) GROUP BY external_id ``` ```bash cURL theme={"theme":"kanagawa-wave"} curl -X POST https://api.unkey.com/v2/analytics.getVerifications \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "query": "SELECT external_id, SUM(spent_credits) as credits_this_period FROM key_verifications_per_day_v1 WHERE external_id = '\''user_123'\'' AND time >= 1704067200000 AND time < 1706745600000 GROUP BY external_id" }' ``` ### Credit-based tier calculation Determine user tiers based on monthly credit consumption. ```sql SQL theme={"theme":"kanagawa-wave"} SELECT external_id, SUM(spent_credits) as total_credits, CASE WHEN total_credits <= 1000 THEN 'free' WHEN total_credits <= 10000 THEN 'starter' WHEN total_credits <= 100000 THEN 'pro' ELSE 'enterprise' END as tier FROM key_verifications_per_day_v1 WHERE time >= toStartOfMonth(now()) AND external_id = 'user_123' GROUP BY external_id ``` ```bash cURL theme={"theme":"kanagawa-wave"} curl -X POST https://api.unkey.com/v2/analytics.getVerifications \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "query": "SELECT external_id, SUM(spent_credits) as total_credits, CASE WHEN total_credits <= 1000 THEN '\''free'\'' WHEN total_credits <= 10000 THEN '\''starter'\'' WHEN total_credits <= 100000 THEN '\''pro'\'' ELSE '\''enterprise'\'' END as tier FROM key_verifications_per_day_v1 WHERE time >= toStartOfMonth(now()) AND external_id = '\''user_123'\'' GROUP BY external_id" }' ``` ### Daily credit usage and cost Track daily credit consumption and calculate estimated costs. ```sql SQL theme={"theme":"kanagawa-wave"} SELECT time as date, SUM(spent_credits) as credits_used, credits_used * 0.001 as estimated_cost -- $0.001 per credit FROM key_verifications_per_day_v1 WHERE external_id = 'user_123' AND time >= now() - INTERVAL 30 DAY GROUP BY date ORDER BY date ``` ```bash cURL theme={"theme":"kanagawa-wave"} curl -X POST https://api.unkey.com/v2/analytics.getVerifications \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "query": "SELECT time as date, SUM(spent_credits) as credits_used, credits_used * 0.001 as estimated_cost FROM key_verifications_per_day_v1 WHERE external_id = '\''user_123'\'' AND time >= now() - INTERVAL 30 DAY GROUP BY date ORDER BY date" }' ``` ## Advanced Queries **Use this for:** Complex analytical patterns, cohort analysis, moving averages, advanced insights. **Key patterns:** User retention, trend smoothing, complex joins, window functions. ### Cohort analysis: New vs returning users Perform cohort analysis to understand user retention patterns. ```sql SQL theme={"theme":"kanagawa-wave"} WITH first_seen AS ( SELECT external_id, min(time) as first_verification FROM key_verifications_per_day_v1 WHERE external_id != '' GROUP BY external_id ) SELECT toDate(kv.time) as date, SUM(CASE WHEN kv.time = fs.first_verification THEN kv.count ELSE 0 END) as new_users, SUM(CASE WHEN kv.time > fs.first_verification THEN kv.count ELSE 0 END) as returning_users FROM key_verifications_per_day_v1 kv JOIN first_seen fs ON kv.external_id = fs.external_id WHERE kv.time >= now() - INTERVAL 30 DAY GROUP BY date ORDER BY date ``` ```bash cURL theme={"theme":"kanagawa-wave"} curl -X POST https://api.unkey.com/v2/analytics.getVerifications \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "query": "WITH first_seen AS ( SELECT external_id, min(time) as first_verification FROM key_verifications_per_day_v1 WHERE external_id != '\'''\'' GROUP BY external_id ) SELECT toDate(kv.time) as date, SUM(CASE WHEN kv.time = fs.first_verification THEN kv.count ELSE 0 END) as new_users, SUM(CASE WHEN kv.time > fs.first_verification THEN kv.count ELSE 0 END) as returning_users FROM key_verifications_per_day_v1 kv JOIN first_seen fs ON kv.external_id = fs.external_id WHERE kv.time >= now() - INTERVAL 30 DAY GROUP BY date ORDER BY date" }' ``` ### Moving average (7-day) Calculate 7-day moving average to smooth out daily fluctuations. ```sql SQL theme={"theme":"kanagawa-wave"} SELECT date, verifications, avg(verifications) OVER ( ORDER BY date ROWS BETWEEN 6 PRECEDING AND CURRENT ROW ) as moving_avg_7d FROM ( SELECT time as date, SUM(count) as verifications FROM key_verifications_per_day_v1 WHERE time >= now() - INTERVAL 60 DAY GROUP BY date ) ORDER BY date ``` ```bash cURL theme={"theme":"kanagawa-wave"} curl -X POST https://api.unkey.com/v2/analytics.getVerifications \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "query": "SELECT date, verifications, avg(verifications) OVER ( ORDER BY date ROWS BETWEEN 6 PRECEDING AND CURRENT ROW ) as moving_avg_7d FROM ( SELECT time as date, SUM(count) as verifications FROM key_verifications_per_day_v1 WHERE time >= now() - INTERVAL 60 DAY GROUP BY date ) ORDER BY date" }' ``` ## Using Aggregated Tables For better performance on large time ranges, use pre-aggregated tables: ### Hourly aggregates Query hourly verification counts for the last 7 days. ```sql SQL theme={"theme":"kanagawa-wave"} SELECT time, SUM(count) as total FROM key_verifications_per_hour_v1 WHERE time >= toStartOfHour(now() - INTERVAL 7 DAY) GROUP BY time ORDER BY time ``` ```bash cURL theme={"theme":"kanagawa-wave"} curl -X POST https://api.unkey.com/v2/analytics.getVerifications \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "query": "SELECT time, SUM(count) as total FROM key_verifications_per_hour_v1 WHERE time >= toStartOfHour(now() - INTERVAL 7 DAY) GROUP BY time ORDER BY time" }' ``` ### Daily aggregates Query daily verification counts for the last 30 days. ```sql SQL theme={"theme":"kanagawa-wave"} SELECT time, SUM(count) as total FROM key_verifications_per_day_v1 WHERE time >= toStartOfDay(now() - INTERVAL 30 DAY) GROUP BY time ORDER BY time ``` ```bash cURL theme={"theme":"kanagawa-wave"} curl -X POST https://api.unkey.com/v2/analytics.getVerifications \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "query": "SELECT time, SUM(count) as total FROM key_verifications_per_day_v1 WHERE time >= toStartOfDay(now() - INTERVAL 30 DAY) GROUP BY time ORDER BY time" }' ``` ### Monthly aggregates Query monthly verification counts for the last year. ```sql SQL theme={"theme":"kanagawa-wave"} SELECT time, SUM(count) as total FROM key_verifications_per_month_v1 WHERE time >= toStartOfMonth(now() - INTERVAL 12 MONTH) GROUP BY time ORDER BY time ``` ```bash cURL theme={"theme":"kanagawa-wave"} curl -X POST https://api.unkey.com/v2/analytics.getVerifications \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "query": "SELECT time, SUM(count) as total FROM key_verifications_per_month_v1 WHERE time >= toStartOfMonth(now() - INTERVAL 12 MONTH) GROUP BY time ORDER BY time" }' ``` ## Filling Gaps in Time Series (WITH FILL) When querying time series data, you may have periods with no activity that result in missing time points. ClickHouse's `WITH FILL` clause ensures all time periods are included in results, filling gaps with zeros. `WITH FILL` is particularly useful for creating charts and visualizations where you need consistent time intervals, even when there's no data for some periods. `WITH FILL` only works when grouping by the time column alone. To include outcome breakdowns or other dimensions, use `sumIf()` to pivot them into columns (see the last example below). **Type matching:** The `time` column type varies by table: * **Hourly/Minute tables**: `DateTime` - use `toStartOfHour(now() - INTERVAL N HOUR)` * **Daily/Monthly tables**: `Date` - use `toDate(now() - INTERVAL N DAY)` or `toDate(toStartOfMonth(...))` WITH FILL expressions must match the column type exactly. ### Hourly data with gaps filled Get hourly verification counts for the last 7 days, including hours with zero activity. ```sql SQL theme={"theme":"kanagawa-wave"} SELECT time, SUM(count) as total FROM key_verifications_per_hour_v1 WHERE time >= toStartOfHour(now() - INTERVAL 7 DAY) AND time <= toStartOfHour(now()) GROUP BY time ORDER BY time ASC WITH FILL FROM toStartOfHour(now() - INTERVAL 7 DAY) TO toStartOfHour(now()) STEP INTERVAL 1 HOUR ``` ```bash cURL theme={"theme":"kanagawa-wave"} curl -X POST https://api.unkey.com/v2/analytics.getVerifications \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "query": "SELECT time, SUM(count) as total FROM key_verifications_per_hour_v1 WHERE time >= toStartOfHour(now() - INTERVAL 7 DAY) AND time <= toStartOfHour(now()) GROUP BY time ORDER BY time ASC WITH FILL FROM toStartOfHour(now() - INTERVAL 7 DAY) TO toStartOfHour(now()) STEP INTERVAL 1 HOUR" }' ``` ### Daily data with gaps filled Get daily verification counts for the last 30 days, ensuring all days are present. ```sql SQL theme={"theme":"kanagawa-wave"} SELECT time, SUM(count) as total FROM key_verifications_per_day_v1 WHERE time >= toDate(now() - INTERVAL 30 DAY) AND time <= toDate(now()) GROUP BY time ORDER BY time ASC WITH FILL FROM toDate(now() - INTERVAL 30 DAY) TO toDate(now()) STEP INTERVAL 1 DAY ``` ```bash cURL theme={"theme":"kanagawa-wave"} curl -X POST https://api.unkey.com/v2/analytics.getVerifications \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "query": "SELECT time, SUM(count) as total FROM key_verifications_per_day_v1 WHERE time >= toDate(now() - INTERVAL 30 DAY) AND time <= toDate(now()) GROUP BY time ORDER BY time ASC WITH FILL FROM toDate(now() - INTERVAL 30 DAY) TO toDate(now()) STEP INTERVAL 1 DAY" }' ``` ### Monthly data with gaps filled Get monthly verification counts for the last 12 months with all months included. ```sql SQL theme={"theme":"kanagawa-wave"} SELECT time, SUM(count) as total FROM key_verifications_per_month_v1 WHERE time >= toDate(toStartOfMonth(now() - INTERVAL 12 MONTH)) AND time <= toDate(toStartOfMonth(now())) GROUP BY time ORDER BY time ASC WITH FILL FROM toDate(toStartOfMonth(now() - INTERVAL 12 MONTH)) TO toDate(toStartOfMonth(now())) STEP INTERVAL 1 MONTH ``` ```bash cURL theme={"theme":"kanagawa-wave"} curl -X POST https://api.unkey.com/v2/analytics.getVerifications \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "query": "SELECT time, SUM(count) as total FROM key_verifications_per_month_v1 WHERE time >= toDate(toStartOfMonth(now() - INTERVAL 12 MONTH)) AND time <= toDate(toStartOfMonth(now())) GROUP BY time ORDER BY time ASC WITH FILL FROM toDate(toStartOfMonth(now() - INTERVAL 12 MONTH)) TO toDate(toStartOfMonth(now())) STEP INTERVAL 1 MONTH" }' ``` ### Filling gaps with aggregations For more complex queries that aggregate by outcome, use a subquery or pivot approach instead of WITH FILL with multiple GROUP BY columns. ```sql SQL theme={"theme":"kanagawa-wave"} -- Pivot outcomes into columns with all days filled SELECT time, sumIf(count, outcome = 'VALID') as valid, sumIf(count, outcome = 'RATE_LIMITED') as rate_limited, sumIf(count, outcome = 'INVALID') as invalid, SUM(count) as total FROM key_verifications_per_day_v1 WHERE time >= toDate(now() - INTERVAL 30 DAY) AND time <= toDate(now()) GROUP BY time ORDER BY time ASC WITH FILL FROM toDate(now() - INTERVAL 30 DAY) TO toDate(now()) STEP INTERVAL 1 DAY ``` ```bash cURL theme={"theme":"kanagawa-wave"} curl -X POST https://api.unkey.com/v2/analytics.getVerifications \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "query": "SELECT time, sumIf(count, outcome = '\''VALID'\'') as valid, sumIf(count, outcome = '\''RATE_LIMITED'\'') as rate_limited, sumIf(count, outcome = '\''INVALID'\'') as invalid, SUM(count) as total FROM key_verifications_per_day_v1 WHERE time >= toDate(now() - INTERVAL 30 DAY) AND time <= toDate(now()) GROUP BY time ORDER BY time ASC WITH FILL FROM toDate(now() - INTERVAL 30 DAY) TO toDate(now()) STEP INTERVAL 1 DAY" }' ``` `WITH FILL` only works when grouping by time alone. For outcome breakdowns, use `sumIf()` to pivot outcomes into separate columns as shown above. ## Tips for Efficient Queries 1. **Always filter by time** - Use indexes by including time filters 2. **Use aggregated tables** - Hourly/daily/monthly tables for longer ranges 3. **Add LIMIT clauses** - Prevent returning too much data 4. **Filter before grouping** - Use WHERE instead of HAVING when possible # Query Restrictions Source: https://unkey.com/docs/platform/analytics/query-restrictions Understand the limits, quotas, and permission requirements for running analytics queries in Unkey including row limits and time ranges. This page explains the restrictions, resource limits, and permissions for analytics queries. ### Only SELECT Allowed Only `SELECT` queries are permitted. All other SQL statement types return a `query_not_supported` error. **Allowed query patterns:** * `SELECT` statements * `WITH` (Common Table Expressions) * `UNION` * Subqueries * Joins * Aggregations * Window functions **Not allowed:** `INSERT`, `UPDATE`, `DELETE`, `DROP`, `ALTER`, `CREATE`, `TRUNCATE`, `GRANT`, `REVOKE` ### Table Access Control Only explicitly allowed analytics tables are accessible. Any attempt to access tables not on the allow list (including `system.*` or `information_schema.*`) will return an `invalid_table` error. ### Function Allow List Only explicitly approved functions are allowed. Any function not on this list will be rejected with an `invalid_function` error. #### Allowed Functions `count`, `sum`, `avg`, `min`, `max`, `any`, `groupArray`, `groupUniqArray`, `uniq`, `uniqExact`, `quantile`, `countIf` `now`, `now64`, `today`, `toDate`, `toDateTime`, `toDateTime64`, `toStartOfDay`, `toStartOfWeek`, `toStartOfMonth`, `toStartOfYear`, `toStartOfHour`, `toStartOfMinute`, `date_trunc`, `formatDateTime`, `fromUnixTimestamp64Milli`, `toUnixTimestamp64Milli`, `toIntervalDay`, `toIntervalWeek`, `toIntervalMonth`, `toIntervalYear`, `toIntervalHour`, `toIntervalMinute`, `toIntervalSecond`, `toIntervalMillisecond`, `toIntervalMicrosecond`, `toIntervalNanosecond`, `toIntervalQuarter` `lower`, `upper`, `substring`, `concat`, `length`, `trim`, `startsWith`, `endsWith` `round`, `floor`, `ceil`, `abs` `if`, `case`, `coalesce` `toString`, `toInt32`, `toInt64`, `toFloat64` `has`, `hasAny`, `hasAll`, `arrayJoin`, `arrayFilter` If you need a function that's not listed, please contact us at [support@unkey.com](mailto:support@unkey.com) and we'll review it for inclusion. ## Resource Limits To ensure fair usage and prevent abuse, queries are subject to resource limits: ### Execution Limits | Resource | Limit | Purpose | | ------------------------------- | --------------------- | ----------------------------- | | Max execution time | 30 seconds | Prevent long-running queries | | Max execution time (per window) | 1800 seconds (30 min) | Total execution time per hour | | Max memory usage | 1 GB | Prevent memory exhaustion | | Max result rows | 10 million | Limit result set size | ### Query Quotas | Quota | Limit | Window | | --------------------- | ----- | -------- | | Queries per workspace | 1000 | Per hour | If you need higher limits for your use case, please contact us at [support@unkey.com](mailto:support@unkey.com) with details about your specific requirements and expected query volume. ### Error Codes When limits are exceeded, you'll receive specific error codes: | Error Code | Description | Solution | | ----------------------------- | --------------------------------- | ---------------------------------------------------------- | | `query_execution_timeout` | Query took longer than 30 seconds | Add more filters, reduce time range, use aggregated tables | | `query_memory_limit_exceeded` | Query used more than 1GB memory | Reduce result set size, add LIMIT clause, use aggregation | | `query_result_rows_exceeded` | Query returned more than 10M rows | Add LIMIT clause, use aggregation, reduce time range | | `query_quota_exceeded` | Exceeded 1000 queries per hour | Wait for quota to reset, optimize query frequency | # Quick Reference Source: https://unkey.com/docs/platform/analytics/quick-reference Quick lookup for common Unkey Analytics SQL patterns, available tables, aggregate functions, and filtering syntax for verification data. # Analytics Quick Reference ## Essential Query Patterns ### Usage Analytics **Use for**: High-level usage metrics and health monitoring ```sql theme={"theme":"kanagawa-wave"} -- Total verifications (last 7 days) SELECT SUM(count) as total FROM key_verifications_per_day_v1 WHERE time >= now() - INTERVAL 7 DAY -- Verifications by outcome (last 30 days) SELECT outcome, SUM(count) as count FROM key_verifications_per_day_v1 WHERE time >= now() - INTERVAL 30 DAY GROUP BY outcome ORDER BY count DESC -- Daily usage trend (last 30 days) SELECT time as date, SUM(count) as verifications FROM key_verifications_per_day_v1 WHERE time >= now() - INTERVAL 30 DAY GROUP BY date ORDER BY date ``` ### User Analytics **Use for**: Understanding user behavior and identifying power users ```sql theme={"theme":"kanagawa-wave"} -- Top users by usage (last 30 days) SELECT external_id, SUM(count) as total_verifications FROM key_verifications_per_day_v1 WHERE time >= now() - INTERVAL 30 DAY AND external_id != '' GROUP BY external_id ORDER BY total_verifications DESC LIMIT 10 -- Specific user activity (last 30 days) SELECT SUM(count) as total_verifications, SUM(CASE WHEN outcome = 'VALID' THEN count ELSE 0 END) as successful FROM key_verifications_per_day_v1 WHERE external_id = 'user_123' AND time >= now() - INTERVAL 30 DAY ``` ### Keyspace Analytics **Use for**: Comparing keyspace performance and usage ```sql theme={"theme":"kanagawa-wave"} -- Usage per keyspace (last 30 days) SELECT key_space_id, SUM(count) as total_verifications FROM key_verifications_per_day_v1 WHERE time >= now() - INTERVAL 30 DAY GROUP BY key_space_id ORDER BY total_verifications DESC -- Keyspace success rate comparison (last 7 days) SELECT key_space_id, SUM(count) as verifications, round(SUM(CASE WHEN outcome = 'VALID' THEN count ELSE 0 END) / SUM(count) * 100, 2) as success_rate FROM key_verifications_per_day_v1 WHERE key_space_id IN ('ks_1234', 'ks_5678') AND time >= now() - INTERVAL 7 DAY GROUP BY key_space_id ``` ### Billing Queries **Use for**: Usage-based billing and credit tracking ```sql theme={"theme":"kanagawa-wave"} -- Monthly credits per user SELECT external_id, toStartOfMonth(time) as month, SUM(spent_credits) as total_credits FROM key_verifications_per_day_v1 WHERE external_id != '' AND time >= toStartOfMonth(now()) GROUP BY external_id, month ORDER BY total_credits DESC -- User tier calculation (current month) SELECT external_id, SUM(spent_credits) as total_credits, CASE WHEN total_credits <= 1000 THEN 'free' WHEN total_credits <= 10000 THEN 'starter' WHEN total_credits <= 100000 THEN 'pro' ELSE 'enterprise' END as tier FROM key_verifications_per_day_v1 WHERE time >= toStartOfMonth(now()) AND external_id = 'user_123' GROUP BY external_id ``` ### Tag-Based Filtering **Use for**: Custom metadata filtering and endpoint analysis ```sql theme={"theme":"kanagawa-wave"} -- Filter by single tag SELECT SUM(count) as total FROM key_verifications_per_day_v1 WHERE has(tags, 'path=/api/v1/users') AND time >= now() - INTERVAL 7 DAY -- Filter by multiple tags (OR) SELECT SUM(count) as total FROM key_verifications_per_day_v1 WHERE hasAny(tags, ['path=/api/v1/users', 'path=/api/v1/posts']) AND time >= now() - INTERVAL 7 DAY -- Group by endpoint (using path tags) SELECT arrayJoin(arrayFilter(x -> startsWith(x, 'path='), tags)) as endpoint, COUNT(*) as requests FROM key_verifications_v1 WHERE time >= now() - INTERVAL 24 HOUR GROUP BY endpoint ORDER BY requests DESC ``` ### Filling Gaps in Time Series **Use for**: Charts and visualizations that need consistent time intervals ```sql theme={"theme":"kanagawa-wave"} -- Daily data with all days included (even zero counts) SELECT time, SUM(count) as total FROM key_verifications_per_day_v1 WHERE time >= toDate(now() - INTERVAL 30 DAY) AND time <= toDate(now()) GROUP BY time ORDER BY time ASC WITH FILL FROM toDate(now() - INTERVAL 30 DAY) TO toDate(now()) STEP INTERVAL 1 DAY ``` See [Query Examples - WITH FILL](/docs/platform/analytics/query-examples#filling-gaps-in-time-series-with-fill) for hourly, daily, and monthly examples with outcome breakdowns. ## Table Selection Guide Choose the right table based on your time range: | Time Range | Recommended Table | When to Use | | --------------- | --------------------------------- | ----------------------------------------- | | **\< 1 hour** | `key_verifications_v1` | Real-time analysis, detailed debugging | | **\< 24 hours** | `key_verifications_per_minute_v1` | Hourly/daily trends, recent activity | | **\< 30 days** | `key_verifications_per_hour_v1` | Daily/weekly analysis, user behavior | | **\< 1 year** | `key_verifications_per_day_v1` | Monthly/quarterly reports, billing cycles | | **> 1 year** | `key_verifications_per_month_v1` | Annual trends, long-term analytics | **Performance Tips:** * Always filter by time first (uses indexes) * Use `SUM(count)` with aggregated tables, not `COUNT(*)` * Add `LIMIT` clauses to prevent large result sets * Filter before grouping when possible ## Common Filters **Automatic filtering:** All queries are automatically filtered based on your root key permissions: * **Workspace:** All queries are scoped to your workspace (no need to filter `workspace_id`) * **API:** If your root key is scoped to a specific API (`api..read_analytics`), queries are filtered to that API's `key_space_id`. With `api.*.read_analytics` permissions, filter by `key_space_id` yourself. ### Time Ranges ```sql theme={"theme":"kanagawa-wave"} -- Relative time ranges WHERE time >= now() - INTERVAL 7 DAY -- Last 7 days WHERE time >= now() - INTERVAL 24 HOUR -- Last 24 hours WHERE time >= toStartOfDay(now()) -- Today WHERE time >= toStartOfMonth(now()) -- This month ``` ### User & Keyspace Filters ```sql theme={"theme":"kanagawa-wave"} -- Specific user WHERE external_id = 'user_123' -- Multiple users WHERE external_id IN ('user_123', 'user_456') -- Specific keyspace WHERE key_space_id = 'ks_1234' -- Multiple keyspaces WHERE key_space_id IN ('ks_1234', 'ks_5678') ``` ### Tag Filters ```sql theme={"theme":"kanagawa-wave"} -- Has specific tag WHERE has(tags, 'environment=production') -- Has any of multiple tags WHERE hasAny(tags, ['team=backend', 'team=frontend']) -- Has all specified tags WHERE hasAll(tags, ['environment=prod', 'tier=premium']) ``` ### Outcome Filters ```sql theme={"theme":"kanagawa-wave"} -- Only successful verifications WHERE outcome = 'VALID' -- Only errors WHERE outcome != 'VALID' -- Specific error types WHERE outcome IN ('RATE_LIMITED', 'USAGE_EXCEEDED') ``` ## Need More Functions? → [ClickHouse Function Reference](https://clickhouse.com/docs/en/sql-reference/functions)\ → [ClickHouse SQL Documentation](https://clickhouse.com/docs/en/sql-reference) # Schema Reference Source: https://unkey.com/docs/platform/analytics/schema-reference Complete reference of tables, columns, and data types available in Unkey Analytics. Explore the verification and key event data schemas. Unkey Analytics stores verification events across multiple time-series tables for efficient querying. This reference documents all available tables and their columns. Use aggregated tables (`per_hour`, `per_day`, `per_month`) for queries spanning long time periods to improve performance. ## Raw Events Table The `key_verifications_v1` table contains individual verification events as they occur. ### Columns | Column | Type | Description | | --------------- | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | | `request_id` | String | Unique identifier for each verification request | | `time` | Int64 | Unix timestamp in milliseconds when verification occurred | | `workspace_id` | String | Workspace identifier (**automatically filtered** - you don't need to filter by this) | | `key_space_id` | String | Your KeySpace identifier (e.g., `ks_1234`). **Automatically filtered** if your root key is scoped to a single keyspace, otherwise filter this yourself. | | `external_id` | String | Your user's identifier (e.g., `user_abc`) - use this to filter by user | | `key_id` | String | Individual API key identifier | | `outcome` | String | Verification result (see [Outcome Values](#outcome-values)) | | `region` | String | Unkey region that handled the verification | | `tags` | Array(String) | Custom tags added during verification | | `spent_credits` | Int64 | Number of credits spent on this verification (0 if no credits were spent) | ### Outcome Values The `outcome` column contains one of these values: | Outcome | Description | | -------------------------- | --------------------------------------- | | `VALID` | Key is valid and verification succeeded | | `RATE_LIMITED` | Verification exceeded rate limit | | `INVALID` | Key not found or malformed | | `EXPIRED` | Key has expired | | `DISABLED` | Key is disabled | | `INSUFFICIENT_PERMISSIONS` | Key lacks required permissions | | `FORBIDDEN` | Operation not allowed for this key | | `USAGE_EXCEEDED` | Key has exceeded usage limit | ## Aggregated Tables Pre-aggregated tables provide better query performance for long time ranges. Each aggregated table includes outcome counts. ### Per Minute Table `key_verifications_per_minute_v1` - Aggregated by minute | Column | Type | Description | | --------------- | ------------- | -------------------------------------------------------------------------------------------------------------------------------- | | `time` | DateTime/Date | **DateTime** for minute/hour tables, **Date** for day/month tables | | `workspace_id` | String | Workspace identifier (**automatically filtered** - you don't need to filter by this) | | `key_space_id` | String | KeySpace identifier. **Automatically filtered** if your root key is scoped to a single keyspace, otherwise filter this yourself. | | `external_id` | String | Your user identifier | | `key_id` | String | API key identifier | | `outcome` | String | Verification outcome (VALID, RATE\_LIMITED, INVALID, etc.) | | `tags` | Array | Tags associated with verifications | | `count` | UInt64 | Total verification count for this aggregation | | `spent_credits` | UInt64 | Total credits spent | ### Per Hour Table `key_verifications_per_hour_v1` - Aggregated by hour. Same columns as per-minute table. ### Per Day Table `key_verifications_per_day_v1` - Aggregated by day. Same columns as per-minute table. ### Per Month Table `key_verifications_per_month_v1` - Aggregated by month. Same columns as per-minute table. ## Filtering by keyspace and user You can use your familiar identifiers directly in queries: * **`key_space_id`** - Your KeySpace ID (e.g., `ks_1234`). Find this in your Keyspace Settings. * **`external_id`** - Your user identifiers (e.g., `user_abc123`) from your application All standard comparison operators are supported: `=`, `!=`, `<`, `>`, `<=`, `>=`, `IN`, `NOT IN` ### Filter by keyspace ```sql theme={"theme":"kanagawa-wave"} SELECT COUNT(*) FROM key_verifications_v1 WHERE key_space_id = 'ks_1234' ``` ### Filter by User ```sql theme={"theme":"kanagawa-wave"} SELECT COUNT(*) FROM key_verifications_v1 WHERE external_id = 'user_abc123' ``` ### Multiple Values ```sql theme={"theme":"kanagawa-wave"} SELECT COUNT(*) FROM key_verifications_v1 WHERE key_space_id IN ('ks_1234', 'ks_5678') AND external_id IN ('user_abc', 'user_xyz') ``` ## Working with Tags Tags are stored as `Array(String)` and require array functions to query. ### Check if tag exists ```sql theme={"theme":"kanagawa-wave"} SELECT COUNT(*) FROM key_verifications_v1 WHERE has(tags, 'path=/api/users') ``` ### Check if any tag exists ```sql theme={"theme":"kanagawa-wave"} SELECT COUNT(*) FROM key_verifications_v1 WHERE hasAny(tags, ['environment=prod', 'environment=staging']) ``` ### Check if all tags exist ```sql theme={"theme":"kanagawa-wave"} SELECT COUNT(*) FROM key_verifications_v1 WHERE hasAll(tags, ['environment=production', 'team=backend']) ``` ### Extract and group by tags ```sql theme={"theme":"kanagawa-wave"} SELECT arrayJoin(tags) as tag, COUNT(*) as count FROM key_verifications_v1 WHERE time >= now() - INTERVAL 24 HOUR GROUP BY tag ORDER BY count DESC ``` ### Filter tags with pattern ```sql theme={"theme":"kanagawa-wave"} -- Get all tags starting with "path=" SELECT arrayJoin(arrayFilter(x -> startsWith(x, 'path='), tags)) as path, COUNT(*) as requests FROM key_verifications_v1 WHERE time >= now() - INTERVAL 24 HOUR GROUP BY path ``` ## Time Functions Timestamps are stored differently depending on the table: * **Raw table (`key_verifications_v1`)**: `time` is `Int64` (Unix milliseconds) * **Aggregated tables**: `time` is `DateTime` ### Current Time ```sql theme={"theme":"kanagawa-wave"} SELECT now() as current_datetime SELECT toUnixTimestamp(now()) * 1000 as current_millis ``` ### Time Ranges (Raw Table) For the raw `key_verifications_v1` table, compare `time` with millisecond timestamps: ```sql theme={"theme":"kanagawa-wave"} -- Last hour WHERE time >= toUnixTimestamp(now() - INTERVAL 1 HOUR) * 1000 -- Last 24 hours WHERE time >= toUnixTimestamp(now() - INTERVAL 24 HOUR) * 1000 -- Last 7 days WHERE time >= toUnixTimestamp(now() - INTERVAL 7 DAY) * 1000 -- Last 30 days WHERE time >= toUnixTimestamp(now() - INTERVAL 30 DAY) * 1000 -- This month WHERE time >= toUnixTimestamp(toStartOfMonth(now())) * 1000 -- Today WHERE time >= toUnixTimestamp(toStartOfDay(now())) * 1000 ``` ### Time Ranges (Aggregated Tables) For aggregated tables, use DateTime comparisons directly: ```sql theme={"theme":"kanagawa-wave"} -- Last 7 days WHERE time >= now() - INTERVAL 7 DAY -- This month WHERE time >= toStartOfMonth(now()) -- Today WHERE time >= toStartOfDay(now()) ``` ### Time Rounding (Raw Table) ```sql theme={"theme":"kanagawa-wave"} -- Round to start of hour SELECT toStartOfHour(toDateTime(time / 1000)) as hour -- Round to start of day SELECT toStartOfDay(toDateTime(time / 1000)) as day -- Round to start of month SELECT toStartOfMonth(toDateTime(time / 1000)) as month -- Convert to date SELECT toDate(toDateTime(time / 1000)) as date ``` ### Specific Date Ranges ```sql theme={"theme":"kanagawa-wave"} -- Between specific dates (Unix milliseconds) WHERE time >= 1704067200000 -- Jan 1, 2024 00:00:00 UTC AND time < 1735689600000 -- Jan 1, 2025 00:00:00 UTC ``` ## Common ClickHouse Functions ### Aggregate Functions | Function | Description | Example | | ----------- | ----------------- | ----------------------------------------------------------- | | `COUNT()` | Count rows | `SELECT COUNT(*) FROM key_verifications_v1` | | `SUM()` | Sum values | `SELECT SUM(valid_count) FROM key_verifications_per_day_v1` | | `AVG()` | Average | `SELECT AVG(spent_credits) FROM key_verifications_v1` | | `MIN()` | Minimum value | `SELECT MIN(time) FROM key_verifications_v1` | | `MAX()` | Maximum value | `SELECT MAX(time) FROM key_verifications_v1` | | `countIf()` | Conditional count | `SELECT countIf(outcome = 'VALID')` | | `uniq()` | Count distinct | `SELECT uniq(key_id) FROM key_verifications_v1` | ### String Functions | Function | Description | Example | | -------------- | -------------------- | ------------------------------------- | | `lower()` | Convert to lowercase | `WHERE lower(outcome) = 'valid'` | | `upper()` | Convert to uppercase | `WHERE upper(region) = 'US-EAST-1'` | | `concat()` | Concatenate strings | `SELECT concat(region, '-', outcome)` | | `substring()` | Extract substring | `SELECT substring(key_id, 1, 8)` | | `startsWith()` | Check prefix | `WHERE startsWith(key_id, 'key_')` | ### Array Functions | Function | Description | Example | | --------------- | ------------------ | ---------------------------------------------------- | | `has()` | Check element | `WHERE has(tags, 'environment=production')` | | `hasAny()` | Check any element | `WHERE hasAny(tags, ['team=backend', 'team=api'])` | | `hasAll()` | Check all elements | `WHERE hasAll(tags, ['environment=prod', 'tier=1'])` | | `arrayJoin()` | Expand array | `SELECT arrayJoin(tags) as tag` | | `arrayFilter()` | Filter array | `arrayFilter(x -> startsWith(x, 'path='), tags)` | | `length()` | Array length | `WHERE length(tags) > 0` | ### Math Functions | Function | Description | Example | | --------- | -------------- | ---------------------------------------------------------- | | `round()` | Round number | `SELECT round(AVG(spent_credits), 2)` | | `floor()` | Round down | `SELECT floor(spent_credits / 100) * 100 as credit_bucket` | | `ceil()` | Round up | `SELECT ceil(spent_credits)` | | `abs()` | Absolute value | `SELECT abs(difference)` | ### Conditional Functions | Function | Description | Example | | -------- | --------------- | --------------------------------------------------------------- | | `if()` | If-then-else | `SELECT if(outcome = 'VALID', 1, 0)` | | `CASE` | Multi-condition | `CASE WHEN outcome = 'VALID' THEN 'success' ELSE 'failure' END` | ## Performance Tips 1. **Always filter by time** - Use time-based WHERE clauses to leverage indexes 2. **Use aggregated tables** - Query hourly/daily/month tables for long ranges 3. **Limit result sets** - Add LIMIT clauses to prevent large results 4. **Filter before grouping** - Use WHERE instead of HAVING when possible 5. **Avoid SELECT \*** - Only select columns you need ## Query Limits | Resource | Limit | Error Code | | ---------------- | ---------- | ----------------------------- | | Execution time | 30 seconds | `query_execution_timeout` | | Memory usage | 1 GB | `query_memory_limit_exceeded` | | Rows to read | 10 million | `query_rows_limit_exceeded` | | Queries per hour | 1000 | `query_quota_exceeded` | See [Query Restrictions](/docs/platform/analytics/query-restrictions) for more details on query limits and restrictions. # Troubleshooting Source: https://unkey.com/docs/platform/analytics/troubleshooting Troubleshoot common Unkey Analytics issues including query timeouts, permission errors, missing data, memory limits, and syntax errors. # Analytics Troubleshooting ## Getting Empty Results? **Check these common causes:** ### Time Range Issues * Your workspace might be new with no verification data yet * Try a shorter time range: `WHERE time >= now() - INTERVAL 1 HOUR` * Verify your time filters are working: `SELECT MAX(time) as latest FROM key_verifications_v1` ### Filter Problems * Wrong `key_space_id` - Find your API ID in dashboard settings * Empty `external_id` values - Filter them out: `WHERE external_id != ''` * Case sensitivity - Check exact values: `SELECT DISTINCT external_id FROM key_verifications_v1 LIMIT 10` ### Data Availability * Analytics may have a few minutes delay * Check recent data: `SELECT COUNT(*) FROM key_verifications_v1 WHERE time >= now() - INTERVAL 1 HOUR` ## Queries Timing Out? **Try these optimizations:** ### Use Aggregated Tables ```sql theme={"theme":"kanagawa-wave"} -- Instead of raw table for long ranges: -- Slow for 7+ days SELECT COUNT(*) FROM key_verifications_v1 WHERE time >= now() - INTERVAL 7 DAY -- Fast for 7+ days SELECT SUM(count) FROM key_verifications_per_day_v1 WHERE time >= now() - INTERVAL 7 DAY ``` ### Add Time Filters First ```sql theme={"theme":"kanagawa-wave"} -- Always filter by time early (uses indexes) WHERE time >= now() - INTERVAL 7 DAY AND external_id = 'user_123' -- Additional filters after time ``` ### Limit Result Size ```sql theme={"theme":"kanagawa-wave"} -- Prevent large result sets SELECT external_id, SUM(count) as usage FROM key_verifications_per_day_v1 WHERE time >= now() - INTERVAL 30 DAY GROUP BY external_id ORDER BY usage DESC LIMIT 100 -- Add LIMIT for large datasets ``` ## API Errors? **Common fixes:** ### Missing Property 'query' ```json theme={"theme":"kanagawa-wave"} // Wrong field name {"sql": "SELECT COUNT(*) FROM key_verifications_v1"} // Correct field name {"query": "SELECT COUNT(*) FROM key_verifications_v1"} ``` See `invalid_input` error for API request format issues. ### Permission Denied * Ensure your root key has `api.*.read_analytics` or `api..read_analytics` * Check key is not expired or revoked * Verify workspace ID matches your key's permissions See the `forbidden` error for permission issues. ### Invalid Function Error * Check [Query Restrictions](/docs/platform/analytics/query-restrictions#function-allow-list) for allowed functions * Some ClickHouse functions are blocked for security * Use alternative approaches from [Quick Reference](/docs/platform/analytics/quick-reference) * See `invalid_analytics_function` error for details ### Invalid Table Error * Only analytics tables are accessible (no `system.*` or `information_schema.*`) * Use table names from [Schema Reference](/docs/platform/analytics/schema-reference) * See `invalid_analytics_table` error for details ### Query Not Supported * Only SELECT queries are allowed in analytics * INSERT, UPDATE, DELETE, etc. are blocked * See `invalid_analytics_query_type` error for details ## Performance Problems? **Optimization tips:** ### Choose Right Table * `< 1 hour`: `key_verifications_v1` (raw) * `< 24 hours`: `key_verifications_per_minute_v1` * `< 30 days`: `key_verifications_per_hour_v1` * `< 1 year`: `key_verifications_per_day_v1` * `> 1 year`: `key_verifications_per_month_v1` ### Filter Before Grouping ```sql theme={"theme":"kanagawa-wave"} -- Less efficient SELECT external_id, SUM(count) as usage FROM key_verifications_per_day_v1 GROUP BY external_id HAVING SUM(count) > 1000 -- More efficient SELECT external_id, SUM(count) as usage FROM key_verifications_per_day_v1 WHERE time >= now() - INTERVAL 30 DAY -- Filter first GROUP BY external_id HAVING SUM(count) > 1000 ``` ### Avoid SELECT \* * Only select columns you need * Reduces memory usage and network transfer For complete error reference, see the Error Documentation in the sidebar. If you continue having issues, contact us at [support@unkey.com](mailto:support@unkey.com) with your query and error details. # Example Source: https://unkey.com/docs/platform/apis/features/authorization/example Walk through a realistic RBAC example showing how to define roles, assign permissions to API keys, and verify access in your application. Let's look at an example app for allowing your users to manage domains. As part of the API, your users will be able to perform CRUD operations against domains or individual dns records. Users of our app can have the following permissions: * `domain.delete_domain` * `domain.dns.create_record` * `domain.dns.read_record` * `domain.dns.update_record` * `domain.dns.delete_record` * `domain.create_domain` * `domain.read_domain` * `domain.update_domain` Sign into your [dashboard](https://app.unkey.com). - For `Roles`, navigate to the `Authorization/Roles`. Default when navigating to `Authorization`. - For `Permissions`, navigate to the `Authorization/Permissions`. Create them in your `Authorization/Permissions` page. Use the button in the upper right. `+ Create new permission` Example permissions We define the following roles: * `admin`: An admin can do everything. * `dns.manager`: Can create, read, update and delete dns records but not access the domain itself. * `read-only`: Can read domain or dns record information. Create them in your `Authorization/Roles` page. Use the button in the upper right. `+ Create new role` Example roles For each role, we need to connect the permissions it should have. Admin roles dns.manager roles read-only roles Now that we have permissions and roles in place, we can connect them to keys. 1. In the sidebar, click on one of your keyspaces 2. Next click on keys in the expanded keyspace you selected. Breadcrumb Navigation 3. On the key you want to use, click on the action menu (`...`) at the end of that table row. 4. Select `Manage roles and permissions...` Unconnected roles and permissions 5. You can connect a role to your key by using the `Assign role` input. Let's give this key the `dns.manager` and `read-only` roles. Unconnected roles and permissions As you can see, the key now contains 2 `roles` and 5 `permissions` shown just above the Roles section: You can attach roles to a key when creating it by providing the role names as an array: ```bash theme={"theme":"kanagawa-wave"} curl -XPOST \ --url https://api.unkey.com/v2/keys.createKey \ -H "Authorization: Bearer ${ROOT_KEY}" \ -H "Content-Type: application/json" \ -d '{ "apiId": "${API_ID}", "roles": [ "role1", "role2", "role3" ] }' ``` See the [API reference](/docs/api-reference/overview) for details. Now you can verify this key and perform permission checks. [Read more](/docs/platform/apis/features/authorization/verifying) # Authorization Overview Source: https://unkey.com/docs/platform/apis/features/authorization/introduction Control what each API key can access using role-based access control (RBAC). Assign roles and fine-grained permissions to your keys. Authorization controls *what* an authenticated key can do. While verification answers "is this key valid?", authorization answers "can this key perform this action?" Unkey provides Role-Based Access Control (RBAC) that lets you: * Define **permissions** (like `documents.read`, `billing.write`) * Group permissions into **roles** (like `admin`, `editor`, `viewer`) * Attach roles or permissions directly to keys * Check permissions during verification ## When to use this Different customers get different feature access. Enterprise keys can do more than free-tier keys. Admin keys can delete resources, editor keys can modify, viewer keys can only read. Only keys with `beta.access` permission can use new features. Keys can only access specific resources: `project.123.read`, `project.456.write`. ## How it works Create permissions that map to actions in your app: `documents.read`, `documents.write`, `users.delete`. Group permissions into roles for easier management. An `editor` role might include `documents.read` and `documents.write`. When creating or updating keys, assign roles or direct permissions. Pass a permission query when verifying. Unkey checks if the key has the required permissions. ## Quick example ```typescript theme={"theme":"kanagawa-wave"} // When verifying, check for required permission try { const { meta, data } = await unkey.keys.verifyKey({ key: "sk_...", permissions: "documents.write", // Key must have this permission }); if (!data.valid) { // Either invalid key OR missing permission console.log(data.code); // "VALID", "FORBIDDEN", "INSUFFICIENT_PERMISSIONS", etc. } } catch (err) { console.error(err); return Response.json({ error: "Internal error" }, { status: 500 }); } ``` ## Permissions vs Roles | Concept | What it is | Example | | -------------- | ---------------------- | ----------------------------------------------------------- | | **Permission** | A specific action | `documents.read`, `billing.manage` | | **Role** | A group of permissions | `admin` = all permissions, `viewer` = read-only permissions | You can attach **either** (or both) directly to keys: * Attach roles when you want predefined access levels * Attach permissions directly for fine-grained control ## Next steps Set up your authorization structure in the dashboard Check permissions when verifying keys See a complete implementation # Roles and Permissions Source: https://unkey.com/docs/platform/apis/features/authorization/roles-and-permissions Create and manage RBAC roles and permissions in Unkey to control what each API key can access. Assign roles via dashboard, API, or SDK. In RBAC, roles represent a collection of permissions. Each role defines a set of actions or operations that a user with that role can perform. Permissions can be associated with various resources within your application, such as endpoints, data objects, or functionality. Common roles may include: * `Administrator`: Has full access to all resources and functionality. * `Editor`: Can create, read, update, and delete specific resources. * `Viewer`: Can only view resources but cannot modify them. ## Roles Creating, updating and deleting roles is available in the dashboard. ### Create 1. From the unkey dashboard [app.unkey.com](https://app.unkey.com). 2. Navigate to the `Authorization` section in the left sidebar. 3. Click `Create New Role`. 4. Enter a unique name for your role. 5. Enter a description for your role. (Optional) 6. Assign keys and permissions to the role. (Optional) 7. Click `Create new role`. After the role is created, you are forwarded and can update/delete the role or connect existing permissions. ### Update 1. From the unkey dashboard [app.unkey.com](https://app.unkey.com). 2. Navigate to the `Authorization` section in the left sidebar. 3. Click on the role you want to update. Optionally you can also click the action menu (`...`) to the right of the role. 4. Make changes to the role as needed. 5. Click `Update role`. ### Delete 1. From the unkey dashboard [app.unkey.com](https://app.unkey.com). 2. Navigate to the `Authorization` section in the left sidebar. 3. Click on the action menu (`...`) to the right of the role you want to delete. 4. Click `Delete role` in the popup menu. 5. Toggle the checkbox confirming the deletion. 6. Click `Delete role` button. ## Permissions Creating, updating and deleting permissions is available in the dashboard. ### Create 1. From the unkey dashboard [app.unkey.com](https://app.unkey.com). 2. Navigate to the `Authorization` section in the left sidebar. 3. Click on `Permissions` in the left sidebar dropdown under the `Authorization` section. 4. Click `Create New Permission`. 5. Enter a human readable name for your permission. 6. Enter a unique identifier slug. 7. Enter a description for your permission. (Optional) 8. Click `Create new permission`. ### Update 1. From the unkey dashboard [app.unkey.com](https://app.unkey.com). 2. Navigate to the `Authorization` section in the left sidebar. 3. Click on `Permissions` in the left sidebar dropdown under the `Authorization` section. 4. Click on the permission you want to update. Optionally you can also click the action menu (`...`) to the right of the permission. 5. Make changes to the name, slug, and description as needed. 6. Click `Update permission`. ### Delete 1. From the unkey dashboard [app.unkey.com](https://app.unkey.com). 2. Navigate to the `Authorization` section in the left sidebar. 3. Click on `Permissions` in the left sidebar dropdown under the `Authorization` section. 4. Click on the action menu (`...`) to the right of the permission you want to delete. 5. Click `Delete permission`. 6. Toggle the checkbox confirming the deletion. 7. Click `Delete permission` button. ## Connecting roles and permissions After you have created at least 1 role and 1 permission, you can start associating them with each other. 1. From the unkey dashboard [app.unkey.com](https://app.unkey.com). 2. Navigate to the `Authorization` section in the left sidebar. 3. Click on the role you want to update. Optionally you can also click the action menu (`...`) to the right of the role. 4. Select the `Add permissions` input. Each item you select will be added to the selected permissions list below the input. This can be repeated to connect multiple permissions. 5. Click `Update role`. Role Update Dialog ## Connecting roles to keys 1. In the sidebar, click on one of your keyspaces. 2. In the sub menu, click on Keys. Sidebar Navigation 3. Select one of your existing keys by clicking the action menu (`...`) to the right of the key. 4. Select the `Manage roles and Permissions...` option. 5. Select a `role` or `permission` from appropriate input. 6. Click `Update key`. Key Authorization Update Dialog ## Creating keys When a user of your app creates a new key, you can attach zero, one or multiple previously created roles to the key. ```bash theme={"theme":"kanagawa-wave"} curl -XPOST \ --url https://api.unkey.com/v2/keys.createKey \ -H "Authorization: Bearer ${ROOT_KEY}" \ -H "Content-Type: application/json" \ -d '{ "apiId": "${API_ID}", "roles": [ "role1", "role2", "role3" ] }' ``` See the [API reference](/docs/api-reference/overview) for details. # Verifying Permissions Source: https://unkey.com/docs/platform/apis/features/authorization/verifying Verify that an API key has the required permissions during key verification. Use permission queries to enforce fine-grained access control. When verifying a key, you can check if it has specific permissions. If the key lacks the required permissions, verification fails with `code: INSUFFICIENT_PERMISSIONS`. ## Basic permission check Pass a permission string to verify: ```bash cURL theme={"theme":"kanagawa-wave"} curl -X POST https://api.unkey.com/v2/keys.verifyKey \ -H "Authorization: Bearer $UNKEY_ROOT_KEY" \ -H "Content-Type: application/json" \ -d '{ "key": "sk_...", "permissions": "documents.read" }' ``` ```typescript TypeScript theme={"theme":"kanagawa-wave"} try { const { data } = await unkey.keys.verifyKey({ key: "sk_...", permissions: "documents.read", }); if (!data.valid) { if (data.code === "INSUFFICIENT_PERMISSIONS") { // Key is valid but lacks the permission return Response.json( { error: "Insufficient permissions" }, { status: 403 }, ); } return Response.json({ error: data.code }, { status: 401 }); } // Key is valid - continue with your API logic } catch (err) { console.error(err); return Response.json({ error: "Internal error" }, { status: 500 }); } ``` ## Permission query syntax Unkey supports logical operators for complex permission checks: ### Single permission ```json theme={"theme":"kanagawa-wave"} { "permissions": "documents.read" } ``` Key must have `documents.read`. ### AND (all required) ```json theme={"theme":"kanagawa-wave"} { "permissions": "documents.read AND documents.write" } ``` Key must have **both** permissions. ### OR (any required) ```json theme={"theme":"kanagawa-wave"} { "permissions": "admin OR editor" } ``` Key must have **at least one** of the permissions. ### Complex queries with parentheses ```json theme={"theme":"kanagawa-wave"} { "permissions": "admin OR (documents.read AND documents.write)" } ``` Key must have `admin` **OR** have both `documents.read` and `documents.write`. ### Real-world example ```typescript theme={"theme":"kanagawa-wave"} // User wants to delete a document // They need: admin OR (documents.delete AND owner of this document) try { const { data } = await unkey.keys.verifyKey({ key: request.apiKey, permissions: "admin OR documents.delete", }); if (!data.valid) { return Response.json({ error: data.code }, { status: 401 }); } // Key is valid - continue with your API logic } catch (err) { console.error(err); return Response.json({ error: "Internal error" }, { status: 500 }); } ``` ## Response structure Successful verification with permissions: ```json theme={"theme":"kanagawa-wave"} { "meta": { "requestId": "req_..." }, "data": { "valid": true, "code": "VALID", "keyId": "key_...", "permissions": ["documents.read", "documents.write", "users.view"] } } ``` Failed permission check: ```json theme={"theme":"kanagawa-wave"} { "meta": { "requestId": "req_..." }, "data": { "valid": false, "code": "INSUFFICIENT_PERMISSIONS", "keyId": "key_..." } } ``` ## Manual permission checking Sometimes you need to check permissions after loading data from your database (e.g., checking if the user owns a resource). In these cases: Verify without permission requirements to get the key's permissions list. ```typescript theme={"theme":"kanagawa-wave"} try { const { data } = await unkey.keys.verifyKey({ key: "sk_...", }); if (!data.valid) { return unauthorized(); } const permissions = data.permissions ?? []; // Continue with your API logic } catch (err) { console.error(err); return Response.json({ error: "Internal error" }, { status: 500 }); } ``` Query your database for the resource. ```typescript theme={"theme":"kanagawa-wave"} const document = await db.documents.find(documentId); ``` Use the permissions array and your data to make the decision. ```typescript theme={"theme":"kanagawa-wave"} const canDelete = permissions.includes("admin") || (permissions.includes("documents.delete") && document.ownerId === data.identity?.externalId); if (!canDelete) { return forbidden(); } ``` ## Wildcard permissions Permissions support wildcards for broader access: ```typescript theme={"theme":"kanagawa-wave"} // Key has: ["documents.*"] // This grants: documents.read, documents.write, documents.delete, etc. try { const { data } = await unkey.keys.verifyKey({ key: "sk_...", permissions: "documents.read", // ✅ Passes (matched by documents.*) }); if (!data.valid) { return Response.json({ error: data.code }, { status: 401 }); } // Key is valid - continue with your API logic } catch (err) { console.error(err); return Response.json({ error: "Internal error" }, { status: 500 }); } ``` Common wildcard patterns: * `*`, All permissions (use carefully!) * `documents.*`, All document permissions * `api.v1.*`, All v1 API permissions ## Best practices Instead of `admin`, define specific permissions like `users.delete`, `billing.manage`. This gives you more control and better audit trails. Don't just check in the UI, always verify permissions server-side during API requests. Instead of attaching 10 permissions to every key, create a role and attach that. Easier to manage and update. ## Next steps Create your authorization structure See a complete implementation # Disabling Keys Source: https://unkey.com/docs/platform/apis/features/enabled Temporarily disable API keys without deleting them. Disabled keys fail verification immediately and can be re-enabled at any time in Unkey. Disabling a key makes it invalid for verification without permanently deleting it. The key can be re-enabled at any time, restoring full access. ## When to use this Customer's payment failed, disable their key until billing is resolved. Investigate potential abuse without losing the key's configuration. Temporarily block access during system updates. Suspend a user's API access while keeping their key for potential reactivation. ## Disable a key ```bash cURL theme={"theme":"kanagawa-wave"} 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 }' ``` ```typescript TypeScript theme={"theme":"kanagawa-wave"} import { Unkey } from "@unkey/api"; const unkey = new Unkey({ rootKey: process.env.UNKEY_ROOT_KEY }); try { const { meta, data } = await unkey.keys.updateKey({ keyId: "key_...", enabled: false, }); } catch (err) { console.error(err); return Response.json({ error: "Internal error" }, { status: 500 }); } ``` ## Verification response When a disabled key is verified, it returns `valid: false` with code `DISABLED`: ```json theme={"theme":"kanagawa-wave"} { "meta": { "requestId": "req_..." }, "data": { "valid": false, "code": "DISABLED", "keyId": "key_...", "enabled": false } } ``` ## Re-enable a key ```bash cURL theme={"theme":"kanagawa-wave"} 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": true }' ``` ```typescript TypeScript theme={"theme":"kanagawa-wave"} try { const { meta, data } = await unkey.keys.updateKey({ keyId: "key_...", enabled: true, }); } catch (err) { console.error(err); return Response.json({ error: "Internal error" }, { status: 500 }); } ``` ## Disabled vs Deleted | Action | Disabled | Deleted | | -------------------- | ----------------------- | ------------------------ | | Key verifies? | ❌ No (`code: DISABLED`) | ❌ No (`code: NOT_FOUND`) | | Can be restored? | ✅ Yes | ❌ No | | Keeps configuration? | ✅ Yes | ❌ No | | Keeps analytics? | ✅ Yes | ⚠️ Limited | Use disabling for temporary blocks. Only delete keys when you're sure the user won't need them again. # Environments Source: https://unkey.com/docs/platform/apis/features/environments Separate your API keys into live and test environments in Unkey. Isolate production traffic from development with environment-scoped keys. Environments let you issue distinct keys for development, staging, and production. Test keys can't affect real resources, while live keys have full access. ## When to use this Developers can test integrations without touching production data. CI/CD pipelines can run tests with keys that won't affect live resources. Test keys might have lower rate limits or be free to use. Prefixes like `sk_test_` make it obvious when using non-production keys. ## How it works Environments are implemented using key metadata and prefixes, there's no special "environment" field. This gives you full flexibility to model environments however you want. Common pattern: * **Prefix**: `sk_test_` vs `sk_live_` (visible to users) * **Metadata**: `{ "environment": "test" }` (returned on verification) ## Create environment-specific keys ```bash Test key theme={"theme":"kanagawa-wave"} 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_test", "meta": { "environment": "test" }, "ratelimits": [{ "name": "requests", "limit": 100, "duration": 60000 }] }' ``` ```bash Live key theme={"theme":"kanagawa-wave"} 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", "meta": { "environment": "production" }, "ratelimits": [{ "name": "requests", "limit": 1000, "duration": 60000 }] }' ``` Using prefixes makes it obvious to users which environment a key is for. This prevents accidental use of test keys in production (or worse, live keys in tests). ## Verify and check environment The metadata is returned during verification, so your API can behave differently: ```bash theme={"theme":"kanagawa-wave"} curl -X POST https://api.unkey.com/v2/keys.verifyKey \ -H "Content-Type: application/json" \ -d '{ "key": "sk_test_abc123..." }' ``` Response: ```json theme={"theme":"kanagawa-wave"} { "meta": { "requestId": "req_..." }, "data": { "valid": true, "keyId": "key_...", "meta": { "environment": "test" } } } ``` ## Handle environments in your API ```typescript theme={"theme":"kanagawa-wave"} try { const { data } = await unkey.keys.verifyKey({ key: request.apiKey }); if (!data.valid) { return unauthorized(); } const environment = data.meta?.environment ?? "production"; if (environment === "test") { // Use test database, sandbox resources, etc. return handleTestRequest(request); } // Production request return handleLiveRequest(request); } catch (err) { console.error(err); return Response.json({ error: "Internal error" }, { status: 500 }); } ``` ## Common patterns ### Different rate limits per environment ```typescript theme={"theme":"kanagawa-wave"} // Test: 100/min, Live: 10,000/min const ratelimits = environment === "test" ? [{ name: "requests", limit: 100, duration: 60000 }] : [{ name: "requests", limit: 10000, duration: 60000 }]; try { const { meta, data } = await unkey.keys.createKey({ apiId, prefix: environment === "test" ? "sk_test" : "sk_live", meta: { environment }, ratelimits, }); } catch (err) { console.error(err); return Response.json({ error: "Internal error" }, { status: 500 }); } ``` ### Sandbox vs real resources ```typescript theme={"theme":"kanagawa-wave"} if (data.meta?.environment === "test") { // Return mock data or use sandbox return { charged: false, response: mockResponse, }; } // Real billing, real data try { await chargeUser(data.identity.externalId, amount); return { charged: true, response: realResponse }; } catch (err) { console.error(err); return Response.json({ error: "Internal error" }, { status: 500 }); } ``` ### Block test keys from production endpoint ```typescript theme={"theme":"kanagawa-wave"} if (data.meta?.environment === "test" && isProductionEndpoint) { return Response.json( { error: "Test keys cannot access production endpoints" }, { status: 403 }, ); } // Continue with your API logic ``` ## Next steps Learn about key metadata and features Set different limits per environment # Key Rate Limits Source: https://unkey.com/docs/platform/apis/features/ratelimiting/overview Attach rate limits directly to individual API keys in Unkey. Configure request limits, refill intervals, and burst allowances per key. Unkey's ratelimiting system controls the number of requests a key can make within a given timeframe. This prevents abuse, protects your API from being overwhelmed, and enables usage-based billing models. ## Multi-Ratelimit System Unkey supports multiple named ratelimits per key, providing fine-grained control over different aspects of your API usage. You can define separate limits for different operations, time windows, and use cases within a single key. The system offers two types of ratelimits: auto-apply ratelimits that check automatically during verification, and manual ratelimits that require explicit specification in requests. ## Auto-Apply vs Manual Ratelimits Auto-apply ratelimits (`autoApply: true`) are checked automatically during every key verification without needing explicit specification. These work well for general usage limits that should always be enforced. ```bash theme={"theme":"kanagawa-wave"} # Create a key with auto-apply ratelimit curl -X POST https://api.unkey.com/v2/keys.createKey \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "apiId": "api_123", "ratelimits": [ { "name": "general-requests", "limit": 100, "duration": 60000, "autoApply": true } ] }' ``` ```bash theme={"theme":"kanagawa-wave"} # Verify key - auto-apply ratelimits are checked automatically curl -X POST https://api.unkey.com/v2/keys.verifyKey \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "key": "your_api_key_here" }' ``` Manual ratelimits (`autoApply: false`) must be explicitly specified in verification requests. Use these for operation-specific limits that only apply to certain endpoints. ```bash theme={"theme":"kanagawa-wave"} # Create a key with manual ratelimit curl -X POST https://api.unkey.com/v2/keys.createKey \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "apiId": "api_123", "ratelimits": [ { "name": "expensive-operations", "limit": 5, "duration": 60000 } ] }' ``` ```bash theme={"theme":"kanagawa-wave"} # Verify key - override cost explicitly curl -X POST https://api.unkey.com/v2/keys.verifyKey \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "key": "your_api_key_here", "ratelimits": [ { "name": "expensive-operations", "cost": 2 } ] }' ``` ## Configuration Configure ratelimits on a per-key or per-identity basis through the dashboard or API. Each ratelimit requires a unique name, limit count, and duration in milliseconds. ```bash theme={"theme":"kanagawa-wave"} curl -X POST https://api.unkey.com/v2/keys.createKey \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "apiId": "api_123", "ratelimits": [ { "name": "requests", "limit": 100, "duration": 60000, "autoApply": true }, { "name": "daily-quota", "limit": 1000, "duration": 86400000, "autoApply": true } ] }' ``` You can apply different costs to operations by specifying a cost parameter during verification. This allows resource-intensive operations to consume more of the rate limit than simple operations. ## Identity-Level Ratelimits Configure ratelimits at the identity level to share limits across multiple keys. Identity ratelimits work alongside key-level ratelimits, with key-level settings taking precedence for naming conflicts. ```bash theme={"theme":"kanagawa-wave"} curl -X POST https://api.unkey.com/v2/identities.createIdentity \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "externalId": "user_123", "ratelimits": [ { "name": "user-requests", "limit": 1000, "duration": 3600000 } ] }' ``` This approach works well for user-based quotas where multiple API keys should share the same usage limits. # Auto-Refill Source: https://unkey.com/docs/platform/apis/features/refill Automatically restore usage credits for API keys on a daily or monthly schedule. Configure auto-refill amounts and intervals in Unkey. Auto-refill works with [usage limits](/docs/platform/apis/features/remaining) to automatically restore a key's credits on a schedule. Perfect for subscription models where users get a fresh allocation each billing period. ## When to use this "Pro plan: 50,000 requests/month", credits reset on the 1st of each month. Free tier users get 100 requests per day, resets at midnight UTC. Automatically restore credits without manual intervention. ## How it works 1. You create a key with `credits.remaining` and `credits.refill` configured 2. Unkey automatically refills the credits on your chosen schedule 3. **Daily refills** trigger at midnight UTC 4. **Monthly refills** trigger on a specific day of the month (default: 1st) Refill **replaces** the current credit balance, it doesn't add to it. A key with 50 remaining credits will have exactly 1000 after a refill of 1000, not 1050\. ## Create a key with auto-refill ### Monthly refill (most common) Key gets 10,000 credits that reset on the 1st of each month: ```bash cURL theme={"theme":"kanagawa-wave"} 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 } } }' ``` ```typescript TypeScript theme={"theme":"kanagawa-wave"} try { const { meta, data } = await unkey.keys.createKey({ apiId: "api_...", credits: { remaining: 10000, refill: { interval: "monthly", amount: 10000, }, }, }); console.log(data.keyId); } catch (error) { console.error(error); return Response.json({ error: "Internal error" }, { status: 500 }); } ``` ### Monthly refill on a specific day If your billing cycle is mid-month, set `refillDay`: ```json theme={"theme":"kanagawa-wave"} { "apiId": "api_...", "credits": { "remaining": 10000, "refill": { "interval": "monthly", "amount": 10000, "refillDay": 15 } } } ``` This refills on the 15th of each month. For months with fewer days (e.g., February), refills on the last day of the month if `refillDay` exceeds the month's length. ### Daily refill Key gets 100 credits that reset every night at midnight UTC: ```bash cURL theme={"theme":"kanagawa-wave"} 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": 100, "refill": { "interval": "daily", "amount": 100 } } }' ``` ```typescript TypeScript theme={"theme":"kanagawa-wave"} try { const { meta, data } = await unkey.keys.createKey({ apiId: "api_...", credits: { remaining: 100, refill: { interval: "daily", amount: 100, }, }, }); console.log(data.keyId); } catch (error) { console.error(error); return Response.json({ error: "Internal error" }, { status: 500 }); } ``` ## Update refill settings Change an existing key's refill configuration: ```bash theme={"theme":"kanagawa-wave"} curl -X POST https://api.unkey.com/v2/keys.updateKey \ -H "Authorization: Bearer $UNKEY_ROOT_KEY" \ -H "Content-Type: application/json" \ -d '{ "keyId": "key_...", "credits": { "remaining": 50000, "refill": { "interval": "monthly", "amount": 50000 } } }' ``` ## Disable refill To stop auto-refill but keep the current credits: ```bash theme={"theme":"kanagawa-wave"} curl -X POST https://api.unkey.com/v2/keys.updateKey \ -H "Authorization: Bearer $UNKEY_ROOT_KEY" \ -H "Content-Type: application/json" \ -d '{ "keyId": "key_...", "credits": { "refill": null } }' ``` ## Common patterns ### Tiered subscription plans ```typescript theme={"theme":"kanagawa-wave"} // Free tier: 100/day try { const { meta, data } = await unkey.keys.createKey({ apiId, credits: { remaining: 100, refill: { interval: "daily", amount: 100 }, }, }); console.log(data.keyId); } catch (error) { console.error(error); return Response.json({ error: "Internal error" }, { status: 500 }); } // Pro tier: 50,000/month try { const { meta, data } = await unkey.keys.createKey({ apiId, credits: { remaining: 50000, refill: { interval: "monthly", amount: 50000 }, }, }); console.log(data.keyId); } catch (error) { console.error(error); return Response.json({ error: "Internal error" }, { status: 500 }); } // Enterprise: Unlimited (no credits object) try { const { meta, data } = await unkey.keys.createKey({ apiId }); console.log(data.keyId); } catch (error) { console.error(error); return Response.json({ error: "Internal error" }, { status: 500 }); } ``` ### Upgrade a user's plan When a user upgrades, update their key: ```typescript theme={"theme":"kanagawa-wave"} // User upgraded from Free to Pro mid-month try { const { meta, data } = await unkey.keys.updateKey({ keyId: "key_...", credits: { remaining: 50000, // Give them the new allocation immediately refill: { interval: "monthly", amount: 50000, }, }, }); } catch (error) { console.error(error); return Response.json({ error: "Internal error" }, { status: 500 }); } ``` ## Refill vs manual credit management | Approach | Best for | | -------------------- | ------------------------------------------------------ | | **Auto-refill** | Subscription models with predictable, recurring quotas | | **Manual increment** | Pay-as-you-go, top-ups, credit purchases | You can use both: set a base refill for the subscription, and manually increment when users purchase additional credits. ## Next steps Learn more about credits and custom costs Track usage patterns per key # Usage Limits (Credits) Source: https://unkey.com/docs/platform/apis/features/remaining Set usage limits on API keys to cap the total number of requests. Unkey automatically enforces credit-based quotas and blocks excess usage. Usage limits let you set a maximum number of requests a key can make. After the limit is reached, the key becomes invalid until you add more credits or reset it. This is different from rate limiting, which controls *frequency*, usage limits control *total volume*. ## When to use this Sell API credits that get consumed with each request. Customer buys 10,000 requests, key stops working at 10,001. Give new users 100 free requests to try your API. They upgrade or stop. "Pro plan includes 50,000 requests/month", combine with [auto-refill](/docs/platform/apis/features/refill) for recurring limits. Create a key that works exactly once, like a single-use download link. ## How it works 1. Create a key with `credits.remaining` set to your limit 2. Each verification decrements the remaining count 3. When remaining hits 0, verification fails with `code: USAGE_EXCEEDED` 4. You can add more credits anytime via the API ## Create a key with usage limits ```bash cURL theme={"theme":"kanagawa-wave"} 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 } }' ``` ```typescript TypeScript theme={"theme":"kanagawa-wave"} import { Unkey } from "@unkey/api"; const unkey = new Unkey({ rootKey: process.env.UNKEY_ROOT_KEY }); try { const { meta, data } = await unkey.keys.createKey({ apiId: "api_...", credits: { remaining: 1000, }, }); console.log(data.keyId); // Give this key to your user } catch (err) { console.error(err); return Response.json({ error: "Internal error" }, { status: 500 }); } ``` ## Verification response When you verify a key with usage limits, the response includes the current credit count: ```json theme={"theme":"kanagawa-wave"} { "meta": { "requestId": "req_..." }, "data": { "valid": true, "code": "VALID", "keyId": "key_...", "credits": 999 } } ``` The `credits` value shows remaining credits **after** this verification. A value of `999` means the key can be verified 999 more times. When credits are exhausted: ```json theme={"theme":"kanagawa-wave"} { "meta": { "requestId": "req_..." }, "data": { "valid": false, "code": "USAGE_EXCEEDED", "keyId": "key_...", "credits": 0 } } ``` ## Custom cost per request By default, each verification costs 1 credit. But some operations should cost more, maybe a complex query costs 10 credits while a simple lookup costs 1. Specify the cost at verification time: ```bash cURL theme={"theme":"kanagawa-wave"} curl -X POST https://api.unkey.com/v2/keys.verifyKey \ -H "Content-Type: application/json" \ -d '{ "key": "sk_...", "credits": { "cost": 10 } }' ``` ```typescript TypeScript theme={"theme":"kanagawa-wave"} try { const { data } = await unkey.keys.verifyKey({ key: "sk_...", credits: { cost: 10 }, // Deduct 10 credits for this request }); if (!data.valid) { return Response.json({ error: data.code }, { status: 401 }); } } catch (err) { console.error(err); return Response.json({ error: "Internal error" }, { status: 500 }); } ``` If the key doesn't have enough remaining credits, the verification fails. A key with 5 remaining credits will reject a request with `cost: 10`. ### Cost of 0 (check without consuming) Set `cost: 0` to verify a key without consuming any credits. Useful for checking key validity or metadata without affecting the balance. ```json theme={"theme":"kanagawa-wave"} { "key": "sk_...", "credits": { "cost": 0 } } ``` ## Add more credits When a user purchases more credits or you need to refill manually: ```bash cURL theme={"theme":"kanagawa-wave"} 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 }' ``` ```typescript TypeScript theme={"theme":"kanagawa-wave"} try { const { meta, data } = await unkey.keys.updateCredits({ keyId: "key_...", operation: "increment", value: 5000, }); } catch (err) { console.error(err); return Response.json({ error: "Internal error" }, { status: 500 }); } ``` Operations available: * `set`, Set credits to an exact value * `increment`, Add credits to current balance * `decrement`, Subtract credits from current balance ## Remove usage limits To make a key unlimited again, set credits to `null`: ```bash theme={"theme":"kanagawa-wave"} curl -X POST https://api.unkey.com/v2/keys.updateKey \ -H "Authorization: Bearer $UNKEY_ROOT_KEY" \ -H "Content-Type: application/json" \ -d '{ "keyId": "key_...", "credits": null }' ``` ## Usage limits vs rate limits | Feature | Usage Limits | Rate Limits | | --------------------- | ------------------------ | ---------------------------- | | What it controls | Total requests ever | Requests per time window | | Resets automatically? | No (unless using refill) | Yes, after window expires | | Use case | Billing, quotas, trials | Abuse protection, fair usage | | Example | "1000 requests total" | "100 requests per minute" | **Use both together:** Rate limits protect against burst abuse, usage limits enforce billing quotas. ## Next steps Automatically reset credits on a schedule (daily, monthly) Control request frequency, not just total volume # Key Rerolling Source: https://unkey.com/docs/platform/apis/features/rerolling-key Rotate API keys in Unkey while preserving their configuration, permissions, and metadata. Set grace periods for seamless key transitions. ## What is Key Rerolling? Key rerolling (or key rotation) is the process of generating a new API key token while preserving all the configuration from an existing key. This is a critical security practice that allows you to regularly rotate credentials without disrupting your application's permissions or settings. ## Why Reroll Keys? Key rerolling serves several important purposes: * **Security Compliance**: Many security frameworks require regular credential rotation * **Compromise Recovery**: Quickly replace keys that may have been exposed * **Proactive Security**: Regularly rotate keys as a preventive measure * **Graceful Migration**: Overlap periods allow zero-downtime key transitions ## How Key Rerolling Works ### What Gets Copied When you reroll a key, the new key is an exact copy of the original in terms of configuration: **Preserved Settings:** * Permissions and RBAC roles * Custom metadata fields * Rate limiting rules * Identity associations (for tracking usage across keys) * Remaining credits balance * Recovery/encryption settings * Keyspace association ### What's New The rerolled key gets fresh values for: * Key ID (a new unique identifier) * API key token (the actual secret) * Creation timestamp ### What Happens to the Original Key The original key remains active for a configurable grace period: * You specify the `expiration` duration (in milliseconds) * Set to `0` for immediate revocation * Common grace periods: 1 hour (3600000ms), 24 hours (86400000ms), 7 days (604800000ms) ## Rotate a key from the dashboard You can rotate any active API key directly from the keys table without writing code. 1. Open the keyspace and navigate to its **Keys** tab. 2. Click the actions menu (**...**) on the key row and select **Rotate key**. 3. Choose how long the old key should remain valid: * **Revoke immediately** * **1 minute**, **15 minutes**, **1 hour**, **6 hours**, or **24 hours** 4. Click **Rotate key**. 5. Copy the new key secret from the success dialog and deliver it to the user. The plaintext is shown only once. The new key inherits the original key's permissions, metadata, rate limits, credits, identity, and expiration (if any). The old key keeps verifying until the grace period elapses, then is revoked automatically. The grace period never extends past the original key's existing expiration. Expired keys cannot be rotated. The **Rotate key** action is disabled for keys that are past their `expires` timestamp. ## Rotate a key with the API To reroll a key programmatically, make a `POST` request to `/v2/keys.rerollKey`: ```bash theme={"theme":"kanagawa-wave"} curl --request POST \ --url https://api.unkey.com/v2/keys.rerollKey \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "keyId": "key_2cGKbMxRyIzhCxo1Idjz8q", "expiration": 86400000 }' ``` ### Request Parameters * `keyId` (required): The database identifier of the key to reroll (NOT the API key token) * `expiration` (required): Duration in milliseconds until the original key is revoked ### Response ```json theme={"theme":"kanagawa-wave"} { "meta": { "requestId": "req_abc123def456" }, "data": { "keyId": "key_3dHLcNyRzJaiDyo2Jekz9r", "key": "prod_2cGKbMxRjIzhCxo1IdjH3arELti7Sdyc8w6XYbvtcyuBowPT" } } ``` **Security Critical**: The `key` field contains the actual API key token. This is the only time you'll receive it - Unkey stores only a hashed version. Never log or expose this value. Transmit it directly to the end user via secure channels only. ## Common Use Cases ### Zero-Downtime Key Rotation For production systems that can't afford downtime: 1. Reroll the key with a grace period (e.g., 24 hours) 2. Deploy the new key to your systems 3. Verify the new key is working 4. The old key automatically expires after the grace period ```bash theme={"theme":"kanagawa-wave"} # Give 24 hours for migration curl --request POST \ --url https://api.unkey.com/v2/keys.rerollKey \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "keyId": "key_2cGKbMxRyIzhCxo1Idjz8q", "expiration": 86400000 }' ``` ### Emergency Key Replacement When a key is compromised and needs immediate revocation: ```bash theme={"theme":"kanagawa-wave"} # Revoke immediately curl --request POST \ --url https://api.unkey.com/v2/keys.rerollKey \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "keyId": "key_2cGKbMxRyIzhCxo1Idjz8q", "expiration": 0 }' ``` ## Analytics and Usage Tracking An important aspect of key rerolling is that analytics remain consistent: * **Key-level metrics**: Each key has its own usage statistics * **Identity-level metrics**: If the original key has an identity, the new key inherits it * This allows you to track usage across both individual keys and the overall identity * Historical data from the original key remains accessible ## Required Permissions Your root key needs the following permissions to reroll keys: * `api.*.create_key` or `api..create_key` * `api.*.encrypt_key` or `api..encrypt_key` (only when the original key is recoverable) ## Limitations * The new key uses the keyspace's default configuration for prefix and byte length * You cannot modify permissions or settings during reroll - use the update endpoint afterward if needed # Key Revocation Source: https://unkey.com/docs/platform/apis/features/revocation Revoke API keys instantly by deleting or disabling them in Unkey. Revoked keys fail verification immediately with no propagation delay. When a key is compromised or a user's access should end, you can revoke it immediately. Unkey supports both permanent deletion and temporary disabling. ## When to use this Key was leaked in a public repo or logs. Delete it immediately. Customer canceled or employee left. Revoke their API access. Unusual traffic patterns. Disable the key while investigating. Payment failed. Temporarily disable until resolved. ## Delete vs Disable | Action | Effect | Reversible? | Use when | | ----------- | --------------------------------- | ----------- | ----------------------------------- | | **Delete** | Permanently removes the key | No | Key is compromised, user churned | | **Disable** | Key exists but can't authenticate | Yes | Temporary suspension, investigation | ## Delete a key permanently Use when the key should never work again: ```bash cURL theme={"theme":"kanagawa-wave"} curl -X POST https://api.unkey.com/v2/keys.deleteKey \ -H "Authorization: Bearer $UNKEY_ROOT_KEY" \ -H "Content-Type: application/json" \ -d '{ "keyId": "key_..." }' ``` ```typescript TypeScript theme={"theme":"kanagawa-wave"} try { const { meta, data } = await unkey.keys.deleteKey({ keyId: "key_...", }); } catch (err) { console.error(err); return Response.json({ error: "Internal error" }, { status: 500 }); } ``` The key is invalidated within 60 seconds globally. Deletion is permanent. The key cannot be recovered. If you might need to restore access, use disable instead. ## Disable a key temporarily Use when you want to suspend access but may restore it later: ```bash cURL theme={"theme":"kanagawa-wave"} 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 }' ``` ```typescript TypeScript theme={"theme":"kanagawa-wave"} try { const { meta, data } = await unkey.keys.updateKey({ keyId: "key_...", enabled: false, }); } catch (err) { console.error(err); return Response.json({ error: "Internal error" }, { status: 500 }); } ``` Verification response when disabled: ```json theme={"theme":"kanagawa-wave"} { "data": { "valid": false, "code": "DISABLED", "keyId": "key_..." } } ``` ## Re-enable a disabled key ```typescript theme={"theme":"kanagawa-wave"} try { const { meta, data } = await unkey.keys.updateKey({ keyId: "key_...", enabled: true, }); } catch (err) { console.error(err); return Response.json({ error: "Internal error" }, { status: 500 }); } ``` The key works again immediately. ## Propagation time * **Delete**: Up to 60 seconds for global invalidation * **Disable**: Up to 60 seconds for global propagation For immediate revocation of a compromised key, you may want to also: 1. Rotate any affected secrets downstream 2. Review audit logs for unauthorized access 3. Alert the user if appropriate ## Bulk revocation To revoke all keys for a specific user, query their keys first: ```typescript theme={"theme":"kanagawa-wave"} try { let cursor: string | undefined; // Page through all keys for the user 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); } catch (err) { console.error(err); return Response.json({ error: "Internal error" }, { status: 500 }); } ``` For security incidents, consider using [key rerolling](/docs/platform/apis/features/rerolling-key) if you need to maintain the user's configuration while replacing the compromised key. ## Next steps Replace a key while preserving its configuration Track who revoked which keys # Temporary Keys Source: https://unkey.com/docs/platform/apis/features/temp-keys Create temporary API keys that automatically expire after a set duration. Use expiring keys for trials, short-lived sessions, or demos. Temporary keys automatically become invalid after a specified time. No cleanup required, Unkey handles expiration and deletion for you. ## When to use this Give new users a 7-day trial key. It stops working automatically. Partner needs API access for a project? Key expires when the project ends. Generate a key that lasts for a user's session (e.g., 24 hours). Force key rotation by having keys expire periodically. ## How it works 1. Create a key with an `expires` timestamp (Unix milliseconds) 2. Key works normally until the expiration time 3. After expiration, verification returns `code: EXPIRED` 4. Unkey automatically cleans up expired keys ## Create a temporary key Set `expires` to a Unix timestamp in **milliseconds**: ```bash cURL theme={"theme":"kanagawa-wave"} # Key expires in 24 hours 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' }' ``` ```typescript TypeScript theme={"theme":"kanagawa-wave"} // Key expires in 24 hours const expires = Date.now() + 24 * 60 * 60 * 1000; try { const { meta, data } = await unkey.keys.createKey({ apiId: "api_...", expires, }); } catch (err) { console.error(err); return Response.json({ error: "Internal error" }, { status: 500 }); } ``` ```python Python theme={"theme":"kanagawa-wave"} import time # Key expires in 24 hours expires = int(time.time() * 1000) + (24 * 60 * 60 * 1000) result = unkey.keys.create_key( api_id="api_...", expires=expires ) ``` ## Common expiration times | Duration | Calculation | | ------------- | --------------------------------------- | | 1 hour | `Date.now() + 60 * 60 * 1000` | | 24 hours | `Date.now() + 24 * 60 * 60 * 1000` | | 7 days | `Date.now() + 7 * 24 * 60 * 60 * 1000` | | 30 days | `Date.now() + 30 * 24 * 60 * 60 * 1000` | | Specific date | `new Date("2024-12-31").getTime()` | ## Verification response When verifying an expired key: ```json theme={"theme":"kanagawa-wave"} { "meta": { "requestId": "req_..." }, "data": { "valid": false, "code": "EXPIRED", "keyId": "key_..." } } ``` ## Extend or change expiration Update the expiration time on an existing key: ```bash cURL theme={"theme":"kanagawa-wave"} # Extend by 7 more 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' }' ``` ```typescript TypeScript theme={"theme":"kanagawa-wave"} // Extend by 7 more days from now try { const { meta, data } = await unkey.keys.updateKey({ keyId: "key_...", expires: Date.now() + 7 * 24 * 60 * 60 * 1000, }); } catch (err) { console.error(err); return Response.json({ error: "Internal error" }, { status: 500 }); } ``` ## Remove expiration Make a temporary key permanent by setting `expires` to `null`: ```bash theme={"theme":"kanagawa-wave"} 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 }' ``` ## Combining with other features Temporary keys work with all other key features: ```typescript theme={"theme":"kanagawa-wave"} // 7-day trial with 1000 requests and rate limiting try { const { meta, data } = await unkey.keys.createKey({ apiId: "api_...", expires: Date.now() + 7 * 24 * 60 * 60 * 1000, credits: { remaining: 1000, }, ratelimits: [ { name: "requests", limit: 100, duration: 60000, // 100 per minute }, ], }); } catch (err) { console.error(err); return Response.json({ error: "Internal error" }, { status: 500 }); } ``` ## Temporary vs usage-limited keys | Feature | Temporary Keys | Usage-Limited Keys | | ------------- | -------------------- | --------------------- | | Controlled by | Time | Request count | | Expires when | Clock hits timestamp | Credits reach 0 | | Good for | Time-bound access | Pay-per-use, quotas | | Example | "Access for 7 days" | "1000 requests total" | **Use both together** for trial limits: "7 days OR 1000 requests, whichever comes first." ## Next steps Cap total requests per key Generate new keys while maintaining config # IP Whitelisting Source: https://unkey.com/docs/platform/apis/features/whitelist Restrict API key usage to specific IP addresses or CIDR ranges. Add an IP whitelist to keys for network-level access control in Unkey. IP whitelisting lets you restrict which IP addresses can use a key. Even with a valid key, requests from non-whitelisted IPs are rejected. This feature is available as an addon or with an Enterprise plan. [Contact us](https://unkey.com/contact) to enable it. ## When to use this Keys should only work from your customer's known server IPs. Restrict partner keys to their office or datacenter IPs. Ensure internal API keys only work from corporate network. Meet security requirements that mandate IP-based access control. ## How it works 1. Configure allowed IP addresses or CIDR ranges on a key 2. When the key is verified, Unkey checks the request's source IP 3. If the IP isn't in the whitelist, verification fails with `code: FORBIDDEN` ## Configuration IP whitelisting is configured through the dashboard: Supports: * Individual IPv4 addresses: `192.168.1.100` * IPv4 CIDR ranges: `10.0.0.0/8` * IPv6 addresses and ranges: `2001:db8::/32` ## Verification response When a request comes from a non-whitelisted IP: ```json theme={"theme":"kanagawa-wave"} { "data": { "valid": false, "code": "FORBIDDEN", "keyId": "key_..." } } ``` ## Combining with other security features IP whitelisting works alongside other key features: * **Rate limiting**: Still applies after IP check passes * **Permissions**: Authorization checks happen after IP verification * **Expiration**: Key must be valid AND IP must be whitelisted ## Best practices Instead of listing individual IPs, use CIDR notation (`10.0.0.0/24`) so customers can add servers without updating the whitelist. Let users know they need to provide IPs when requesting keys. Dynamic IPs won't work reliably. For users with dynamic IPs, you might offer keys without IP restrictions but with stricter rate limits. ## Next steps Add another layer of protection Enable IP whitelisting for your account # API Keys Source: https://unkey.com/docs/platform/apis/introduction Issue, verify, and manage API keys at any scale with Unkey. Explore key creation, verification, rotation, and access control features. API keys are the most common way to authenticate requests to your API. Unkey handles the entire lifecycle, creating keys, verifying them on every request, tracking usage, and revoking them when needed, so you can focus on your actual API logic. ## When to use API keys API keys work best when you need: * **Machine-to-machine authentication**: Backend services, scripts, or integrations calling your API * **Usage tracking per customer**: Know who's making requests and how many * **Simple integration**: A single header (`Authorization: Bearer sk_...`) that developers understand * **Granular control**: Per-key rate limits, expiration, and permissions For user-facing authentication (login flows, sessions), you'll typically use OAuth, JWTs, or an auth provider like Clerk or Auth0 alongside Unkey. Unkey handles the API key layer, not user sessions. ## How it works A keyspace in Unkey is a container for keys. You might have separate keyspaces for "Production" and "Staging", or for different products. When a user signs up or requests API access, create a key for them. You can attach metadata (user ID, plan tier), set limits, and configure expiration. When a request hits your API, extract the key from the header and verify it with Unkey. We'll tell you if it's valid, who it belongs to, and how many requests they have left. Revoke keys when users churn. Rotate keys when security requires it. Update limits when users upgrade. All through the dashboard or API. ## Quick example Here's what verification looks like in practice: ```ts TypeScript theme={"theme":"kanagawa-wave"} import { Unkey } from "@unkey/api"; const unkey = new Unkey({ rootKey: process.env.UNKEY_ROOT_KEY! }); export async function handler(req: Request) { // 1. Extract the key from the Authorization header const key = req.headers.get("Authorization")?.replace("Bearer ", ""); if (!key) { return new Response("Missing API key", { status: 401 }); } try { // 2. Verify with Unkey const { meta, data } = await unkey.keys.verifyKey({ key }); if (!data.valid) { // Key is invalid, expired, rate limited, or has no remaining uses return new Response("Unauthorized", { status: 401 }); } // Continue with your API logic... return new Response("Hello!"); } catch (err) { console.error(err); return new Response("Service unavailable", { status: 503 }); } } ``` ```python Python theme={"theme":"kanagawa-wave"} from unkey import Unkey unkey = Unkey(root_key="unkey_...") def handler(request): key = request.headers.get("Authorization", "").replace("Bearer ", "") if not key: return Response("Missing API key", status=401) result = unkey.keys.verify_key(key=key) if not result.valid: return Response("Unauthorized", status=401) # Key is valid, continue with your API logic return Response("Hello!") ``` ```go Go theme={"theme":"kanagawa-wave"} package main import ( "context" "net/http" "os" "strings" unkey "github.com/unkeyed/sdks/api/go/v2" "github.com/unkeyed/sdks/api/go/v2/components" ) func handler(w http.ResponseWriter, r *http.Request) { auth := r.Header.Get("Authorization") key := strings.TrimPrefix(strings.TrimSpace(auth), "Bearer ") client := unkey.New(unkey.WithSecurity(os.Getenv("UNKEY_ROOT_KEY"))) res, err := client.Keys.VerifyKey(context.Background(), components.V2KeysVerifyKeyRequestBody{ Key: key, }) if err != nil || !res.Valid { http.Error(w, "Unauthorized", http.StatusUnauthorized) return } w.Write([]byte("Hello!")) } ``` ```bash cURL theme={"theme":"kanagawa-wave"} curl -X POST https://api.unkey.com/v2/keys.verifyKey \ -H "Authorization: Bearer $UNKEY_ROOT_KEY" \ -H "Content-Type: application/json" \ -d '{"key": "sk_1234..."}' ``` ## What you get with each verification When you verify a key, Unkey returns: | Field | Type | Description | | ------------- | ----------- | ------------------------------------------------------------------- | | `valid` | `boolean` | Whether the key passed all checks | | `code` | `string` | Status code (`VALID`, `NOT_FOUND`, `RATE_LIMITED`, etc.) | | `keyId` | `string` | The key's unique identifier | | `name` | `string?` | Human-readable name of the key | | `meta` | `object?` | Custom metadata associated with the key | | `expires` | `number?` | Unix timestamp (in milliseconds) when the key will expire. (if set) | | `credits` | `number?` | Remaining uses (if usage limits set) | | `enabled` | `boolean` | Whether the key is enabled | | `roles` | `string[]?` | Permissions attached to the key | | `permissions` | `string[]?` | Permissions attached to the key | | `identity` | `object?` | Identity info if `externalId` was set when creating the key | | `ratelimits` | `object[]?` | Rate limit states (if rate limiting configured) | ## Features Unkey keys support much more than basic authentication: Limit requests per key, per second, minute, hour, or any window. Cap total requests per key. Perfect for API credits or trial limits. Automatically restore usage limits on a schedule (daily, monthly, etc). Create keys that automatically expire after a set time. Attach roles and permissions for fine-grained access control. See usage patterns, top consumers, and verification trends. ## Next steps Full walkthrough for Next.js, Bun, Express, or Hono. All endpoints for creating, updating, and managing keys. # Keys Source: https://unkey.com/docs/platform/apis/keys API keys are credentials your users include in requests. Unkey handles creation, verification, rotation, revocation, and metadata. A key is the credential your users include in API requests to authenticate with your service. Unkey manages the full lifecycle: creation, verification, rotation, and revocation. Unkey never stores keys in plain text. On creation, the key is returned once and only its SHA-256 hash is persisted. Verification works by hashing the incoming key and comparing hashes. ## Key lifecycle ```text theme={"theme":"kanagawa-wave"} Create → Active → (optional: Disable / Expire / Exhaust credits) → Revoke ``` | State | Description | | --------- | ----------------------------------------------------------------- | | Active | Key passes verification and deducts from rate limits and credits | | Disabled | Key exists but fails verification with `DISABLED` | | Expired | Key passed its expiration timestamp, fails with `EXPIRED` | | Exhausted | Key's remaining credits reached zero, fails with `USAGE_EXCEEDED` | | Revoked | Key is permanently deleted and can't be restored | ## Create a key Keys are created through the API, SDK, or dashboard. Each key requires an `apiId` to associate it with a keyspace. On creation, you can configure: | Option | Description | | ------------- | --------------------------------------------------------------------- | | `prefix` | Override the keyspace's default prefix for this key | | `name` | Human-readable label | | `meta` | Arbitrary JSON metadata returned on every verification | | `expires` | Unix timestamp (ms) when the key expires | | `remaining` | Number of verifications before the key is exhausted | | `refill` | Automatic credit refill schedule (interval and amount) | | `ratelimit` | One or more rate limit configurations | | `roles` | Roles to attach for RBAC | | `permissions` | Permissions to attach directly | | `externalId` | Link the key to an identity in your system | | `enabled` | Set to `false` to create a disabled key | | `environment` | Tag the key with an environment label (for example, `live` or `test`) | The response includes the full key value. This is the only time the plaintext key is available. Store it securely or deliver it to your user immediately. ## Verify a key Verification checks the key's validity and enforces all attached policies in a single call. Unkey evaluates the following checks in order: 1. **Existence.** The key hash must match a record in the keyspace. 2. **Enabled.** The key must not be disabled. 3. **Expiration.** The key must not have passed its expiration timestamp. 4. **Credits.** If remaining credits are configured, at least one credit must be available. Verification deducts one credit. 5. **Rate limits.** All rate limit configurations on the key are evaluated. If any limit is exceeded, the request is rejected. 6. **Permissions.** If you pass a permission query, the key must satisfy it. The response includes: | Field | Description | | ------------- | ------------------------------------------------------------------------------------------------------------------------------------- | | `valid` | Whether all checks passed | | `code` | Outcome code (`VALID`, `NOT_FOUND`, `RATE_LIMITED`, `EXPIRED`, `DISABLED`, `INSUFFICIENT_PERMISSIONS`, `USAGE_EXCEEDED`, `FORBIDDEN`) | | `keyId` | The key's unique identifier | | `meta` | Custom metadata attached to the key | | `remaining` | Credits left after this verification (if configured) | | `ratelimit` | Rate limit state (limit, remaining, reset timestamp) | | `identity` | Identity information if the key is linked via `externalId` | | `permissions` | Permissions attached to the key | | `roles` | Roles attached to the key | | `enabled` | Whether the key is enabled | | `expires` | Expiration timestamp (if set) | ## Features Each key can carry several optional features: | Feature | What it controls | Learn more | | --------------------- | ----------------------------------------- | ------------------------------------------------------------------- | | Rate limits | Requests per time window | [Key rate limits](/docs/platform/apis/features/ratelimiting/overview) | | Credits | Total request quota | [Usage limits](/docs/platform/apis/features/remaining) | | Refill | Automatic credit reset (daily or monthly) | [Auto-refill](/docs/platform/apis/features/refill) | | Expiration | Time-based validity | [Temporary keys](/docs/platform/apis/features/temp-keys) | | Roles and permissions | RBAC access control | [Authorization](/docs/platform/apis/features/authorization/introduction) | | Metadata | Arbitrary JSON returned on verification | Passed as `meta` at creation | | Environment | Label for separating live and test keys | [Environments](/docs/platform/apis/features/environments) | ## Rotate a key Rotation creates a new key and revokes the old one. The new key inherits the same configuration (metadata, limits, permissions) but has a new value and key ID. See [Rerolling keys](/docs/platform/apis/features/rerolling-key) for details. ## Revoke a key Revoking a key permanently deletes it. Revoked keys can't be restored. If you need to temporarily block a key, disable it instead. See [Revocation](/docs/platform/apis/features/revocation) for details. ## Key storage By default, Unkey stores only the SHA-256 hash of each key. The plaintext is returned once at creation and never stored. If the keyspace has **Store encrypted keys** enabled, Unkey also stores an AES-encrypted copy of the key in the vault. This lets you recover the key later through the API with the `decrypt` parameter. Use this when your workflow requires showing users their existing key. See [Recovering keys](/docs/security/recovering-keys) for details. ## Constraints | Limit | Value | | --------------------------------- | ------------ | | Key prefix max length | 8 characters | | Metadata max size | 64 KB (JSON) | | Rate limit configurations per key | 10 | | Permissions per key | 1,000 | | Roles per key | 1,000 | ## Related pages | Page | Description | | ------------------------------------------------------------ | ----------------------------------------- | | [Keyspaces overview](/docs/platform/apis/overview) | Keyspace configuration | | [API keys](/docs/platform/apis/introduction) | Quick start guide with code examples | | [Identities](/docs/platform/identities/overview) | Link keys to users for shared rate limits | | [Sentinel authentication](/docs/platform/sentinel/authentication) | Verify keys at the gateway | | [API reference](/docs/api-reference/overview) | Full endpoint documentation | # Overview Source: https://unkey.com/docs/platform/apis/migrations/introduction Migrate your existing API keys to Unkey without downtime or user disruption. Import pre-hashed keys and preserve your current key format. Already have API keys in production? Migrate them to Unkey without requiring your users to generate new keys. ## How migration works Extract key hashes from your current system (database, auth provider, etc.) Contact us to set up your migration and receive a `migrationId` Use the migration API to import your key hashes with their configuration Point your API verification to Unkey, existing keys work immediately ## Why key hashes? Unkey never stores plaintext keys. During migration, you provide the **hash** of each key, not the key itself. Your users continue using their existing keys. When they call your API: 1. You extract the key from the request 2. Unkey hashes it and matches against stored hashes 3. Verification succeeds if there's a match **Result:** Zero changes required from your users. ## What can be migrated? | Setting | Migrated? | | ------------------- | --------- | | Key hash | ✅ | | Custom metadata | ✅ | | Roles & permissions | ✅ | | Rate limits | ✅ | | Credits/remaining | ✅ | | Expiration | ✅ | | Identity/owner | ✅ | ## Migration paths Export hashes from PostgreSQL, MySQL, MongoDB, etc. Migrate from Auth0, Clerk, Firebase, or custom auth. Moving from Kong, Tyk, or homegrown solutions. We'll help you figure out the best approach. ## Get started Email [support@unkey.com](mailto:support@unkey.com) with: - Your workspace ID - Current key storage system - Approximate number of keys - Hash algorithm used (SHA-256, bcrypt, etc.) We'll help you design a migration strategy that minimizes risk. Follow our [step-by-step guide](/docs/platform/apis/migrations/keys) to migrate your keys. Every system is different. We're here to help, reach out at [support@unkey.com](mailto:support@unkey.com) and we'll guide you through your specific migration. ## Next steps Step-by-step instructions for migrating keys Full v2 API documentation # Migrate keys to Unkey Source: https://unkey.com/docs/platform/apis/migrations/keys Follow this step-by-step guide to import existing API keys into an Unkey keyspace. Migrate pre-hashed keys from any provider while keeping them valid. This guide walks you through migrating existing API keys to Unkey. ## Prerequisites Sign up at [app.unkey.com](https://app.unkey.com/auth/sign-up) Note your `workspaceId` and `apiId`: - **Workspace ID**: Settings → General (upper right corner) Workspace settings page * **API ID**: Keyspaces → Your keyspace (upper right corner) Keyspace page Go to Settings → Root Keys and create one with `api.*.create_key` and `api.*.verify_key` permissions. Root key creation Email [support@unkey.com](mailto:support@unkey.com) with: - Your workspace ID - Source system (PostgreSQL, Auth0, etc.) - Hash algorithm used We'll send you a `migrationId`. ## Supported hash formats | Variant | Description | | --------------- | ----------------------- | | `sha256_base64` | SHA-256, base64 encoded | | `sha256_hex` | SHA-256, hex encoded | | `bcrypt` | bcrypt hash | Need a different hash format? Contact us, we can add support for your algorithm. ## Export your keys Extract key hashes from your current system. **Never include plaintext keys.** ### Example: PostgreSQL ```sql theme={"theme":"kanagawa-wave"} SELECT key_hash, user_id, created_at, metadata FROM api_keys WHERE revoked = false; ``` ### Example: MongoDB ```javascript theme={"theme":"kanagawa-wave"} db.apiKeys.find({ revoked: false }, { hash: 1, userId: 1, metadata: 1 }); ``` ## Hash your keys (if not already hashed) If you have plaintext keys, hash them before migration: ```javascript theme={"theme":"kanagawa-wave"} const { createHash } = require("node:crypto"); function hashKey(plaintext) { return { value: createHash("sha256").update(plaintext).digest("base64"), variant: "sha256_base64", }; } ``` ## Migrate to Unkey Use the migration API to import your keys: ```javascript theme={"theme":"kanagawa-wave"} const UNKEY_ROOT_KEY = process.env.UNKEY_ROOT_KEY; const MIGRATION_ID = "mig_..."; // From support const API_ID = "api_..."; // Your exported keys const keysToMigrate = [ { hash: { value: "abc123...", // The hash value variant: "sha256_base64", }, externalId: "user_123", // Link to your user meta: { plan: "pro", migratedFrom: "legacy-system", }, ratelimits: [ { name: "requests", limit: 1000, duration: 60000, }, ], }, // ... more keys ]; const response = await fetch("https://api.unkey.com/v2/keys.migrateKeys", { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${UNKEY_ROOT_KEY}`, }, body: JSON.stringify({ migrationId: MIGRATION_ID, apiId: API_ID, keys: keysToMigrate, }), }); const result = await response.json(); if (!response.ok) { console.error( "Migration failed:", result?.error || `HTTP ${response.status}`, ); return; } if (!result?.data) { console.error("Migration response missing data"); return; } console.log("Migrated:", result.data.migrated.length); console.log("Failed:", result.data.failed.length); ``` ## Response ```json theme={"theme":"kanagawa-wave"} { "meta": { "requestId": "req_..." }, "data": { "migrated": [ { "hash": "abc123...", "keyId": "key_..." } ], "failed": [ { "hash": "xyz789...", "error": "Key already exists" } ] } } ``` ## Key configuration options Each key in the migration can include: | Field | Description | | ------------- | ---------------------------------------------------- | | `hash` | Required. The hash object with `value` and `variant` | | `prefix` | Key prefix (for display only, not verified) | | `name` | Human-readable name | | `externalId` | Link to your user/identity | | `meta` | Custom JSON metadata | | `roles` | Array of role names | | `permissions` | Array of permission names | | `ratelimits` | Rate limit configuration | | `credits` | Usage limit configuration | | `expires` | Expiration timestamp (Unix ms) | | `enabled` | Whether key is active (default: true) | ## Batch migrations For large migrations, batch your requests: ```javascript theme={"theme":"kanagawa-wave"} const BATCH_SIZE = 100; async function migrateAll(allKeys) { const results = { migrated: 0, failed: 0 }; for (let i = 0; i < allKeys.length; i += BATCH_SIZE) { const batch = allKeys.slice(i, i + BATCH_SIZE); const response = await fetch("https://api.unkey.com/v2/keys.migrateKeys", { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${UNKEY_ROOT_KEY}`, }, body: JSON.stringify({ migrationId: MIGRATION_ID, apiId: API_ID, keys: batch, }), }); const result = await response.json(); const batchNum = Math.floor(i / BATCH_SIZE) + 1; if (!response.ok) { console.error( `Batch ${batchNum} failed:`, result?.error || `HTTP ${response.status}`, ); results.failed += batch.length; continue; } if (!result?.data) { console.error(`Batch ${batchNum} missing data`); results.failed += batch.length; continue; } results.migrated += result.data.migrated.length; results.failed += result.data.failed.length; console.log(`Batch ${batchNum}: ${result.data.migrated.length} migrated`); } return results; } ``` ## Update your verification After migration, update your API to verify keys with Unkey: ```javascript theme={"theme":"kanagawa-wave"} // Before: Custom verification const isValid = await myDatabase.verifyKey(apiKey); // After: Unkey verification const response = await fetch("https://api.unkey.com/v2/keys.verifyKey", { method: "POST", headers: { "Content-Type": "application/json", Authorization: "Bearer " + process.env.UNKEY_ROOT_KEY, }, body: JSON.stringify({ key: apiKey }), }); const result = await response.json(); if (!response.ok || !result?.data) { // Handle error appropriately return { valid: false, error: result?.error || "Verification failed" }; } const isValid = result.data.valid; ``` ## Rollback plan Keep your old verification system running in parallel during migration: ```javascript theme={"theme":"kanagawa-wave"} async function verifyKey(apiKey) { // Try Unkey first const unkeyResult = await verifyWithUnkey(apiKey); if (unkeyResult.valid) { return unkeyResult; } // Fall back to legacy system (temporary) const legacyResult = await verifyWithLegacy(apiKey); if (legacyResult.valid) { const maskedKey = apiKey.slice(0, 5) + "..." + apiKey.slice(-5); console.log("Key found in legacy, not yet migrated:", maskedKey); } return legacyResult; } ``` Remove the fallback once migration is complete and verified. ## Need help? We'll help you plan and execute your migration safely. # Keyspaces Source: https://unkey.com/docs/platform/apis/overview A keyspace in Unkey groups related keys, configuration, and analytics. Learn how to create and manage keyspaces within your workspace. A keyspace is a container for related API keys. It doesn't represent a running service or HTTP endpoint. Instead, it provides a boundary for organizing keys by product, environment, or tier. In the dashboard, keyspaces appear under **Keyspaces (APIs)**. Every key belongs to exactly one keyspace. When you create a key, you specify which keyspace it belongs to by passing its `apiId`. ## When to create separate keyspaces Use multiple keyspaces when you need independent key prefixes, permissions, or analytics boundaries. | Strategy | Example names | Why | | -------------- | ---------------------------- | ------------------------------------------------------ | | By product | `payments-api`, `search-api` | Different key prefixes and permission sets per product | | By environment | `production`, `staging` | Separate keys and usage tracking per stage | | By tier | `free-tier`, `enterprise` | Different default rate limits per plan | A single keyspace works fine if all your keys share the same configuration and you filter by metadata or tags instead. ## Create a keyspace 1. Navigate to **Keyspaces (APIs)** in the sidebar. 2. Click **Create new keyspace**. 3. Enter a name for your keyspace. Unkey generates a unique **API ID** (`api_xxx`) for the keyspace. This is the identifier you pass when creating keys and querying analytics, so keep it handy. ## Keyspace settings Each keyspace exposes configuration through its settings page. | Setting | Description | | -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | | Name | Human-readable label shown in the dashboard | | Default prefix | Prefix prepended to every key created in this keyspace (for example, `sk_live`) | | Default bytes | Number of random bytes used to generate new keys (default: 16) | | Store encrypted keys | When enabled, Unkey stores an encrypted copy of each key so it can be recovered later. See [Recovering keys](/docs/security/recovering-keys). | | IP whitelist | Restrict key verification to specific IP addresses. See [IP whitelist](/docs/platform/apis/features/whitelist). | ## Identifiers Each keyspace has two identifiers: | Identifier | Format | Used for | | ----------- | --------- | ----------------------------------------------------------------------- | | API ID | `api_xxx` | Creating keys and most API calls — the identifier you'll use most often | | KeySpace ID | `ks_xxx` | Analytics queries (as `key_space_id`) and Sentinel configuration | ## Storage and validation A keyspace is the storage and validation boundary for all of its keys (internally, it's backed by a `key_auth` record). When Sentinel or the verification endpoint receives a key, it resolves the keyspace by key prefix and validates the key against that keyspace's configuration. The keyspace stores: | Property | Description | | ---------------------- | ------------------------------------------------------------------------------------------------------ | | `id` | Unique identifier (`ks_xxx`), used in Sentinel policies and analytics queries | | `default_prefix` | Prefix for new keys (for example, `sk_live`). The prefix appears before the random portion of the key. | | `default_bytes` | Number of random bytes per key (default: 16, producing a 24-character base58 string) | | `store_encrypted_keys` | Whether to store a recoverable encrypted copy of each key | | `size_approx` | Approximate number of live keys in this keyspace | KeySpace IDs appear in analytics queries as `key_space_id` and in Sentinel configuration when connecting a keyspace to a deployment. ### Key format Each key is composed of a prefix and a random portion: ```text theme={"theme":"kanagawa-wave"} sk_live_abc123def456ghi789 └─────┘ └──────────────────┘ prefix random (base58-encoded) ``` The prefix is optional but recommended. It makes keys visually identifiable and lets Unkey route verification requests to the correct keyspace without a database lookup. ## Delete a keyspace Deleting a keyspace permanently removes it and all of its keys. This action cannot be undone. Delete protection prevents accidental deletion. Disable it in the keyspace settings before deleting. ## Constraints | Limit | Value | | ----------------------- | -------------- | | Keyspaces per workspace | Varies by plan | | Key prefix max length | 8 characters | | Default bytes range | 16 to 32 | ## Related pages | Page | Description | | ------------------------------------------------------------------- | -------------------------------------- | | [API keys](/docs/platform/apis/introduction) | Issue, verify, and manage keys | | [Rate limiting](/docs/platform/apis/features/ratelimiting/overview) | Per-key rate limits | | [Authorization](/docs/platform/apis/features/authorization/introduction) | Roles and permissions | | [Analytics](/docs/platform/analytics/overview) | Query verification data by keyspace | | [Sentinel authentication](/docs/platform/sentinel/authentication) | Connect keyspaces to Sentinel policies | # Apps Source: https://unkey.com/docs/platform/apps/overview An app is a deployable service within an Unkey project. Each app has its own environments, build configuration, runtime settings, and domain routing. Deploying applications on Unkey is in public beta. To try it, open the product switcher in the top-left of the dashboard and select **Deploy**. During beta, deployed resources are free. We're eager for feedback, so let us know what you think on [Discord](https://unkey.com/discord), [X](https://x.com/unkeydev), or email [support@unkey.com](mailto:support@unkey.com). An app represents a single deployable service within a [project](/docs/platform/projects/overview). Each app has its own [environments](/docs/environments/overview), build configuration, runtime settings, and deployment history. If you're familiar with Railway, an app fills a similar role to a Railway service. ## How apps fit in Apps sit between projects and environments in the deployment hierarchy: ```text theme={"theme":"kanagawa-wave"} Workspace └── Project (one per codebase) └── App (one per service) ├── Environments (production, preview) │ ├── Deployments │ ├── Variables │ └── Custom domains └── Settings (build, runtime, Sentinel) ``` A project can contain multiple apps. Each app has its own pair of default environments (production and preview), its own [build and runtime settings](/docs/platform/apps/settings), and its own [Sentinel configuration](/docs/platform/sentinel/overview). ## Default app When you create a project, Unkey creates a default app automatically. For projects with a single service, this is all you need. The dashboard manages the default app transparently, so you interact with it through your project's **Settings** and **Deployments** tabs without selecting an app. ## Multiple apps A project can contain multiple apps when your codebase produces more than one deployable service. For example, a monorepo might have an API server and a background worker that share the same repository but build and deploy independently. Each app in a multi-app project has: * Its own Dockerfile and build context * Its own runtime settings (CPU, memory, storage, port, health check) * Its own environments and deployment history * Its own Sentinel policies The dashboard does not yet support creating or managing multiple apps within a project. You can deploy to a specific app using the CLI. Contact [support@unkey.com](mailto:support@unkey.com) if you need multi-app projects configured. ## Deploy to an app When deploying with the CLI, you can target a specific app using the `--app` flag: ```bash theme={"theme":"kanagawa-wave"} unkey deploy ghcr.io/acme/api:v1.0.0 \ --project=acme-platform \ --app=api \ --env=production ``` The `--app` flag defaults to `default`, so you can omit it for single-app projects. You can also set the `UNKEY_APP` environment variable instead of passing the flag each time. ## Environments Each app gets two environments when created: | Environment | Purpose | | -------------- | ------------------------------------------------------------------------------------------------------- | | **Production** | Serves live traffic. Gets three [Sentinel](/docs/platform/sentinel/overview) replicas for high availability. | | **Preview** | For testing branches before merging. Gets one Sentinel replica. | Environments are scoped to an app, not to the project. This means each app in a multi-app project has its own independent production and preview environments with separate [variables](/docs/platform/variables/overview) and [custom domains](/docs/networking/domains). See [Environments](/docs/environments/overview) for details on how environments work. ## Current deployment and rollbacks Each app tracks which deployment is live. When a deployment reaches the ready state, it becomes the current deployment, and [sticky domains](/docs/networking/wildcard-domains) (live and environment-scoped) point to it automatically. You can [roll back](/docs/build-and-deploy/rollbacks) to a previous deployment, which atomically switches traffic to the older version. The app tracks its rollback state so the dashboard can show whether you're running the latest deployment or a rolled-back version. ## GitHub integration Each app can connect to a GitHub repository. Pushes to the default branch trigger production deployments, and pushes to other branches trigger preview deployments. See [GitHub integration](/docs/build-and-deploy/github) for setup instructions. In a multi-app project, each app connects to the same repository but can use different Dockerfiles and [watch paths](/docs/platform/apps/settings#watch-paths) to build independently. # App settings Source: https://unkey.com/docs/platform/apps/settings Configure build, runtime, and advanced settings for an app. Settings are scoped per environment and take effect on the next deployment. Deploying applications on Unkey is in public beta. To try it, open the product switcher in the top-left of the dashboard and select **Deploy**. During beta, deployed resources are free. We're eager for feedback, so let us know what you think on [Discord](https://unkey.com/discord), [X](https://x.com/unkeydev), or email [support@unkey.com](mailto:support@unkey.com). App settings control how Unkey builds and runs your service. Each [app](/docs/platform/apps/overview) has its own settings, and most settings are scoped per environment so production and preview can differ. You can configure these during project creation or update them from the **Settings** tab in your project dashboard. Most settings take effect on the next deployment. After saving changes, trigger a new deployment to apply them. ## Build settings Build settings determine how Unkey turns your source code into a container image. ### GitHub repository Connect a GitHub repository to trigger automatic deployments on push events. Pushes to the default branch deploy to production, and pushes to other branches deploy to preview environments. See [GitHub integration](/docs/build-and-deploy/github) for details. ### Root directory The directory your application is built from. It is the directory Unkey [analyzes and builds](/docs/builds/overview); for [Dockerfile builds](/docs/builds/dockerfile) it is the build context that `COPY` and `ADD` commands are relative to. Defaults to the repository root (`.`). Set this when your application lives in a subdirectory, for example `services/api`. ### Dockerfile By default this is set to **Automatic (no Dockerfile)**: Unkey detects your language and [builds the image for you](/docs/builds/overview). Configure a path only when you want [full control over the image with a Dockerfile](/docs/builds/dockerfile). Unkey auto-detects Dockerfiles in your build context and offers them in the dropdown. If your Dockerfile has a non-standard name or location, enter a custom path relative to the root directory. ### Watch paths Glob patterns that control which file changes trigger a deployment. When configured, Unkey skips deployments if none of the changed files match any pattern. Leave empty to deploy on all changes. Examples: * `src/**` deploys only when files in the `src` directory change * `**/*.go` deploys only when Go files change You can add multiple patterns. A deployment triggers if any pattern matches a changed file. ### Auto deploy Controls whether GitHub pushes automatically trigger deployments. Configured per environment so you can deploy production manually while letting preview branches deploy on every push (or vice versa). | Toggle | Behavior when enabled (default) | | ---------- | ---------------------------------------------------------------------------- | | Production | Pushes to the default branch deploy to production automatically | | Preview | Pushes to non-default branches deploy to a preview environment automatically | When auto deploy is disabled for an environment, Unkey records each matching push as a **Skipped** deployment for visibility, but no build runs. You can still ship changes by triggering a manual deployment from the dashboard or by [pushing an image with the CLI](/docs/build-and-deploy/cli). Auto deploy runs after [watch paths](#watch-paths) are evaluated. If auto deploy is off, the deployment is skipped before watch paths are checked. Common reasons to disable auto deploy: * Keep production stable and promote releases manually after reviewing a preview deployment * Avoid noisy preview deployments while iterating on a long-lived feature branch * Build images in your own CI and push them with the CLI instead ## Runtime settings Runtime settings control the resources and behavior of your running instances. ### Regions Geographic regions where your app runs. You must select at least one region. Traffic routes to the nearest healthy region automatically. See [Regions](/docs/build-and-deploy/regions) for the full list of available regions. Production and preview environments can have different region configurations. ### Instances The autoscaling range for each region, set as a minimum and maximum number of instances. Unkey runs at least the minimum number of instances in every selected region and scales up to the maximum based on CPU usage. Both values default to 1. Set the minimum and maximum to the same value to pin instance count and disable autoscaling. Set them to different values to let Unkey scale instances based on load: * **Minimum replicas**: the fewest instances to keep running per region, even under zero load. Increase this for redundancy or to avoid cold-start latency on the first request. * **Maximum replicas**: the ceiling Unkey scales up to under peak load. Production and preview environments can use different ranges. For example, you might run production with a range of `2 – 4` for redundancy and headroom while keeping preview at `1 – 1` to reduce cost. During the beta, the maximum is 4 instances per region. Contact [support@unkey.com](mailto:support@unkey.com) if you need more. ### Autoscaling When the minimum and maximum replicas differ, Unkey automatically adjusts instance count between those bounds based on CPU utilization. Instances scale up when sustained CPU usage is high and scale back down toward the minimum when load drops. Advanced autoscaling thresholds (memory, RPS, custom CPU targets) are not yet configurable in the dashboard. Contact [support@unkey.com](mailto:support@unkey.com) if you need to tune them. ### CPU CPU allocation for each instance. Available options: | Option | Millicores | | -------- | ---------- | | 1/4 vCPU | 256 | | 1/2 vCPU | 512 | | 1 vCPU | 1,024 | | 2 vCPU | 2,048 | Defaults to 1/4 vCPU. During the beta, the maximum is 2 vCPU. Contact [support@unkey.com](mailto:support@unkey.com) if you need more. ### Memory Memory allocation for each instance. Available options: * 256 MiB (default) * 512 MiB * 1 GiB * 2 GiB * 4 GiB During the beta, the maximum is 4 GiB. Contact [support@unkey.com](mailto:support@unkey.com) if you need more. ### Storage Two writable storage locations are available to your instance: | Path | Description | Always available | | ------- | --------------------------------------------------------------------- | ------------------------------- | | `/tmp` | Small in-memory tmpdir for scratch files, sockets, and runtime caches | Yes | | `/data` | Dedicated ephemeral disk volume (see below) | Only when storage is configured | #### `/tmp` Every instance gets a writable `/tmp` directory backed by memory. Use it for small temporary files that your runtime or libraries may need (lock files, Unix sockets, etc.). Because it shares the instance's memory allocation, avoid writing large files here, use ephemeral storage instead. #### Ephemeral storage Ephemeral disk space per instance. When configured, Unkey attaches a dedicated volume to each instance mounted at `/data`. The volume is created when the instance starts and destroyed when it stops, data does not persist across restarts. Available options: * None (default) * 512 MiB * 1 GiB * 2 GiB * 5 GiB * 10 GiB Use this for workloads that need temporary disk access, such as media processing (ffmpeg), file format conversion, or caching data before uploading to external storage. Your application can read the mount path from the `UNKEY_EPHEMERAL_DISK_PATH` environment variable, which is set to `/data` when storage is configured. During the beta, the maximum is 10 GiB. Contact [support@unkey.com](mailto:support@unkey.com) if you need more. ### Port The port your application listens on. Defaults to 8080. Must be between 1 and 65,535. Unkey injects this value as the `PORT` environment variable at runtime. If your Dockerfile sets `ENV PORT=8080` but you configure port 8081 in your app settings, your application receives `PORT=8081`. Runtime settings always take precedence over defaults set in the container image. ### Upstream protocol The protocol Unkey uses to forward requests to your application. Available options: | Option | Description | | ------------ | ------------------------------------------------------------------------------------------------------------------------------- | | HTTP/1.1 | Standard HTTP protocol. Works with all frameworks and languages. (default) | | HTTP/2 (h2c) | Cleartext HTTP/2 for applications that support h2c. Enables multiplexing and header compression between the proxy and your app. | Defaults to HTTP/1.1. Only change this if your application explicitly supports h2c (HTTP/2 without TLS). Most applications should use HTTP/1.1. ### Command The command to start your application. This overrides the default `CMD` in your Dockerfile. Leave empty to use your image's default command. Arguments are split on whitespace. Example: `npm start`. ### Shutdown signal The signal Unkey sends to your application when stopping an instance during deployments or scale-down. Defaults to `SIGTERM`. Supported signals: * `SIGTERM` (default) * `SIGINT` * `SIGQUIT` * `SIGKILL` Your application should handle the configured signal to shut down gracefully (close connections, flush buffers, finish in-progress requests). If the process doesn't exit within the shutdown timeout, Unkey sends `SIGKILL`. The shutdown signal is not yet configurable in the dashboard. Contact [support@unkey.com](mailto:support@unkey.com) to change it from the default `SIGTERM`. ### Health check An endpoint Unkey uses to verify your instances are healthy. When configured, Unkey sends periodic HTTP requests to the specified path and uses the response to determine instance health. | Setting | Description | Default | | ----------------- | --------------------------------------------------------------- | ------- | | Method | `GET` or `POST` | `GET` | | Path | The endpoint path (must start with `/`) | - | | Interval | How often to check (for example, `30s`, `2m`, `1h`) | `30s` | | Timeout | How long to wait for a response before marking the check failed | 5s | | Failure threshold | Consecutive failures before marking the instance unhealthy | 3 | | Initial delay | Time to wait after instance start before the first check | 0s | The health check endpoint must return a `2xx` status code to pass. After the failure threshold is reached, Unkey marks the instance as unhealthy and stops routing traffic to it. Health checks are optional. If you don't configure one, Unkey relies on process-level health monitoring. Timeout, failure threshold, and initial delay are not yet configurable in the dashboard. Contact [support@unkey.com](mailto:support@unkey.com) to adjust these values. ## Environment variables Environment variables have moved to their own dedicated **Environment Variables** tab in the project dashboard. You can search, filter by environment, sort, and bulk-import variables from `.env` files. See [Variables](/docs/platform/variables/overview) for full details on creating, editing, and managing variables. ## Networking settings ### Custom domains Serve your deployment from your own domain name. Each custom domain is scoped to an environment and requires DNS verification before it becomes active. See [Custom domains](/docs/networking/domains) for setup instructions, including DNS record configuration and verification. ### OpenAPI spec path The server path where your application serves its OpenAPI specification (for example, `/openapi.yaml` or `/api/spec.json`). When set, Unkey fetches the spec from your running deployment for use with [OpenAPI validation](/docs/platform/sentinel/policies/openapi-validation) and API documentation. Maximum length is 512 characters. Leave empty to disable spec discovery. ## App-level settings These settings apply to the app itself, not to a specific environment. ### Default branch The Git branch that maps to the production environment. Defaults to `main`. Unkey updates this automatically when you connect a GitHub repository. Pushes to the default branch trigger production deployments. Pushes to any other branch trigger preview deployments. ### Delete protection When enabled, prevents the app from being deleted. You must disable delete protection before you can delete the app. Delete protection is not yet configurable in the dashboard. Contact [support@unkey.com](mailto:support@unkey.com) to enable it. # Overview Source: https://unkey.com/docs/platform/identities/overview Group multiple API keys under a single identity like a user, team, or organization. Manage shared configuration and rate limits in Unkey. Identities let you connect multiple API keys to a single entity, a user, an organization, a service account, or anything else in your system. Once connected, keys can share configuration, metadata, and rate limits. ## Why use identities? Without identities, each API key is standalone. If a user has 3 keys, you need to update each one individually. With identities: | Without Identities | With Identities | | ----------------------------------------- | ------------------------------------------------ | | Update rate limits on each key separately | Update once on the identity, applies to all keys | | Metadata stored per-key | Shared metadata across all keys | | Analytics scattered across keys | Aggregated analytics per identity | | No concept of "this user's total usage" | Shared rate limit pool across all keys | ## Real-world example Imagine you're building a developer platform like Stripe. Each customer might have: * A **test key** for development * A **production key** for live traffic * **Multiple restricted keys** for different services All these keys belong to the same customer. With identities: ```ts theme={"theme":"kanagawa-wave"} // Create an identity for the customer try { await unkey.identities.createIdentity({ externalId: "customer_acme_123", // Your internal customer ID meta: { plan: "pro", company: "Acme Corp", }, }); // Keys link to the identity by reusing the same externalId await unkey.keys.createKey({ apiId: "api_xxx", externalId: "customer_acme_123", name: "Production Key", }); await unkey.keys.createKey({ apiId: "api_xxx", externalId: "customer_acme_123", name: "Test Key", }); } catch (err) { console.error(err); throw err; } ``` Now when you verify any of these keys, you get the identity metadata: ```ts theme={"theme":"kanagawa-wave"} const { meta, data } = await unkey.keys.verifyKey({ key: "sk_..." }); console.log(data.identity); // { id: "id_xxx", externalId: "customer_acme_123", meta: { plan: "pro", company: "Acme Corp" } } ``` ## Use cases Set a rate limit on the identity, and all associated keys share the same pool. If a user has 1000 requests/hour across all their keys, they can't bypass the limit by using different keys. [Learn more →](/docs/platform/identities/ratelimits) Instead of piecing together usage from individual keys, get total usage per identity. Perfect for billing dashboards and customer insights. Store plan tier, feature flags, or custom limits on the identity. All keys inherit this configuration without needing individual updates. Map identities to organizations in your system. When an org adds team members or creates new keys, they all share the same identity and limits. ## Data model Unkey's internal identifier for this identity. Use this to reference the identity in API calls. **Your** identifier for this entity. This links the Unkey identity to your system, typically a user ID, org ID, or customer ID. If you use Clerk, Auth0, WorkOS, or similar: use their user/org ID as the externalId. ```ts theme={"theme":"kanagawa-wave"} // Example with Clerk const identity = await unkey.identities.createIdentity({ externalId: clerkUserId, // "user_2NNEqL2nrIRdJ194ndJqAHwEfxC" }); ``` Arbitrary JSON metadata. Store anything useful: plan tier, feature flags, company name, etc. ```json theme={"theme":"kanagawa-wave"} { "plan": "enterprise", "features": ["advanced-analytics", "sso"], "billingEmail": "billing@acme.com" } ``` ## Next steps Enforce limits across all keys for an identity Step-by-step guide to implementing identities # Shared Rate Limits Source: https://unkey.com/docs/platform/identities/ratelimits Share rate limits across multiple API keys using Unkey identities. Enforce a single quota for all keys belonging to the same entity. Identities let you enforce a **single rate limit** across all of a user's API keys. Without this, a user with 5 keys at 100 req/s each could make 500 req/s total, probably not what you want. ## The problem Without shared limits, rate limits apply per-key: ```text theme={"theme":"kanagawa-wave"} User has 3 API keys, each with 100 req/s limit → User can actually make 300 req/s (100 × 3 keys) ``` ## The solution Create an identity for the user, attach a rate limit to it, then link all their keys to that identity: ```text theme={"theme":"kanagawa-wave"} Identity: user_123 └── Rate limit: 100 req/s (shared pool) ├── Key A (production) ├── Key B (staging) └── Key C (mobile app) → User can only make 100 req/s total across ALL keys ``` ## Step 1: Create an identity with rate limits ```bash cURL theme={"theme":"kanagawa-wave"} 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": 1000 } ] }' ``` ```typescript TypeScript theme={"theme":"kanagawa-wave"} import { Unkey } from "@unkey/api"; const unkey = new Unkey({ rootKey: process.env.UNKEY_ROOT_KEY }); const { meta, data } = await unkey.identities.createIdentity({ externalId: "user_123", ratelimits: [ { name: "requests", limit: 100, duration: 1000, // 100 requests per second }, ], }); console.log(data.identityId); // Save this ``` ## Step 2: Create keys linked to the identity ```bash cURL theme={"theme":"kanagawa-wave"} 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 Key" }' ``` ```typescript TypeScript theme={"theme":"kanagawa-wave"} // All keys with the same externalId share the identity's rate limits try { const { meta, data } = await unkey.keys.createKey({ apiId: "api_...", externalId: "user_123", // Links to the identity name: "Production Key", }); await unkey.keys.createKey({ apiId: "api_...", externalId: "user_123", // Same identity name: "Staging Key", }); } catch (err) { console.error(err); throw err; } ``` ## Step 3: Verify, limits are shared automatically When you verify any key linked to the identity, the shared rate limit is enforced: ```typescript theme={"theme":"kanagawa-wave"} const { data } = await unkey.keys.verifyKey({ key: "sk_prod_..." }); // If the identity is rate limited, data.valid will be false if (!data.valid && data.code === "RATE_LIMITED") { // User exceeded 100 req/s across ALL their keys } ``` *** ## Multiple rate limits You can set multiple limits on an identity, useful for different resource types: ```typescript theme={"theme":"kanagawa-wave"} try { const { meta, data } = await unkey.identities.createIdentity({ externalId: "user_123", ratelimits: [ { name: "requests", limit: 500, duration: 3600000, // 500 requests per hour }, { name: "tokens", limit: 20000, duration: 86400000, // 20k tokens per day }, ], }); } catch (err) { console.error(err); throw err; } ``` Then specify which limit to check during verification: ```typescript theme={"theme":"kanagawa-wave"} // Check the "tokens" limit and consume 150 tokens const { data } = await unkey.keys.verifyKey({ key: "sk_...", ratelimits: [{ name: "tokens", cost: 150 }], }); ``` If *any* specified limit is exceeded, verification fails. *** ## Next steps Step-by-step tutorial Learn more about identities # Instances Source: https://unkey.com/docs/platform/instances/overview Understand instances in Unkey, running container replicas of your app deployed to specific regions with automatic scaling and routing. Deploying applications on Unkey is in public beta. To try it, open the product switcher in the top-left of the dashboard and select **Deploy**. During beta, deployed resources are free. We're eager for feedback, so let us know what you think on [Discord](https://unkey.com/discord), [X](https://x.com/unkeydev), or email [support@unkey.com](mailto:support@unkey.com). An instance is a single running container of your [app](/docs/platform/apps/overview) in a specific [region](/docs/build-and-deploy/regions). When Unkey deploys your app, it creates one or more instances per region based on your configuration. Each instance runs the same container image with the same [variables](/docs/platform/variables/overview) and serves traffic independently. Instances are ephemeral, [ephemeral storage](/docs/platform/apps/settings#storage) does not persist across deployments or restarts. During deployments and scale-down, Unkey sends the configured [shutdown signal](/docs/platform/apps/settings#shutdown-signal) (default `SIGTERM`) to give your app time to drain connections and finish in-flight requests before the instance stops. ## How instances fit in Instances sit at the bottom of the deployment hierarchy. A [deployment](/docs/build-and-deploy/deployments) produces instances across your configured regions: ```text theme={"theme":"kanagawa-wave"} Workspace └── Project └── App └── Environment (production, preview) └── Deployment (specific version) └── Instances (1+ per region) ``` A deployment with two regions and two instances per region creates four instances total. Each instance receives traffic through the [Sentinel](/docs/platform/sentinel/overview) gateway running in the same region. ## Configure instance count Set the maximum number of instances per region in **Settings > Runtime settings > Instances**. The default maximum is one. Unkey starts with one instance and [scales up automatically](#autoscaling) based on CPU load, up to the maximum you configure. Running multiple instances in a region provides redundancy and distributes load. If one instance fails, the rest keep serving requests without interruption. During the beta, the maximum is 4 instances per region. Contact [support@unkey.com](mailto:support@unkey.com) if you need more. ## Resource allocation Each instance has configurable limits for CPU, memory, and storage. CPU and memory work differently for billing: * CPU is a maximum limit. Your instance can burst up to the configured amount, but Unkey only charges for the CPU time actually used. * Memory is a dedicated allocation. The configured amount is reserved for your instance, and you are charged for the full allocation. Configure limits in **Settings > Runtime settings**: * CPU: 1/4 vCPU to 2 vCPU (default 1/4 vCPU, billed on usage) * Memory: 256 MiB to 4 GiB (default 256 MiB, billed on allocation) * Storage: None to 10 GiB ephemeral disk (default none, mounted at `/data`) A `/tmp` directory backed by memory is always available for scratch files. For larger temporary storage needs, configure [ephemeral storage](/docs/platform/apps/settings#ephemeral-storage) to attach a dedicated disk volume per instance. All instances in a deployment share the same resource configuration. Changing these values takes effect on the next deployment. These limits apply during the beta. Larger instance sizes are planned once the beta ends. Contact [support@unkey.com](mailto:support@unkey.com) if you need more resources now. See [App settings](/docs/platform/apps/settings#cpu) for details on each option. ## Load balancing The [Sentinel](/docs/platform/sentinel/overview) gateway in each region distributes incoming requests uniformly at random across all running instances. Each instance has an equal probability of handling any given request. If no running instances exist in the local region, traffic reroutes to the nearest region that has healthy instances. ## Health checks Unkey sends periodic HTTP requests to a health check endpoint in your app to verify each instance can receive traffic. Instances that fail consecutive checks are removed from the load-balancing pool until they recover or the deployment is replaced. Without a health check configured, Unkey considers an instance healthy as long as its container process is running. See [App settings](/docs/platform/apps/settings#health-check) for the full list of configuration options. ### Design a health check endpoint Your health check endpoint should confirm the instance can handle requests, not that every downstream dependency is reachable. A simple endpoint that returns `200 OK` is enough: ```plaintext theme={"theme":"kanagawa-wave"} GET /health → 200 { "status": "ok" } ``` Avoid checking databases or third-party APIs in your health endpoint. A temporary outage in a dependency would mark all instances unhealthy, taking your entire app offline instead of returning errors for the affected queries. ## Autoscaling Autoscaling is enabled by default. Unkey automatically adjusts the number of instances in each region between 1 and the maximum you configure in the dashboard, targeting 80% CPU utilization. When CPU usage exceeds 80%, Unkey adds instances. When it drops below that threshold, Unkey removes instances down to a minimum of one. Additional autoscaling configuration options (memory thresholds, RPS thresholds, custom minimums) are not yet available in the dashboard. ## Observability Each instance reports metrics independently. In the dashboard, you can view per-instance data under the **Network** tab: * Requests per second (RPS) per instance * CPU and memory utilization * Instance status and region This visibility helps you identify whether a performance issue affects a single instance or the entire deployment. See [Observability](/docs/observability/overview) for more details. ## Next steps Configure CPU, memory, storage, and other runtime settings Choose where your instances run # Projects Source: https://unkey.com/docs/platform/projects/overview A project in Unkey organizes deployments, environments, and configuration for a single codebase. Learn how to create and manage them. Deploying applications on Unkey is in public beta. To try it, open the product switcher in the top-left of the dashboard and select **Deploy**. During beta, deployed resources are free. We're eager for feedback, so let us know what you think on [Discord](https://unkey.com/discord), [X](https://x.com/unkeydev), or email [support@unkey.com](mailto:support@unkey.com). A project is the top-level organizational unit for deploying applications on Unkey. Each project maps to a single codebase (typically a GitHub repository) and groups the [apps](/docs/platform/apps/overview), [environments](/docs/environments/overview), and configuration needed to build, deploy, and run your application. ## Project hierarchy Projects live inside a workspace. A project contains one or more apps, and each app has its own environments, deployments, and configuration. ```text theme={"theme":"kanagawa-wave"} Workspace └── Project (one per codebase) └── App (deployable service) ├── Environments (production, preview, custom) │ ├── Deployments │ ├── Variables │ └── Custom domains └── Configuration (build, runtime, Sentinel) ``` An [app](/docs/platform/apps/overview) represents a single deployable service within your project. When you create a project, Unkey creates a default app automatically. A project can contain multiple apps, for example an API server and a background worker that share the same repository. Build settings, runtime configuration, and [Sentinel policies](/docs/platform/sentinel/overview) are configured per app. [Variables](/docs/platform/variables/overview) and [custom domains](/docs/networking/domains) are scoped to an app's environment. ## Create a project 1. Navigate to your workspace's **Projects** page. 2. Click **New project**. 3. Enter a project name. Unkey generates a URL-friendly slug from the name, which you can edit. 4. Connect your GitHub account if you haven't already. You can skip this step and connect later. 5. Select the repository to deploy from your connected GitHub accounts. To finish onboarding without a repository, for example, if you plan to deploy with the [CLI](/docs/build-and-deploy/cli) or want to connect GitHub later, click **Skip for now**. 6. Configure initial [build and runtime settings](/docs/platform/projects/settings). You can change these later. 7. Click **Deploy** to trigger your first deployment. The slug must be unique within your workspace and can only contain lowercase letters, numbers, and hyphens. If you skipped the repository step, the project is created without a connected repo. You can connect one later from the project's **Settings** tab and trigger your first deployment from there. ## Project dashboard After creation, the project dashboard gives you four tabs: | Tab | What it shows | | --------------- | --------------------------------------------------------------------------------------------------- | | **Deployments** | Active and past deployments across all environments, with commit info and status | | **Logs** | Runtime logs from your running instances | | **Requests** | HTTP request analytics processed by [the Sentinel](/docs/platform/sentinel/overview) | | **Settings** | Build, runtime, and advanced configuration. See [Settings](/docs/platform/projects/settings) for details | The deployments tab is the default view when you open a project. ## Delete a project Deleting a project permanently removes all deployments, environments, custom domains, variables, and associated data. This action cannot be undone. To delete a project: 1. Open the project actions menu (three-dot icon on the project card). 2. Select **Delete project**. 3. Check the confirmation box acknowledging the permanent deletion. 4. Confirm the deletion. Deletion is asynchronous. Unkey removes all associated resources (deployments, environments, domains, variables) in the background. The project disappears from your workspace immediately, but cleanup of underlying resources completes shortly after. # Settings Source: https://unkey.com/docs/platform/projects/settings Configure project settings in Unkey including build commands, environment variables, domains, and production branch per app and environment. Deploying applications on Unkey is in public beta. To try it, open the product switcher in the top-left of the dashboard and select **Deploy**. During beta, deployed resources are free. We're eager for feedback, so let us know what you think on [Discord](https://unkey.com/discord), [X](https://x.com/unkeydev), or email [support@unkey.com](mailto:support@unkey.com). The **Settings** tab in your project dashboard configures the [app](/docs/platform/apps/overview) within your project. Each app has its own build, runtime, and advanced settings scoped per environment. For single-app projects (the default), you manage settings directly from the project dashboard. For multi-app projects, each app has independent configuration. See [App settings](/docs/platform/apps/settings) for the full reference of configurable options, including: * [Build settings](/docs/platform/apps/settings#build-settings) (GitHub repository, Dockerfile, root directory, watch paths) * [Runtime settings](/docs/platform/apps/settings#runtime-settings) (regions, instances, CPU, memory, storage, port, command, health check) * [Advanced configurations](/docs/platform/apps/settings#advanced-configurations) (environment variables, custom domains, OpenAPI spec, Sentinel keyspaces) # Automated Overrides Source: https://unkey.com/docs/platform/ratelimiting/automated-overrides Manage rate limit overrides programmatically through the Unkey API or SDK. Automate dynamic limits based on user plans or usage tiers. Unkey's ratelimit override API allows you to manage dynamic overrides in response to events in your system. For example when your customer upgrades to an enterprise plan, you might want to create overrides for them to give them higher quotas. Let's look at common scenarios and how to implement them using our [@unkey/api SDK](https://www.unkey.com/docs/libraries/ts/sdk/overview). Our application has a ratelimit namespace called `email.send`, which ratelimits users from sending OTP emails during login. As identifier we're using their email address. ## Set Override In this example, we'll set an override for all users of our fictional customer `calendso.com`. How you detect a change is up to you, typically it's either through a user or admin action, or some form of incoming webhook from your billing or auth provider. ```ts theme={"theme":"kanagawa-wave"} import { Unkey } from "@unkey/api"; const unkey = new Unkey({ rootKey: process.env.UNKEY_ROOT_KEY!, }); await unkey.ratelimit.setOverride({ namespaceName: "email.send", // set the override for all users with this domain identifier: "*@calendso.com", limit: 10, duration: 60_000, // 1 minute async: true, }); ``` [API Reference ->](/docs/api-reference/overview) Now, when we're ratelimiting `tim@calendso.com`, it will use the override settings and ratelimit them to 10 per minute. ## Get Override Retrieve a single override for an identifier within a namespace. ```ts theme={"theme":"kanagawa-wave"} import { Unkey } from "@unkey/api"; const unkey = new Unkey({ rootKey: process.env["UNKEY_ROOT_KEY"] ?? "", }); async function run() { const result = await unkey.ratelimit.getOverride({ namespace: "api.requests", identifier: "premium_user_123", }); console.log(result); } run(); { meta: { requestId: "req_123", }, data: { overrideId: "", duration: 949068, identifier: "", limit: 641349, }, } ``` [API Reference ->](/docs/api-reference/overview) ## List Overrides You can list all of the configured overirdes for a namespace to build your own dashboards. ```ts theme={"theme":"kanagawa-wave"} import { Unkey } from "@unkey/api"; const unkey = new Unkey({ rootKey: process.env["UNKEY_ROOT_KEY"] ?? "", }); async function run() { const result = await unkey.ratelimit.listOverrides({ namespace: "api.requests", limit: 20, }); console.log(result); } run(); { meta: { requestId: "req_123", }, data: [], pagination: { cursor: "eyJrZXkiOiJrZXlfMTIzNCIsInRzIjoxNjk5Mzc4ODAwfQ==", hasMore: true, }, } ``` [API Reference ->](/docs/api-reference/overview) ## Delete Override Once they downgrade their plan, we can revoke any overrides: ```ts theme={"theme":"kanagawa-wave"} import { Unkey } from "@unkey/api"; const unkey = new Unkey({ rootKey: process.env["UNKEY_ROOT_KEY"] ?? "", }); async function run() { const result = await unkey.ratelimit.deleteOverride({ namespace: "api.requests", identifier: "premium_user_123", }); console.log(result); } run(); { meta: { requestId: "req_123", }, data: {}, } ``` [API Reference ->](/docs/api-reference/overview) # How rate limiting works Source: https://unkey.com/docs/platform/ratelimiting/how-it-works Learn how Unkey's distributed rate limiting works, what response fields mean, and what consistency to expect across regions. Unkey rate limiting checks requests close to your users and shares usage across regions. You get low-latency decisions, regional consistency, and global convergence without managing rate limit infrastructure yourself. ## Evaluate a request Each rate limit check answers one question: can this identifier spend the requested cost inside this time window? You choose the identifier. It can be a user ID, API key ID, IP address, organization ID, or any stable string that represents the actor you want to limit. The response includes the limit state you need to decide what to do next. A successful response means your application can continue. A denied response usually maps to a `429 Too Many Requests` response, but your application can apply its own fallback behavior. ## Use a sliding window Unkey uses a sliding window instead of a fixed window. A sliding window counts the current window plus a weighted portion of the previous window, so traffic can't burst at the exact moment a fixed window resets. ```plaintext theme={"theme":"kanagawa-wave"} Previous minute Current minute |-----------------------------|-----------------------------| ^ now, halfway through current minute Effective count = current minute + 50% of previous minute + request cost ``` For example, with a limit of `100` per minute, a request halfway through the current minute counts all requests in the current minute plus half of the previous minute. This smooths traffic and prevents a user from sending 100 requests at `00:59` and another 100 at `01:00`. ## Share counts across regions Unkey is globally distributed. A request first affects the node that handles it, then converges within the region, then contributes to the global view for longer windows. ```plaintext theme={"theme":"kanagawa-wave"} Incoming request │ ▼ ╭──────────────────────────────╮ │ Node that receives request │ ╰──────────────┬───────────────╯ │ local decision stays fast ▼ ╭──────────────────────────────╮ │ Regional view │ │ Nodes in the region converge │ ╰──────────────┬───────────────╯ │ meaningful usage is shared ▼ ╭──────────────────────────────╮ │ Global view │ │ Longer windows converge │ ╰──────────────┬───────────────╯ │ remote usage affects future decisions ▼ ╭──────────────────────────────╮ │ Later rate limit decision │ │ Allow or deny │ ╰──────────────────────────────╯ ``` Within a region, nodes converge so a request accepted by one node affects later decisions on neighboring nodes. Across regions, identifiers that are using a meaningful share of their limit are included in the global view and affect later decisions elsewhere. This model is eventually consistent. Requests that arrive in different regions at the same time can be accepted before every region has the latest remote usage. After propagation, each region includes the other regions' observed traffic in its decision. ## Set expectations Rate limiting is a safety control for distributed systems, not a financial ledger. The table below describes what to expect in common situations. | Situation | What to expect | | --------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | One region receives most traffic | Enforcement is tight because the local counter sees the pressure immediately. | | Traffic is split across regions | Regions converge as usage propagates. A short burst can pass in more than one region before propagation catches up, especially during the first window. Sliding-window math reduces reset-boundary bursts after that. | | Window duration is shorter than 60 seconds | Enforcement is regional. Use these windows for local burst protection. | | Window duration is 60 seconds or longer | Regional usage can contribute to the global view before the window expires. Use these windows when cross-region convergence matters. | | The identifier uses a low percentage of its limit in one region | Unkey doesn't share that region's count globally until it becomes meaningful for remote decisions. | | A dependency has a temporary issue | Unkey fails gracefully by continuing to make local rate limit decisions and recovering convergence later. | For most API protection use cases, this gives the right tradeoff: low request latency, strong local enforcement, and global convergence within seconds for identifiers approaching their limit. ## Read the response Every rate limit check returns the current decision and state. | Field | Type | Meaning | | ----------- | --------- | -------------------------------------------------------------- | | `success` | `boolean` | `true` when the request fits inside the limit | | `limit` | `number` | The configured maximum for the window | | `remaining` | `number` | Tokens left after this request, clamped to `0` on denial | | `reset` | `number` | Unix timestamp in milliseconds for the current window boundary | `remaining` reflects the region's view at decision time. During cross-region propagation, another region may have accepted traffic that isn't reflected yet. Use `success` as the source of truth for the current request. ## Use cost-based limits Not every request has the same cost. Use cost-based limits when one operation consumes more than one token. With a limit of `100` per minute, the identifier can make 100 cost-1 requests, 20 cost-5 requests, or any mix that stays within the same token budget. ### Track token consumption in the dashboard When you use cost-based limiting, the rate limit overview in your Unkey dashboard surfaces token usage per identifier alongside request counts. Each row in the namespace logs table includes passed requests, blocked requests, passed tokens, and blocked tokens. This helps you find identifiers that consume expensive capacity, not only identifiers that make many requests. If you don't pass `cost`, every request costs one token and the token columns match the request columns. ## Choose limits Start with a limit that matches your product contract, then tune it from dashboard traffic data. * Use longer windows for quota-style controls, such as 1,000 requests per hour. * Use shorter windows for burst protection, such as 20 requests per 10 seconds. * Use `cost` for expensive operations, such as AI generation or report exports. * Use overrides when a specific customer needs a different limit. * Use stable identifiers. Don't include random request IDs or timestamps in the identifier. ## Next steps Give specific identifiers different limits without redeploying. Review constructor options, response fields, and overrides. # Overview Source: https://unkey.com/docs/platform/ratelimiting/introduction Protect any API endpoint from abuse with Unkey's distributed rate limiting. No infrastructure required, just a single API call. Rate limiting controls how many requests a user, IP, or any identifier can make in a given time window. Unkey provides distributed rate limiting that runs across the Unkey network without infrastructure for you to manage. ## When to use rate limiting Stop bad actors from hammering your endpoints or scraping your data. Limit expensive operations (AI calls, database queries) before they blow up your bill. Ensure no single user monopolizes shared resources. Enforce contractual limits (e.g., 10,000 requests/month on a Basic plan). ## How it works Decide what you're limiting: a user ID, API key, IP address, organization, or any string that uniquely identifies the requester. Define how many requests are allowed and over what duration. Example: 100 requests per minute. Call `limiter.limit(identifier)` and Unkey tells you whether to allow or reject the request. ## Quick example ```ts TypeScript theme={"theme":"kanagawa-wave"} import { Ratelimit } from "@unkey/ratelimit"; const limiter = new Ratelimit({ rootKey: process.env.UNKEY_ROOT_KEY, namespace: "my-app", // Group related limits together limit: 10, // 10 requests... duration: "60s", // ...per minute }); export async function handler(req: Request) { // Use any identifier: user ID, API key, IP, etc. const identifier = req.headers.get("x-user-id") ?? getClientIP(req); const { success, remaining, reset } = await limiter.limit(identifier); if (!success) { return new Response("Too many requests", { status: 429, headers: { "X-RateLimit-Remaining": "0", "X-RateLimit-Reset": reset.toString(), }, }); } // Request allowed, continue with your logic return new Response(`Hello! ${remaining} requests remaining.`); } ``` ```bash cURL theme={"theme":"kanagawa-wave"} curl -X POST https://api.unkey.com/v2/ratelimit.limit \ -H "Authorization: Bearer unkey_..." \ -H "Content-Type: application/json" \ -d '{ "namespace": "my-app", "identifier": "user_123", "limit": 10, "duration": 60000 }' ``` ## Standalone vs Key-attached rate limits Unkey offers two ways to rate limit: | Approach | Best for | How it works | | ---------------- | ------------------------------- | --------------------------------------------------------------------- | | **Standalone** | Any endpoint, public or private | You call `limiter.limit()` with any identifier | | **Key-attached** | API key authenticated endpoints | Rate limits are configured per-key and checked during `keys.verify()` | **Standalone** is what this section covers, it works anywhere, with or without API keys. **Key-attached** rate limits are configured when you [create API keys](/docs/platform/apis/features/ratelimiting/overview) and are automatically enforced during verification. You can use both. Standalone rate limits work well for public endpoints such as login and signup. Key-attached rate limits work well for authenticated API calls. ## What makes Unkey rate limiting different? No Redis clusters, no Upstash accounts, no connection strings. Install the SDK and call the API. Requests are processed across Unkey's globally distributed infrastructure. Your rate limits are checked close to your users, not in a single region. Identifiers approaching their limit converge globally within seconds. See [how rate limiting works](/docs/platform/ratelimiting/how-it-works#share-counts-across-regions). See real-time performance metrics at [ratelimit.unkey.com](https://ratelimit.unkey.com), our global latency and throughput benchmarks updated live. Configure custom timeout and fallback behavior for resilience when network issues occur. Give specific users higher limits without changing code. "User X gets 1000/min instead of 100/min." See which identifiers are hitting limits, when, and how often, in your Unkey dashboard. ## Get started Go to [Settings → Root Keys](https://app.unkey.com/settings/root-keys) and create a new key with these permissions: - `ratelimit.*.create_namespace` - `ratelimit.*.limit` `bash npm install @unkey/ratelimit ` See the [Next.js](/docs/quickstart/ratelimiting/nextjs), [Bun](/docs/quickstart/ratelimiting/bun), [Express](/docs/quickstart/ratelimiting/express), or [Hono](/docs/quickstart/ratelimiting/hono) guides for complete examples. ## Next steps Full walkthrough for your framework. Understand global consistency and response behavior. Give specific users custom limits. All configuration options and methods. # Custom Overrides Source: https://unkey.com/docs/platform/ratelimiting/overrides Create custom rate limit overrides for specific users or identifiers in Unkey. Grant higher or lower limits without changing your code. Override default rate limits for specific identifiers directly from the dashboard. No code changes or deploys needed, changes roll out globally in seconds. ## When to use this Enterprise customers need higher limits than your default tier. Integration partners building on your API need room to grow. Reduce limits for suspicious users without blocking entirely. Temporarily increase limits for specific test accounts. ## How it works 1. You define default limits in your code 2. Create overrides in the dashboard for specific identifiers 3. When that identifier is rate limited, Unkey uses the override instead of the default **Override priority:** Exact matches > Wildcard matches > Default limits ## Create an override Click **Ratelimit** in the sidebar → select your namespace → **Overrides** tab. If you don't have a namespace yet, create one first. New override form Enter: * **Identifier**: The exact identifier or wildcard pattern * **Limit**: Custom request limit * **Duration**: Time window for the limit Click **Override Identifier**. Changes propagate globally within \~60 seconds (usually much faster). ## Example: Enterprise customer Your default limit is 100 requests/minute. Acme Corp needs 10,000/minute. | Identifier | Limit | Duration | | ----------- | ----- | -------- | | `acme-corp` | 10000 | 60s | Now when `acme-corp` hits your API, they get 10,000/min instead of 100/min. ## Wildcard patterns Use `*` to match multiple identifiers at once. ### Examples | Pattern | Matches | | -------------- | ------------------------------------------------------ | | `*@acme.com` | `alice@acme.com`, `bob@acme.com`, `api@acme.com` | | `enterprise:*` | `enterprise:123`, `enterprise:acme`, `enterprise:test` | | `user_*_prod` | `user_123_prod`, `user_abc_prod` | ### Priority Exact matches always win over wildcards: | Override | Limit | | -------------- | --------- | | `*@acme.com` | 500/min | | `ceo@acme.com` | 10000/min | **Result:** * `ceo@acme.com` → 10,000/min (exact match) * `anyone-else@acme.com` → 500/min (wildcard match) * `user@other.com` → default limit Wildcard override example ## Managing overrides via API Create overrides programmatically: ```bash theme={"theme":"kanagawa-wave"} curl -X POST https://api.unkey.com/v2/ratelimits.setOverride \ -H "Authorization: Bearer $UNKEY_ROOT_KEY" \ -H "Content-Type: application/json" \ -d '{ "namespaceId": "rl_...", "identifier": "enterprise:acme", "limit": 10000, "duration": 60000 }' ``` This enables workflows like: * Automatically increasing limits when users upgrade * Syncing limits from your billing system * Temporary increases during promotions ## Common patterns ### Tier-based limits ```text theme={"theme":"kanagawa-wave"} free:* → 100/min pro:* → 1000/min enterprise:* → 10000/min ``` Use prefixed identifiers in your code: `${plan}:${userId}` ### Domain-based limits ```text theme={"theme":"kanagawa-wave"} *@bigcustomer.com → 5000/min *@partner.io → 2000/min ``` ### Temporary boost Need to give someone extra capacity for a demo or migration? Add an override, then remove it when done. No code changes needed. ## Removing overrides Delete an override from the dashboard or API. The identifier immediately falls back to default limits (or the next matching wildcard). ## Next steps Understand rate limiting architecture Configure multiple limits per key # Overview Source: https://unkey.com/docs/platform/root-keys/overview Create and manage root keys for programmatic access to the Unkey API. Root keys authenticate CLI tools, SDKs, and server-side requests. Root keys authenticate your requests to the Unkey API. Each root key belongs to a single workspace and can only access resources within that workspace. Use them to create API keys, manage identities, configure rate limits, and perform other administrative operations from your server. Root keys have powerful permissions. Never expose them in client-side code, commit them to git, or share them publicly. Root key list Root key list ## Root keys vs API keys | | Root keys | API keys | | ---------------- | ------------------------------------------- | ------------------------- | | **Purpose** | Manage Unkey resources | Authenticate your users | | **Who uses it** | You (the developer) | Your customers | | **Permissions** | Create/update/delete keys, manage keyspaces | Access your API endpoints | | **Where stored** | Your server's environment variables | Given to customers | ## Create a root key Create root key dialog Create root key dialog 1. Navigate to **Settings > Root Keys**. 2. Click **Create New Key**. 3. Enter a descriptive name (for example, "Vercel Production" or "CI Pipeline"). 4. Select the permissions the key needs. Grant only what the key requires. 5. Click **Create root key**. The key secret is displayed once after creation. Copy it immediately, as you cannot retrieve it later. Unkey only stores a hash of the key. Copy root key secret Copy root key secret Store it in your environment variables: ```bash .env theme={"theme":"kanagawa-wave"} UNKEY_ROOT_KEY=unkey_... ``` ## Edit a root key 1. Navigate to **Settings > Root Keys**. 2. Click the actions menu (**...**) on the key row. 3. Select **Edit root key...**. 4. Update the name or permissions. 5. Click **Update root key**. Edit root key dialog Edit root key dialog ## Delete a root key Delete root key confirmation Delete root key confirmation 1. Navigate to **Settings > Root Keys**. 2. Click the actions menu (**...**) on the key row. 3. Select **Delete root key**. 4. Check the confirmation box and click **Delete permanently**. Deleting a root key is immediate and permanent. Any application using the key loses access. ## Rotate a root key Rotation issues a new root key with the same permissions as the original and schedules the old key to expire after a grace period you choose. Use it for routine credential rotation or to replace a leaked key without downtime. 1. Navigate to **Settings > Root Keys**. 2. Click the actions menu (**...**) on the key row. 3. Select **Rotate root key**. 4. Choose a grace period for the old key: * **Revoke immediately** – revoke the old key as soon as the new one is created. * **1 minute**, **15 minutes**, **1 hour**, **6 hours**, or **24 hours** – keep the old key valid for that long so deployed services keep working while you roll out the new key. 5. Click **Rotate key**. 6. Copy the new key secret from the success dialog and store it. The plaintext is shown only once. 7. Update your application's environment variables with the new key and deploy. The new key inherits the original permissions and any expiration that was already set. The old key continues to verify until the grace period elapses, then it is revoked automatically. Expired keys cannot be rotated; create a new key instead. Choose a grace period that covers your slowest deploy. If a service still uses the old key after the grace period ends, it will receive an `EXPIRED` response. ## Best practices Only grant the permissions each root key actually needs. A key that only verifies API keys does not need `delete_key` permission. Create dedicated root keys for each service or environment. For example, `production-api-server` for verifying and creating keys, `admin-dashboard` for full management access, and `billing-service` for updating key credits only. Even without a breach, rotate root keys every few months as a security practice. Create a new key, update your services, then delete the old one. Ensure your logging does not capture root keys in request bodies or headers. ## If a root key is leaked Act immediately: 1. Go to **Settings > Root Keys** and delete the compromised key. 2. Create a replacement key with the same permissions. 3. Deploy the new key to your environment. 4. Review [audit logs](https://app.unkey.com/audit) for any unauthorized activity. 5. If you suspect API keys were created or modified, consider [rerolling](/docs/platform/apis/features/rerolling-key) them. Enable [GitHub secret scanning](/docs/security/github-scanning) to get automatic alerts if your root key is accidentally committed. # Permissions Source: https://unkey.com/docs/platform/root-keys/permissions Complete reference of all root key permissions in Unkey. Control which API operations each root key can perform across your workspace. Each root key has a set of permissions that control which API operations it can perform. ## Permission format Permissions follow the pattern `{resource}.{scope}.{action}`. The scope determines which specific resource the permission applies to. ### Wildcards vs specific IDs The scope can be either a wildcard (`*`) or a specific resource ID: * `api.*.create_key` grants `create_key` on **all** keyspaces in the workspace. * `api.api_abc123.create_key` grants `create_key` on **only** that specific keyspace. The wildcard `*` means "all current and future resources of this type." A root key with `api.*.read_key` automatically gains read access to any keyspace created after the key was issued. A key with `api.api_abc123.read_key` can only read keys belonging to that one keyspace, even if new keyspaces are created later. You can mix wildcards and specific IDs on the same root key. For example, a key for your billing service might have: * `api.*.verify_key` to verify keys across all keyspaces * `api.api_billing.create_key` to create keys in only the billing keyspace * `ratelimit.*.limit` to enforce rate limits in any namespace When configuring a root key in the dashboard, workspace-wide permissions (using `*`) are listed at the top under "Workspace." Below them, under "From APIs," each keyspace in your workspace is listed so you can grant keyspace-scoped permissions selectively. Permission selection sheet Permission selection sheet ### Insufficient permissions If a root key attempts an operation it does not have permission for, the Unkey API returns a `403 Forbidden` response with an error indicating which permission is missing. Check the error message to determine which permission to add to the key. ## Keyspace permissions These permissions control management of keyspaces in your workspace. Create new keyspaces in the workspace. There is no keyspace-scoped variant of this permission, since the keyspace does not exist yet at creation time. Read information about a keyspace, including its name, configuration, and settings. Use `api.*.read_api` to read all keyspaces. Update a keyspace's configuration. Use `api.*.update_api` to update any keyspace. Delete a keyspace and all its associated keys. Granting this on a specific keyspace does not grant delete access to other keyspaces. Use `api.*.delete_api` to delete any keyspace. Query analytics data for a keyspace using SQL. Use `api.*.read_analytics` to query across all keyspaces. ## Key permissions These permissions control creation, verification, and management of API keys. Create new keys in a keyspace. Use `api.*.create_key` to create keys in any keyspace. Read information and analytics about keys, including metadata, expiration, remaining uses, and rate limit configuration. Use `api.*.read_key` to read keys across all keyspaces. Update a key's metadata, rate limits, expiration, remaining uses, or other properties. Use `api.*.update_key` to update keys in any keyspace. Delete keys belonging to a keyspace. Use `api.*.delete_key` to delete keys in any keyspace. Verify API keys and enforce their rate limits and permissions. This is the permission your backend server needs to validate incoming requests from your users. Use `api.*.verify_key` to verify keys across all keyspaces. Encrypt keys belonging to a keyspace. Unkey stores API keys as hashes by default. With this permission, you can store an encrypted copy of the plaintext key that can be retrieved later. Use `api.*.encrypt_key` for all keyspaces. Decrypt keys belonging to a keyspace. Required to retrieve the original plaintext value of a key that was stored with encryption. Without this permission, you can verify a key but cannot recover its value. Use `api.*.decrypt_key` for all keyspaces. ## Rate limit permissions These permissions control rate limit namespaces and overrides. All rate limit permissions use the `ratelimit.*` scope. Create new rate limit namespaces in the workspace. Namespaces group related rate limit configurations together. Read information about rate limit namespaces, including their configuration and current state. Update rate limit namespace configuration, such as changing the default limit or window duration. Delete rate limit namespaces from the workspace. Execute a rate limit check against an identifier. This is the permission your backend needs to enforce rate limits at runtime. Set a rate limit override for a specific identifier. Overrides let you grant higher or lower limits to individual users or entities without changing the namespace defaults. Read rate limit overrides for an identifier. Delete a rate limit override, reverting the identifier to the namespace default. ## RBAC permissions These permissions control your ability to manage the [RBAC system](/docs/platform/apis/features/authorization/introduction) through the Unkey API. They govern who can create roles, define permissions, and assign them to API keys. This is a separate layer from the roles and permissions you define within RBAC itself. For example, `rbac.*.create_role` on a root key lets you call the API to create a new role, while the roles you create (like "editor" or "viewer") are assigned to your end-user API keys and checked at verification time. All RBAC permissions use the `rbac.*` scope. ### Roles Create a new role in the workspace. Roles are named collections of permissions that you can assign to API keys. Read roles and their associated permissions in the workspace. Delete a role from the workspace. Keys that had this role lose the permissions it granted. ### Permissions Create a new permission definition in the workspace. Permissions are the building blocks you assign to roles or directly to keys. Read permission definitions in the workspace. Delete a permission definition from the workspace. ### Key assignments Assign a role to an API key. The key inherits all permissions associated with that role. Remove a role from an API key. The key loses the permissions that role granted, unless another assigned role also grants them. Assign a permission directly to an API key, bypassing roles. Remove a directly assigned permission from an API key. ## Identity permissions These permissions control [identities](/docs/platform/identities/overview), which let you associate multiple API keys with a single user or entity. All identity permissions use the `identity.*` scope. Create a new identity in the workspace. Read identity information, including external ID and metadata. Update an identity's metadata or other properties. Delete an identity from the workspace. ## Deployment permissions These permissions control deployment operations. All deployment permissions use the `project.*` scope. Create new deployments in the workspace. Required for CI/CD pipelines that deploy through the Unkey API. Read deployment details and status, including build logs and instance health. Generate upload URLs for build contexts. Required during the deployment process to upload your application code. ## Common permission sets | Use case | Permissions | | --------------------- | ---------------------------------------------------------------------------- | | Verify keys only | `api.*.verify_key` | | Create keys for users | `api.*.create_key`, `api.*.read_key` | | Full key management | `api.*.create_key`, `api.*.read_key`, `api.*.update_key`, `api.*.delete_key` | | Rate limiting | `ratelimit.*.limit` | | Rate limit overrides | `ratelimit.*.set_override`, `ratelimit.*.read_override` | | CI/CD deployments | `project.*.create_deployment`, `project.*.generate_upload_url` | | RBAC management | `rbac.*.create_role`, `rbac.*.create_permission`, `rbac.*.add_role_to_key` | # Overview Source: https://unkey.com/docs/platform/sentinel/authentication Learn how the Sentinel reverse proxy authenticates incoming requests and forwards verified identity information to your application. Deploying applications on Unkey is in public beta. To try it, open the product switcher in the top-left of the dashboard and select **Deploy**. During beta, deployed resources are free. We're eager for feedback, so let us know what you think on [Discord](https://unkey.com/discord), [X](https://x.com/unkeydev), or email [support@unkey.com](mailto:support@unkey.com). Authentication policies verify credentials before requests reach your app. On success, the Sentinel produces a [Principal](/docs/platform/sentinel/principal/overview), a verified identity object, and forwards it to your app via the `X-Unkey-Principal` request header. Your app receives the authenticated identity without performing its own credential checks. The Sentinel supports [API key authentication](/docs/platform/sentinel/policies/api-key) today, with JWT coming soon. All authentication methods produce the same [Principal structure](/docs/platform/sentinel/principal/overview), so your app handles identity the same way regardless of how the request was authenticated. ## How it works ```mermaid theme={"theme":"kanagawa-wave"} sequenceDiagram participant C as Client participant S as Sentinel participant A as Your App C->>S: Request with credentials S->>S: Strip any existing X-Unkey-Principal header S->>S: Evaluate authentication policy alt Credentials valid S->>S: Build Principal from verified identity S->>A: Forward request with X-Unkey-Principal header A->>A: Parse Principal, read subject and source A->>C: Response else Credentials invalid or missing S->>C: 401 Unauthorized end ``` After all policies pass, the Sentinel serializes the Principal as JSON and sets the `X-Unkey-Principal` header on the proxied request. The Sentinel always strips any client-supplied `X-Unkey-Principal` header before policy evaluation, so clients can't forge identity information. Since all traffic to your deployment routes through the Sentinel, you can trust the header unconditionally. Only one Principal exists per request. If multiple authentication policies match, the first successful one sets the Principal and subsequent policies are skipped. Configuring multiple policies lets a caller authenticate with either method (for example, an API key or a JWT), but sending both on the same request is not meaningful: only the first successful policy produces the Principal, and the other credential is ignored. ## The Principal Your app reads the authenticated identity from the `X-Unkey-Principal` request header. If the header is absent, the request is anonymous. Here's an example for an API key linked to an identity: ```json theme={"theme":"kanagawa-wave"} { "version": "v1", "subject": "user_42", "type": "API_KEY", "identity": { "externalId": "user_42", "meta": { "plan": "pro" } }, "source": { "key": { "keyId": "key_3xMpL9kF2nR", "keySpaceId": "ks_abc123", "meta": {}, "roles": ["admin"], "permissions": ["api.read", "api.write"] } } } ``` See the [Principal reference](/docs/platform/sentinel/principal/overview) for the full field definitions, the [API key source](/docs/platform/sentinel/principal/sources/api-key) page for source-specific details, and [framework examples](/docs/platform/sentinel/principal/examples) for integration patterns. ## Downstream policies Other policies can use the Principal for their own decisions without knowing which authentication method produced it. For example, a [rate limit policy](/docs/platform/sentinel/policies/rate-limiting) can throttle requests per `subject`, and an API key policy can enforce [permissions](/docs/platform/apis/features/authorization/introduction) before requests reach your app. This decoupling means you can swap authentication methods without reconfiguring downstream policies. # Sentinel Source: https://unkey.com/docs/platform/sentinel/overview A Sentinel is a reverse proxy that sits in front of your deployment, enforcing policies on every request before it reaches your app. Deploying applications on Unkey is in public beta. To try it, open the product switcher in the top-left of the dashboard and select **Deploy**. During beta, deployed resources are free. We're eager for feedback, so let us know what you think on [Discord](https://unkey.com/discord), [X](https://x.com/unkeydev), or email [support@unkey.com](mailto:support@unkey.com). A Sentinel is a reverse proxy that sits in front of your deployment. If you're familiar with API gateways, a Sentinel fills the same role: it processes every inbound request, enforces [policies](/docs/platform/sentinel/policies/overview) like authentication and rate limiting, and only forwards traffic that passes all checks to your app. ## Environment isolation Every environment in your project gets its own isolated Sentinel. You don't need to create, configure, or manage them. Unkey provisions them automatically when you create an environment. * **Production environments** get three Sentinel replicas for high availability * **Preview environments** get one Sentinel replica Isolation between environments means a misconfigured policy or unusual traffic in a preview environment can't affect production. It also means each Sentinel only caches data for its own environment, resulting in high cache hit rates and low latency. The Sentinel routes requests to instances in the same region. When multiple instances of your deployment are running, the Sentinel distributes requests randomly across them. There is no session affinity. ## Policy enforcement When a request arrives at your deployment's URL, the Sentinel evaluates all configured [policies](/docs/platform/sentinel/policies/overview) in order. If every policy passes, the Sentinel selects a healthy instance and forwards the request. If any policy rejects the request, the Sentinel returns an error response and your app never sees it. All of the Sentinel's request processing is built on [policies](/docs/platform/sentinel/policies/overview). The most common ones are: * [**Authentication**](/docs/platform/sentinel/authentication): Verifies API keys before your code runs, and forwards the authenticated identity to your app via a request header * [**Rate limiting**](/docs/platform/sentinel/policies/rate-limiting): Enforces rate limits on specific routes or subjects, rejecting excess traffic before it reaches your instances * [**Logging**](/docs/platform/sentinel/policies/logging): Records the full HTTP request and response, including headers and body, for debugging and observability Other policy types include the [Firewall](/docs/platform/sentinel/policies/firewall), [OpenAPI request validation](/docs/platform/sentinel/policies/openapi-validation), and more. See [Policies](/docs/platform/sentinel/policies/overview) for the full list. ## Configuration The Sentinel dashboard is still in development. Today you can configure API key authentication, rate limiting, firewall, and OpenAPI validation policies through the dashboard. For other policy types, contact [support@unkey.com](mailto:support@unkey.com). To configure policies: 1. Navigate to your project's **Sentinel Policies** page. 2. Create a new policy and select the policy type (API key authentication, rate limiting, firewall, or OpenAPI validation). 3. Configure match conditions and policy-specific settings. 4. Save your changes. See [Authentication](/docs/platform/sentinel/authentication) for details on the Principal header your app receives after successful API key verification, or [Rate limiting](/docs/platform/sentinel/policies/rate-limiting) for configuring request limits. # API key authentication Source: https://unkey.com/docs/platform/sentinel/policies/api-key Configure the Sentinel to verify Unkey API keys on incoming requests and forward the authenticated identity and metadata to your app. Deploying applications on Unkey is in public beta. To try it, open the product switcher in the top-left of the dashboard and select **Deploy**. During beta, deployed resources are free. We're eager for feedback, so let us know what you think on [Discord](https://unkey.com/discord), [X](https://x.com/unkeydev), or email [support@unkey.com](mailto:support@unkey.com). The API key authentication policy verifies Unkey API keys before requests reach your app. On success, it produces a [Principal](/docs/platform/sentinel/principal/overview) containing the key's identity, metadata, roles, and permissions. ## Configure API key authentication To enable API key authentication for your deployment: 1. Navigate to your project's **Sentinel Policies** page from the sidebar. 2. Click **Add policy**. 3. Select **Key Auth** as the policy type. 4. Choose one or more keyspaces to verify against. 5. Optionally configure [match conditions](/docs/platform/sentinel/policies/overview#match-expressions), a custom [key location](#key-location), or a [permission query](#permission-query). 6. Select which environments (production, preview, or both) to enable the policy for. 7. Save the policy. Once configured, the Sentinel verifies every incoming request that matches the policy's conditions against the selected keyspaces. Requests without a valid API key receive a `401` response and never reach your app. ## How verification works The Sentinel extracts the API key from the `Authorization` header (as a Bearer token) and verifies it against your configured keyspaces. The following checks run in order: 1. **Existence.** The key must belong to one of the configured keyspaces. 2. **Status.** The key must not be disabled or revoked. 3. **Expiration.** The key must not have passed its expiration timestamp. 4. **Credits.** If remaining credits are configured, at least one credit must be available. Verification deducts one credit. 5. **Rate limits.** All rate limit configurations attached to the key are evaluated. See [rate limiting](/docs/platform/sentinel/policies/rate-limiting) for details on response headers and behavior. 6. **Permissions.** If a permission query is configured, the key must satisfy it. If all checks pass, the Sentinel produces a [Principal](/docs/platform/sentinel/principal/overview) and forwards the request with the `X-Unkey-Principal` header. See the [API key source](/docs/platform/sentinel/principal/sources/api-key) for the full list of fields your app receives, including roles, permissions, and key metadata. ## Key location By default, the Sentinel extracts the API key from the `Authorization` header as a Bearer token. You can override this by adding a custom key location when creating or editing a policy: | Location | Description | Example | | ----------- | --------------------------------------------------------------- | ------------------------------ | | Bearer | Extract from the `Authorization: Bearer ` header (default) | `Authorization: Bearer sk_123` | | Header | Extract from a custom header, with an optional prefix to strip | `X-API-Key: sk_123` | | Query param | Extract from a URL query parameter | `?api_key=sk_123` | ## Permission query You can enforce Unkey RBAC permissions directly in the Sentinel by setting a permission query on a policy. If the authenticated key lacks the required permissions, the request receives a `403` response before reaching your app. Permission queries support `AND` and `OR` operators: * `api.read AND api.write` requires both permissions * `api.read OR api.write` requires either permission ## Error responses | Scenario | Status | Description | | --------------------------------- | ------ | ---------------------------------- | | No credentials provided | 401 | The request is missing an API key | | Invalid, disabled, or expired key | 401 | The API key failed verification | | Insufficient permissions | 403 | The key lacks required permissions | | Rate limit exceeded | 429 | The key's rate limit was exceeded | # Firewall Source: https://unkey.com/docs/platform/sentinel/policies/firewall Block unwanted HTTP requests before they reach your app using Sentinel firewall rules. Filter traffic by path, method, header, or query parameter. Deploying applications on Unkey is in public beta. To try it, open the product switcher in the top-left of the dashboard and select **Deploy**. During beta, deployed resources are free. We're eager for feedback, so let us know what you think on [Discord](https://unkey.com/discord), [X](https://x.com/unkeydev), or email [support@unkey.com](mailto:support@unkey.com). The Firewall policy rejects requests before they reach your application. It is the Sentinel's surface for blocking unwanted traffic at the deployment layer. ## Actions Every firewall rule denies the request when its match conditions hit. Sentinel responds with HTTP `403 Forbidden` and skips all downstream policies, your upstream service is never invoked. Rules are evaluated top-to-bottom. The first matching rule blocks the request. ## Match conditions Firewall rules reuse Sentinel's shared match conditions: path, method, request header (including `User-Agent`), and query parameter. A rule can combine multiple conditions, all of them must match for the rule to apply. ## Observability Denied requests are not currently written to the request log. Per-request visibility for firewall matches will land in a later release. ## Not a DDoS mitigation The Sentinel firewall runs after traffic has entered the platform. It's the right place to protect your application from unwanted traffic and avoid invoking your instances for denied requests, but it is not an infrastructure-level DDoS shield. Platform-level abuse protection runs at our network ingress and is handled separately. # Logging Source: https://unkey.com/docs/platform/sentinel/policies/logging Enable request and response logging through the Sentinel for debugging and observability. Record full HTTP headers, bodies, and metadata. Deploying applications on Unkey is in public beta. To try it, open the product switcher in the top-left of the dashboard and select **Deploy**. During beta, deployed resources are free. We're eager for feedback, so let us know what you think on [Discord](https://unkey.com/discord), [X](https://x.com/unkeydev), or email [support@unkey.com](mailto:support@unkey.com). The logging policy records the full HTTP request and response for every matched request. Use it to debug issues, audit API usage, and understand traffic patterns without adding instrumentation to your application. ## What gets logged The Sentinel captures the following for each request: **Request data** * HTTP method, host, path, and query string * Request headers (Authorization headers are redacted) * Request body **Response data** * Status code * Response headers * Response body **Metadata** * Request ID * Timestamp * Deployment and instance identifiers * Client IP address and user agent * Region **Latency breakdown** | Metric | Description | | ---------------- | ----------------------------------------------------------- | | Total latency | End-to-end time from when the Sentinel received the request | | Instance latency | Time your app spent processing the request | | Sentinel latency | Overhead added by the Sentinel (policy evaluation, routing) | ## View logs Logs are available in the **Requests** tab of your project in the Unkey dashboard. You can filter by: * Status code * HTTP method * Path * Deployment * Environment The dashboard supports live streaming for real-time monitoring. ## Retention Logs are retained for 30 days. # OpenAPI validation Source: https://unkey.com/docs/platform/sentinel/policies/openapi-validation Validate incoming HTTP requests against your OpenAPI specification using the Sentinel. Reject malformed requests before they reach your app. Deploying applications on Unkey is in public beta. To try it, open the product switcher in the top-left of the dashboard and select **Deploy**. During beta, deployed resources are free. We're eager for feedback, so let us know what you think on [Discord](https://unkey.com/discord), [X](https://x.com/unkeydev), or email [support@unkey.com](mailto:support@unkey.com). The OpenAPI validation policy checks incoming requests against an OpenAPI 3.0 or 3.1 specification. Requests that don't conform to the spec are rejected with HTTP `400 Bad Request` before they reach your app. ## When to use it Add an OpenAPI validation policy when you want to: * Reject malformed requests at the gateway, so your app only handles well-formed input * Enforce a contract between your API and its clients without writing per-route validation in your code * Surface schema drift early — clients sending requests that don't match the spec get a clear `400` instead of an opaque server error ## What gets validated The Sentinel validates the following parts of each request against your spec: * Path parameters * Query parameters * Request headers * Request body (content type and schema) Requests that don't match any defined operation in the spec are rejected. Response validation is not performed by the Sentinel. ## Configuration The OpenAPI validation policy uses your application's auto-scraped OpenAPI specification — there is no separate spec input on the policy itself. 1. Configure the **OpenAPI spec path** in your app's [networking settings](/docs/platform/apps/settings#openapi-spec-path) (for example, `/openapi.yaml` or `/api/spec.json`). Unkey scrapes the spec from your running deployment. 2. Navigate to your project's **Sentinel Policies** page. 3. Create a new policy and select **OpenAPI Validation** as the policy type. 4. Optionally configure [match conditions](/docs/platform/sentinel/policies/overview#match-expressions) to scope validation to specific routes. 5. Save the policy. The Sentinel uses the deployment's most recently scraped spec, keyed by deployment ID, and refreshes its cache as your spec changes between deploys. If your app doesn't expose an OpenAPI spec at the configured path, the policy has no spec to validate against. Set the **OpenAPI spec path** in your app settings before enabling the policy. ## Error response When a request fails validation, the Sentinel returns a structured error following [the standard error format](/docs/platform/sentinel/policies/overview#error-response-format): ```json theme={"theme":"kanagawa-wave"} { "meta": { "requestId": "req_abc123" }, "error": { "title": "Bad Request", "detail": "request body does not match schema for /users", "status": 400, "type": "https://unkey.com/docs/errors/sentinel/openapi-validation-failed" } } ``` The `detail` field includes the specific validation failure (for example, missing required field, type mismatch, or unknown operation) to help clients fix the request. # Policies Source: https://unkey.com/docs/platform/sentinel/policies/overview Policies are configurable rules the Sentinel evaluates on every request. Combine authentication, rate limiting, IP rules, and more. Deploying applications on Unkey is in public beta. To try it, open the product switcher in the top-left of the dashboard and select **Deploy**. During beta, deployed resources are free. We're eager for feedback, so let us know what you think on [Discord](https://unkey.com/discord), [X](https://x.com/unkeydev), or email [support@unkey.com](mailto:support@unkey.com). A policy is a rule that the Sentinel evaluates on incoming requests. Each policy combines a condition (which requests to match) with an action (what to do). Policies are the building blocks for all of the Sentinel's request processing, including [authentication](/docs/platform/sentinel/authentication), rate limiting, and access control. ## Policy structure Every policy has four components: | Field | Description | | --------- | ------------------------------------------------------------------------------ | | `name` | Human-readable label for identification | | `enabled` | Toggle to disable the policy without removing it | | `match` | Conditions that determine which requests the policy applies to | | `config` | The action to perform (authentication, rate limiting, IP rules, or validation) | You can disable a policy by setting `enabled` to `false`. This is useful during incidents when you need to bypass a misbehaving policy without deleting its configuration or triggering a redeploy. ## Evaluation order The Sentinel evaluates policies in the order they appear in the configuration: 1. Skip the policy if `enabled` is `false`. 2. Evaluate all match conditions. Skip the policy if any condition doesn't match the request. 3. Execute the policy action. If the action rejects the request, return an error response immediately. 4. Move to the next policy. If all matching policies pass, the Sentinel forwards the request to your app. Order matters. Place authentication policies before policies that depend on an authenticated identity (for example, rate limiting by authenticated subject). ## Match expressions Match expressions control which requests a policy applies to. You can add match conditions when creating or editing a policy from the **Sentinel Policies** page in your project dashboard. A policy can have multiple match conditions, and all conditions must match for the policy to run (AND logic). An empty match list applies the policy to all requests. ### Available match types | Type | Matches on | Example use case | | --------------- | ------------------------ | -------------------------------------------------- | | Path | URL path | Apply auth to `/api/v1` paths only | | Method | HTTP method | Rate limit `POST` requests but not `GET` | | Header | Request header and value | Enforce policies when `X-Custom-Header` is present | | Query parameter | URL query parameter | Match requests with a specific `version` parameter | All string matching supports three modes: * **Exact**: The value must match exactly (for example, `/healthz`) * **Prefix**: The value must start with the specified string (for example, `/api/v1`) * **Regex**: The value must match an RE2 regular expression (for example, `^/users/[0-9]+$`) Each mode supports optional case-insensitive matching. ### Combine conditions To create AND conditions, add multiple match expressions to a single policy. All expressions must match for the policy to run. To create OR conditions, create separate policies with the same action but different match expressions. ## Policy types | Type | Status | Description | | -------------------------------------------------------------------- | ----------- | ---------------------------------------------------------- | | [API key authentication](/docs/platform/sentinel/policies/api-key) | Available | Verify Unkey API keys and forward identity to your app | | [Logging](/docs/platform/sentinel/policies/logging) | Available | Record full HTTP requests and responses for debugging | | JWT authentication | Coming soon | Validate Bearer JWTs using JWKS, OIDC, or PEM public keys | | [Rate limiting](/docs/platform/sentinel/policies/rate-limiting) | Available | Enforce rate limits | | [Firewall](/docs/platform/sentinel/policies/firewall) | Available | Deny requests based on path, method, header, or query | | [OpenAPI validation](/docs/platform/sentinel/policies/openapi-validation) | Available | Validate requests against an OpenAPI 3.0/3.1 specification | ## Error response format When a policy rejects a request, the Sentinel returns a structured JSON error following the RFC 7807 Problem Details format: ```json theme={"theme":"kanagawa-wave"} { "meta": { "requestId": "req_abc123" }, "error": { "title": "Unauthorized", "detail": "API key is invalid or expired", "status": 401, "type": "https://unkey.com/docs/errors/sentinel/unauthorized" } } ``` The `type` URI is stable per error kind and suitable for programmatic handling. Each policy type maps to a standard HTTP status code: | Policy type | Rejection status | | ----------------------------------------- | ---------------- | | Authentication (missing or invalid) | 401 | | Authentication (insufficient permissions) | 403 | | Rate limiting | 429 | | IP rules | 403 | | OpenAPI validation | 400 | # Rate limiting Source: https://unkey.com/docs/platform/sentinel/policies/rate-limiting Enforce rate limits on matching routes using the Sentinel reverse proxy. Configure limits per route pattern with no application code changes. Deploying applications on Unkey is in public beta. To try it, open the product switcher in the top-left of the dashboard and select **Deploy**. During beta, deployed resources are free. We're eager for feedback, so let us know what you think on [Discord](https://unkey.com/discord), [X](https://x.com/unkeydev), or email [support@unkey.com](mailto:support@unkey.com). The rate limiting policy enforces request limits on any route that matches the policy's [match expressions](/docs/platform/sentinel/policies/overview#match-expressions). Requests that exceed a configured limit receive a `429` response, protecting your app from traffic spikes and abuse. Each rate limit policy specifies a maximum number of requests within a time window (for example, 100 requests per 60 seconds) and an identifier that determines how the Sentinel groups requests for counting. Rate limit state is managed by Unkey's distributed rate limiting service, so limits are consistent across multiple Sentinel replicas. ## Configuration You can create and manage rate limit policies from the **Sentinel Policies** page in your project dashboard. Each policy requires: * **Limit**: the maximum number of requests allowed in the window * **Window**: the time window in which the limit applies (for example, 60 seconds) * **Identifier**: how the Sentinel determines which requests share a rate limit bucket * **Match conditions**: which requests the policy applies to (optional, an empty match list applies to all requests) Place authentication policies before rate limit policies in your policy list if you want to use an authenticated identifier (such as the authenticated subject or a principal field). ## Identifiers The identifier determines how the Sentinel groups requests for counting: | Identifier | Description | | --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Remote IP | Limit by client IP address. Effective for anonymous traffic but can over-limit users behind shared NATs or proxies. | | Header value | Limit by a specific request header (for example, `X-Tenant-Id`). Use only when the header is set by a trusted upstream. | | Authenticated subject | Limit by the authenticated [Principal's](/docs/platform/sentinel/principal/overview) `subject` field. Requires an authentication policy earlier in the list. | | URL path | Create separate limits per endpoint, useful for protecting expensive routes. | | Principal field | Limit by a dotted-path field from the Principal (for example, `source.key.meta.org_id` for per-organization limits). Requires an authentication policy earlier in the list. | ## Response headers When the Sentinel evaluates a rate limit, it includes the rate limit state in the response headers: | Header | Description | | ----------------------- | ---------------------------------------------------------- | | `X-RateLimit-Limit` | Maximum number of requests allowed in the window | | `X-RateLimit-Remaining` | Requests remaining in the current window | | `X-RateLimit-Reset` | Unix timestamp (seconds) when the window resets | | `Retry-After` | Seconds until the client can retry (only present on `429`) | These headers appear on both successful and rate-limited responses, so your clients can monitor their usage proactively. When multiple policies write rate limit headers (for example, a per-key limit from API key authentication and a standalone rate limit policy), the Sentinel keeps the most restrictive values. ## Exceeded rate limit behavior When a rate limit is exceeded, the Sentinel returns HTTP status `429 Too Many Requests` with the `Retry-After` header and a JSON error body: ```json theme={"theme":"kanagawa-wave"} { "meta": { "requestId": "req_abc123" }, "error": { "title": "Rate Limited", "detail": "Rate limit exceeded. Please try again later.", "status": 429, "type": "https://unkey.com/docs/errors/sentinel/rate-limited" } } ``` Your app never sees rate-limited requests. # Framework examples Source: https://unkey.com/docs/platform/sentinel/principal/examples Code examples for reading the Sentinel Principal object in Hono, Next.js, Express, and Go. Parse the forwarded identity in your framework. Deploying applications on Unkey is in public beta. To try it, open the product switcher in the top-left of the dashboard and select **Deploy**. During beta, deployed resources are free. We're eager for feedback, so let us know what you think on [Discord](https://unkey.com/discord), [X](https://x.com/unkeydev), or email [support@unkey.com](mailto:support@unkey.com). These examples show how to parse the [Principal](/docs/platform/sentinel/principal/overview) from the `X-Unkey-Principal` header in common frameworks using API key authentication. ```ts Hono theme={"theme":"kanagawa-wave"} import { Hono } from "hono"; type KeyPrincipal = { version: string; subject: string; type: "API_KEY"; identity?: { externalId: string; meta: Record; }; source: { key: { keyId: string; keySpaceId: string; name?: string; expiresAt?: number; meta: Record; roles?: string[]; permissions?: string[]; }; }; }; const app = new Hono(); app.use("*", async (c, next) => { const header = c.req.header("x-unkey-principal"); if (!header) { return c.json({ error: "Not authenticated" }, 401); } const principal: KeyPrincipal = JSON.parse(header); c.set("principal", principal); await next(); }); app.get("/api/resource", (c) => { const principal = c.get("principal") as KeyPrincipal; if (!principal.source.key.permissions?.includes("api.read")) { return c.json({ error: "Insufficient permissions" }, 403); } const userId = principal.subject; const org = principal.identity?.meta?.org; return c.json({ userId, org }); }); export default app; ``` ```ts Next.js theme={"theme":"kanagawa-wave"} import { NextRequest, NextResponse } from "next/server"; type KeyPrincipal = { version: string; subject: string; type: "API_KEY"; identity?: { externalId: string; meta: Record; }; source: { key: { keyId: string; keySpaceId: string; name?: string; expiresAt?: number; meta: Record; roles?: string[]; permissions?: string[]; }; }; }; function getPrincipal(req: NextRequest): KeyPrincipal | null { const header = req.headers.get("x-unkey-principal"); if (!header) { return null; } return JSON.parse(header) as KeyPrincipal; } export async function GET(req: NextRequest) { const principal = getPrincipal(req); if (!principal) { return NextResponse.json( { error: "Not authenticated" }, { status: 401 } ); } const userId = principal.subject; const plan = principal.identity?.meta?.plan ?? "free"; const canWrite = principal.source.key.permissions?.includes("api.write") ?? false; return NextResponse.json({ userId, plan, canWrite }); } ``` ```ts Express theme={"theme":"kanagawa-wave"} import express, { Request, Response, NextFunction } from "express"; type KeyPrincipal = { version: string; subject: string; type: "API_KEY"; identity?: { externalId: string; meta: Record; }; source: { key: { keyId: string; keySpaceId: string; name?: string; expiresAt?: number; meta: Record; roles?: string[]; permissions?: string[]; }; }; }; declare global { namespace Express { interface Request { principal?: KeyPrincipal; } } } const app = express(); app.use((req: Request, res: Response, next: NextFunction) => { const header = req.header("x-unkey-principal"); if (!header) { return res.status(401).json({ error: "Not authenticated" }); } req.principal = JSON.parse(header) as KeyPrincipal; next(); }); app.get("/api/resource", (req: Request, res: Response) => { const principal = req.principal!; if (!principal.source.key.permissions?.includes("api.read")) { return res.status(403).json({ error: "Insufficient permissions" }); } const userId = principal.subject; const plan = principal.identity?.meta?.plan ?? "free"; res.json({ userId, plan }); }); app.listen(8080); ``` ```go Go theme={"theme":"kanagawa-wave"} package main import ( "encoding/json" "net/http" "slices" ) type KeyPrincipal struct { Version string `json:"version"` Subject string `json:"subject"` Type string `json:"type"` Identity *Identity `json:"identity,omitempty"` Source struct { Key KeySource `json:"key"` } `json:"source"` } type Identity struct { ExternalID string `json:"externalId"` Meta map[string]any `json:"meta"` } type KeySource struct { KeyID string `json:"keyId"` KeySpaceID string `json:"keySpaceId"` Name string `json:"name,omitempty"` ExpiresAt int64 `json:"expiresAt,omitempty"` // unix milliseconds Meta map[string]any `json:"meta"` Roles []string `json:"roles,omitempty"` Permissions []string `json:"permissions,omitempty"` } func parsePrincipal(r *http.Request) (*KeyPrincipal, bool) { header := r.Header.Get("X-Unkey-Principal") if header == "" { return nil, false } var p KeyPrincipal if err := json.Unmarshal([]byte(header), &p); err != nil { return nil, false } return &p, true } func handler(w http.ResponseWriter, r *http.Request) { principal, ok := parsePrincipal(r) if !ok { http.Error(w, `{"error":"Not authenticated"}`, http.StatusUnauthorized) return } // Permissions is nil when the key has no permissions attached. // slices.Contains on a nil slice returns false without panicking. if !slices.Contains(principal.Source.Key.Permissions, "api.read") { http.Error(w, `{"error":"Insufficient permissions"}`, http.StatusForbidden) return } userID := principal.Subject json.NewEncoder(w).Encode(map[string]string{"userId": userID}) } func main() { http.HandleFunc("/api/resource", handler) http.ListenAndServe(":8080", nil) } ``` # Local development Source: https://unkey.com/docs/platform/sentinel/principal/local-development Test your application's Principal header handling locally without running a Sentinel. Mock the identity object for local development and testing. Deploying applications on Unkey is in public beta. To try it, open the product switcher in the top-left of the dashboard and select **Deploy**. During beta, deployed resources are free. We're eager for feedback, so let us know what you think on [Discord](https://unkey.com/discord), [X](https://x.com/unkeydev), or email [support@unkey.com](mailto:support@unkey.com). The [Principal](/docs/platform/sentinel/principal/overview) is plain JSON with no encryption or signing. During local development, you can set the `X-Unkey-Principal` header yourself to test your application's authentication handling without running a Sentinel. The `X-Unkey-Principal` header has no cryptographic signature. When you deploy to Unkey, the Sentinel always sits in front of your app and strips any client-supplied header before setting its own. Traffic cannot reach your API without going through the Sentinel, so forged headers are not a concern. If you self-host or expose your app through other infrastructure (direct port-forward, misconfigured ingress, or similar), anyone who reaches it directly can forge the header. Never expose your app to untrusted traffic without a Sentinel in front of it. ## Send a Principal with curl Pass the Principal as a JSON string in the header: ```bash theme={"theme":"kanagawa-wave"} curl http://localhost:8080/api/resource \ -H 'X-Unkey-Principal: {"version":"v1","subject":"test_user","type":"API_KEY","source":{"key":{"keyId":"key_test","keySpaceId":"ks_test","meta":{},"roles":["admin"],"permissions":["api.read","api.write"]}}}' ``` ## Use a Principal file For repeated testing, store the Principal in a file and reference it. This keeps your curl commands readable and makes it easy to switch between test scenarios. ```bash theme={"theme":"kanagawa-wave"} cat > principal.json << 'EOF' { "version": "v1", "subject": "test_user", "type": "API_KEY", "identity": { "externalId": "test_user", "meta": { "plan": "pro" } }, "source": { "key": { "keyId": "key_test", "keySpaceId": "ks_test", "meta": {}, "roles": ["admin"], "permissions": ["api.read", "api.write"] } } } EOF curl http://localhost:8080/api/resource \ -H "X-Unkey-Principal: $(cat principal.json | jq -c)" ``` # Principal Source: https://unkey.com/docs/platform/sentinel/principal/overview The Principal is the verified identity object the Sentinel produces after authentication and forwards to your application via headers. Deploying applications on Unkey is in public beta. To try it, open the product switcher in the top-left of the dashboard and select **Deploy**. During beta, deployed resources are free. We're eager for feedback, so let us know what you think on [Discord](https://unkey.com/discord), [X](https://x.com/unkeydev), or email [support@unkey.com](mailto:support@unkey.com). The Principal is a JSON object the Sentinel sets on the `X-Unkey-Principal` header after a request passes [authentication](/docs/platform/sentinel/authentication). It contains a stable set of top-level fields that are the same across all authentication methods, plus a `source` object with method-specific detail. When no authentication policy is configured or the request has no credentials, the header is absent. Check for header presence to distinguish authenticated from anonymous requests. ```json theme={"theme":"kanagawa-wave"} { "version": "v1", "subject": "user_42", "type": "API_KEY", "identity": { "externalId": "user_42", "meta": { "plan": "pro" } }, "source": { "key": { "keyId": "key_3xMpL9kF2nR", "keySpaceId": "ks_abc123", "meta": {}, "roles": ["admin"], "permissions": ["api.read", "api.write"] } } } ``` ## Top-level fields The schema version of the Principal payload. Currently `"v1"`. Check this field if you need to handle different payload shapes during migrations. Unkey bumps the version when fields are removed or renamed, but adding new optional fields does not change the version. The primary identifier of the authenticated entity. This is the field you use for database lookups, audit logging, and as the key for per-user logic in your application. How the subject is determined depends on the authentication method: | Type | Subject value | | ---------------------------- | ------------------------------ | | `API_KEY` (with identity) | The identity's external ID | | `API_KEY` (without identity) | The key ID | | `JWT` | The `sub` claim from the token | For API keys linked to an [identity](/docs/platform/identities/overview), the subject is the identity's external ID rather than the key ID. This means all keys belonging to the same identity share the same subject, which is what you want for rate limiting and usage tracking at the user level rather than the key level. The specific key ID is always available at `source.key.keyId` when you need it. The authentication method that produced this Principal. One of `"API_KEY"` or `"JWT"`. The value tells you which variant of `source` is populated: `"API_KEY"` → `source.key`, `"JWT"` → `source.jwt`. The [identity](/docs/platform/identities/overview) behind the credential. Present only when the credential is linked to an Unkey identity. When no identity is linked, this field is absent from the JSON entirely (not `null`, not an empty object). Identities let you group multiple credentials under a single entity. For example, a customer might have separate API keys for staging and production, but both keys are linked to the same identity. Use `identity.externalId` to correlate requests across credentials. The external ID you assigned when creating the identity in Unkey. This is your application's identifier for the entity (for example, a user ID from your database or an organization ID from your auth provider). Custom key-value metadata you attached to the identity. This is the same metadata you set through the Unkey API when creating or updating the identity. Useful for passing user attributes like plan tier, organization, or feature flags to your application without an extra database lookup. Empty `{}` when no metadata is set. Method-specific data from the authentication source. Contains exactly one field depending on `type`: `source.key` is populated for `API_KEY`, and `source.jwt` is populated for `JWT`. Most applications only need `subject` and `identity`, but `source` provides the full detail when you need it: key permissions, JWT claims, expiration times, and other method-specific attributes. Each source type has its own reference page: * [API key source](/docs/platform/sentinel/principal/sources/api-key) ## Versioning The `version` field starts at `"v1"`. Unkey follows these rules for changes: * **Additive changes** (new optional fields, new source types) don't bump the version. Applications that don't know about new fields ignore them. * **Breaking changes** (removing fields, changing field types, renaming fields) bump the version. Unkey supports both versions during a migration period so you have time to update your application code. # API key Source: https://unkey.com/docs/platform/sentinel/principal/sources/api-key Reference for Principal fields produced by Sentinel API key authentication including key ID, owner, permissions, and custom metadata. Deploying applications on Unkey is in public beta. To try it, open the product switcher in the top-left of the dashboard and select **Deploy**. During beta, deployed resources are free. We're eager for feedback, so let us know what you think on [Discord](https://unkey.com/discord), [X](https://x.com/unkeydev), or email [support@unkey.com](mailto:support@unkey.com). When the [Principal's](/docs/platform/sentinel/principal/overview) `type` is `"API_KEY"`, the `source.key` object contains the full details of the verified API key. This page documents every field in the API key source. ## Fields The ID of the API key that authenticated the request. When the key is linked to an identity, the top-level `subject` is the identity's external ID, so use this field when you need to identify the specific key that was used (for example, for key-level analytics or revocation). The ID of the keyspace the key belongs to. Useful when your Sentinel is configured with multiple keyspaces and your application needs to distinguish which keyspace matched. The human-readable name of the key, if one was set when the key was created. Absent when the key has no name. Useful for displaying which key was used in dashboards or audit logs (for example, "ACME Production Key"). Unix timestamp in **milliseconds** when the key expires. Absent when the key has no expiry. The Sentinel already rejects expired keys, so if this field is present the key is still valid. Your application can use it to warn users about upcoming expiration. Custom key-value metadata attached to the key. This is the same metadata you set through the Unkey API when creating or updating the key. Use it to pass per-key attributes to your application, for example an environment label or a customer tier. Empty `{}` when no metadata is set. The roles attached to the key, as flat string identifiers (for example, `["admin", "billing"]`). Omitted entirely when no roles are attached. These are the raw role names from Unkey, available for your application's own authorization logic. The permissions attached to the key, as flat string identifiers (for example, `["api.read", "api.write"]`). Omitted entirely when no permissions are attached. These are the raw permissions from Unkey. If the Sentinel enforced a [permission query](/docs/platform/sentinel/policies/api-key) and the key lacked the required permissions, the request was already rejected before reaching your app. The permissions here let your application make additional fine-grained authorization decisions beyond what the Sentinel enforces. ## Examples ```json Without identity theme={"theme":"kanagawa-wave"} { "version": "v1", "subject": "key_3xMpL9kF2nR", "type": "API_KEY", "source": { "key": { "keyId": "key_3xMpL9kF2nR", "keySpaceId": "ks_abc123", "name": "ACME Production Key", "expiresAt": 1717200000000, "meta": { "environment": "production" }, "roles": ["admin", "billing"], "permissions": ["api.read", "api.write", "billing.manage"] } } } ``` ```json With linked identity theme={"theme":"kanagawa-wave"} { "version": "v1", "subject": "user_42", "type": "API_KEY", "identity": { "externalId": "user_42", "meta": { "plan": "pro", "org": "acme" } }, "source": { "key": { "keyId": "key_3xMpL9kF2nR", "keySpaceId": "ks_abc123", "name": "ACME Production Key", "meta": {}, "roles": ["admin"], "permissions": ["api.read", "api.write"] } } } ``` ```json Minimal theme={"theme":"kanagawa-wave"} { "version": "v1", "subject": "key_3xMpL9kF2nR", "type": "API_KEY", "source": { "key": { "keyId": "key_3xMpL9kF2nR", "keySpaceId": "ks_abc123", "meta": {} } } } ``` # Variables Source: https://unkey.com/docs/platform/variables/overview Manage encrypted environment variables in Unkey. Inject key-value pairs into your app at runtime, scoped by environment for each deploy. Deploying applications on Unkey is in public beta. To try it, open the product switcher in the top-left of the dashboard and select **Deploy**. During beta, deployed resources are free. We're eager for feedback, so let us know what you think on [Discord](https://unkey.com/discord), [X](https://x.com/unkeydev), or email [support@unkey.com](mailto:support@unkey.com). Every variable is **encrypted before it is written to the database**, Unkey never stores unencrypted values. Variables are injected into your application as environment variables at runtime. Each variable is scoped to a specific [environment](/docs/environments/overview), so production and preview can have different values. You manage variables from the **Environment Variables** tab in your project dashboard. Environment variables list Environment variables list ## Create variables 1. Open your project and go to the **Environment Variables** tab. 2. Click **Add Environment Variable** to open the slide panel. 3. Enter one or more key-value pairs. 4. Select the target environment, or choose **All Environments** to apply the variable everywhere. 5. Toggle **Sensitive** if the value should be write-only. 6. Click **Save**. Add environment variable panel Add environment variable panel ### Bulk import You can add multiple variables at once: * **Paste**: paste content in `KEY=value` format (one per line) directly into the form. Unkey parses each line into a separate entry. * **Drag and drop**: drag a `.env` file onto the form to import all variables from the file. * **File picker**: click the import button and select a `.env` file from your file system. ### Form persistence If you close the panel or navigate away before saving, Unkey preserves your unsaved entries for the current session. When you reopen the panel, your draft is restored so you can pick up where you left off. A leave-prevention prompt also appears if you attempt to refresh or close the browser tab while the panel is open. ## Search, filter, and sort The toolbar above the variable list lets you: * **Search**: filter variables by name using the search field. * **Filter by environment**: show only variables for a specific environment, or select **All Environments** to see everything. * **Sort**: order variables by **Last Updated** or **Name A-Z**. ## Edit and delete variables Click any variable row to open the inline editor. You can update the key, value, or note. Use the action menu on each row to delete a variable. Edit variable inline editor Edit variable inline editor Unkey validates variable names for uniqueness within the same environment. If a variable with the same key already exists in the target environment, the form shows an error. ## Sensitive variables All variables are encrypted before they are stored, the **Sensitive** toggle does not change how a variable is encrypted. It only controls whether the value can be read back in the dashboard after saving. When **Sensitive** is enabled, the value becomes write-only: you can update or delete it, but you cannot reveal it again. Use this for API keys, database passwords, and tokens. Leave it off for non-sensitive configuration like feature flags, service URLs, and port numbers where being able to check the current value is useful. ## Environment scoping Variables are tied to a specific environment. This lets you use different configuration for production and preview without managing separate config files: | Variable | Production | Preview | | --------------- | --------------------------------- | -------------------------------- | | `DATABASE_URL` | `postgres://prod-db.acme.com/api` | `postgres://dev-db.acme.com/api` | | `LOG_LEVEL` | `warn` | `debug` | | `FEATURE_V2_UI` | `false` | `true` | When adding a variable, you can select **All Environments** to set the same key-value pair across every environment in one step. ## Precedence over image defaults Variables configured in Unkey take precedence over any environment variables set in your Dockerfile with `ENV`. For example, if your Dockerfile sets `ENV PORT=8080` but you configure `PORT=8081` in Unkey, your application receives `8081` at runtime. This applies to all variables, not just `PORT`, Unkey's runtime values always override image defaults. ## System variables In addition to variables you define, Unkey automatically injects several environment variables into every running instance. These provide context about the current deployment and runtime configuration. | Variable | Description | | --------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | | `PORT` | The [port](/docs/platform/apps/settings#port) your app should listen on | | `UNKEY_DEPLOYMENT_ID` | Unique identifier for the current deployment | | `UNKEY_ENVIRONMENT_SLUG` | Slug of the environment (e.g., `production`, `staging`) | | `UNKEY_REGION` | Region where the instance is running | | `UNKEY_INSTANCE_ID` | Unique identifier for the specific running instance | | `UNKEY_GIT_COMMIT_SHA` | Git commit SHA that triggered the deployment | | `UNKEY_GIT_BRANCH` | Git branch that triggered the deployment | | `UNKEY_GIT_REPO` | Full repository name (e.g., `org/repo`) | | `UNKEY_GIT_COMMIT_MESSAGE` | Commit message of the deployed commit | | `UNKEY_EPHEMERAL_DISK_PATH` | Mount path for [ephemeral storage](/docs/platform/apps/settings#ephemeral-storage) (`/data`). Only set when storage is configured. | Git-related variables are populated when your deployment is connected to a GitHub repository. You cannot override system variables, if you create a variable with the same name, the system value takes precedence. ## Variables and deployments Deployments are immutable. Variable changes don't affect running deployments. To apply updated variables, trigger a new deployment. A redeploy banner appears at the top of the page after you make changes as a reminder. ## Next steps How environments isolate configuration and deployments Full reference for build, runtime, and variable settings # Billing Source: https://unkey.com/docs/platform/workspaces/billing Manage your Unkey workspace billing, subscription plans, payment methods, and invoices. View usage and upgrade or downgrade your plan. Unkey uses Stripe for all billing. Each workspace has its own subscription, payment method, and invoices. You manage billing from **Settings > Billing** in the dashboard. ## Plans Unkey offers multiple plan tiers. Every workspace starts on the Free plan. Paid plans unlock higher [limits](/docs/platform/workspaces/quotas), longer data retention, and [team collaboration](/docs/platform/workspaces/team-members). | Capability | Free plan | Paid plans | | ---------------------- | ------------- | -------------- | | API requests per month | 150,000 | Varies by plan | | Log retention | 7 days | Varies by plan | | Audit log retention | 30 days | Varies by plan | | Team members | Not available | Included | Visit [unkey.com/pricing](https://unkey.com/pricing) for the latest plan details and pricing. ## Add a payment method Before upgrading, you must add a payment method. 1. Navigate to **Settings > Billing**. 2. Click **Add payment method**. 3. Complete the Stripe checkout form with your card details and billing address. 4. After saving, you return to the billing page where you can select a plan. Billing page on Free plan Billing page on Free plan ## Upgrade your plan 1. Navigate to **Settings > Billing**. 2. Click **Change plan** on the current plan card. 3. Select a plan from the available options. Each plan shows its monthly price and included request quota. 4. Confirm the selection. Plan selection modal Plan selection modal Unkey prorates charges when you upgrade mid-cycle. The difference is applied to your next invoice. ## Downgrade or cancel 1. Navigate to **Settings > Billing**. 2. Click **Cancel plan** at the bottom of the billing page. 3. Confirm the cancellation. Cancellation takes effect at the end of your current billing period. Your workspace continues to operate with paid plan limits until that date. After the period ends, the workspace reverts to Free plan [defaults](/docs/platform/workspaces/quotas#free-plan-defaults). If you cancel by mistake, you can resume your subscription from the same billing page before the period ends. Once subscribed, the billing page shows your current plan, usage, and options to change your plan, open the Stripe billing portal, or cancel. Billing page on paid plan Billing page on paid plan ## Billing portal The billing portal lets you manage payment methods, view invoices, and update billing details through Stripe. 1. Navigate to **Settings > Billing**. 2. Click **Open Portal**. The portal opens in a new page hosted by Stripe. From there you can: * Update or replace your payment method * Download past invoices * Update billing contact information ## Usage tracking The billing page and sidebar display your workspace's usage as a percentage of your monthly request quota. This combines key verifications and rate limit operations for the current billing month. Unkey sends email notifications when your workspace approaches or exceeds its quota. # Overview Source: https://unkey.com/docs/platform/workspaces/overview A workspace is the top-level container in Unkey that holds your projects, apps, API keys, billing, and team member access controls. A workspace is the highest level of separation in Unkey. All resources you create belong to exactly one workspace, and nothing is shared between workspaces. Each workspace isolates: * **[Billing](/docs/platform/workspaces/billing)**: every workspace has its own Stripe subscription, payment method, and invoices. Usage in one workspace does not affect another. * **[Team members](/docs/platform/workspaces/team-members)**: each workspace has its own set of members and roles. Being an admin in one workspace does not grant access to another. * **[Limits](/docs/platform/workspaces/quotas)**: request limits, data retention, and resource allocations are tracked per workspace. * **[Keyspaces](/docs/platform/apis/keys) and keys**: keyspaces, API keys, identities, roles, and permissions are scoped to the workspace that created them. * **[Projects](/docs/platform/projects/overview)**: deploy projects, apps, environments, and deployments belong to a single workspace. * **[Root keys](/docs/platform/root-keys/overview)**: API credentials for programmatic access are issued per workspace and only grant access to that workspace's resources. Most teams use a single workspace. Create separate workspaces when you need hard isolation between billing accounts or teams (for example, separate workspaces for different business units or clients). ## Slug and ID Every workspace has two identifiers: * **Slug** is the URL handle you choose when creating the workspace (for example, `acme`). It appears in all dashboard URLs (`app.unkey.com/acme/...`) and must be globally unique. The slug accepts lowercase letters, numbers, and hyphens, and cannot start or end with a hyphen. The slug cannot be changed after creation. * **Workspace ID** is a system-generated identifier prefixed with `ws_` (for example, `ws_abc123`). Use this when calling the Unkey API. You can copy it from **Settings > General**. ## Create a workspace 1. Navigate to the workspace creation page at `app.unkey.com/new`. 2. Enter a workspace name (3 to 50 characters). 3. Set a slug. Unkey auto-generates one from the name, but you can edit it. 4. Click **Create workspace**. After creation, Unkey redirects you to the workspace dashboard. The workspace starts on the Free plan with [default limits](/docs/platform/workspaces/quotas#free-plan-defaults). ## Switch between workspaces If you belong to multiple workspaces, use the workspace switcher in the sidebar to move between them. Workspace switcher Workspace switcher ## Delete a workspace To delete a workspace, contact [support@unkey.com](mailto:support@unkey.com). # Limits Source: https://unkey.com/docs/platform/workspaces/quotas Review the default limits enforced on your Unkey workspace including API key count, rate limit namespaces, and monthly active key caps. Every workspace has limits that define its operational boundaries. Limits control how many API requests you can make, how long Unkey retains your data, and how many resources your deployments can consume. When you [upgrade your plan](/docs/platform/workspaces/billing#upgrade-your-plan), Unkey updates your workspace limits immediately. When you [downgrade or cancel](/docs/platform/workspaces/billing#downgrade-or-cancel), limits revert to Free plan defaults at the end of the billing period. Visit [unkey.com/pricing](https://unkey.com/pricing) for the latest plan limits and pricing. If you need limits beyond what the available plans offer, contact [support@unkey.com](mailto:support@unkey.com) to discuss custom arrangements. ## Requests per month How many billable API operations your workspace can perform per billing month. This includes key verifications and rate limit checks. Requests are not blocked when you exceed this limit. Unkey sends email notifications as your workspace approaches and exceeds the threshold, prompting you to upgrade. The billing page and sidebar display your usage as a percentage of this limit, combining key verifications and rate limit operations for the current billing month. ## Log retention How long Unkey keeps operational logs for your workspace. Operational logs include request logs, verification logs, and other runtime data visible in the dashboard. Logs older than the retention period are automatically deleted and cannot be recovered. ## Audit log retention How long Unkey keeps audit logs. Audit logs record security-relevant events such as key creation, key deletion, permission changes, and workspace setting updates. Audit logs older than the retention period are automatically deleted and cannot be recovered. ## Team members Whether your workspace supports multiple users with different roles. Workspaces on the Free plan are restricted to a single user and cannot invite team members. Upgrading to any paid plan enables [team collaboration](/docs/platform/workspaces/team-members), including invitations, role management, and multiple admins. ## CPU allocation How much CPU your workspace can allocate across all running deployments at the same time. If a new deployment would push the workspace's total CPU usage beyond the limit, Unkey rejects the deployment. To free up capacity, scale down or remove existing deployments. ## Memory allocation How much memory your workspace can allocate across all running deployments at the same time. If a new deployment would push the workspace's total memory usage beyond the limit, Unkey rejects the deployment. To free up capacity, scale down or remove existing deployments. ## Workspace API rate limit Some plans include a workspace-level API rate limit that caps the number of requests your workspace can make within a time window. This is separate from the monthly request limit and operates on a shorter window (for example, requests per second). When the rate limit is exceeded, the Unkey API returns a `429 Too Many Requests` response. Responses include standard rate limit headers: | Header | Description | | --------------------- | -------------------------------------------------------- | | `RateLimit-Limit` | Maximum requests allowed in the window | | `RateLimit-Remaining` | Requests remaining in the current window | | `RateLimit-Reset` | Seconds until the window resets | | `Retry-After` | Seconds until the client can retry (only present on 429) | If no workspace rate limit is configured, these headers are omitted and requests are not rate-limited at the workspace level. # Settings Source: https://unkey.com/docs/platform/workspaces/settings Configure your Unkey workspace name, manage team members, rotate root keys, and update billing settings from the workspace dashboard. Navigate to **Settings** in the sidebar to manage your workspace. Settings are grouped into tabs: General, Team, Root Keys, and Billing. ## General General settings General settings ### Workspace name The display name for your workspace (3 to 50 characters). This is not customer-facing. It helps you and your team identify the workspace in the dashboard. 1. Navigate to **Settings > General**. 2. Enter a new name in the **Workspace Name** field. 3. Click **Save**. ### Workspace ID A read-only, system-generated identifier for your workspace (for example, `ws_abc123`). Click the copy button to copy it to your clipboard. You need this ID when calling the Unkey API. ## Team Manage who has access to your workspace and what they can do. See [Team members](/docs/platform/workspaces/team-members) for details on roles, invitations, and member management. Team members require a paid plan. [Upgrade your plan](/docs/platform/workspaces/billing#upgrade-your-plan) to access this tab. ## Root keys Manage root keys for programmatic access to the Unkey API. See [Root keys](/docs/platform/root-keys/overview) for details on creating keys, permissions, and best practices. ## Billing Manage your subscription, payment methods, and invoices. See [Billing](/docs/platform/workspaces/billing) for details on plans, upgrades, and the billing portal. # Team members Source: https://unkey.com/docs/platform/workspaces/team-members Invite team members to your Unkey workspace, assign roles, and manage access permissions. Control who can view and modify your resources. Team collaboration lets multiple people work in the same workspace. Team members page Team members page Team members require a paid plan. Workspaces on the Free plan are limited to a single user. [Upgrade your plan](/docs/platform/workspaces/billing#upgrade-your-plan) to invite team members. All members have full access to workspace resources. The only distinction is that admins can manage team members (invite, remove, and change roles), while regular members cannot. ## Invite team members 1. Navigate to **Settings > Team**. 2. Enter one or more email addresses in the invite form. 3. Click **Invite**. Each invitee receives an email with an invitation link. Invitations expire after 24 hours. If an invitation expires, send a new one. ## Manage pending invitations 1. Navigate to **Settings > Team**. 2. Select the **Pending Invitations** tab. 3. To revoke an invitation, click the revoke button next to the invitation. The tab shows all pending and expired invitations. Only pending invitations can be revoked. ## Remove a team member 1. Navigate to **Settings > Team**. 2. Find the member in the **Team Members** tab. 3. Click **Remove** next to their name. You cannot remove yourself. If you need to leave a workspace, ask another admin to remove you. Removing a member revokes their access immediately. They lose access to all workspace resources. # Bun Source: https://unkey.com/docs/quickstart/apis/bun Add API key authentication to your Bun server with Unkey. Verify keys on each request to protect your endpoints in a few lines of code. ## What you'll build A Bun HTTP server that requires a valid API key on every request. Invalid or missing keys get rejected with a 401. **Time to complete:** \~3 minutes ## Prerequisites * [Unkey account](https://app.unkey.com/auth/sign-up) (free) * [Keyspace created](https://app.unkey.com/apis) in your Unkey dashboard * [Bun](https://bun.sh) installed Clone the complete example and run it locally. ```bash theme={"theme":"kanagawa-wave"} mkdir unkey-bun && cd unkey-bun bun init -y ``` ```bash theme={"theme":"kanagawa-wave"} bun add @unkey/api ``` Create a `.env` file with your credentials from the [Unkey dashboard](https://app.unkey.com/settings/root-keys): ```bash .env theme={"theme":"kanagawa-wave"} UNKEY_ROOT_KEY="unkey_..." ``` Replace the contents of `index.ts`: ```ts index.ts theme={"theme":"kanagawa-wave"} import { Unkey } from "@unkey/api"; const unkey = new Unkey({ rootKey: Bun.env.UNKEY_ROOT_KEY! }); const server = Bun.serve({ async fetch(req) { // 1. Extract the key from the Authorization header const key = req.headers.get("Authorization")?.replace("Bearer ", ""); if (!key) { return Response.json({ error: "Missing API key" }, { status: 401 }); } // 2. Verify with Unkey try { const { data } = await unkey.keys.verifyKey({ key }); if (!data.valid) { // Key is invalid, expired, rate limited, etc. return Response.json( { error: "Invalid API key", code: data.code }, { status: 401 }, ); } // 3. Key is valid, return your response return Response.json({ message: "Hello from protected endpoint!", keyId: data.keyId, identity: data.identity, }); } catch (err) { console.error(err); return Response.json({ error: "Verification failed" }, { status: 500 }); } }, port: 3000, }); console.log(`Server running at http://localhost:${server.port}`); ``` ```bash theme={"theme":"kanagawa-wave"} bun run index.ts ``` Create a test key in your [Unkey dashboard](https://app.unkey.com), then: ```bash Test with valid key theme={"theme":"kanagawa-wave"} curl http://localhost:3000 \ -H "Authorization: Bearer YOUR_API_KEY" ``` You should see: ```json theme={"theme":"kanagawa-wave"} { "message": "Hello from protected endpoint!", "keyId": "key_...", "identity": null } ``` Try without a key: ```bash Test without key theme={"theme":"kanagawa-wave"} curl http://localhost:3000 ``` You'll get: ```json theme={"theme":"kanagawa-wave"} { "error": "Missing API key" } ``` ## What's in `data`? After successful verification: | Field | Type | Description | | ------------- | ----------- | ------------------------------------------------------------------- | | `valid` | `boolean` | Whether the key passed all checks | | `code` | `string` | Status code (`VALID`, `NOT_FOUND`, `RATE_LIMITED`, etc.) | | `keyId` | `string` | The key's unique identifier | | `name` | `string?` | Human-readable name of the key | | `meta` | `object?` | Custom metadata associated with the key | | `expires` | `number?` | Unix timestamp (in milliseconds) when the key will expire. (if set) | | `credits` | `number?` | Remaining uses (if usage limits set) | | `enabled` | `boolean` | Whether the key is enabled | | `roles` | `string[]?` | Permissions attached to the key | | `permissions` | `string[]?` | Permissions attached to the key | | `identity` | `object?` | Identity info if `externalId` was set when creating the key | | `ratelimits` | `object[]?` | Rate limit states (if rate limiting configured) | ## Adding routes Bun's built-in server uses a single `fetch` handler. For multiple routes, pattern match on the URL: ```ts index.ts theme={"theme":"kanagawa-wave"} import { Unkey } from "@unkey/api"; const unkey = new Unkey({ rootKey: Bun.env.UNKEY_ROOT_KEY! }); // Helper to verify key async function authenticate(req: Request) { const key = req.headers.get("Authorization")?.replace("Bearer ", ""); if (!key) return { valid: false, error: "Missing API key" }; try { const { data } = await unkey.keys.verifyKey({ key }); return data; } catch (err) { console.error(err); return { valid: false, error: "Verification failed" }; } } const server = Bun.serve({ async fetch(req) { const url = new URL(req.url); // Public route if (url.pathname === "/") { return Response.json({ message: "Welcome! Try /api/secret" }); } // Protected routes if (url.pathname.startsWith("/api/")) { const auth = await authenticate(req); if (!auth.valid) { return Response.json( { error: auth.error || "Unauthorized" }, { status: 401 }, ); } // Route to specific handlers if (url.pathname === "/api/secret") { return Response.json({ secret: "data", keyId: auth.keyId }); } if (url.pathname === "/api/user") { return Response.json({ user: auth.identity }); } } return Response.json({ error: "Not found" }, { status: 404 }); }, port: 3000, }); ``` ## Next steps Protect endpoints from abuse Cap total requests per key Fine-grained access control Full TypeScript SDK docs ## Troubleshooting * Ensure the key hasn't expired or been revoked - Verify the header format: `Authorization: Bearer YOUR_KEY` (note the space) Bun automatically loads `.env` files. Make sure: - The `.env` file is in your project root - You're using `Bun.env.VAR_NAME` not `process.env.VAR_NAME` - Restart the server after changing `.env` Run `bun init -y` to ensure you have a proper `tsconfig.json`. Bun handles TypeScript natively, no extra setup needed. # Express Source: https://unkey.com/docs/quickstart/apis/express Add API key authentication to your Express API using Unkey. Protect routes by verifying keys on every incoming request with minimal setup. ## What you'll build An Express server with a protected `/secret` route that requires a valid API key. Requests without a valid key get rejected with a 401. **Time to complete:** \~5 minutes ## Prerequisites * [Unkey account](https://app.unkey.com/auth/sign-up) (free) * [Keyspace created](https://app.unkey.com/apis) in your Unkey dashboard * Node.js 18+ Clone the complete example and run it locally. ```bash theme={"theme":"kanagawa-wave"} mkdir unkey-express && cd unkey-express npm init -y npm install express @unkey/api dotenv ``` Get a root key from [Settings → Root Keys](https://app.unkey.com/settings/root-keys) and create a `.env` file: ```bash .env theme={"theme":"kanagawa-wave"} UNKEY_ROOT_KEY="unkey_..." ``` Never commit your `.env` file. Add it to `.gitignore`. Create `index.js` with a protected route: ```js index.js theme={"theme":"kanagawa-wave"} const express = require("express"); const { Unkey } = require("@unkey/api"); require("dotenv").config(); const app = express(); const port = process.env.PORT || 3000; const unkey = new Unkey({ rootKey: process.env.UNKEY_ROOT_KEY }); // Public route app.get("/", (req, res) => { res.json({ message: "Welcome! Try /secret with an API key." }); }); // Protected route app.get("/secret", async (req, res) => { // 1. Extract the key from the Authorization header const authHeader = req.headers.authorization; const key = authHeader?.replace("Bearer ", ""); if (!key) { return res.status(401).json({ error: "Missing API key" }); } // 2. Verify with Unkey try { const { data } = await unkey.keys.verifyKey({ key }); if (!data.valid) { // Key is invalid, expired, rate limited, etc. return res.status(401).json({ error: "Invalid API key", code: data.code, }); } // 3. Key is valid, return protected data res.json({ message: "Welcome to the secret route!", keyId: data.keyId, // Include any metadata you attached to the key identity: data.identity, }); } catch (err) { console.error(err); return res.status(500).json({ error: "Could not verify key" }); } }); app.listen(port, () => { console.log(`Server running at http://localhost:${port}`); }); ``` ```bash theme={"theme":"kanagawa-wave"} node index.js ``` First, create a test key in your [Unkey dashboard](https://app.unkey.com), then: ```bash Test with valid key theme={"theme":"kanagawa-wave"} curl http://localhost:3000/secret \ -H "Authorization: Bearer YOUR_API_KEY" ``` You should see: ```json theme={"theme":"kanagawa-wave"} { "message": "Welcome to the secret route!", "keyId": "key_...", "identity": null } ``` Now try without a key: ```bash Test without key theme={"theme":"kanagawa-wave"} curl http://localhost:3000/secret ``` You'll get: ```json theme={"theme":"kanagawa-wave"} { "error": "Missing API key" } ``` ## What's in `data`? After successful verification, `data` contains: | Field | Type | Description | | ------------- | ----------- | ------------------------------------------------------------------- | | `valid` | `boolean` | Whether the key passed all checks | | `code` | `string` | Status code (`VALID`, `NOT_FOUND`, `RATE_LIMITED`, etc.) | | `keyId` | `string` | The key's unique identifier | | `name` | `string?` | Human-readable name of the key | | `meta` | `object?` | Custom metadata associated with the key | | `expires` | `number?` | Unix timestamp (in milliseconds) when the key will expire. (if set) | | `credits` | `number?` | Remaining uses (if usage limits set) | | `enabled` | `boolean` | Whether the key is enabled | | `roles` | `string[]?` | Permissions attached to the key | | `permissions` | `string[]?` | Permissions attached to the key | | `identity` | `object?` | Identity info if `externalId` was set when creating the key | | `ratelimits` | `object[]?` | Rate limit states (if rate limiting configured) | ## Using as middleware For cleaner code, extract verification into middleware: ```js middleware/auth.js theme={"theme":"kanagawa-wave"} const { Unkey } = require("@unkey/api"); const unkey = new Unkey({ rootKey: process.env.UNKEY_ROOT_KEY }); async function unkeyAuth(req, res, next) { const key = req.headers.authorization?.replace("Bearer ", ""); if (!key) { return res.status(401).json({ error: "Missing API key" }); } try { const { data } = await unkey.keys.verifyKey({ key }); if (!data.valid) { return res.status(401).json({ error: "Invalid API key" }); } // Attach key info to request for use in route handlers req.unkey = data; next(); } catch (err) { console.error(err); return res.status(500).json({ error: "Could not verify key" }); } } module.exports = { unkeyAuth }; ``` Then use it on any route: ```js theme={"theme":"kanagawa-wave"} const { unkeyAuth } = require("./middleware/auth"); app.get("/secret", unkeyAuth, (req, res) => { // req.unkey contains the verification result res.json({ message: "Secret data", keyId: req.unkey.keyId }); }); app.get("/another-secret", unkeyAuth, (req, res) => { res.json({ data: "More protected content" }); }); ``` ## Next steps Limit requests per key Cap total requests per key Fine-grained access control Full TypeScript SDK docs ## Troubleshooting * Ensure the key hasn't expired or been revoked - Verify the `Authorization` header format: `Bearer YOUR_KEY` (note the space) - Check that your root key has the `verify_key` permission * Check that `UNKEY_ROOT_KEY` is set correctly in your `.env` - Make sure you're calling `require("dotenv").config()` before using env vars - Check the Unkey dashboard for any service issues The code above uses CommonJS. For TypeScript, install types and use imports: ```bash theme={"theme":"kanagawa-wave"} npm install -D typescript @types/express @types/node ``` ```ts theme={"theme":"kanagawa-wave"} import express from "express"; import { Unkey } from "@unkey/api"; ``` # Go Source: https://unkey.com/docs/quickstart/apis/go Add API key authentication to your Go application using the Unkey Go SDK. Verify keys on each request to protect your API endpoints. This guide shows how to add API key verification to your Go applications using the official Unkey Go SDK. ## Prerequisites * Go 1.21 or higher * An Unkey account (free at [unkey.com](https://unkey.com)) ## 1. Install the SDK ```bash theme={"theme":"kanagawa-wave"} go get github.com/unkeyed/sdks/api/go/v2@latest ``` ## 2. Set up your Unkey credentials 1. Create a keyspace in the [Unkey Dashboard](https://app.unkey.com/apis) 2. Create a root key at [Settings → Root Keys](https://app.unkey.com/settings/root-keys) 3. Copy your API ID (looks like `api_xxxx`) Set your root key as an environment variable: ```bash theme={"theme":"kanagawa-wave"} export UNKEY_ROOT_KEY="unkey_xxxx" ``` ## 3. Create middleware Here's how to verify API keys with standard library `net/http`: ```go theme={"theme":"kanagawa-wave"} package main import ( "context" "net/http" "os" "strings" unkey "github.com/unkeyed/sdks/api/go/v2" "github.com/unkeyed/sdks/api/go/v2/models/components" ) var unkeyClient *unkey.Unkey func init() { unkeyClient = unkey.New( unkey.WithSecurity(os.Getenv("UNKEY_ROOT_KEY")), ) } // AuthMiddleware verifies API keys on incoming requests func AuthMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Extract API key from header authHeader := r.Header.Get("Authorization") if authHeader == "" { http.Error(w, `{"error": "Missing Authorization header"}`, http.StatusUnauthorized) return } apiKey := strings.TrimPrefix(authHeader, "Bearer ") // Verify with Unkey res, err := unkeyClient.Keys.VerifyKey(r.Context(), components.V2KeysVerifyKeyRequestBody{ Key: apiKey, }) if err != nil { http.Error(w, `{"error": "Verification service unavailable"}`, http.StatusServiceUnavailable) return } result := res.V2KeysVerifyKeyResponseBody.Data if !result.Valid { http.Error(w, `{"error": "Invalid API key"}`, http.StatusUnauthorized) return } // Add key info to context for handlers ctx := context.WithValue(r.Context(), "keyId", *result.KeyID) next.ServeHTTP(w, r.WithContext(ctx)) }) } func main() { mux := http.NewServeMux() // Protected route mux.HandleFunc("/api/protected", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.Write([]byte(`{"message": "Access granted!"}`)) }) // Wrap with auth middleware http.ListenAndServe(":8080", AuthMiddleware(mux)) } ``` ## 4. Run your server ```bash theme={"theme":"kanagawa-wave"} go run main.go ``` Test it with a valid API key: ```bash theme={"theme":"kanagawa-wave"} curl -H "Authorization: Bearer YOUR_API_KEY" http://localhost:8080/api/protected ``` ## Using with Gin If you're using the [Gin framework](https://gin-gonic.com/): ```go theme={"theme":"kanagawa-wave"} package main import ( "net/http" "os" "strings" "github.com/gin-gonic/gin" unkey "github.com/unkeyed/sdks/api/go/v2" "github.com/unkeyed/sdks/api/go/v2/models/components" ) var unkeyClient *unkey.Unkey func init() { unkeyClient = unkey.New( unkey.WithSecurity(os.Getenv("UNKEY_ROOT_KEY")), ) } func UnkeyAuth() gin.HandlerFunc { return func(c *gin.Context) { authHeader := c.GetHeader("Authorization") if authHeader == "" { c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Missing API key"}) return } apiKey := strings.TrimPrefix(authHeader, "Bearer ") res, err := unkeyClient.Keys.VerifyKey(c.Request.Context(), components.V2KeysVerifyKeyRequestBody{ Key: apiKey, }) if err != nil { c.AbortWithStatusJSON(http.StatusServiceUnavailable, gin.H{"error": "Verification failed"}) return } result := res.V2KeysVerifyKeyResponseBody.Data if !result.Valid { c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{ "error": "Invalid API key", "code": string(result.Code), }) return } // Store verification result in context c.Set("unkeyResult", &result) c.Next() } } func main() { r := gin.Default() // Protected routes api := r.Group("/api", UnkeyAuth()) { api.GET("/data", func(c *gin.Context) { result := c.MustGet("unkeyResult").(*components.V2KeysVerifyKeyResponseData) c.JSON(http.StatusOK, gin.H{ "message": "Access granted", "key_id": *result.KeyID, }) }) } r.Run(":8080") } ``` ## Using with Echo For the [Echo framework](https://echo.labstack.com/): ```go theme={"theme":"kanagawa-wave"} package main import ( "net/http" "os" "strings" "github.com/labstack/echo/v4" "github.com/labstack/echo/v4/middleware" unkey "github.com/unkeyed/sdks/api/go/v2" "github.com/unkeyed/sdks/api/go/v2/models/components" ) var unkeyClient *unkey.Unkey func init() { unkeyClient = unkey.New( unkey.WithSecurity(os.Getenv("UNKEY_ROOT_KEY")), ) } func UnkeyAuthMiddleware() echo.MiddlewareFunc { return func(next echo.HandlerFunc) echo.HandlerFunc { return func(c echo.Context) error { authHeader := c.Request().Header.Get("Authorization") if authHeader == "" { return c.JSON(http.StatusUnauthorized, map[string]string{"error": "Missing API key"}) } apiKey := strings.TrimPrefix(authHeader, "Bearer ") res, err := unkeyClient.Keys.VerifyKey(c.Request().Context(), components.V2KeysVerifyKeyRequestBody{ Key: apiKey, }) if err != nil { return c.JSON(http.StatusServiceUnavailable, map[string]string{"error": "Verification failed"}) } result := res.V2KeysVerifyKeyResponseBody.Data if !result.Valid { return c.JSON(http.StatusUnauthorized, map[string]string{ "error": "Invalid API key", "code": string(result.Code), }) } // Store in context c.Set("keyId", *result.KeyID) return next(c) } } } func main() { e := echo.New() e.Use(middleware.Logger()) e.Use(middleware.Recover()) // Protected route e.GET("/api/protected", func(c echo.Context) error { keyId, ok := c.Get("keyId").(string) if !ok || keyId == "" { return c.JSON(http.StatusInternalServerError, map[string]string{ "error": "Key ID not found in context", }) } return c.JSON(http.StatusOK, map[string]string{ "message": "Access granted", "key_id": keyId, }) }, UnkeyAuthMiddleware()) e.Start(":8080") } ``` ## What's next? Protect your endpoints from abuse Complete Go SDK documentation Add roles and permissions More Go recipes and examples # Hono Source: https://unkey.com/docs/quickstart/apis/hono Add API key authentication to your Hono application using Unkey middleware. Protect routes with automatic key verification on requests. ## What you'll build A Hono app with API key authentication using the `@unkey/hono` middleware. All routes (or specific ones) require a valid API key. **Time to complete:** \~5 minutes ## Prerequisites * [Unkey account](https://app.unkey.com/auth/sign-up) (free) * [Keyspace created](https://app.unkey.com/apis) in your Unkey dashboard * Node.js 18+ or Bun Clone the complete example and run it locally. ```bash npm theme={"theme":"kanagawa-wave"} npm create hono@latest unkey-hono cd unkey-hono ``` ```bash pnpm theme={"theme":"kanagawa-wave"} pnpm create hono@latest unkey-hono cd unkey-hono ``` ```bash bun theme={"theme":"kanagawa-wave"} bun create hono@latest unkey-hono cd unkey-hono ``` Choose your preferred runtime (Node.js, Bun, Cloudflare Workers, etc.) ```bash npm theme={"theme":"kanagawa-wave"} npm install @unkey/hono ``` ```bash pnpm theme={"theme":"kanagawa-wave"} pnpm add @unkey/hono ``` ```bash bun theme={"theme":"kanagawa-wave"} bun add @unkey/hono ``` Create a `.env` file: ```bash .env theme={"theme":"kanagawa-wave"} UNKEY_ROOT_KEY="unkey_..." ``` The Hono middleware verifies keys directly against your root key. Update `src/index.ts`: ```ts src/index.ts theme={"theme":"kanagawa-wave"} import { Hono } from "hono"; import { unkey, UnkeyContext } from "@unkey/hono"; // Type the context so you get autocomplete for c.get("unkey") const app = new Hono<{ Variables: { unkey: UnkeyContext } }>(); // Protect all routes with API key authentication app.use( "*", unkey({ rootKey: process.env.UNKEY_ROOT_KEY!, }), ); // This route now requires a valid API key app.get("/", (c) => { // Access verification result from context const keyInfo = c.get("unkey"); return c.json({ message: "Hello from protected route!", keyId: keyInfo.keyId, valid: keyInfo.valid, }); }); // Another protected route app.get("/secret", (c) => { const keyInfo = c.get("unkey"); return c.json({ secret: "data", identity: keyInfo.identity, meta: keyInfo.meta, }); }); export default app; ``` ```bash npm theme={"theme":"kanagawa-wave"} npm run dev ``` ```bash pnpm theme={"theme":"kanagawa-wave"} pnpm dev ``` ```bash bun theme={"theme":"kanagawa-wave"} bun run dev ``` Create a test key in your [Unkey dashboard](https://app.unkey.com), then: ```bash Test with valid key theme={"theme":"kanagawa-wave"} curl http://localhost:3000 \ -H "Authorization: Bearer YOUR_API_KEY" ``` You should see: ```json theme={"theme":"kanagawa-wave"} { "message": "Hello from protected route!", "keyId": "key_...", "valid": true } ``` Without a key, you'll get a 401: ```bash Test without key theme={"theme":"kanagawa-wave"} curl http://localhost:3000 ``` ```json theme={"theme":"kanagawa-wave"} { "error": "Unauthorized" } ``` ## Protecting specific routes Instead of protecting all routes, you can apply the middleware to specific paths: ```ts src/index.ts theme={"theme":"kanagawa-wave"} import { Hono } from "hono"; import { unkey, UnkeyContext } from "@unkey/hono"; const app = new Hono<{ Variables: { unkey: UnkeyContext } }>(); // Public route, no middleware app.get("/", (c) => { return c.json({ message: "Welcome! This route is public." }); }); // Protected routes, apply middleware to /api/* paths only app.use("/api/*", unkey({ rootKey: process.env.UNKEY_ROOT_KEY! })); app.get("/api/secret", (c) => { const keyInfo = c.get("unkey"); return c.json({ secret: "data", keyId: keyInfo.keyId }); }); app.get("/api/user", (c) => { const keyInfo = c.get("unkey"); return c.json({ identity: keyInfo.identity }); }); export default app; ``` ## What's in the context? After verification, `c.get("unkey")` contains: | Field | Type | Description | | ------------- | ----------- | ------------------------------------------------------------------------- | | `valid` | `boolean` | Whether the key passed all checks | | `code` | `string` | Status code (`VALID`, `NOT_FOUND`, `RATE_LIMITED`, etc.) | | `keyId` | `string` | The key's unique identifier | | `name` | `string?` | Human-readable name of the key | | `meta` | `object?` | Custom metadata associated with the key | | `expires` | `number?` | Unix timestamp (in milliseconds) when the key will expire. (if set) | | `credits` | `number?` | Remaining uses (if usage limits set) | | `enabled` | `boolean` | Whether the key is enabled | | `roles` | `string[]?` | Named role(s) assigned to the key, each representing a set of permissions | | `permissions` | `string[]?` | List of individual permissions granted to the key | | `identity` | `object?` | Identity info if `externalId` was set when creating the key | | `ratelimits` | `object[]?` | Rate limit states (if rate limiting configured) | ## Middleware options ```ts theme={"theme":"kanagawa-wave"} unkey({ rootKey: process.env.UNKEY_ROOT_KEY!, // Required: Your root key // Optional: Custom error handling onError: (c, error) => { console.error("Unkey error:", error); return c.json({ error: "Auth service unavailable" }, 503); }, // Optional: Custom unauthorized response handleInvalidKey: (c, result) => { return c.json( { error: "Invalid API key", code: result.code, }, 401, ); }, }); ``` ## Next steps Limit requests per key Cap total requests per key Fine-grained access control Full middleware documentation ## Troubleshooting * Ensure the key hasn't expired or been revoked - Verify the header format: `Authorization: Bearer YOUR_KEY` * For Node.js: Install `dotenv` and add `import 'dotenv/config'` at the top - For Bun: `.env` is loaded automatically - For Cloudflare Workers: Use `wrangler secret` or `wrangler.toml` Use wrangler secrets for your root key: ```bash theme={"theme":"kanagawa-wave"} npx wrangler secret put UNKEY_ROOT_KEY ``` Then access it from your Hono bindings. Add `UNKEY_ROOT_KEY` to your `Bindings` type and read it via `ctx.env.UNKEY_ROOT_KEY` or `env.UNKEY_ROOT_KEY` in your handlers. # Next.js Source: https://unkey.com/docs/quickstart/apis/nextjs Add API key authentication to your Next.js API routes using Unkey. Verify keys in route handlers to protect your server-side endpoints. ## What you'll build A Next.js API route that requires a valid API key on every request. Invalid or missing keys get rejected with a 401. **Time to complete:** \~5 minutes ## Prerequisites * [Unkey account](https://app.unkey.com/auth/sign-up) (free) * [Keyspace created](https://app.unkey.com/apis) in your Unkey dashboard Skip this if you have an existing project. ```bash npm theme={"theme":"kanagawa-wave"} npx create-next-app@latest my-api cd my-api ``` ```bash pnpm theme={"theme":"kanagawa-wave"} pnpm create next-app@latest my-api cd my-api ``` ```bash bun theme={"theme":"kanagawa-wave"} bunx create-next-app my-api cd my-api ``` ```bash npm theme={"theme":"kanagawa-wave"} npm install @unkey/nextjs ``` ```bash pnpm theme={"theme":"kanagawa-wave"} pnpm add @unkey/nextjs ``` ```bash bun theme={"theme":"kanagawa-wave"} bun add @unkey/nextjs ``` Get a root key from *Settings → Root Keys* and add it to your environment: ```bash .env.local theme={"theme":"kanagawa-wave"} UNKEY_ROOT_KEY="unkey_..." ``` Never commit your root key. Add `.env.local` to `.gitignore`. Create a new API route that requires authentication: ```ts app/api/protected/route.ts theme={"theme":"kanagawa-wave"} import { NextRequestWithUnkeyContext, withUnkey } from "@unkey/nextjs"; export const POST = withUnkey( async (req: NextRequestWithUnkeyContext) => { // The key has already been verified at this point // Access verification details via req.unkey.data // Your API logic here // // req.unkey.data contains all verification details. return Response.json({ message: "Hello!", keyId: req.unkey.data.keyId, // If you set an externalId when creating the key: externalId: req.unkey.data.identity?.externalId, }); }, { rootKey: process.env.UNKEY_ROOT_KEY! }, ); ``` The `withUnkey` wrapper handles key extraction, verification, and error responses automatically. Invalid keys never reach your handler. ```bash npm theme={"theme":"kanagawa-wave"} npm run dev ``` ```bash pnpm theme={"theme":"kanagawa-wave"} pnpm dev ``` ```bash bun theme={"theme":"kanagawa-wave"} bun dev ``` First, create a test key in your [Unkey dashboard](https://app.unkey.com), then: ```bash Test with valid key theme={"theme":"kanagawa-wave"} curl -X POST http://localhost:3000/api/protected \ -H "Authorization: Bearer YOUR_API_KEY" ``` You should see: ```json theme={"theme":"kanagawa-wave"} { "message": "Hello!", "keyId": "key_..." } ``` Now try without a key: ```bash Test without key theme={"theme":"kanagawa-wave"} curl -X POST http://localhost:3000/api/protected ``` You'll get a `401 Unauthorized` response. ## What's in `req.unkey`? After verification, `req.unkey.data` contains: | Field | Type | Description | | ------------- | ----------- | ------------------------------------------------------------------- | | `valid` | `boolean` | Whether the key passed all checks | | `code` | `string` | Status code (`VALID`, `NOT_FOUND`, `RATE_LIMITED`, etc.) | | `keyId` | `string` | The key's unique identifier | | `name` | `string?` | Human-readable name of the key | | `meta` | `object?` | Custom metadata associated with the key | | `expires` | `number?` | Unix timestamp (in milliseconds) when the key will expire. (if set) | | `credits` | `number?` | Remaining uses (if usage limits set) | | `enabled` | `boolean` | Whether the key is enabled | | `roles` | `string[]?` | Permissions attached to the key | | `permissions` | `string[]?` | Permissions attached to the key | | `identity` | `object?` | Identity info if `externalId` was set when creating the key | | `ratelimits` | `object[]?` | Rate limit states (if rate limiting configured) | `data.meta` is your custom key metadata (set via `meta` when creating the key). This is different from the response's top-level `meta` which contains `requestId`. ## Next steps Limit requests per key Cap total requests per key Fine-grained access control Full SDK documentation ## Troubleshooting * Ensure the key hasn't expired or been revoked - Verify the `Authorization` header format: `Bearer YOUR_KEY` (note the space) - Check that your root key has the `verify_key` permission * Restart your dev server after adding `.env.local` - Make sure the file is in your project root - Check for typos in the variable name # Python Source: https://unkey.com/docs/quickstart/apis/python Add API key authentication to your Python application with the Unkey Python SDK. Verify keys on each request to secure your API routes. This guide shows how to add API key verification to your Python applications using the official Unkey Python SDK (v2). ## Prerequisites * Python 3.9 or higher * An Unkey account (free at [unkey.com](https://unkey.com)) ## 1. Install the SDK ```bash pip theme={"theme":"kanagawa-wave"} pip install unkey-py ``` ```bash poetry theme={"theme":"kanagawa-wave"} poetry add unkey-py ``` ```bash uv theme={"theme":"kanagawa-wave"} uv add unkey-py ``` ## 2. Set up your Unkey credentials 1. Create a keyspace in the [Unkey Dashboard](https://app.unkey.com/apis) 2. Create a root key at [Settings → Root Keys](https://app.unkey.com/settings/root-keys) Set your root key as an environment variable: ```bash theme={"theme":"kanagawa-wave"} export UNKEY_ROOT_KEY="unkey_xxxx" ``` ## 3. FastAPI Integration Here's how to protect your FastAPI endpoints using the v2 SDK: ```python theme={"theme":"kanagawa-wave"} from fastapi import FastAPI, Header, HTTPException from unkey_py import Unkey import os app = FastAPI() @app.get("/api/protected") async def protected_route(x_api_key: str = Header(..., alias="X-API-Key")): with Unkey(bearer_auth=os.environ["UNKEY_ROOT_KEY"]) as unkey: res = unkey.keys.verify_key(request={"key": x_api_key}) result = res.data if not result.valid: raise HTTPException( status_code=401 if result.code == "NOT_FOUND" else 403, detail=f"Unauthorized: {result.code}" ) return { "message": "Access granted", "key_id": result.key_id, "remaining_credits": result.remaining } ``` Run your FastAPI app: ```bash theme={"theme":"kanagawa-wave"} uvicorn main:app --reload ``` Test with a valid API key: ```bash theme={"theme":"kanagawa-wave"} curl -H "X-API-Key: YOUR_API_KEY" http://localhost:8000/api/protected ``` ## 4. Flask Integration For Flask applications using the v2 SDK: ```python theme={"theme":"kanagawa-wave"} from flask import Flask, request, jsonify from unkey_py import Unkey import os app = Flask(__name__) @app.route("/api/protected") def protected_route(): api_key = request.headers.get("X-API-Key") if not api_key: return jsonify({"error": "Missing API key"}), 401 with Unkey(bearer_auth=os.environ["UNKEY_ROOT_KEY"]) as unkey: res = unkey.keys.verify_key(request={"key": api_key}) result = res.data if not result.valid: return jsonify({"error": result.code}), 401 return jsonify({ "message": "Access granted", "key_id": result.key_id, "meta": result.meta }) if __name__ == "__main__": app.run(debug=True) ``` ## 5. Django Integration For Django, create a custom middleware using the v2 SDK: ```python theme={"theme":"kanagawa-wave"} # middleware/unkey_auth.py from django.http import JsonResponse from django.conf import settings from unkey_py import Unkey class UnkeyAuthMiddleware: def __init__(self, get_response): self.get_response = get_response def __call__(self, request): # Skip auth for unprotected paths if request.path.startswith('/admin/'): return self.get_response(request) api_key = request.headers.get('X-API-Key') if not api_key: return JsonResponse({'error': 'Missing API key'}, status=401) with Unkey(bearer_auth=settings.UNKEY_ROOT_KEY) as unkey: res = unkey.keys.verify_key(request={'key': api_key}) result = res.data if not result.valid: return JsonResponse({'error': result.code}, status=401) # Attach key info to request request.unkey_key_id = result.key_id request.unkey_meta = result.meta return self.get_response(request) # settings.py import os MIDDLEWARE = [ # ... other middleware 'myapp.middleware.unkey_auth.UnkeyAuthMiddleware', ] UNKEY_ROOT_KEY = os.environ.get("UNKEY_ROOT_KEY") ``` Then in your views: ```python theme={"theme":"kanagawa-wave"} from django.http import JsonResponse def protected_view(request): return JsonResponse({ "message": "Access granted", "key_id": request.unkey_key_id, }) ``` ## Creating and Managing Keys Here's how to create API keys programmatically using the v2 SDK: ```python theme={"theme":"kanagawa-wave"} from datetime import datetime, timedelta from unkey_py import Unkey import os with Unkey(bearer_auth=os.environ["UNKEY_ROOT_KEY"]) as unkey: # Create a new API key res = unkey.keys.create_key(request={ "api_id": "api_...", "prefix": "sk_live", "external_id": "user_123", # Link to your user "name": "Production key", "expires": int((datetime.now() + timedelta(days=30)).timestamp() * 1000), "credits": { "remaining": 1000, "refill": { "amount": 1000, "interval": "monthly", }, }, "ratelimits": [{ "name": "requests", "limit": 100, "duration": 60000, # 100 per minute "auto_apply": True, }], "meta": { "plan": "pro", "customer_id": "cust_123", }, }) result = res.data print(f"Created key: {result.key}") # Only time you'll see it! print(f"Key ID: {result.key_id}") ``` The full API key is only returned once at creation. Store it securely and never show it again. ## Error Handling Always handle Unkey errors gracefully: ```python theme={"theme":"kanagawa-wave"} from unkey_py import Unkey import os with Unkey(bearer_auth=os.environ["UNKEY_ROOT_KEY"]) as unkey: try: res = unkey.keys.create_key(request={ "api_id": "api_...", "name": "My Key" }) result = res.data print(f"Created: {result.key}") except Exception as e: print(f"API Error: {e}") # Handle specific errors based on exception type ``` ## What's next? Protect your endpoints from abuse Complete Python SDK documentation Add roles and permissions More Python recipes and examples # Deploy your first app Source: https://unkey.com/docs/quickstart/deploy Deploy your first application on Unkey from a GitHub repository. Connect your repo, configure build settings, and go live in minutes. Deploying applications on Unkey is in public beta. To try it, open the product switcher in the top-left of the dashboard and select **Deploy**. During beta, deployed resources are free. We're eager for feedback, so let us know what you think on [Discord](https://unkey.com/discord), [X](https://x.com/unkeydev), or email [support@unkey.com](mailto:support@unkey.com). This guide walks you through deploying an application on Unkey for the first time. You'll create a project, connect a GitHub repository, and have a running app with a live URL. ## Prerequisites * An Unkey account * A GitHub repository ## Create a project Navigate to your workspace and click **New project**. Enter a project name and slug, then click **Create Project**. Create project form Create project form Click **Import from GitHub**. If you haven't installed the Unkey GitHub App yet, you'll be prompted to install it on your organization or personal account. Import from GitHub Import from GitHub Choose your repository and branch. Unkey auto-detects Dockerfiles and shows their status next to each repo. Repository selection Repository selection Don't have a repository ready or want to deploy with the [CLI](/docs/build-and-deploy/cli) instead? Click **Skip for now** to finish onboarding without selecting a repository. You can connect GitHub later from your project's settings. Review the defaults for root directory, Dockerfile path, and watch paths. Expand **Runtime settings** to configure regions, CPU, and memory. Expand **Advanced configurations** to add environment variables. Configure deployment Configure deployment Click **Deploy**. Unkey builds your image, provisions infrastructure across your selected regions, and assigns domains. Deployment progress Deployment progress Once the deployment reaches **Ready**, your app is live. Unkey assigns several domains automatically, including an environment domain and a commit-specific domain. ## What happens next After the initial setup, every push to your repository triggers a new deployment automatically: * Pushes to the default branch (typically `main`) deploy to **production**. * Pushes to any other branch deploy to **preview**. Each deployment gets its own unique URL, so you can test any version independently. ## Next steps Branch mapping, fork protection, and watch paths Configure regions, CPU, memory, health checks, and more Add environment-specific configuration Serve your app from your own domain # Shared Rate Limits Source: https://unkey.com/docs/quickstart/identities/shared-ratelimits Create your first identity in Unkey and attach shared rate limits across multiple API keys. Group keys by user, team, or organization. This quickstart will guide you through creating your first identity with shared ratelimits and a key that is connected to the identity. The example is written in TypeScript, purposefully using the `fetch` API to make requests as transparent as possible. You can use any language or library to make requests to the Unkey API. ### Requirements You will need your api id and root key to make requests to the Unkey API. You can find these in the Unkey dashboard. ```ts theme={"theme":"kanagawa-wave"} const apiId = "api_XXX"; const rootKey = "unkey_XXX"; ``` The root key requires the following permissions: ```ts theme={"theme":"kanagawa-wave"} "identity.*.create_identity"; "identity.*.read_identity"; "identity.*.update_identity"; "api.*.create_key"; ``` ### Create an Identity To create an identity, you need to make a request to the `/v2/identities.createIdentity` endpoint. You can specify an `externalId` and `meta` object to store additional information about the identity. Unkey does not care what the `externalId` is, but it must be unique for each identity. Commonly used are user or organization ids. The `meta` object can be used to store any additional information you want to associate with the identity. ```ts theme={"theme":"kanagawa-wave"} const externalId = "user_1234abc"; const createIdentityResponse = await fetch( "https://api.unkey.com/v2/identities.createIdentity", { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${rootKey}`, }, body: JSON.stringify({ externalId, meta: { stripeCustomerId: "cus_123", }, }), }, ); const { identityId } = await createIdentityResponse.json<{ identityId: string; }>(); ``` ### Retrieve an Identity Let's retrieve the identity to make sure it got created successfully: ```ts theme={"theme":"kanagawa-wave"} const getIdentityResponse = await fetch( `https://api.unkey.com/v2/identities.getIdentity`, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${rootKey}`, }, body: JSON.stringify({ identity: identityId, }), }, ); const identity = await getIdentityResponse.json<{ id: string; externalId: string; meta: unknown; ratelimits: Array<{ name: string; limit: number; duration: number }>; }>(); ``` ### Create a Key Let's create a key and connect it to the identity: ```ts theme={"theme":"kanagawa-wave"} const createKeyResponse = await fetch( `https://api.unkey.com/v2/keys.createKey`, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${rootKey}`, }, body: JSON.stringify({ apiId: apiId, prefix: "acme", // by providing the same externalId as the identity, we connect the key to the identity externalId: externalId, }), }, ); const key = await createKeyResponse.json<{ keyId: string; key: string; }>(); ``` ### Verify the Key When you verify the key, you will receive the identity that the key is connected to and can act accordingly in your API handler. ```ts theme={"theme":"kanagawa-wave"} const verifyKeyResponse = await fetch( `https://api.unkey.com/v2/keys.verifyKey`, { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ apiId: apiId, key: key.key, }), }, ); const verified = await verifyKeyResponse.json<{ valid: boolean; identity: { id: string; externalId: string; meta: unknown; }; }>(); ``` ### Ratelimits Ratelimits can be set on the identity level. Ratelimits set on the identity level are shared across all keys connected to the identity. ```ts theme={"theme":"kanagawa-wave"} const updateRes = await fetch( "https://api.unkey.com/v2/identities.updateIdentity", { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${rootKey}`, }, body: JSON.stringify({ identity: identity.id, ratelimits: [ /** * We define a limit that allows 10 requests per day */ { name: "requests", limit: 10, duration: 24 * 60 * 60 * 1000, // 24h }, /** * And a second limit that allows 1000 tokens per minute */ { name: "tokens", limit: 1000, duration: 60 * 1000, // 1 minute }, ], }), }, ); ``` ### Verify the Key with Ratelimits Now let's verify the key again and specify the limits In this case, we pretend like a user is requesting to use 200 tokens. We specify the `requests` ratelimit to enforce a limit of 10 requests per day and the `tokens` ratelimit to enforce a limit of 1000 tokens per minute. Additionally we specify the cost of the tokens to be 200. ```ts theme={"theme":"kanagawa-wave"} const verifiedWithRatelimitsResponse = await fetch( `https://api.unkey.com/v2/keys.verifyKey`, { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ apiId: apiId, key: key.key, ratelimits: [ { name: "requests", }, { name: "tokens", cost: 200, }, ], }), }, ); const verifiedWithRatelimits = await verifiedWithRatelimitsResponse.json<{ valid: boolean; identity: { id: string; externalId: string; meta: unknown; }; }>(); ``` That's it, you have successfully created an identity and key with shared ratelimits. You can now use the key to verify requests and enforce ratelimits in your API handler. # Customer Portal Source: https://unkey.com/docs/quickstart/portal Give your end users a white-labeled self-service portal for API key management, usage analytics, and docs, with a Stripe-style session auth flow. The Customer Portal is a white-labeled web app you can offer to your end users. They get key management, usage analytics, and API documentation — without you building any UI. Authentication uses a Stripe-style session flow: your backend creates a session, redirects the user, and the portal handles the rest. The portal is in development. Key management, analytics, and docs pages are placeholder UI for now. ## How it works ``` Your Backend Unkey API Portal │ │ │ ├─ POST /v2/portal.createSession ─►│ │ │◄── sessionId + portal URL ───┤ │ │ │ │ ├─ Redirect user to portal URL ─────────────────────────────►│ │ │◄─ POST /v2/portal.exchangeSession ─┤ │ │──── browser session token ──►│ │ │ │ │ │◄── Direct API calls ───────┤ ``` 1. Your backend authenticates the user in your own system 2. Your backend calls `POST /v2/portal.createSession` with a root key 3. You redirect the user to the returned portal URL 4. The portal exchanges the session ID for a 24-hour browser session 5. The browser calls Unkey API directly with the session token ## 1. Configure a portal Enable the Customer Portal for your workspace in the Unkey dashboard. Dashboard configuration UI is coming soon. During early access, reach out to the Unkey team to get your portal configured. When configuring your portal, you'll choose a slug — a short, human-readable identifier like `my-portal` or `billing-dashboard`. Use this slug when creating sessions. Slugs must be 3–64 characters, lowercase alphanumeric and hyphens only, and cannot start or end with a hyphen. Optionally, you can customize branding with your logo and brand colors. ## 2. Create a session When your user wants to access the portal, create a session from your backend: ```bash cURL theme={"theme":"kanagawa-wave"} curl -X POST https://api.unkey.com/v2/portal.createSession \ -H "Authorization: Bearer YOUR_ROOT_KEY" \ -H "Content-Type: application/json" \ -d '{ "slug": "my-portal", "externalId": "user_123", "permissions": ["api.*.read_key", "api.*.create_key", "api.*.read_analytics"] }' ``` ```typescript TypeScript theme={"theme":"kanagawa-wave"} const response = await fetch("https://api.unkey.com/v2/portal.createSession", { method: "POST", headers: { "Authorization": `Bearer ${process.env.UNKEY_ROOT_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ slug: "my-portal", externalId: "user_123", permissions: ["api.*.read_key", "api.*.create_key", "api.*.read_analytics"], }), }); const { data } = await response.json(); // data.sessionId — short-lived token (15 min) // data.url — full portal URL with session param // data.expiresAt — expiry timestamp (unix ms) ``` ```go Go theme={"theme":"kanagawa-wave"} // Use your preferred HTTP client body := map[string]any{ "slug": "my-portal", "externalId": "user_123", "permissions": []string{"api.*.read_key", "api.*.create_key", "api.*.read_analytics"}, } ``` The response: ```json theme={"theme":"kanagawa-wave"} { "meta": { "requestId": "req_..." }, "data": { "sessionId": "pst_xxx", "url": "https://portal.unkey.com/?session=pst_xxx", "expiresAt": 1742000000000 } } ``` ### Required parameters | Parameter | Type | Description | | ------------- | ---------- | ------------------------------------------------------- | | `slug` | `string` | Human-readable identifier for your portal configuration | | `externalId` | `string` | Your user's identifier in your system | | `permissions` | `string[]` | Controls which portal tabs are visible | ### Optional parameters | Parameter | Type | Description | | --------- | --------- | --------------------------------------------------------------------- | | `preview` | `boolean` | Shows a "Preview mode" banner — useful for testing as a specific user | ```bash theme={"theme":"kanagawa-wave"} curl -X POST https://api.unkey.com/v2/portal.createSession \ -H "Authorization: Bearer YOUR_ROOT_KEY" \ -H "Content-Type: application/json" \ -d '{ "slug": "my-portal", "externalId": "user_123", "permissions": ["api.*.read_key", "api.*.read_analytics"], "preview": true }' ``` ## 3. Redirect your user Send the user to the portal URL. The session ID is valid for 15 minutes and can only be used once. ```typescript theme={"theme":"kanagawa-wave"} // In your backend route handler return Response.redirect(data.url, 302); ``` The portal will: 1. Exchange the session ID for a 24-hour browser session 2. Set an httpOnly cookie 3. Redirect to the first visible tab based on permissions ## Permissions and tabs Permissions use the RBAC tuple format `{resourceType}.{resourceId}.{action}`. Use `*` as the resourceId to grant access to all resources of that type. Tab visibility is derived from the action segment (third part) of each permission: | Action | Tab | | ---------------------------------------------------- | ------------- | | `read_key`, `create_key`, `update_key`, `delete_key` | API Keys | | `read_analytics` | Analytics | | Any permission present | Documentation | Examples: * `api.*.read_key` → shows the Keys tab * `api.api_123.create_key` → shows the Keys tab (specific resource) * `api.*.read_analytics` → shows the Analytics tab * Docs tab is visible whenever at least one permission is present The API requires at least one permission. An empty permissions array is rejected with HTTP 400. ## Session lifecycle | Token | Lifetime | Usage | | ---------------------- | ---------- | --------------------------------------------- | | Session ID (`pst_xxx`) | 15 minutes | Single-use, exchanged for browser session | | Browser session | 24 hours | Stored as httpOnly cookie, used for API calls | When the browser session expires: * If `return_url` is set on the portal config → redirects to `{return_url}?reason=session_expired` * Otherwise → shows a "Session expired" error page ## Branding The portal supports basic white-labeling: | Setting | Default | | ------------- | --------- | | Primary color | `#2563eb` | | Logo | None | Logo URLs must be HTTPS. ## Error responses | Scenario | Status | Message | | ---------------------------------------- | ------ | -------------------------------------------------------- | | Missing or invalid JSON body | 400 | `Bad Request` | | Invalid root key | 401 | `Unauthorized` | | Portal disabled | 403 | `Portal is disabled.` | | Portal config not found | 404 | `Portal configuration not found.` | | Invalid/expired/used session on exchange | 401 | `Session is invalid, expired, or has already been used.` | # Quickstart Source: https://unkey.com/docs/quickstart/quickstart Get started with Unkey in under 5 minutes. Create a keyspace, issue your first API key, and verify it with a simple HTTP request. This guide gets you from zero to a working API key verification as fast as possible. We'll create a key, then verify it using your preferred method. Need an account? [Sign up free](https://app.unkey.com), takes 30 seconds. ## 1. Create a keyspace A keyspace in Unkey is a container for your keys. Head to your [dashboard](https://app.unkey.com/apis) and create one, or use one you already have. Copy your **API ID**, it looks like `api_xxxx`. ## 2. Create a root key Root keys authenticate *your* requests to the Unkey API (for creating and managing keys). 1. Go to Workspace Settings → Root Keys 2. Click **Create New Root Key** 3. Give it a name and select the permissions you need 4. Copy the key, you won't see it again Keep your root key secret. Never expose it in client-side code or commit it to git. ## 3. Create an API key Now let's create a key that your users would use to authenticate: ```bash cURL theme={"theme":"kanagawa-wave"} curl -X POST https://api.unkey.com/v2/keys.createKey \ -H "Authorization: Bearer YOUR_ROOT_KEY" \ -H "Content-Type: application/json" \ -d '{ "apiId": "api_xxxx", "name": "My First Key" }' ``` ```typescript TypeScript theme={"theme":"kanagawa-wave"} import { Unkey } from "@unkey/api"; const unkey = new Unkey({ rootKey: process.env.UNKEY_ROOT_KEY }); try { const { meta, data } = await unkey.keys.createKey({ apiId: "api_xxxx", name: "My First Key", }); console.log(data.key); // This is the key to give to your user } catch (err) { console.error(err); } ``` ```python Python theme={"theme":"kanagawa-wave"} from unkey import Unkey unkey = Unkey(root_key="your_root_key") result = unkey.keys.create_key( api_id="api_xxxx", name="My First Key" ) print(result.key) # This is the key to give to your user ``` ```go Go theme={"theme":"kanagawa-wave"} package main import ( "context" "fmt" "os" unkey "github.com/unkeyed/sdks/api/go/v2" "github.com/unkeyed/sdks/api/go/v2/models/components" ) func main() { ctx := context.Background() client := unkey.New( unkey.WithSecurity(os.Getenv("UNKEY_ROOT_KEY")), ) res, err := client.Keys.CreateKey(ctx, components.V2KeysCreateKeyRequestBody{ APIID: "api_xxxx", Name: stringPtr("My First Key"), }) if err != nil { panic(err) } fmt.Println(res.V2KeysCreateKeyResponseBody.Key) // Give this to your user } func stringPtr(s string) *string { return &s } ``` Save the returned `key` value, that's what you'll verify in the next step. ## 4. Verify the key This is what you'll do on every API request to check if a key is valid: ```bash cURL theme={"theme":"kanagawa-wave"} curl -X POST https://api.unkey.com/v2/keys.verifyKey \ -H "Authorization: Bearer YOUR_ROOT_KEY" \ -H "Content-Type: application/json" \ -d '{ "key": "THE_KEY_FROM_STEP_3" }' ``` ```typescript TypeScript theme={"theme":"kanagawa-wave"} import { Unkey } from "@unkey/api"; const unkey = new Unkey({ rootKey: process.env.UNKEY_ROOT_KEY! }); try { const { data } = await unkey.keys.verifyKey({ key: "THE_KEY_FROM_STEP_3", }); if (!data.valid) { // Key is invalid, expired, rate limited, etc. console.log("Denied:", data.code); } // Key is valid, continue with your API logic console.log("Key ID:", data.keyId); } catch (err) { console.error(err); } ``` ```python Python theme={"theme":"kanagawa-wave"} from unkey import Unkey unkey = Unkey(root_key="your_root_key") result = unkey.keys.verify_key(key="THE_KEY_FROM_STEP_3") if not result.valid: print("Denied:", result.code) else: print("Key ID:", result.key_id) ``` ```go Go theme={"theme":"kanagawa-wave"} package main import ( "context" "fmt" "os" unkey "github.com/unkeyed/sdks/api/go/v2" "github.com/unkeyed/sdks/api/go/v2/models/components" ) func main() { ctx := context.Background() client := unkey.New( unkey.WithSecurity(os.Getenv("UNKEY_ROOT_KEY")), ) res, err := client.Keys.VerifyKey(ctx, components.V2KeysVerifyKeyRequestBody{ Key: "THE_KEY_FROM_STEP_3", }) if err != nil { panic(err) } result := res.V2KeysVerifyKeyResponseBody.Data if !result.Valid { code := "unknown" if result.Code != "" { code = string(result.Code) } fmt.Println("Denied:", code) return } fmt.Println("Key ID:", result.KeyId) } ``` A successful response looks like: ```json theme={"theme":"kanagawa-wave"} { "meta": { "requestId": "req_..." }, "data": { "valid": true, "code": "VALID", "keyId": "key_xxxx" } } ``` **That's it!** You've just verified an API key with Unkey. 🎉 ## What's in the verification response? The `data` object contains everything you need to make authorization decisions: | Field | Type | Description | | ------------- | ----------- | ------------------------------------------------------------------- | | `valid` | `boolean` | Whether the key passed all checks | | `code` | `string` | Status code (`VALID`, `NOT_FOUND`, `RATE_LIMITED`, etc.) | | `keyId` | `string` | The key's unique identifier | | `name` | `string?` | Human-readable name of the key | | `meta` | `object?` | Custom metadata associated with the key | | `expires` | `number?` | Unix timestamp (in milliseconds) when the key will expire. (if set) | | `credits` | `number?` | Remaining uses (if usage limits set) | | `enabled` | `boolean` | Whether the key is enabled | | `roles` | `string[]?` | Permissions attached to the key | | `permissions` | `string[]?` | Permissions attached to the key | | `identity` | `object?` | Identity info if `externalId` was set when creating the key | | `ratelimits` | `object[]?` | Rate limit states (if rate limiting configured) | Fields marked with `?` are optional and only included when relevant (e.g., `remaining` only appears if you set a usage limit). ## Next steps Now integrate Unkey into your actual application: Protect API routes with middleware Add key verification to Express routes Fast verification with Bun's native server Globally distributed API key verification Or explore more features: Protect endpoints from abuse Cap requests per key for billing tiers Fine-grained access control Full TypeScript SDK docs # Bun Source: https://unkey.com/docs/quickstart/ratelimiting/bun Step-by-step Bun rate limiting tutorial using @unkey/ratelimit. Build a Bun HTTP server that throttles abusive clients with no Redis required. ## What you'll build A Bun HTTP server with rate limiting. Users who exceed the limit get a 429 response. **Time to complete:** \~3 minutes ## Prerequisites * [Unkey account](https://app.unkey.com/auth/sign-up) (free) * [Root key](https://app.unkey.com/settings/root-keys) with `ratelimit.*.limit` permission * [Bun](https://bun.sh) installed ```bash theme={"theme":"kanagawa-wave"} mkdir unkey-bun-ratelimit && cd unkey-bun-ratelimit bun init -y ``` ```bash theme={"theme":"kanagawa-wave"} bun add @unkey/ratelimit ``` Create a `.env` file: ```bash .env theme={"theme":"kanagawa-wave"} UNKEY_ROOT_KEY="unkey_..." ``` Replace `index.ts`: ```ts index.ts theme={"theme":"kanagawa-wave"} import { Ratelimit } from "@unkey/ratelimit"; // Create limiter instance const limiter = new Ratelimit({ rootKey: Bun.env.UNKEY_ROOT_KEY!, namespace: "bun-api", limit: 10, // 10 requests... duration: "60s", // ...per minute }); const server = Bun.serve({ async fetch(req) { const url = new URL(req.url); // Public route if (url.pathname === "/") { return Response.json({ message: "Welcome! Try /api/data" }); } // Rate-limited route if (url.pathname === "/api/data") { // 1. Identify the user const identifier = req.headers.get("x-user-id") ?? req.headers.get("x-forwarded-for") ?? "anonymous"; // 2. Check the rate limit const { success, remaining, reset } = await limiter.limit(identifier); // 3. Set headers const headers = { "X-RateLimit-Limit": "10", "X-RateLimit-Remaining": remaining.toString(), "X-RateLimit-Reset": reset.toString(), }; if (!success) { return Response.json( { error: "Too many requests. Try again later." }, { status: 429, headers }, ); } // 4. Request allowed return Response.json( { message: "Here's your data!", remaining }, { headers }, ); } return Response.json({ error: "Not found" }, { status: 404 }); }, port: 3000, }); console.log(`Server running at http://localhost:${server.port}`); ``` ```bash theme={"theme":"kanagawa-wave"} bun run index.ts ``` ```bash theme={"theme":"kanagawa-wave"} # Hit the endpoint 12 times for i in {1..12}; do curl http://localhost:3000/api/data -H "x-user-id: test-user" echo "" done ``` First 10 requests return data. Requests 11+ get: ```json theme={"theme":"kanagawa-wave"} { "error": "Too many requests. Try again later." } ``` ## What's in the response? `limiter.limit()` returns: | Field | Type | Description | | ----------- | --------- | ------------------------------------------ | | `success` | `boolean` | `true` if allowed, `false` if rate limited | | `remaining` | `number` | Requests left in current window | | `reset` | `number` | Unix timestamp (ms) when window resets | | `limit` | `number` | The configured limit | ## Multiple rate limiters Create different limiters for different use cases: ```ts index.ts theme={"theme":"kanagawa-wave"} import { Ratelimit } from "@unkey/ratelimit"; // General API: 100/min const apiLimiter = new Ratelimit({ rootKey: Bun.env.UNKEY_ROOT_KEY!, namespace: "api", limit: 100, duration: "60s", }); // Auth endpoints: 5/min (prevent brute force) const authLimiter = new Ratelimit({ rootKey: Bun.env.UNKEY_ROOT_KEY!, namespace: "auth", limit: 5, duration: "60s", }); // Helper to apply rate limiting async function checkLimit(limiter: Ratelimit, identifier: string) { const result = await limiter.limit(identifier); if (!result.success) { return Response.json({ error: "Rate limit exceeded" }, { status: 429 }); } return null; // Allowed } const server = Bun.serve({ async fetch(req) { const url = new URL(req.url); const ip = req.headers.get("x-forwarded-for") ?? "unknown"; if (url.pathname === "/api/login") { const blocked = await checkLimit(authLimiter, ip); if (blocked) return blocked; return Response.json({ message: "Login endpoint" }); } if (url.pathname.startsWith("/api/")) { const blocked = await checkLimit(apiLimiter, ip); if (blocked) return blocked; return Response.json({ message: "API data" }); } return Response.json({ message: "Welcome" }); }, port: 3000, }); ``` ## Next steps Understand the architecture Give specific users higher limits All configuration options Combine with API key authentication ## Troubleshooting * Verify `UNKEY_ROOT_KEY` is in your `.env` file - Use `Bun.env.UNKEY_ROOT_KEY` (not `process.env`) - Check your root key has `ratelimit.*.limit` permission Bun loads `.env` automatically. Make sure the file is in your project root and restart the server. # Express Source: https://unkey.com/docs/quickstart/ratelimiting/express Step-by-step Express.js rate limiting tutorial with @unkey/ratelimit. Set per-route limits, return 429 responses, and skip the Redis dependency. ## What you'll build An Express server with rate-limited endpoints. Users who exceed the limit get a 429 response. **Time to complete:** \~5 minutes ## Prerequisites * [Unkey account](https://app.unkey.com/auth/sign-up) (free) * [Root key](https://app.unkey.com/settings/root-keys) with `ratelimit.*.limit` permission * Node.js 18+ ```bash theme={"theme":"kanagawa-wave"} mkdir unkey-express-ratelimit && cd unkey-express-ratelimit npm init -y npm install express @unkey/ratelimit dotenv ``` Create a `.env` file: ```bash .env theme={"theme":"kanagawa-wave"} UNKEY_ROOT_KEY="unkey_..." ``` Never commit your `.env` file. Add it to `.gitignore`. Create `index.js`: ```js index.js theme={"theme":"kanagawa-wave"} const express = require("express"); const { Ratelimit } = require("@unkey/ratelimit"); require("dotenv").config(); const app = express(); const port = process.env.PORT || 3000; // Create limiter instance const limiter = new Ratelimit({ rootKey: process.env.UNKEY_ROOT_KEY, namespace: "express-api", limit: 10, // 10 requests... duration: "60s", // ...per minute }); // Public route app.get("/", (req, res) => { res.json({ message: "Welcome! Try /api/data" }); }); // Rate-limited route app.get("/api/data", async (req, res) => { // 1. Identify the user (IP, user ID, API key, etc.) const identifier = req.headers["x-user-id"] || req.ip || "anonymous"; // 2. Check the rate limit const { success, remaining, reset } = await limiter.limit(identifier); // 3. Set rate limit headers res.set({ "X-RateLimit-Limit": "10", "X-RateLimit-Remaining": remaining.toString(), "X-RateLimit-Reset": reset.toString(), }); if (!success) { return res.status(429).json({ error: "Too many requests. Please try again later.", retryAfter: Math.ceil((reset - Date.now()) / 1000), }); } // 4. Request allowed res.json({ message: "Here's your data!", remaining, }); }); app.listen(port, () => { console.log(`Server running at http://localhost:${port}`); }); ``` ```bash theme={"theme":"kanagawa-wave"} node index.js ``` ```bash theme={"theme":"kanagawa-wave"} # Hit the endpoint 12 times for i in {1..12}; do curl http://localhost:3000/api/data -H "x-user-id: test-user" echo "" done ``` First 10 requests succeed. Requests 11+ get: ```json theme={"theme":"kanagawa-wave"} { "error": "Too many requests. Please try again later.", "retryAfter": 45 } ``` ## What's in the response? `limiter.limit()` returns: | Field | Type | Description | | ----------- | --------- | ------------------------------------------ | | `success` | `boolean` | `true` if allowed, `false` if rate limited | | `remaining` | `number` | Requests left in current window | | `reset` | `number` | Unix timestamp (ms) when window resets | | `limit` | `number` | The configured limit | ## Using as middleware For cleaner code, create reusable middleware: ```js middleware/ratelimit.js theme={"theme":"kanagawa-wave"} const { Ratelimit } = require("@unkey/ratelimit"); const limiter = new Ratelimit({ rootKey: process.env.UNKEY_ROOT_KEY, namespace: "api", limit: 100, duration: "1m", }); async function rateLimit(req, res, next) { const identifier = req.headers["x-user-id"] || req.ip || "anonymous"; const { success, remaining, reset } = await limiter.limit(identifier); res.set({ "X-RateLimit-Remaining": remaining.toString(), "X-RateLimit-Reset": reset.toString(), }); if (!success) { return res.status(429).json({ error: "Rate limit exceeded" }); } next(); } module.exports = { rateLimit }; ``` ### createMiddleware helper Create an Express middleware from a Ratelimit instance: ```js theme={"theme":"kanagawa-wave"} function createMiddleware(limiter) { return async (req, res, next) => { const identifier = req.ip ?? req.headers["x-forwarded-for"] ?? "unknown"; const { success, remaining, reset } = await limiter.limit(identifier); if (!success) { res.set("Retry-After", Math.ceil((reset - Date.now()) / 1000).toString()); res.set("X-RateLimit-Remaining", "0"); return res.status(429).json({ error: "Too many requests" }); } res.set("X-RateLimit-Remaining", remaining.toString()); next(); }; } ``` Use on any route: ```js theme={"theme":"kanagawa-wave"} const { rateLimit } = require("./middleware/ratelimit"); app.get("/api/data", rateLimit, (req, res) => { res.json({ data: "protected content" }); }); app.post("/api/submit", rateLimit, (req, res) => { res.json({ success: true }); }); ``` ## Different limits per route Create multiple limiters: ```js theme={"theme":"kanagawa-wave"} const apiLimiter = new Ratelimit({ rootKey: process.env.UNKEY_ROOT_KEY, namespace: "api", limit: 100, duration: "1m", }); const authLimiter = new Ratelimit({ rootKey: process.env.UNKEY_ROOT_KEY, namespace: "auth", limit: 5, duration: "1m", }); // 100/min for general API app.use("/api", createMiddleware(apiLimiter)); // 5/min for login (prevent brute force) app.post("/login", createMiddleware(authLimiter), loginHandler); ``` ## Next steps Understand the architecture Give specific users higher limits All configuration options Combine with API key authentication ## Troubleshooting * Verify `UNKEY_ROOT_KEY` is set and has `ratelimit.*.limit` permission - Make sure you're using a consistent identifier per user - Check that `.env` is loaded before creating the limiter Install types and use imports: ```bash theme={"theme":"kanagawa-wave"} npm install -D typescript @types/express @types/node ``` ```ts theme={"theme":"kanagawa-wave"} import express from "express"; import { Ratelimit } from "@unkey/ratelimit"; ``` # Hono Source: https://unkey.com/docs/quickstart/ratelimiting/hono Step-by-step Hono rate limiting tutorial with @unkey/ratelimit. Protect routes on Node.js, Bun, or Cloudflare Workers without running Redis. ## What you'll build A Hono app with rate-limited endpoints. Users who exceed the limit get a 429 response. **Time to complete:** \~5 minutes ## Prerequisites * [Unkey account](https://app.unkey.com/auth/sign-up) (free) * [Root key](https://app.unkey.com/settings/root-keys) with `ratelimit.*.limit` permission * Node.js 18+ or Bun ```bash npm theme={"theme":"kanagawa-wave"} npm create hono@latest unkey-hono-ratelimit cd unkey-hono-ratelimit ``` ```bash pnpm theme={"theme":"kanagawa-wave"} pnpm create hono@latest unkey-hono-ratelimit cd unkey-hono-ratelimit ``` ```bash bun theme={"theme":"kanagawa-wave"} bun create hono@latest unkey-hono-ratelimit cd unkey-hono-ratelimit ``` Choose your preferred runtime. ```bash npm theme={"theme":"kanagawa-wave"} npm install @unkey/ratelimit ``` ```bash pnpm theme={"theme":"kanagawa-wave"} pnpm add @unkey/ratelimit ``` ```bash bun theme={"theme":"kanagawa-wave"} bun add @unkey/ratelimit ``` Create a `.env` file: ```bash .env theme={"theme":"kanagawa-wave"} UNKEY_ROOT_KEY="unkey_..." ``` Update `src/index.ts`: ```ts src/index.ts theme={"theme":"kanagawa-wave"} import { Hono } from "hono"; import { Ratelimit } from "@unkey/ratelimit"; const app = new Hono(); // Create limiter instance const limiter = new Ratelimit({ rootKey: process.env.UNKEY_ROOT_KEY!, namespace: "hono-api", limit: 10, // 10 requests... duration: "60s", // ...per minute }); // Public route app.get("/", (c) => { return c.json({ message: "Welcome! Try /api/data" }); }); // Rate-limited route app.get("/api/data", async (c) => { // 1. Identify the user const identifier = c.req.header("x-user-id") ?? c.req.header("x-forwarded-for") ?? "anonymous"; // 2. Check the rate limit const { success, remaining, reset } = await limiter.limit(identifier); // 3. Set headers c.header("X-RateLimit-Limit", "10"); c.header("X-RateLimit-Remaining", remaining.toString()); c.header("X-RateLimit-Reset", reset.toString()); if (!success) { return c.json({ error: "Too many requests. Try again later." }, 429); } // 4. Request allowed return c.json({ message: "Here's your data!", remaining }); }); export default app; ``` ```bash npm theme={"theme":"kanagawa-wave"} npm run dev ``` ```bash pnpm theme={"theme":"kanagawa-wave"} pnpm dev ``` ```bash bun theme={"theme":"kanagawa-wave"} bun dev ``` ```bash theme={"theme":"kanagawa-wave"} # Hit the endpoint 12 times for i in {1..12}; do curl http://localhost:3000/api/data -H "x-user-id: test-user" echo "" done ``` First 10 requests succeed. Requests 11+ get: ```json theme={"theme":"kanagawa-wave"} { "error": "Too many requests. Try again later." } ``` ## What's in the response? `limiter.limit()` returns: | Field | Type | Description | | ----------- | --------- | ------------------------------------------ | | `success` | `boolean` | `true` if allowed, `false` if rate limited | | `remaining` | `number` | Requests left in current window | | `reset` | `number` | Unix timestamp (ms) when window resets | | `limit` | `number` | The configured limit | ## Using as middleware Create reusable middleware for cleaner code: ```ts src/middleware/ratelimit.ts theme={"theme":"kanagawa-wave"} import { Context, Next } from "hono"; import { Ratelimit } from "@unkey/ratelimit"; const limiter = new Ratelimit({ rootKey: process.env.UNKEY_ROOT_KEY!, namespace: "api", limit: 100, duration: "60s", }); export async function rateLimit(c: Context, next: Next) { const identifier = c.req.header("x-user-id") ?? c.req.header("x-forwarded-for") ?? "anonymous"; const { success, remaining, reset } = await limiter.limit(identifier); c.header("X-RateLimit-Remaining", remaining.toString()); c.header("X-RateLimit-Reset", reset.toString()); if (!success) { return c.json({ error: "Rate limit exceeded" }, 429); } await next(); } ``` Apply to routes: ```ts src/index.ts theme={"theme":"kanagawa-wave"} import { Hono } from "hono"; import { rateLimit } from "./middleware/ratelimit"; const app = new Hono(); // Public app.get("/", (c) => c.json({ message: "Welcome" })); // Rate-limited routes app.use("/api/*", rateLimit); app.get("/api/data", (c) => { return c.json({ data: "protected content" }); }); app.get("/api/user", (c) => { return c.json({ user: "info" }); }); export default app; ``` ## Different limits per route group ```ts theme={"theme":"kanagawa-wave"} const apiLimiter = new Ratelimit({ rootKey: process.env.UNKEY_ROOT_KEY!, namespace: "api", limit: 100, duration: "60s", }); const authLimiter = new Ratelimit({ rootKey: process.env.UNKEY_ROOT_KEY!, namespace: "auth", limit: 5, duration: "60s", }); // Apply different limiters to different route groups app.use("/api/*", createRateLimitMiddleware(apiLimiter)); app.use("/auth/*", createRateLimitMiddleware(authLimiter)); ``` ## Deploying to Cloudflare Workers For Cloudflare Workers, access env through the context: ```ts theme={"theme":"kanagawa-wave"} app.get("/api/data", async (c) => { const limiter = new Ratelimit({ rootKey: c.env.UNKEY_ROOT_KEY, // Access from c.env namespace: "api", limit: 10, duration: "60s", }); const { success } = await limiter.limit("user-id"); // ... }); ``` Set your secret with wrangler: ```bash theme={"theme":"kanagawa-wave"} npx wrangler secret put UNKEY_ROOT_KEY ``` ## Next steps Understand the architecture Give specific users higher limits All configuration options Combine with API key authentication ## Troubleshooting * Verify `UNKEY_ROOT_KEY` is set correctly - For Workers: use `c.env.UNKEY_ROOT_KEY`, not `process.env` - Check your root key has `ratelimit.*.limit` permission * For Node.js: Install `dotenv` and add `import 'dotenv/config'` - For Bun: `.env` loads automatically - Restart the dev server after changes # Next.js Source: https://unkey.com/docs/quickstart/ratelimiting/nextjs Step-by-step Next.js rate limiting tutorial with @unkey/ratelimit. Throttle API routes and return 429 responses without provisioning Redis. ## What you'll build A Next.js API route that limits each user to a set number of requests per time window. Excess requests get rejected with a 429. **Time to complete:** \~5 minutes ## Prerequisites * [Unkey account](https://app.unkey.com/auth/sign-up) (free) * [Root key](https://app.unkey.com/settings/root-keys) with `ratelimit.*.limit` permission * Node.js 18+ Skip if you have an existing project. ```bash npm theme={"theme":"kanagawa-wave"} npx create-next-app@latest my-app cd my-app ``` ```bash pnpm theme={"theme":"kanagawa-wave"} pnpm create next-app@latest my-app cd my-app ``` ```bash bun theme={"theme":"kanagawa-wave"} bunx create-next-app my-app cd my-app ``` ```bash npm theme={"theme":"kanagawa-wave"} npm install @unkey/ratelimit ``` ```bash pnpm theme={"theme":"kanagawa-wave"} pnpm add @unkey/ratelimit ``` ```bash bun theme={"theme":"kanagawa-wave"} bun add @unkey/ratelimit ``` Create or update `.env.local`: ```bash .env.local theme={"theme":"kanagawa-wave"} UNKEY_ROOT_KEY="unkey_..." ``` Never commit your root key. Add `.env.local` to `.gitignore`. ```ts app/api/protected/route.ts theme={"theme":"kanagawa-wave"} import { NextResponse } from "next/server"; import { Ratelimit } from "@unkey/ratelimit"; // Create limiter instance outside the handler const limiter = new Ratelimit({ rootKey: process.env.UNKEY_ROOT_KEY!, namespace: "my-app", // Group related limits limit: 10, // 10 requests... duration: "60s", // ...per minute }); export async function POST(req: Request) { // 1. Identify the user (IP, user ID, API key, etc.) const identifier = req.headers.get("x-user-id") ?? req.headers.get("x-forwarded-for") ?? "anonymous"; // 2. Check the rate limit const { success, remaining, reset } = await limiter.limit(identifier); // 3. Add rate limit headers (optional but nice for clients) const headers = { "X-RateLimit-Limit": "10", "X-RateLimit-Remaining": remaining.toString(), "X-RateLimit-Reset": reset.toString(), }; if (!success) { return NextResponse.json( { error: "Too many requests. Please try again later." }, { status: 429, headers }, ); } // 4. Request allowed, do your thing return NextResponse.json({ message: "Hello!", remaining }, { headers }); } ``` ```bash npm theme={"theme":"kanagawa-wave"} npm run dev ``` ```bash pnpm theme={"theme":"kanagawa-wave"} pnpm dev ``` ```bash bun theme={"theme":"kanagawa-wave"} bun dev ``` ```bash theme={"theme":"kanagawa-wave"} # Hit the endpoint multiple times for i in {1..12}; do curl -X POST http://localhost:3000/api/protected \ -H "x-user-id: test-user" echo "" done ``` First 10 requests return `200`. Requests 11+ return `429`: ```json theme={"theme":"kanagawa-wave"} { "error": "Too many requests. Please try again later." } ``` Wait 60 seconds and the limit resets. ## What's in the response? `limiter.limit()` returns: | Field | Type | Description | | ----------- | --------- | ----------------------------------------------------- | | `success` | `boolean` | `true` if request is allowed, `false` if rate limited | | `remaining` | `number` | Requests remaining in current window | | `reset` | `number` | Unix timestamp (ms) when the window resets | | `limit` | `number` | The configured limit | ## Choosing an identifier The identifier determines *who* gets rate limited. Common choices: | Identifier | Use case | Example | | ---------- | -------------------------- | ------------------------------------ | | User ID | Authenticated users | `req.auth.userId` | | API key | Per-key limits | `req.headers.get("x-api-key")` | | IP address | Anonymous/public endpoints | `req.headers.get("x-forwarded-for")` | | Combo | Extra specificity | `${userId}:${endpoint}` | ## Creating a reusable limiter For cleaner code, create a utility: ```ts lib/ratelimit.ts theme={"theme":"kanagawa-wave"} import { Ratelimit } from "@unkey/ratelimit"; export const apiLimiter = new Ratelimit({ rootKey: process.env.UNKEY_ROOT_KEY!, namespace: "api", limit: 100, duration: "1m", }); export const authLimiter = new Ratelimit({ rootKey: process.env.UNKEY_ROOT_KEY!, namespace: "auth", limit: 5, duration: "1m", }); ``` Then use in routes: ```ts app/api/login/route.ts theme={"theme":"kanagawa-wave"} import { authLimiter } from "@/lib/ratelimit"; export async function POST(req: Request) { const ip = req.headers.get("x-forwarded-for") ?? "unknown"; const { success } = await authLimiter.limit(ip); if (!success) { return Response.json({ error: "Too many login attempts" }, { status: 429 }); } // Handle login... } ``` ## Next steps Understand the rate limiting architecture Give specific users higher limits All configuration options Combine rate limiting with authentication ## Troubleshooting * Check that `UNKEY_ROOT_KEY` is set in `.env.local` - Verify your root key has `ratelimit.*.limit` permission - Make sure you're using the same identifier each request - Restart the dev server after changing `.env.local` * Unkey's SDK retries failed requests automatically - If errors persist, check [status.unkey.com](https://status.unkey.com) - Check [status.unkey.com](https://status.unkey.com) if issues persist Create multiple `Ratelimit` instances with different namespaces and limits. Each namespace tracks limits independently. # Delete Protection Source: https://unkey.com/docs/security/delete-protection Enable delete protection on Unkey resources to prevent accidental deletion. Protected resources require you to disable protection before removal. # Delete Protection Delete Protection is a safety feature that prevents accidental deletion of a resource. When enabled, it blocks all deletion attempts through both the dashboard and API. ## Overview **Delete Protection** adds an extra layer of security by: * Preventing accidental deletion of critical resources * Requiring explicit confirmation to disable protection * Blocking deletion attempts through both UI and API ## Supported Resources Currently, Delete Protection is available for: * Keyspaces: Protect your keyspace configurations and settings from accidental deletion More resources will be added in future updates. ## Enabling Keyspace Delete Protection 1. Navigate to your Keyspace Settings in the dashboard 2. Click "Enable Delete Protection" 3. Type the keyspace name to confirm 4. Click "Enable Keyspace Delete Protection" to add protection Delete Protection confirmation dialog showing the Enable Keyspace Delete Protection button Once enabled, the keyspace cannot be deleted until protection is explicitly disabled. ## Disabling Keyspace Delete Protection 1. Navigate to your Keyspace Settings in the dashboard 2. Click "Disable Delete Protection" 3. Type the keyspace name to confirm 4. Click "Disable Keyspace Delete Protection" to remove protection ## Resource Behavior When Delete Protection is enabled: * All deletion attempts are blocked * The resource returns a `DELETE_PROTECTED` error * The error includes a link to this documentation ### Example API Error Response ```json theme={"theme":"kanagawa-wave"} { "error": { "code": "DELETE_PROTECTED", "docs": "https://unkey.com/docs/api-reference/errors/code/DELETE_PROTECTED", "message": "api [apiId] is protected from deletions", "requestId": "req_1234" } } ``` ## Best Practices 1. **Enable for Production resources**: Always enable Delete Protection for resources in production 2. **Use for Critical resources**: Protect resources that contain important data or configurations 3. **Regular Review**: Periodically review protection status of your resources 4. **Team Communication**: Inform team members about protected resources 5. **Documentation**: Document which resources are protected and why ## Limitations * Delete Protection only prevents deletion of the resource * It does not prevent modifications to the resource * It does not prevent deletion of children of a resource, like keys or ratelimits that belong to a protected resource * Protection can be disabled by any user with appropriate permissions ## Related Errors * [DELETE\_PROTECTED](https://www.unkey.com/docs/api-reference/errors/code/DELETE_PROTECTED) - Returned when attempting to delete a protected API # GitHub Secret Scanning Source: https://unkey.com/docs/security/github-scanning Unkey partners with GitHub Secret Scanning to detect leaked root keys in public repositories. Learn how automatic revocation keeps you safe. Unkey has partnered with GitHub to scan repositories for leaked keys. GitHub Secret Scanning uses regular expressions to scan repositories for keys matching Unkey root keys. If a key is found, GitHub will notify Unkey, and we will validate the key and notify users via email. To ensure the production environment remains up and running, we do not disable the key. This is a service that is automatic and requires nothing from you to function. However, outside of GitHub, we will not be able to inform you if a key has leaked. Learn more: [GitHub Secret Scanning](https://docs.github.com/en/code-security/secret-scanning) ## Sources Scanned * content * commit * pull\_request\_title * pull\_request\_description * pull\_request\_comment * issue\_title * issue\_description * issue\_comment * discussion\_title * discussion\_body * discussion\_comment * commit\_comment * gist\_content * gist\_comment * npm * unknown # Overview Source: https://unkey.com/docs/security/overview Learn how Unkey protects your API keys and data with encryption at rest, secure key hashing, workspace isolation, and access controls. Security is foundational to Unkey. We handle API keys for your production systems, so we take this responsibility seriously. ## Key security principles We hash your API keys before storage. Even if our database were compromised, attackers couldn't recover the original keys. Create, rotate, and revoke keys instantly. Changes propagate globally within seconds. Root keys use explicit permissions. Grant only what's needed for each use case. Every key operation is logged. Know who did what, when, from where. ## How API key storage works When you create an API key through Unkey: We generate a cryptographically random key (e.g., `sk_live_abc123xyz...`) The plaintext key is returned in the API response. **This is the only time you'll see it.** We compute a SHA-256 hash of the key and store only the hash in our database. When a key is verified, we hash the provided key and compare it to the stored hash. Match = valid. ```text theme={"theme":"kanagawa-wave"} Your key: sk_live_abc123xyz789... Stored: a3f2b8c9d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1 ↑ SHA-256 hash (cannot be reversed) ``` This is the same approach used by GitHub, Stripe, and other security-conscious platforms. Even with full database access, an attacker cannot recover your original keys. ## Root keys Root keys authenticate your requests to the Unkey API itself. They're how you create, manage, and verify API keys programmatically. **Best practices:** * Use separate root keys for different environments (dev, staging, production) * Grant minimal permissions, only what each service needs * Rotate root keys periodically * Never commit root keys to version control [Learn more about root keys →](/docs/platform/root-keys/overview) ## Infrastructure security | Layer | Protection | | ------------------ | -------------------------------------------------------------------------------------------- | | **Transport** | All API traffic uses TLS 1.3. No plaintext connections accepted. | | **Infrastructure** | Requests are processed across our globally distributed infrastructure, with DDoS protection. | | **Database** | Encrypted at rest and in transit. Regular backups with point-in-time recovery. | | **Access** | Internal access requires multi-factor authentication and is logged. | ## Compliance & certifications We're actively working toward SOC 2 Type II certification. If you have specific compliance requirements, [contact us](mailto:support@unkey.com) to discuss. ## Responsible disclosure Found a security issue? We appreciate responsible disclosure. * Email: [security@unkey.com](mailto:security@unkey.com) * Please include steps to reproduce * We'll acknowledge within 48 hours ## More security features Fine-grained access control for your root keys Restrict key verification to specific IP ranges What to do if a key is compromised Automatic detection of leaked keys in public repos Prevent accidental deletion of critical keys # Recovering Keys Source: https://unkey.com/docs/security/recovering-keys Understand how Unkey handles key visibility after creation. Learn about key hashing, why keys cannot be shown again, and recovery options. Best practice is to create a key, show it to your users and never store it yourself. If the user loses the key, they can create a new one. This way you don't have to worry about storing the key securely. Without recovery, we would generate a new key and only store a hash of it. This way we can check if the key is correct but nobody, not even someone with access to the database, can recover the key. However there are some reasons why you might want to recover keys and show them again. * API playgrounds that need the key to call an API * Better DX for your users, it's annoying to create a new key and update it everywhere ## Vault Vault is our secure storage for secrets, such as keys. It follows a few principles: * Secrets are encrypted at rest * A leak of vaults data does not expose secrets * A leak of the main database does not expose secrets * A leak of the main encryption keys does not expose secrets An attacker would need access to the vault, the main database and the main encryption keys to decrypt the secrets. In order to make this even harder, we rotate the encryption keys regularly and do not run vault on the same servers as the main database to prevent an attacker from getting access to all the required information at once. To learn more about how it works under the hood, you can head over to our [engineering docs](https://engineering.unkey.dev/services/vault). ## Opting in By default we only store key hashes, not encrypted keys. If you want us to store keys in a way that we can recover them, you need to opt in: When creating new keys, your root key must have permission to encrypt. Head over to the [dashboard](https://app.unkey.com/settings/root-keys) and make sure the `encrypt_key` permission is enabled. Do not skip this step. Otherwise your root key will get rejected when trying to create new keys. To opt in to recovery, send us an email at [support@unkey.com](mailto:support@unkey.com?subject=Recovery%20Opt%20In). Send us the email from the email address associated with your workspace and include the `API ID` that you want to enable recovery for. Please note that this is not retroactively applied. Existing keys were never stored and cannot be recovered. Only keys created after opting in to recovery can be recovered. ## Creating keys When creating a key, you can set the `recoverable` field to `true`. This will store the key in a way that we can recover it later. ```shell theme={"theme":"kanagawa-wave"} curl --request POST \ --url https://api.unkey.com/v2/keys.createKey \ --header 'Authorization: Bearer {ROOT_KEY}' \ --header 'Content-Type: application/json' \ -d '{ "apiId": "{API_ID}", "recoverable": true }' ``` ## Recovering plaintext keys Both `getKey` and `listKeys` accept a `decrypt` field in the JSON request body. When set to `true`, the key will be decrypted and returned in the response as `plaintext`. When recovering keys, your root key must have permission to decrypt. Head over to the [dashboard](https://app.unkey.com/settings/root-keys) and make sure the `decrypt_key` permission is enabled. ```shell theme={"theme":"kanagawa-wave"} curl --request POST \ --url https://api.unkey.com/v2/keys.getKey \ --header 'Authorization: Bearer {ROOT_KEY}' \ --header 'Content-Type: application/json' \ -d '{ "keyId": "{KEY_ID}", "decrypt": true }' ``` ```json theme={"theme":"kanagawa-wave"} { // ... "plaintext": "your-key-here" } ``` If you have any questions about recovery, please reach out to us at [support@unkey.com](mailto:support@unkey.com). For security concerns, please disclose them responsibly by emailing [security@unkey.com](mailto:security@unkey.com) instead.